From 9e415b420c56b548c4088c8a5a58049fd658eb06 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 12:43:38 +1100 Subject: [PATCH] Refactor for scheduled tasks --- app/Factory/SchedulerFactory.php | 32 ++ app/Http/Controllers/SchedulerController.php | 9 + .../Controllers/TaskSchedulerController.php | 177 ++++++++-- .../CreateScheduledTaskRequest.php | 39 -- .../TaskScheduler/CreateSchedulerRequest.php | 28 ++ .../TaskScheduler/DestroySchedulerRequest.php | 27 ++ .../TaskScheduler/ShowSchedulerRequest.php | 27 ++ .../TaskScheduler/StoreSchedulerRequest.php | 44 +++ .../UpdateScheduledJobRequest.php | 25 -- ...Request.php => UpdateSchedulerRequest.php} | 30 +- app/Jobs/Ninja/TaskScheduler.php | 1 + app/Models/BaseModel.php | 1 + app/Models/Scheduler.php | 88 ++--- app/Policies/SchedulerPolicy.php | 31 ++ app/Providers/AuthServiceProvider.php | 3 + app/Providers/RouteServiceProvider.php | 17 + app/Repositories/SchedulerRepository.php | 38 ++ app/Repositories/TaskSchedulerRepository.php | 16 - app/Services/Invoice/InvoiceService.php | 7 +- .../SchedulerService.php} | 6 +- .../TaskScheduler/TaskSchedulerService.php | 2 + ...ansformer.php => SchedulerTransformer.php} | 16 +- database/factories/SchedulerFactory.php | 37 ++ ...t_auto_bill_on_regular_invoice_setting.php | 32 ++ routes/api.php | 3 +- tests/Feature/Scheduler/SchedulerTest.php | 334 +++++++++++++----- tests/MockAccountData.php | 14 + 27 files changed, 806 insertions(+), 278 deletions(-) create mode 100644 app/Factory/SchedulerFactory.php delete mode 100644 app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php create mode 100644 app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php create mode 100644 app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php create mode 100644 app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php create mode 100644 app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php delete mode 100644 app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php rename app/Http/Requests/TaskScheduler/{UpdateScheduleRequest.php => UpdateSchedulerRequest.php} (51%) create mode 100644 app/Policies/SchedulerPolicy.php create mode 100644 app/Repositories/SchedulerRepository.php delete mode 100644 app/Repositories/TaskSchedulerRepository.php rename app/Services/{Schedule/ScheduleService.php => Scheduler/SchedulerService.php} (92%) rename app/Transformers/{TaskSchedulerTransformer.php => SchedulerTransformer.php} (66%) create mode 100644 database/factories/SchedulerFactory.php diff --git a/app/Factory/SchedulerFactory.php b/app/Factory/SchedulerFactory.php new file mode 100644 index 0000000000..c6603e148d --- /dev/null +++ b/app/Factory/SchedulerFactory.php @@ -0,0 +1,32 @@ +name = ''; + $scheduler->company_id = $company_id; + $scheduler->user_id = $user_id; + $scheduler->parameters = []; + $scheduler->is_paused = false; + $scheduler->is_deleted = false; + $scheduler->template = ''; + + return $scheduler; + } +} diff --git a/app/Http/Controllers/SchedulerController.php b/app/Http/Controllers/SchedulerController.php index e1788ce4e8..dcd7b35d1b 100644 --- a/app/Http/Controllers/SchedulerController.php +++ b/app/Http/Controllers/SchedulerController.php @@ -1,4 +1,13 @@ scheduler_repository = $scheduler_repository; } /** * @OA\GET( - * path="/api/v1/task_scheduler/", + * path="/api/v1/task_schedulers/", * operationId="getTaskSchedulers", - * tags={"task_scheduler"}, + * tags={"task_schedulers"}, * summary="Task Scheduler Index", * description="Get all schedulers with associated jobs", * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), @@ -67,11 +70,57 @@ class TaskSchedulerController extends BaseController return $this->listResponse($schedulers); } + /** + * Show the form for creating a new resource. + * + * @param CreateSchedulerRequest $request The request + * + * @return Response + * + * + * @OA\Get( + * path="/api/v1/invoices/task_schedulers", + * operationId="getTaskScheduler", + * tags={"task_schedulers"}, + * summary="Gets a new blank scheduler object", + * description="Returns a blank object with default values", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/include"), + * @OA\Response( + * response=200, + * description="A blank scheduler object", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskSchedulerSchema"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function create(CreateSchedulerRequest $request) + { + $scheduler = SchedulerFactory::create(auth()->user()->company()->id, auth()->user()->id); + + return $this->itemResponse($scheduler); + } + /** * @OA\Post( - * path="/api/v1/task_scheduler/", + * path="/api/v1/task_schedulers/", * operationId="createTaskScheduler", - * tags={"task_scheduler"}, + * tags={"task_schedulers"}, * summary="Create task scheduler with job ", * description="Create task scheduler with a job (action(job) request should be sent via request also. Example: We want client report to be job which will be run * multiple times, we should send the same parameters in the request as we would send if we wanted to get report, see example", @@ -100,19 +149,18 @@ class TaskSchedulerController extends BaseController * ), * ) */ - public function store(CreateScheduledTaskRequest $request) + public function store(StoreSchedulerRequest $request) { - $scheduler = new Scheduler(); - $scheduler->service()->store($scheduler, $request); + $scheduler = $this->scheduler_repository->save($request->all(), SchedulerFactory::create(auth()->user()->company()->id, auth()->user()->id)); return $this->itemResponse($scheduler); } /** * @OA\GET( - * path="/api/v1/task_scheduler/{id}", + * path="/api/v1/task_schedulers/{id}", * operationId="showTaskScheduler", - * tags={"task_scheduler"}, + * tags={"task_schedulers"}, * summary="Show given scheduler", * description="Get scheduler with associated job", * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), @@ -142,16 +190,16 @@ class TaskSchedulerController extends BaseController * ), * ) */ - public function show(Scheduler $scheduler) + public function show(ShowSchedulerRequest $request, Scheduler $scheduler) { return $this->itemResponse($scheduler); } /** * @OA\PUT( - * path="/api/v1/task_scheduler/{id}", + * path="/api/v1/task_schedulers/{id}", * operationId="updateTaskScheduler", - * tags={"task_scheduler"}, + * tags={"task_schedulers"}, * summary="Update task scheduler ", * description="Update task scheduler", * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), @@ -168,7 +216,7 @@ class TaskSchedulerController extends BaseController * ), * ), * @OA\RequestBody( * required=true, - * @OA\JsonContent(ref="#/components/schemas/UpdateTaskSchedulerSchema") + * @OA\JsonContent(ref="#/components/schemas/TaskSchedulerSchema") * ), * @OA\Response( * response=200, @@ -189,18 +237,18 @@ class TaskSchedulerController extends BaseController * ), * ) */ - public function update(Scheduler $scheduler, UpdateScheduleRequest $request) + public function update(UpdateSchedulerRequest $request, Scheduler $scheduler) { - $scheduler->service()->update($scheduler, $request); + $this->scheduler_repository->save($request->all(), $scheduler); return $this->itemResponse($scheduler); } /** * @OA\DELETE( - * path="/api/v1/task_scheduler/{id}", + * path="/api/v1/task_schedulers/{id}", * operationId="destroyTaskScheduler", - * tags={"task_scheduler"}, + * tags={"task_schedulers"}, * summary="Destroy Task Scheduler", * description="Destroy task scheduler and its associated job", * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), @@ -230,10 +278,83 @@ class TaskSchedulerController extends BaseController * ), * ) */ - public function destroy(Scheduler $scheduler) + public function destroy(DestroySchedulerRequest $request, Scheduler $scheduler) { $this->scheduler_repository->delete($scheduler); return $this->itemResponse($scheduler->fresh()); } + + + /** + * Perform bulk actions on the list view. + * + * @return Response + * + * + * @OA\Post( + * path="/api/v1/task_schedulers/bulk", + * operationId="bulkTaskSchedulerActions", + * tags={"task_schedulers"}, + * summary="Performs bulk actions on an array of task_schedulers", + * description="", + * @OA\Parameter(ref="#/components/parameters/X-Api-Secret"), + * @OA\Parameter(ref="#/components/parameters/X-Api-Token"), + * @OA\Parameter(ref="#/components/parameters/X-Requested-With"), + * @OA\Parameter(ref="#/components/parameters/index"), + * @OA\RequestBody( + * description="array of ids", + * required=true, + * @OA\MediaType( + * mediaType="application/json", + * @OA\Schema( + * type="array", + * @OA\Items( + * type="integer", + * description="Array of hashed IDs to be bulk 'actioned", + * example="[0,1,2,3]", + * ), + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="The TaskSchedule response", + * @OA\Header(header="X-MINIMUM-CLIENT-VERSION", ref="#/components/headers/X-MINIMUM-CLIENT-VERSION"), + * @OA\Header(header="X-RateLimit-Remaining", ref="#/components/headers/X-RateLimit-Remaining"), + * @OA\Header(header="X-RateLimit-Limit", ref="#/components/headers/X-RateLimit-Limit"), + * @OA\JsonContent(ref="#/components/schemas/TaskScheduleSchema"), + * ), + * @OA\Response( + * response=422, + * description="Validation error", + * @OA\JsonContent(ref="#/components/schemas/ValidationError"), + * ), + * @OA\Response( + * response="default", + * description="Unexpected Error", + * @OA\JsonContent(ref="#/components/schemas/Error"), + * ), + * ) + */ + public function bulk() + { + $action = request()->input('action'); + + if(!in_array($action, ['archive', 'restore', 'delete'])) + return response()->json(['message' => 'Bulk action does not exist'], 400); + + $ids = request()->input('ids'); + + $task_schedulers = Scheduler::withTrashed()->find($this->transformKeys($ids)); + + $task_schedulers->each(function ($task_scheduler, $key) use ($action) { + if (auth()->user()->can('edit', $task_scheduler)) { + $this->scheduler_repository->{$action}($task_scheduler); + } + }); + + return $this->listResponse(Scheduler::withTrashed()->whereIn('id', $this->transformKeys($ids))); + } + } diff --git a/app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php b/app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php deleted file mode 100644 index 80b4fc76bf..0000000000 --- a/app/Http/Requests/TaskScheduler/CreateScheduledTaskRequest.php +++ /dev/null @@ -1,39 +0,0 @@ -user()->isAdmin(); - } - - public function rules() - { - return [ - 'paused' => 'sometimes|bool', - 'repeat_every' => 'required|string|in:DAY,WEEK,MONTH,3MONTHS,YEAR', - 'start_from' => 'sometimes|string', - 'job' => 'required', - ]; - } - - public function prepareForValidation() - { - $input = $this->all(); - - if (! array_key_exists('start_from', $input)) { - $input['start_from'] = now(); - } - - $this->replace($input); - } -} diff --git a/app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php new file mode 100644 index 0000000000..71c0a8f3d9 --- /dev/null +++ b/app/Http/Requests/TaskScheduler/CreateSchedulerRequest.php @@ -0,0 +1,28 @@ +user()->isAdmin(); + } + +} diff --git a/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php b/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php new file mode 100644 index 0000000000..93e15f06df --- /dev/null +++ b/app/Http/Requests/TaskScheduler/DestroySchedulerRequest.php @@ -0,0 +1,27 @@ +user()->isAdmin(); + } +} diff --git a/app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php b/app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php new file mode 100644 index 0000000000..a459edab3c --- /dev/null +++ b/app/Http/Requests/TaskScheduler/ShowSchedulerRequest.php @@ -0,0 +1,27 @@ +user()->can('view', $this->scheduler); + } +} diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php new file mode 100644 index 0000000000..0c63f95a95 --- /dev/null +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -0,0 +1,44 @@ +user()->isAdmin(); + } + + public function rules() + { + + $rules = [ + 'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)], + 'is_paused' => 'bail|sometimes|boolean', + 'frequency_id' => 'bail|required|integer|digits_between:1,12', + 'next_run' => 'bail|required|date:Y-m-d', + 'template' => 'bail|required|string', + 'parameters' => 'bail|array', + ]; + + return $rules; + + } +} diff --git a/app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php b/app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php deleted file mode 100644 index 0db34ccc00..0000000000 --- a/app/Http/Requests/TaskScheduler/UpdateScheduledJobRequest.php +++ /dev/null @@ -1,25 +0,0 @@ -user()->isAdmin(); - } - - public function rules(): array - { - return [ - 'action_name' => 'sometimes|string', - ]; - } -} diff --git a/app/Http/Requests/TaskScheduler/UpdateScheduleRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php similarity index 51% rename from app/Http/Requests/TaskScheduler/UpdateScheduleRequest.php rename to app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index 69b819e845..7e3ec32671 100644 --- a/app/Http/Requests/TaskScheduler/UpdateScheduleRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -8,14 +8,12 @@ * * @license https://www.elastic.co/licensing/elastic-license */ - namespace App\Http\Requests\TaskScheduler; use App\Http\Requests\Request; -use Carbon\Carbon; use Illuminate\Validation\Rule; -class UpdateScheduleRequest extends Request +class UpdateSchedulerRequest extends Request { /** * Determine if the user is authorized to make this request. @@ -29,23 +27,17 @@ class UpdateScheduleRequest extends Request public function rules(): array { - return [ - 'paused' => 'sometimes|bool', - 'repeat_every' => 'sometimes|string|in:DAY,WEEK,BIWEEKLY,MONTH,3MONTHS,YEAR', - 'start_from' => 'sometimes', - 'scheduled_run'=>'sometimes', + + $rules = [ + 'name' => ['bail', 'sometimes', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)->ignore($this->task_scheduler->id)], + 'is_paused' => 'bail|sometimes|boolean', + 'frequency_id' => 'bail|required|integer|digits_between:1,12', + 'next_run' => 'bail|required|date:Y-m-d', + 'template' => 'bail|required|string', + 'parameters' => 'bail|array', ]; - } - public function prepareForValidation() - { - $input = $this->all(); - - if (isset($input['start_from'])) { - $input['scheduled_run'] = Carbon::parse((int) $input['start_from']); - $input['start_from'] = Carbon::parse((int) $input['start_from']); - } - - $this->replace($input); + return $rules; + } } diff --git a/app/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php index 76ebad1883..ce660c756f 100644 --- a/app/Jobs/Ninja/TaskScheduler.php +++ b/app/Jobs/Ninja/TaskScheduler.php @@ -21,6 +21,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +//@rebuild it class TaskScheduler implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php index 22fc9ba129..fdd51dff88 100644 --- a/app/Models/BaseModel.php +++ b/app/Models/BaseModel.php @@ -170,6 +170,7 @@ class BaseModel extends Model */ public function resolveRouteBinding($value, $field = null) { + if (is_numeric($value)) { throw new ModelNotFoundException("Record with value {$value} not found"); } diff --git a/app/Models/Scheduler.php b/app/Models/Scheduler.php index 287129638f..09732f875a 100644 --- a/app/Models/Scheduler.php +++ b/app/Models/Scheduler.php @@ -11,7 +11,7 @@ namespace App\Models; -use App\Services\TaskScheduler\TaskSchedulerService; +use App\Services\Scheduler\SchedulerService; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -20,8 +20,8 @@ use Illuminate\Support\Carbon; * @property bool paused * @property bool is_deleted * @property \Carbon\Carbon|mixed start_from - * @property string repeat_every - * @property \Carbon\Carbon|mixed scheduled_run + * @property int frequency_id + * @property \Carbon\Carbon|mixed next_run * @property int company_id * @property int updated_at * @property int created_at @@ -33,22 +33,20 @@ use Illuminate\Support\Carbon; */ class Scheduler extends BaseModel { - use HasFactory, SoftDeletes; + use SoftDeletes; protected $fillable = [ 'start_from', - 'paused', + 'is_paused', 'repeat_every', 'scheduled_run', 'action_class', 'action_name', 'parameters', - 'company_id', ]; protected $casts = [ - 'start_from' => 'timestamp', - 'scheduled_run' => 'timestamp', + 'next_run' => 'datetime', 'created_at' => 'timestamp', 'updated_at' => 'timestamp', 'deleted_at' => 'timestamp', @@ -57,6 +55,10 @@ class Scheduler extends BaseModel 'parameters' => 'array', ]; + protected $appends = [ + 'hashed_id', + ]; + const DAILY = 'DAY'; const WEEKLY = 'WEEK'; @@ -100,9 +102,9 @@ class Scheduler extends BaseModel /** * Service entry points. */ - public function service(): TaskSchedulerService + public function service(): SchedulerService { - return new TaskSchedulerService($this); + return new SchedulerService($this); } public function company(): \Illuminate\Database\Eloquent\Relations\BelongsTo @@ -110,43 +112,43 @@ class Scheduler extends BaseModel return $this->belongsTo(Company::class); } - public function nextScheduledDate(): ?Carbon - { - $offset = 0; + // public function nextScheduledDate(): ?Carbon + // { + // $offset = 0; - $entity_send_time = $this->company->settings->entity_send_time; + // $entity_send_time = $this->company->settings->entity_send_time; - if ($entity_send_time != 0) { - $timezone = $this->company->timezone(); + // if ($entity_send_time != 0) { + // $timezone = $this->company->timezone(); - $offset -= $timezone->utc_offset; - $offset += ($entity_send_time * 3600); - } + // $offset -= $timezone->utc_offset; + // $offset += ($entity_send_time * 3600); + // } - /* - As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need - to add ON a day - a day = 86400 seconds - */ + // /* + // As we are firing at UTC+0 if our offset is negative it is technically firing the day before so we always need + // to add ON a day - a day = 86400 seconds + // */ - if ($offset < 0) { - $offset += 86400; - } + // if ($offset < 0) { + // $offset += 86400; + // } - switch ($this->repeat_every) { - case self::DAILY: - return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset); - case self::WEEKLY: - return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset); - case self::BIWEEKLY: - return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset); - case self::MONTHLY: - return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); - case self::QUARTERLY: - return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); - case self::ANNUALLY: - return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset); - default: - return null; - } - } + // switch ($this->repeat_every) { + // case self::DAILY: + // return Carbon::parse($this->scheduled_run)->startOfDay()->addDay()->addSeconds($offset); + // case self::WEEKLY: + // return Carbon::parse($this->scheduled_run)->startOfDay()->addWeek()->addSeconds($offset); + // case self::BIWEEKLY: + // return Carbon::parse($this->scheduled_run)->startOfDay()->addWeeks(2)->addSeconds($offset); + // case self::MONTHLY: + // return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthNoOverflow()->addSeconds($offset); + // case self::QUARTERLY: + // return Carbon::parse($this->scheduled_run)->startOfDay()->addMonthsNoOverflow(3)->addSeconds($offset); + // case self::ANNUALLY: + // return Carbon::parse($this->scheduled_run)->startOfDay()->addYearNoOverflow()->addSeconds($offset); + // default: + // return null; + // } + // } } diff --git a/app/Policies/SchedulerPolicy.php b/app/Policies/SchedulerPolicy.php new file mode 100644 index 0000000000..b5eaba785f --- /dev/null +++ b/app/Policies/SchedulerPolicy.php @@ -0,0 +1,31 @@ +isAdmin(); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index eef723dcaa..353e7d6caa 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -36,6 +36,7 @@ use App\Models\Quote; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Scheduler; use App\Models\Subscription; use App\Models\Task; use App\Models\TaskStatus; @@ -67,6 +68,7 @@ use App\Policies\QuotePolicy; use App\Policies\RecurringExpensePolicy; use App\Policies\RecurringInvoicePolicy; use App\Policies\RecurringQuotePolicy; +use App\Policies\SchedulerPolicy; use App\Policies\SubscriptionPolicy; use App\Policies\TaskPolicy; use App\Policies\TaskStatusPolicy; @@ -109,6 +111,7 @@ class AuthServiceProvider extends ServiceProvider RecurringExpense::class => RecurringExpensePolicy::class, RecurringInvoice::class => RecurringInvoicePolicy::class, RecurringQuote::class => RecurringQuotePolicy::class, + Scheduler::class => SchedulerPolicy::class, Subscription::class => SubscriptionPolicy::class, Task::class => TaskPolicy::class, TaskStatus::class => TaskStatusPolicy::class, diff --git a/app/Providers/RouteServiceProvider.php b/app/Providers/RouteServiceProvider.php index 2684f395df..0a993436e4 100644 --- a/app/Providers/RouteServiceProvider.php +++ b/app/Providers/RouteServiceProvider.php @@ -11,7 +11,9 @@ namespace App\Providers; +use App\Models\Scheduler; use App\Utils\Traits\MakesHash; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Route; @@ -27,6 +29,21 @@ class RouteServiceProvider extends ServiceProvider public function boot() { parent::boot(); + + + Route::bind('task_scheduler', function ($value) { + + if (is_numeric($value)) { + throw new ModelNotFoundException("Record with value {$value} not found"); + } + + return Scheduler::query() + ->withTrashed() + ->where('id', $this->decodePrimaryKey($value))->firstOrFail(); + + }); + + } /** diff --git a/app/Repositories/SchedulerRepository.php b/app/Repositories/SchedulerRepository.php new file mode 100644 index 0000000000..5c9b9ad4e1 --- /dev/null +++ b/app/Repositories/SchedulerRepository.php @@ -0,0 +1,38 @@ +fill($data); + + $scheduler->save(); + + return $scheduler; + + } + +} diff --git a/app/Repositories/TaskSchedulerRepository.php b/app/Repositories/TaskSchedulerRepository.php deleted file mode 100644 index 8eddc76377..0000000000 --- a/app/Repositories/TaskSchedulerRepository.php +++ /dev/null @@ -1,16 +0,0 @@ -invoice = $invoice; - } + public function __construct(public Invoice $invoice){} /** * Marks as invoice as paid diff --git a/app/Services/Schedule/ScheduleService.php b/app/Services/Scheduler/SchedulerService.php similarity index 92% rename from app/Services/Schedule/ScheduleService.php rename to app/Services/Scheduler/SchedulerService.php index b9638095b4..ff3fe469aa 100644 --- a/app/Services/Schedule/ScheduleService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -9,9 +9,11 @@ * @license https://www.elastic.co/licensing/elastic-license */ -namespace App\Services\Schedule; +namespace App\Services\Scheduler; -class ScheduleService +use App\Models\Scheduler; + +class SchedulerServicer { public function __construct(public Scheduler $scheduler) {} diff --git a/app/Services/TaskScheduler/TaskSchedulerService.php b/app/Services/TaskScheduler/TaskSchedulerService.php index c0320a8b1c..2b4302eaf6 100644 --- a/app/Services/TaskScheduler/TaskSchedulerService.php +++ b/app/Services/TaskScheduler/TaskSchedulerService.php @@ -38,6 +38,8 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Validator; use Symfony\Component\HttpFoundation\Request; + +//@deprecated - never used.... class TaskSchedulerService { diff --git a/app/Transformers/TaskSchedulerTransformer.php b/app/Transformers/SchedulerTransformer.php similarity index 66% rename from app/Transformers/TaskSchedulerTransformer.php rename to app/Transformers/SchedulerTransformer.php index 88fbf9d299..584ea4cbe9 100644 --- a/app/Transformers/TaskSchedulerTransformer.php +++ b/app/Transformers/SchedulerTransformer.php @@ -14,7 +14,7 @@ namespace App\Transformers; use App\Models\Scheduler; use App\Utils\Traits\MakesHash; -class TaskSchedulerTransformer extends EntityTransformer +class SchedulerTransformer extends EntityTransformer { use MakesHash; @@ -22,17 +22,17 @@ class TaskSchedulerTransformer extends EntityTransformer { return [ 'id' => $this->encodePrimaryKey($scheduler->id), + 'name' => (string) $scheduler->name, + 'frequency_id' => (string) $scheduler->frequency_id, + 'next_run' => $scheduler->next_run, + 'template' => (string) $scheduler->template, + 'is_paused' => (bool) $scheduler->is_paused, + 'is_deleted' => (bool) $scheduler->is_deleted, + 'parameters'=> (array) $scheduler->parameters, 'is_deleted' => (bool) $scheduler->is_deleted, - 'paused' => (bool) $scheduler->paused, - 'repeat_every' => (string) $scheduler->repeat_every, - 'start_from' => (int) $scheduler->start_from, - 'scheduled_run' => (int) $scheduler->scheduled_run, 'updated_at' => (int) $scheduler->updated_at, 'created_at' => (int) $scheduler->created_at, 'archived_at' => (int) $scheduler->deleted_at, - 'action_name' => (string) $scheduler->action_name, - 'action_class' => (string) $scheduler->action_class, - 'parameters'=> (array) $scheduler->parameters, ]; } } diff --git a/database/factories/SchedulerFactory.php b/database/factories/SchedulerFactory.php new file mode 100644 index 0000000000..92c86d8ad8 --- /dev/null +++ b/database/factories/SchedulerFactory.php @@ -0,0 +1,37 @@ + $this->faker->name(), + 'is_paused' => rand(0,1), + 'is_deleted' => rand(0,1), + 'parameters' => [], + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => now()->addSeconds(rand(86400,8640000)), + 'template' => 'statement_task', + ]; + } +} diff --git a/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php b/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php index 94339df56d..fade7d7ad3 100644 --- a/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php +++ b/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php @@ -24,6 +24,38 @@ return new class extends Migration $table->boolean('invoice_task_hours')->default(false); }); + Schema::table('schedulers', function (Blueprint $table) + { + + $table->dropColumn('repeat_every'); + $table->dropColumn('start_from'); + $table->dropColumn('scheduled_run'); + $table->dropColumn('action_name'); + $table->dropColumn('action_class'); + $table->dropColumn('paused'); + $table->dropColumn('company_id'); + + }); + + + Schema::table('schedulers', function (Blueprint $table) + { + + $table->unsignedInteger('company_id'); + $table->boolean('is_paused')->default(false); + $table->unsignedInteger('frequency_id')->nullable(); + $table->datetime('next_run')->nullable(); + $table->datetime('next_run_client')->nullable(); + $table->unsignedInteger('user_id'); + $table->string('name', 191); + $table->string('template', 191); + + $table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade'); + + $table->unique(['company_id', 'name']); + $table->index(['company_id', 'deleted_at']); + + }); } diff --git a/routes/api.php b/routes/api.php index 62d2cb6b95..0969994a5f 100644 --- a/routes/api.php +++ b/routes/api.php @@ -275,7 +275,8 @@ Route::group(['middleware' => ['throttle:300,1', 'api_db', 'token_auth', 'locale Route::post('reports/tasks', TaskReportController::class); Route::post('reports/profitloss', ProfitAndLossController::class); - Route::resource('task_scheduler', TaskSchedulerController::class)->except('edit')->parameters(['task_scheduler' => 'scheduler']); + Route::resource('task_schedulers', TaskSchedulerController::class); + Route::post('task_schedulers/bulk', [TaskSchedulerController::class, 'bulk'])->name('task_schedulers.bulk'); Route::get('scheduler', [SchedulerController::class, 'index']); Route::post('support/messages/send', SendingController::class); diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 11561e0f5a..42bfba81eb 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -1,4 +1,13 @@ withoutExceptionHandling(); + $this->withoutExceptionHandling(); } - public function testSchedulerCantBeCreatedWithWrongData() + public function testDeleteSchedule() { + $data = [ - 'repeat_every' => Scheduler::DAILY, - 'job' => Scheduler::CREATE_CLIENT_REPORT, - 'date_key' => '123', - 'report_keys' => ['test'], - 'date_range' => 'all', - // 'start_from' => '2022-01-01' - ]; - - $response = false; - - $response = $this->withHeaders([ - 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, - ])->post('/api/v1/task_scheduler/', $data); - - $response->assertSessionHasErrors(); - } - - public function testSchedulerCanBeUpdated() - { - $response = $this->createScheduler(); - - $arr = $response->json(); - $id = $arr['data']['id']; - - $scheduler = Scheduler::find($this->decodePrimaryKey($id)); - - $updateData = [ - 'start_from' => 1655934741, - ]; - $response = $this->withHeaders([ - 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, - ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData); - - $responseData = $response->json(); - $this->assertEquals($updateData['start_from'], $responseData['data']['start_from']); - } - - public function testSchedulerCanBeSeen() - { - $response = $this->createScheduler(); - - $arr = $response->json(); - $id = $arr['data']['id']; - - $scheduler = Scheduler::find($this->decodePrimaryKey($id)); - - $response = $this->withHeaders([ - 'X-API-SECRET' => config('ninja.api_secret'), - 'X-API-TOKEN' => $this->token, - ])->get('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id)); - - $arr = $response->json(); - $this->assertEquals('create_client_report', $arr['data']['action_name']); - } - - public function testSchedulerJobCanBeUpdated() - { - $response = $this->createScheduler(); - - $arr = $response->json(); - $id = $arr['data']['id']; - - $scheduler = Scheduler::find($this->decodePrimaryKey($id)); - - $this->assertSame('create_client_report', $scheduler->action_name); - - $updateData = [ - 'job' => Scheduler::CREATE_CREDIT_REPORT, - 'date_range' => 'all', - 'report_keys' => ['test1'], + 'ids' => [$this->scheduler->hashed_id], ]; $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData); + ])->postJson('/api/v1/task_schedulers/bulk?action=delete', $data) + ->assertStatus(200); - $updatedSchedulerJob = Scheduler::first()->action_name; - $arr = $response->json(); - $this->assertSame('create_credit_report', $arr['data']['action_name']); - } - - public function createScheduler() - { $data = [ - 'repeat_every' => Scheduler::DAILY, - 'job' => Scheduler::CREATE_CLIENT_REPORT, - 'date_key' => '123', - 'report_keys' => ['test'], - 'date_range' => 'all', - 'start_from' => '2022-01-01', + 'ids' => [$this->scheduler->hashed_id], ]; - return $response = $this->withHeaders([ + $response = $this->withHeaders([ 'X-API-SECRET' => config('ninja.api_secret'), 'X-API-TOKEN' => $this->token, - ])->post('/api/v1/task_scheduler/', $data); + ])->postJson('/api/v1/task_schedulers/bulk?action=restore', $data) + ->assertStatus(200); + + } + + public function testRestoreSchedule() + { + + $data = [ + 'ids' => [$this->scheduler->hashed_id], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers/bulk?action=archive', $data) + ->assertStatus(200); + + + $data = [ + 'ids' => [$this->scheduler->hashed_id], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers/bulk?action=restore', $data) + ->assertStatus(200); + + } + + public function testArchiveSchedule() + { + + $data = [ + 'ids' => [$this->scheduler->hashed_id], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers/bulk?action=archive', $data) + ->assertStatus(200); + } + + public function testSchedulerPost() + { + + $data = [ + 'name' => 'A different Name', + 'frequency_id' => 5, + 'next_run' => now()->addDays(2)->format('Y-m-d'), + 'template' =>'statement', + 'parameters' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + $response->assertStatus(200); + } + + public function testSchedulerPut() + { + + $data = [ + 'name' => 'A different Name', + 'frequency_id' => 5, + 'next_run' => now()->addDays(2)->format('Y-m-d'), + 'template' =>'statement', + 'parameters' => [], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->putJson('/api/v1/task_schedulers/'.$this->scheduler->hashed_id, $data); + + $response->assertStatus(200); + } + + public function testSchedulerGet() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/task_schedulers'); + + $response->assertStatus(200); + } + + public function testSchedulerCreate() + { + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->get('/api/v1/task_schedulers/create'); + + $response->assertStatus(200); + } + + + // public function testSchedulerPut() + // { + // $data = [ + // 'description' => $this->faker->firstName(), + // ]; + + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->put('/api/v1/task_schedulers/'.$this->encodePrimaryKey($this->task->id), $data); + + // $response->assertStatus(200); + // } + + + + // public function testSchedulerCantBeCreatedWithWrongData() + // { + // $data = [ + // 'repeat_every' => Scheduler::DAILY, + // 'job' => Scheduler::CREATE_CLIENT_REPORT, + // 'date_key' => '123', + // 'report_keys' => ['test'], + // 'date_range' => 'all', + // // 'start_from' => '2022-01-01' + // ]; + + // $response = false; + + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->post('/api/v1/task_scheduler/', $data); + + // $response->assertSessionHasErrors(); + // } + + // public function testSchedulerCanBeUpdated() + // { + // $response = $this->createScheduler(); + + // $arr = $response->json(); + // $id = $arr['data']['id']; + + // $scheduler = Scheduler::find($this->decodePrimaryKey($id)); + + // $updateData = [ + // 'start_from' => 1655934741, + // ]; + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData); + + // $responseData = $response->json(); + // $this->assertEquals($updateData['start_from'], $responseData['data']['start_from']); + // } + + // public function testSchedulerCanBeSeen() + // { + // $response = $this->createScheduler(); + + // $arr = $response->json(); + // $id = $arr['data']['id']; + + // $scheduler = Scheduler::find($this->decodePrimaryKey($id)); + + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->get('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id)); + + // $arr = $response->json(); + // $this->assertEquals('create_client_report', $arr['data']['action_name']); + // } + + // public function testSchedulerJobCanBeUpdated() + // { + // $response = $this->createScheduler(); + + // $arr = $response->json(); + // $id = $arr['data']['id']; + + // $scheduler = Scheduler::find($this->decodePrimaryKey($id)); + + // $this->assertSame('create_client_report', $scheduler->action_name); + + // $updateData = [ + // 'job' => Scheduler::CREATE_CREDIT_REPORT, + // 'date_range' => 'all', + // 'report_keys' => ['test1'], + // ]; + + // $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->put('/api/v1/task_scheduler/'.$this->encodePrimaryKey($scheduler->id), $updateData); + + // $updatedSchedulerJob = Scheduler::first()->action_name; + // $arr = $response->json(); + + // $this->assertSame('create_credit_report', $arr['data']['action_name']); + // } + + // public function createScheduler() + // { + // $data = [ + // 'repeat_every' => Scheduler::DAILY, + // 'job' => Scheduler::CREATE_CLIENT_REPORT, + // 'date_key' => '123', + // 'report_keys' => ['test'], + // 'date_range' => 'all', + // 'start_from' => '2022-01-01', + // ]; + + // return $response = $this->withHeaders([ + // 'X-API-SECRET' => config('ninja.api_secret'), + // 'X-API-TOKEN' => $this->token, + // ])->post('/api/v1/task_scheduler/', $data); + // } } diff --git a/tests/MockAccountData.php b/tests/MockAccountData.php index f4cb34216d..09752d0873 100644 --- a/tests/MockAccountData.php +++ b/tests/MockAccountData.php @@ -48,6 +48,7 @@ use App\Models\QuoteInvitation; use App\Models\RecurringExpense; use App\Models\RecurringInvoice; use App\Models\RecurringQuote; +use App\Models\Scheduler; use App\Models\Task; use App\Models\TaskStatus; use App\Models\TaxRate; @@ -177,6 +178,11 @@ trait MockAccountData */ public $tax_rate; + /** + * @var + */ + public $scheduler; + public function makeTestData() { config(['database.default' => config('ninja.db.default')]); @@ -804,6 +810,14 @@ trait MockAccountData $this->client = $this->client->fresh(); $this->invoice = $this->invoice->fresh(); + + $this->scheduler = Scheduler::factory()->create([ + 'user_id' => $user_id, + 'company_id' => $this->company->id, + ]); + + $this->scheduler->save(); + } /**