diff --git a/.env.example b/.env.example index d1f8a6a..fde9c84 100644 --- a/.env.example +++ b/.env.example @@ -25,8 +25,9 @@ SESSION_LIFETIME=120 MEMCACHED_HOST=127.0.0.1 -REDIS_HOST=127.0.0.1 -REDIS_PASSWORD=null +REDIS_HOST=redis +REDIS_PASSWORD=root +REDIS_USER=root REDIS_PORT=6379 MAIL_MAILER=smtp diff --git a/app/Console/Commands/CostCommand.php b/app/Console/Commands/CostCommand.php index 99a9db5..9845050 100644 --- a/app/Console/Commands/CostCommand.php +++ b/app/Console/Commands/CostCommand.php @@ -2,9 +2,12 @@ namespace App\Console\Commands; -use DB; +use Throwable; +use Carbon\Carbon; use App\Models\Business; use Illuminate\Console\Command; +use Morilog\Jalali\CalendarUtils; +use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Cache; class CostCommand extends Command @@ -28,109 +31,143 @@ class CostCommand extends Command public function handle() { - // infinte loop while (true) { - // to baghali ha - $recorded_month = jdate($business->calculated_at)->format("Y-m-01"); - - $calculated_at = Cache::get('calculated_at'); - if ($calculated_at === null) { - $business = Business::orderBy('calculated_at')->first()->load('users', 'cost'); - $calculated_at = $business->calculated_at; - $until_now = jdate()->getMonth() > jdate($business->calculated_at)->getMonth() - ? jdate($business->calculated_at)->toCarbon()->setTime("00", "00", "00") - : \Carbon\Carbon::now(); - - Cache::put('calculated_at', $until_now, 60); + $business = Business::find(221); + if ($business === null) { + continue; + } + + $year = jdate()->getYear(); + $month = jdate()->getMonth(); + $day = jdate()->getDay(); + $hour = jdate()->getHour(); + $minute = jdate()->getMinute(); + $second = jdate()->getSecond(); + + if (jdate()->getYear() > jdate($business->calculated_at)->getYear()) { + $year = jdate()->getYear(); + $month = 1; + $day = 1; } + if ( + jdate()->getYear() > jdate($business->calculated_at)->getYear() + && + jdate()->getMonth() > jdate($business->calculated_at)->getMonth() + ) { + $year = jdate()->getYear(); + $month = jdate()->getMonth(); + $day = 1; + } + + $gDate = CalendarUtils::toGregorian($year, $month, $day); + $carbon = Carbon::createFromDate($gDate[0], $gDate[1], $gDate[2]); + $carbon->setTime($hour, $minute, $second); + + $now = $carbon; + // if calculated_at less than an hour stop - if (\Carbon\Carbon::now()->diffInMinutes($until_now) <= 60) { - $this->info('nothing to cost'); + if ($now->diffInMinutes($business->calculated_at)) { + $this->info('Must be one hour after the last audit.'); continue; } try { DB::beginTransaction(); + // Fixed amounts of expenses + $business->load('users', 'cost'); - // business order by last_calculated_at take first - if (!isset($business)) { - $business = Business::orderBy('calculated_at')->first()->load('users', 'cost'); - } - - $user_fee = enum('business.fee.user'); - - // get business employee - $users_cost = $business->cost - ->where('type', '=', 'users') - ->where('fee', '=', $user_fee) - ->where('month', '=', $recorded_month) - ->where('amount', '=', $business->users->count()) - ->first(); - - if ($users_cost === null) { - $business->cost()->create([ - 'type' => 'users', - 'month' => $recorded_month, - 'amount' => $business->users->count(), - 'fee' => $user_fee, - 'duration' => $duration = $until_now->diffInSeconds($calculated_at), // from the created_at time of the newset fifth user - 'additional' => $business->users->pluck('id')->toArray(), - ]); - } else { - $users_cost->update([ - 'duration' => $duration = $until_now->diffInMinutes($calculated_at), // last calc - (current month - now else last calc - end of the past month), - 'additional' => $business->users->pluck('id')->toArray(), - ]); - } - - $costs = $user_fee * $duration; - - // do the math in php - if (intdiv($business->files_volume, 200) === 0) { - $pads = 0; - } else { - $pads = intdiv($business->files_volume, 200) - 1; - } - - $file_fee = enum('business.fee.file'); - - $files = $business->cost - ->where('type', '=', 'files') - ->where('fee', '=', $file_fee) - ->where('month', '=', $recorded_month) - ->where('amount', '=', $business->files_volume) - ->first(); - - if ($files === null) { - $business->cost()->create([ - 'type' => 'files', - 'month' => $recorded_month, - 'amount' => $pads, - 'fee' => $file_fee, - 'duration' => $duration = $until_now->diffInMinutes($calculated_at), // how to determine the file?, - ]); - } else { - $files->update([ - 'duration' => $duration = $until_now->diffInMinutes($calculated_at), // last calc - (current month - now else last calc - end of the past month),, - ]); - } - - $costs += $file_fee * $duration; + $costs = 0; + $costs += $this->calculateCostOfBusinessUsers($business, $now); + $costs += $this->calculateCostOfBusinessFiles($business, $now); // increment and decrement of wallet in php // deduct costs from your business wallet // make sure save the calculated_at $business->update([ 'wallet' => $business->wallet - $costs, - 'calculated_at' => \Carbon\Carbon::now(), + 'calculated_at' => Carbon::now(), ]); + DB::commit(); - } catch (Throwable $thr) { + } catch (Throwable $throwable) { DB::rollback(); - throw $thr; + report($throwable); continue; } } } + + public function calculateCostOfBusinessUsers($business, $until_now) + { + $user_fee = enum('business.fee.user'); + $calculated_at = $business->calculated_at; + $recorded_month = jdate($business->calculated_at)->format("Y-m-01"); + + + // get business employee + $users_cost = $business->cost + ->where('type', '=', 'users') + ->where('fee', '=', $user_fee) + ->where('month', '=', $recorded_month) + ->where('amount', '=', $business->users->count()) + ->first(); + + if ($users_cost === null) { + $business->cost()->create([ + 'type' => 'users', + 'month' => $recorded_month, + 'amount' => $business->users->count(), + 'fee' => $user_fee, + 'duration' => $duration = $until_now->diffInSeconds($calculated_at), // from the created_at time of the newset fifth user + 'additional' => $business->users->pluck('id')->toArray(), + ]); + } else { + $users_cost->update([ + 'duration' => $duration = $until_now->diffInMinutes($calculated_at) + $users_cost->duration, // last calc - (current month - now else last calc - end of the past month), + 'additional' => $business->users->pluck('id')->toArray(), + ]); + } + + return $user_fee * $duration; + } + + public function calculateCostOfBusinessFiles($business, $until_now) + { + $file_fee = enum('business.fee.file'); + $calculated_at = $business->calculated_at; + $recorded_month = jdate($business->calculated_at)->format("Y-m-01"); + + + // do the math in php + if (intdiv($business->files_volume, 200) === 0) { + $pads = 0; + } else { + $pads = intdiv($business->files_volume, 200) - 1; + } + + + $files = $business->cost + ->where('type', '=', 'files') + ->where('fee', '=', $file_fee) + ->where('month', '=', $recorded_month) + ->where('amount', '=', $business->files_volume) + ->first(); + + if ($files === null) { + $business->cost()->create([ + 'type' => 'files', + 'month' => $recorded_month, + 'amount' => $pads, + 'fee' => $file_fee, + 'duration' => $duration = $until_now->diffInMinutes($calculated_at), // how to determine the file?, + ]); + } else { + $files->update([ + 'duration' => $duration = $until_now->diffInMinutes($calculated_at) + $files->duration, // last calc - (current month - now else last calc - end of the past month),, + ]); + } + + return $file_fee * $duration; + } } diff --git a/app/Events/ModelSaved.php b/app/Events/ModelSaved.php new file mode 100644 index 0000000..32a28cc --- /dev/null +++ b/app/Events/ModelSaved.php @@ -0,0 +1,38 @@ +message = $message; + } + + /** + * Get the channels the event should broadcast on. + * + * @return \Illuminate\Broadcasting\Channel|array + */ + public function broadcastOn() + { + return new PrivateChannel('channel-name'); + } +} diff --git a/app/Http/Controllers/ActivityController.php b/app/Http/Controllers/ActivityController.php new file mode 100644 index 0000000..870279c --- /dev/null +++ b/app/Http/Controllers/ActivityController.php @@ -0,0 +1,110 @@ +indexValidation($request); + $per_page = $request->limit > 100 ? 10 : $request->limit; + return $this->indexFiltering($business)->paginate($per_page); + } + + public function indexValidation($request) + { + $bound = 10; + $this->validate($request, [ + 'filter.project_id' => [new MaxBound($bound)] , + 'filter.system_id' => [new MaxBound($bound)] , + 'filter.workflow_id' => [new MaxBound($bound)] , + 'filter.status_id' => [new MaxBound($bound)] , + 'filter.sprint_id' => [new MaxBound($bound)] , + 'filter.actor_id' => [new MaxBound($bound)] , + 'filter.user_id' => [new MaxBound($bound)] , + 'filter.subject_id' => [new MaxBound($bound)] , + //todo: validation for crud_id and table_id + 'filter.creates_before' => 'bail|nullable|date|date_format:Y-m-d' , + 'filter.creates_after' => 'bail|nullable|date|date_format:Y-m-d' , + 'filter.creates_in' => 'bail|nullable|numeric|max:90' , + ]); + } + public function indexFiltering($business) + { + $query = Activity::where('business_id', $business); + $activityQ = QueryBuilder::for($query) + ->allowedFilters([ + AllowedFilter::exact('project_id'), + AllowedFilter::exact('system_id'), + AllowedFilter::exact('workflow_id'), + AllowedFilter::exact('status_id'), + AllowedFilter::exact('sprint_id'), + AllowedFilter::exact('task_id'), + AllowedFilter::exact('actor_id'), + AllowedFilter::exact('user_id'), + AllowedFilter::exact('crud_id'), + AllowedFilter::exact('table_id'), + AllowedFilter::exact('subject_id'), + AllowedFilter::scope('creates_before'), + AllowedFilter::scope('creates_after'), + AllowedFilter::scope('creates_in'), + ]) + ->defaultSort('-id') + ->allowedSorts('id', 'created_at'); + if (\request('_business_info')['info']['users'][\auth()->id()]['level'] != enum('levels.owner.id')) { + $requested_projects = isset(\request('filter')['project_id']) ? + array_unique(explode(',',\request('filter')['project_id'] ?? null )) : + null; + $requested_projects = collect($requested_projects)->keyBy(null)->toArray(); + $project_ids = $this->myStateProjects($requested_projects); + $activityQ->where(function ($q) use ($project_ids) { + $q->whereIn('project_id', $project_ids['non_guest_ids']) + ->orWhere(function ($q) use ($project_ids) { + $q->whereIn('project_id', $project_ids['guest_ids']) + ->where('user_id', auth()->id()); + }); + }); + } + return $activityQ; + } + + public function myStateProjects($requested_projects) + { + $non_guest_ids = []; + $guest_ids = []; + $is_empty = empty($requested_projects); + + foreach (\request('_business_info')['info']['projects'] as $p_id => $p) { + + $level = \request('_business_info')['info']['projects'][$p_id]['members'][\auth()->id()]['level']; + + if (( $is_empty || isset($requested_projects[$p_id])) + && $level > enum('levels.guest.id')) { + array_push($non_guest_ids, $p_id); + } + if (( $is_empty || isset($requested_projects[$p_id])) + && $level == enum('levels.guest.id')) { + array_push($guest_ids, $p_id); + } + } + + return ['non_guest_ids' => $non_guest_ids, 'guest_ids' => $guest_ids]; + } + + public function store($business, Request $request) + { + return Activity::create($request->merge(['business_id' => $business])->all()); + } + + public function delete() + { + + } +} diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 7e9a193..55fe6f7 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -13,7 +13,6 @@ use App\Http\Resources\UserResource; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Cache; -use Laravel\Lumen\Routing\Controller; use Laravel\Socialite\Facades\Socialite; use Illuminate\Session\TokenMismatchException; use Symfony\Component\HttpFoundation\Response; diff --git a/app/Http/Resources/TaskResource.php b/app/Http/Resources/TaskResource.php index eab56f3..94491fb 100644 --- a/app/Http/Resources/TaskResource.php +++ b/app/Http/Resources/TaskResource.php @@ -21,7 +21,7 @@ class TaskResource extends JsonResource } } - $resource['tags'] = $this->tags; + $resource['tags'] = $this->tags()->pluck('tag_id')->toArray(); $resource['works'] = $this->works; $resource['comments'] = $this->comments; diff --git a/app/Listeners/ActivityRegistration.php b/app/Listeners/ActivityRegistration.php new file mode 100644 index 0000000..90b97b8 --- /dev/null +++ b/app/Listeners/ActivityRegistration.php @@ -0,0 +1,73 @@ +message)); + $message = json_decode($event->message); + Activity::create([ + 'business_id' => $message->business, + 'project_id' => $message->project, + 'actor_id' => $message->auth, + 'system_id' => $message->data->system_id, + 'workflow_id' => $message->data->workflow_id, + 'status_id' => $message->data->status_id, + 'sprint_id' => $message->data->sprint_id, + 'task_id' => $message->data->task_id ?? null, + 'subject_id' => $message->data->subject_id ?? null, + 'user_id' => $message->data->user_id, + 'crud_id' => $message->data->crud_id, + 'table_id' => enum('tables.'.$message->data->table_name.'.id'), + 'original' => $message->data->original, + 'diff' => $message->data->diff, + ]); + } +} diff --git a/app/Models/Activity.php b/app/Models/Activity.php index 805e4a8..ed20699 100644 --- a/app/Models/Activity.php +++ b/app/Models/Activity.php @@ -2,8 +2,37 @@ namespace App\Models; -use App\Models\Model; +use Carbon\Carbon; +use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; + class Activity extends Model { + use HasFactory; + + protected $fillable = [ + 'business_id', 'project_id', 'system_id', 'workflow_id', 'status_id', 'sprint_id', + 'actor_id', 'task_id', 'subject_id', 'user_id', 'crud_id', 'table_id', 'original', 'diff' + ]; + + public $casts = [ + 'original' => 'array', + 'diff' => 'array', + ]; + + public function scopeCreatesBefore($query, $date) + { + return $query->whereDate('created_at', '<=', Carbon::parse($date)); + } + public function scopeCreatesAfter($query, $date) + { + return $query->whereDate('created_at', '>=', Carbon::parse($date)); + } + public function scopeCreatesIn($query, $days) + { + return $days != "" ? + $query->whereDate('created_at', '>=', Carbon::now()->modify('-'.$days.' day')->toDate()) : + $query; + } } diff --git a/app/Models/Model.php b/app/Models/Model.php index db30979..b2f1373 100644 --- a/app/Models/Model.php +++ b/app/Models/Model.php @@ -2,10 +2,7 @@ namespace App\Models; -use Anik\Amqp\Exchange; -use Anik\Amqp\Facades\Amqp; -use PhpAmqpLib\Wire\AMQPTable; -use Anik\Amqp\PublishableMessage; +use App\Events\ModelSaved; use Illuminate\Http\JsonResponse; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Auth; @@ -194,21 +191,23 @@ class Model extends EloquentModel 'from' => env('CONTAINER_NAME'), ]; - $message = new PublishableMessage(json_encode($payload)); +// $message = new PublishableMessage(json_encode($payload)); - $routers = [ - "activity_exchange" => ["name" => "activity",], - "notif_exchange" => ["name" => "notif",], - "socket_exchange" => ["name" => "socket",], - ]; - - foreach ($routers as $exchange => $properties) { - $message->setProperties(["application_headers" => new AMQPTable($properties)]); - - $message->setExchange(new Exchange($exchange)); + ModelSaved::dispatch(json_encode($payload)); - Amqp::publish($message, ""); - } +// $routers = [ +// "activity_exchange" => ["name" => "activity",], +// "notif_exchange" => ["name" => "notif",], +// "socket_exchange" => ["name" => "socket",], +// ]; +// +// foreach ($routers as $exchange => $properties) { +// $message->setProperties(["application_headers" => new AMQPTable($properties)]); +// +// $message->setExchange(new Exchange($exchange)); +// +// Amqp::publish($message, ""); +// } } /** diff --git a/app/Models/Task.php b/app/Models/Task.php index ffef4f7..aee207b 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -73,7 +73,7 @@ class Task extends Model 'title' => 'required|string|min:3|max:254', 'description' => 'nullable|string|min:2|max:1000', 'priority' => 'nullable|numeric|between:1,10', - 'estimated_time' => 'nullable|numeric|min:30', + 'estimated_time' => 'bail|nullable|numeric', 'due_date' => 'bail|nullable|date|date_format:Y-m-d|after_or_equal:'. ((request()->method() === 'POST') ? date('yy-m-d') : $this->created_at->toDateString()), 'tags' => 'nullable|array', diff --git a/app/Models/User.php b/app/Models/User.php index 857c7d3..72c3b64 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,13 +5,13 @@ namespace App\Models; use App\Models\File; use App\Models\Model; use App\Models\SoftDeletes; -use App\Models\ReportableRelation; use Illuminate\Validation\Rule; use Illuminate\Http\UploadedFile; use Spatie\MediaLibrary\HasMedia; +use App\Models\ReportableRelation; use Illuminate\Auth\Authenticatable; -use Laravel\Lumen\Auth\Authorizable; use Spatie\MediaLibrary\InteractsWithMedia; +use Illuminate\Foundation\Auth\Access\Authorizable; use Spatie\MediaLibrary\MediaCollections\Models\Media; use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract; use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract; diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index a9f10a6..4d702ef 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -2,6 +2,8 @@ namespace App\Providers; +use App\Events\ModelSaved; +use App\Listeners\ActivityRegistration; use Illuminate\Auth\Events\Registered; use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; @@ -18,6 +20,9 @@ class EventServiceProvider extends ServiceProvider Registered::class => [ SendEmailVerificationNotification::class, ], + ModelSaved::class => [ + ActivityRegistration::class + ] ]; /** diff --git a/composer.json b/composer.json index ca1dcad..a8d0129 100644 --- a/composer.json +++ b/composer.json @@ -20,11 +20,6 @@ "jenssegers/agent": "^2.6", "laravel/socialite": "^5.1", "laravel/framework": "^8.12", - "guzzlehttp/guzzle": "^7.0.1", - "laravel/legacy-factories": "^1", - "fruitcake/laravel-cors": "^2.0", - "laravel/lumen-framework": "^8.0", - "illuminate/notifications": "^8.0", "league/flysystem-aws-s3-v3": "~1.0", "spatie/laravel-medialibrary": "^9.0", "spatie/laravel-query-builder": "^3.3", diff --git a/config/app.php b/config/app.php index 7f44ebe..d517cdc 100644 --- a/config/app.php +++ b/config/app.php @@ -12,7 +12,7 @@ return [ 'asset_url' => env('ASSET_URL', null), - 'timezone' => 'UTC', + 'timezone' => 'Asia/Tehran', 'locale' => 'fa', diff --git a/database/factories/BusinessFactory.php b/database/factories/BusinessFactory.php index f8dab3d..28f0dae 100644 --- a/database/factories/BusinessFactory.php +++ b/database/factories/BusinessFactory.php @@ -12,6 +12,6 @@ $factory->define(Business::class, function (Faker $faker) { 'slug' => Str::slug($name) . $faker->numberBetween(1, 100), 'wallet' => random_int(111111, 999999), 'color' => $faker->colorName, - 'calculated_at' => \Carbon\Carbon::now()->subMinutes(random_int(1, 1000)), + 'calculated_at' => \Carbon\Carbon::now()->subDays(random_int(1, 31)), ]; }); diff --git a/database/migrations/2020_08_18_085017_fingerprints.php b/database/migrations/2020_08_18_085017_fingerprints.php index 67bd1ee..af506ec 100644 --- a/database/migrations/2020_08_18_085017_fingerprints.php +++ b/database/migrations/2020_08_18_085017_fingerprints.php @@ -19,8 +19,8 @@ class Fingerprints extends Migration $table->string('agent'); $table->ipAddress('ip'); $table->string('os'); - $table->decimal('latitude', 10, 2); - $table->decimal('longitude', 11, 2); + $table->decimal('latitude', 10, 4); + $table->decimal('longitude', 11, 4); $table->char('token', 60)->unique(); $table->timestamps(); }); diff --git a/database/migrations/2020_08_18_085018_create_businesses_table.php b/database/migrations/2020_08_18_085018_create_businesses_table.php index e37d713..b0d168f 100644 --- a/database/migrations/2020_08_18_085018_create_businesses_table.php +++ b/database/migrations/2020_08_18_085018_create_businesses_table.php @@ -23,7 +23,7 @@ class CreateBusinessesTable extends Migration $table->string('description')->nullable(); $table->json('cache')->nullable(); $table->boolean('has_avatar')->default(false); - $table->timestamp('calculated_at')->nullable(); + $table->timestamp('calculated_at')->nullable()->index(); $table->timestamp('created_at')->nullable(); $table->timestamp('updated_at')->nullable(); $table->timestamp('deleted_at')->nullable(); diff --git a/database/migrations/2021_03_06_085855_create_activities_table.php b/database/migrations/2021_03_06_085855_create_activities_table.php new file mode 100644 index 0000000..ee00afe --- /dev/null +++ b/database/migrations/2021_03_06_085855_create_activities_table.php @@ -0,0 +1,45 @@ +id(); + $table->unsignedBigInteger('business_id'); + $table->unsignedBigInteger('project_id')->nullable(); + $table->unsignedBigInteger('system_id')->nullable(); + $table->unsignedBigInteger('workflow_id')->nullable(); + $table->unsignedBigInteger('status_id')->nullable(); + $table->unsignedBigInteger('sprint_id')->nullable(); + $table->unsignedBigInteger('task_id')->nullable(); + $table->unsignedBigInteger('subject_id')->nullable();//row id + $table->unsignedBigInteger('actor_id'); + $table->unsignedBigInteger('user_id')->nullable(); + $table->unsignedBigInteger('crud_id')->nullable(); + $table->unsignedBigInteger('table_id')->nullable(); + $table->json('original')->nullable(); // a unique identifier that represent the type of action + $table->json('diff')->nullable(); // all data that has been changed + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('activities'); + } +} diff --git a/database/migrations/2021_03_06_114918_create_failed_jobs_table.php b/database/migrations/2021_03_06_114918_create_failed_jobs_table.php new file mode 100644 index 0000000..6aa6d74 --- /dev/null +++ b/database/migrations/2021_03_06_114918_create_failed_jobs_table.php @@ -0,0 +1,36 @@ +id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +} diff --git a/routes/api.php b/routes/api.php index 4a40912..be7bd04 100644 --- a/routes/api.php +++ b/routes/api.php @@ -182,6 +182,11 @@ $router->group(['prefix' => 'businesses', 'middleware' => 'auth:api'], function $router->delete('/', 'BusinessController@deleteUser'); }); }); + + $router->group(['prefix' => 'activities'], function () use ($router) { + $router->post('/', 'ActivityController@store'); + $router->get('/', 'ActivityController@index'); + }); }); });