From 5389c00c2fe0d4cec3228f8ad73bcb2fdcc85193 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 00:32:54 +1100 Subject: [PATCH 01/17] migrations for companies/accounts table --- app/DataMapper/CompanySettings.php | 6 +++ .../Controllers/PurchaseOrderController.php | 1 - app/Models/Company.php | 1 + app/Transformers/CompanyTransformer.php | 1 + ...t_auto_bill_on_regular_invoice_setting.php | 39 +++++++++++++++++++ .../views/email/template/client.blade.php | 12 +++--- 6 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php diff --git a/app/DataMapper/CompanySettings.php b/app/DataMapper/CompanySettings.php index 04abd19bc01a..6e65ccd3a8de 100644 --- a/app/DataMapper/CompanySettings.php +++ b/app/DataMapper/CompanySettings.php @@ -447,7 +447,13 @@ class CompanySettings extends BaseSettings public $mailgun_domain = ''; + public $auto_bill_standard_invoices = false; + + public $email_alignment = 'center'; // center , left, right + public static $casts = [ + 'email_alignment' => 'string', + 'auto_bill_standard_invoices' => 'bool', 'postmark_secret' => 'string', 'mailgun_secret' => 'string', 'mailgun_domain' => 'string', diff --git a/app/Http/Controllers/PurchaseOrderController.php b/app/Http/Controllers/PurchaseOrderController.php index 452c5895374d..821f0c88847a 100644 --- a/app/Http/Controllers/PurchaseOrderController.php +++ b/app/Http/Controllers/PurchaseOrderController.php @@ -502,7 +502,6 @@ class PurchaseOrderController extends BaseController /* * Download Purchase Order/s */ - if ($action == 'bulk_download' && $purchase_orders->count() >= 1) { $purchase_orders->each(function ($purchase_order) { if (auth()->user()->cannot('view', $purchase_order)) { diff --git a/app/Models/Company.php b/app/Models/Company.php index 7864220ca00d..0d9d6793890b 100644 --- a/app/Models/Company.php +++ b/app/Models/Company.php @@ -66,6 +66,7 @@ class Company extends BaseModel protected $presenter = CompanyPresenter::class; protected $fillable = [ + 'invoice_task_hours', 'markdown_enabled', 'calculate_expense_tax_by_amount', 'invoice_expense_documents', diff --git a/app/Transformers/CompanyTransformer.php b/app/Transformers/CompanyTransformer.php index 2f20c5300617..77bcae0fa469 100644 --- a/app/Transformers/CompanyTransformer.php +++ b/app/Transformers/CompanyTransformer.php @@ -194,6 +194,7 @@ class CompanyTransformer extends EntityTransformer 'convert_payment_currency' => (bool) $company->convert_payment_currency, 'convert_expense_currency' => (bool) $company->convert_expense_currency, 'notify_vendor_when_paid' => (bool) $company->notify_vendor_when_paid, + 'invoice_task_hours' => (bool) $company->invoice_task_hours, ]; } 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 new file mode 100644 index 000000000000..94339df56d24 --- /dev/null +++ b/database/migrations/2023_01_12_125540_set_auto_bill_on_regular_invoice_setting.php @@ -0,0 +1,39 @@ +boolean('is_trial')->default(false); + }); + + Schema::table('companies', function (Blueprint $table) + { + $table->boolean('invoice_task_hours')->default(false); + }); + + + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + // + } +}; diff --git a/resources/views/email/template/client.blade.php b/resources/views/email/template/client.blade.php index 93e772abe888..5fcf2b9bdcd1 100644 --- a/resources/views/email/template/client.blade.php +++ b/resources/views/email/template/client.blade.php @@ -1,5 +1,6 @@ @php $primary_color = isset($settings) ? $settings->primary_color : '#4caf50'; + $email_alignment = isset($settings) ? $settings->email_alignment : 'center'; @endphp @@ -60,7 +61,8 @@ font-size: 13px; padding: 15px 50px; font-weight: 600; - margin-bottom: 30px; + margin-bottom: 5px; + margin-top: 10px; } #content h1 { font-family: 'canada-type-gibson', 'roboto', Arial, Helvetica, sans-serif; @@ -146,8 +148,8 @@ - -
+ +
{{ $slot ?? '' }} @@ -163,8 +165,8 @@ - -
+ +
From 1974f0e5f3660c66ff53b80993f71b073ee1350a Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 00:36:25 +1100 Subject: [PATCH 02/17] Set invoice autobill based on configuration --- app/Services/Invoice/InvoiceService.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Services/Invoice/InvoiceService.php b/app/Services/Invoice/InvoiceService.php index bdc770c7826b..17a878106e6f 100644 --- a/app/Services/Invoice/InvoiceService.php +++ b/app/Services/Invoice/InvoiceService.php @@ -531,6 +531,10 @@ class InvoiceService $this->invoice->exchange_rate = $this->invoice->client->currency()->exchange_rate; } + if ($settings->auto_bill_standard_invoices) { + $this->invoice->auto_bill_enabled = true; + } + if ($settings->counter_number_applied == 'when_saved') { $this->invoice->service()->applyNumber()->save(); } From d1078e1ba16c99d5a649a0e9b4ed2a9485cc6690 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 00:41:54 +1100 Subject: [PATCH 03/17] Change sequence for settings auto bill for recurring invoices --- app/Jobs/RecurringInvoice/SendRecurring.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/Jobs/RecurringInvoice/SendRecurring.php b/app/Jobs/RecurringInvoice/SendRecurring.php index 63f6ecc2d645..81cf7b2f7a00 100644 --- a/app/Jobs/RecurringInvoice/SendRecurring.php +++ b/app/Jobs/RecurringInvoice/SendRecurring.php @@ -66,13 +66,6 @@ class SendRecurring implements ShouldQueue // Generate Standard Invoice $invoice = RecurringInvoiceToInvoiceFactory::create($this->recurring_invoice, $this->recurring_invoice->client); - if ($this->recurring_invoice->auto_bill === 'always') { - $invoice->auto_bill_enabled = true; - } elseif ($this->recurring_invoice->auto_bill === 'optout' || $this->recurring_invoice->auto_bill === 'optin') { - } elseif ($this->recurring_invoice->auto_bill === 'off') { - $invoice->auto_bill_enabled = false; - } - $invoice->date = date('Y-m-d'); nlog("Recurring Invoice Date Set on Invoice = {$invoice->date} - ". now()->format('Y-m-d')); @@ -94,6 +87,14 @@ class SendRecurring implements ShouldQueue ->save(); } + //12-01-2023 i moved this block after fillDefaults to handle if standard invoice auto bill config has been enabled, recurring invoice should override. + if ($this->recurring_invoice->auto_bill === 'always') { + $invoice->auto_bill_enabled = true; + } elseif ($this->recurring_invoice->auto_bill === 'optout' || $this->recurring_invoice->auto_bill === 'optin') { + } elseif ($this->recurring_invoice->auto_bill === 'off') { + $invoice->auto_bill_enabled = false; + } + $invoice = $this->createRecurringInvitations($invoice); /* 09-01-2022 ensure we create the PDFs at this point in time! */ From 9e415b420c56b548c4088c8a5a58049fd658eb06 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 12:43:38 +1100 Subject: [PATCH 04/17] 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 000000000000..c6603e148d4f --- /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 e1788ce4e85c..dcd7b35d1b52 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 80b4fc76bfd0..000000000000 --- 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 000000000000..71c0a8f3d9f2 --- /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 000000000000..93e15f06df95 --- /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 000000000000..a459edab3c99 --- /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 000000000000..0c63f95a9542 --- /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 0db34ccc000f..000000000000 --- 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 69b819e84575..7e3ec3267152 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 76ebad1883d1..ce660c756f5d 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 22fc9ba12915..fdd51dff88d2 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 287129638fb1..09732f875ae9 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 000000000000..b5eaba785f60 --- /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 eef723dcaab3..353e7d6caa3f 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 2684f395df89..0a993436e417 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 000000000000..5c9b9ad4e19c --- /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 8eddc7637775..000000000000 --- 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 b9638095b430..ff3fe469aafc 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 c0320a8b1c89..2b4302eaf62b 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 88fbf9d29999..584ea4cbe9f9 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 000000000000..92c86d8ad8ce --- /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 94339df56d24..fade7d7ad3f4 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 62d2cb6b9586..0969994a5f04 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 11561e0f5a49..42bfba81ebd3 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 f4cb34216d9f..09752d0873bc 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(); + } /** From c5ac9cacaf5c31434b0e05fef91b492784eda9da Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 20:02:32 +1100 Subject: [PATCH 05/17] Schduler tests --- tests/Feature/Scheduler/SchedulerTest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 42bfba81ebd3..e5b40a153a0b 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -12,6 +12,7 @@ namespace Tests\Feature\Scheduler; use App\Export\CSV\ClientExport; +use App\Models\RecurringInvoice; use App\Models\Scheduler; use App\Utils\Traits\MakesHash; use Carbon\Carbon; @@ -51,6 +52,27 @@ class SchedulerTest extends TestCase $this->withoutExceptionHandling(); } + /** + * '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', + */ + + public function testClientStatementGeneration() + { + $data = [ + 'name' => 'A test statement scheduler', + 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, + 'next_run' => '2023-01-31', + 'template' => 'client_statement', + 'clients' => [], + + ]; + } + public function testDeleteSchedule() { From 9e5417ab1c11b9ab25fb8f816b1a247734476976 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 20:16:17 +1100 Subject: [PATCH 06/17] Define the Client Statement Class --- app/DataMapper/Schedule/ClientStatement.php | 96 +++++++++++++++++++ .../TaskScheduler/StoreSchedulerRequest.php | 7 +- app/Models/Scheduler.php | 12 +-- tests/Feature/Scheduler/SchedulerTest.php | 15 ++- 4 files changed, 121 insertions(+), 9 deletions(-) create mode 100644 app/DataMapper/Schedule/ClientStatement.php diff --git a/app/DataMapper/Schedule/ClientStatement.php b/app/DataMapper/Schedule/ClientStatement.php new file mode 100644 index 000000000000..e5f51a0051e4 --- /dev/null +++ b/app/DataMapper/Schedule/ClientStatement.php @@ -0,0 +1,96 @@ +all()); $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', @@ -38,6 +37,10 @@ class StoreSchedulerRequest extends Request 'parameters' => 'bail|array', ]; + $rules['name'] = ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)]; + +nlog($rules); + return $rules; } diff --git a/app/Models/Scheduler.php b/app/Models/Scheduler.php index 09732f875ae9..93d50a42657e 100644 --- a/app/Models/Scheduler.php +++ b/app/Models/Scheduler.php @@ -36,12 +36,12 @@ class Scheduler extends BaseModel use SoftDeletes; protected $fillable = [ - 'start_from', - 'is_paused', - 'repeat_every', + 'name', + 'frequency_id', + 'next_run', 'scheduled_run', - 'action_class', - 'action_name', + 'template', + 'is_paused', 'parameters', ]; @@ -50,7 +50,7 @@ class Scheduler extends BaseModel 'created_at' => 'timestamp', 'updated_at' => 'timestamp', 'deleted_at' => 'timestamp', - 'paused' => 'boolean', + 'is_paused' => 'boolean', 'is_deleted' => 'boolean', 'parameters' => 'array', ]; diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index e5b40a153a0b..13c2c84c0f29 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -69,8 +69,21 @@ class SchedulerTest extends TestCase 'next_run' => '2023-01-31', 'template' => 'client_statement', 'clients' => [], - + 'parameters' => [ + 'date_range' => 'last_month', + 'show_payments_table' => true, + 'show_aging_table' => true, + 'status' => 'paid' + ], ]; + + $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 testDeleteSchedule() From 35fde4a73e7a9b780df2f1456ca3889ffab4e5c7 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 20:23:03 +1100 Subject: [PATCH 07/17] Clean up for scheduler --- app/Console/Kernel.php | 4 +- .../TaskScheduler/StoreSchedulerRequest.php | 7 +- app/Jobs/Ninja/TaskScheduler.php | 99 ++++++++++--------- 3 files changed, 54 insertions(+), 56 deletions(-) diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1183ec91b99a..7587da2e5062 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -71,7 +71,7 @@ class Kernel extends ConsoleKernel $schedule->job(new RecurringInvoicesCron)->hourly()->withoutOverlapping()->name('recurring-invoice-job')->onOneServer(); /* Stale Invoice Cleanup*/ - $schedule->job(new CleanStaleInvoiceOrder)->hourly()->withoutOverlapping()->name('stale-invoice-job')->onOneServer(); + $schedule->job(new CleanStaleInvoiceOrder)->hourlyAt(30)->withoutOverlapping()->name('stale-invoice-job')->onOneServer(); /* Sends recurring invoices*/ $schedule->job(new RecurringExpensesCron)->dailyAt('00:10')->withoutOverlapping()->name('recurring-expense-job')->onOneServer(); @@ -89,7 +89,7 @@ class Kernel extends ConsoleKernel $schedule->job(new SchedulerCheck)->dailyAt('01:10')->withoutOverlapping(); /* Checks for scheduled tasks */ - $schedule->job(new TaskScheduler())->dailyAt('06:50')->withoutOverlapping()->name('task-scheduler-job')->onOneServer(); + $schedule->job(new TaskScheduler())->hourlyAt(10)->withoutOverlapping()->name('task-scheduler-job')->onOneServer(); /* Performs system maintenance such as pruning the backup table */ $schedule->job(new SystemMaintenance)->sundays()->at('02:30')->withoutOverlapping()->name('system-maintenance-job')->onOneServer(); diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index 5e9711340386..0c63f95a9542 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -28,8 +28,9 @@ class StoreSchedulerRequest extends Request public function rules() { -nlog($this->all()); + $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', @@ -37,10 +38,6 @@ nlog($this->all()); 'parameters' => 'bail|array', ]; - $rules['name'] = ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)]; - -nlog($rules); - return $rules; } diff --git a/app/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php index ce660c756f5d..402e29079392 100644 --- a/app/Jobs/Ninja/TaskScheduler.php +++ b/app/Jobs/Ninja/TaskScheduler.php @@ -46,9 +46,10 @@ class TaskScheduler implements ShouldQueue MultiDB::setDB($db); Scheduler::with('company') - ->where('paused', false) + ->where('is_paused', false) ->where('is_deleted', false) - ->where('scheduled_run', '<', now()) + ->whereNotNull('next_run') + ->where('next_run', '<=', now()) ->cursor() ->each(function ($scheduler) { $this->doJob($scheduler); @@ -62,55 +63,55 @@ class TaskScheduler implements ShouldQueue $company = $scheduler->company; - $parameters = $scheduler->parameters; + // $parameters = $scheduler->parameters; - switch ($scheduler->action_name) { - case Scheduler::CREATE_CLIENT_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'contacts.csv'); - break; - case Scheduler::CREATE_CLIENT_CONTACT_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'clients.csv'); - break; - case Scheduler::CREATE_CREDIT_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'credits.csv'); - break; - case Scheduler::CREATE_DOCUMENT_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'documents.csv'); - break; - case Scheduler::CREATE_EXPENSE_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'expense.csv'); - break; - case Scheduler::CREATE_INVOICE_ITEM_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoice_items.csv'); - break; - case Scheduler::CREATE_INVOICE_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoices.csv'); - break; - case Scheduler::CREATE_PAYMENT_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'payments.csv'); - break; - case Scheduler::CREATE_PRODUCT_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'products.csv'); - break; - case Scheduler::CREATE_PROFIT_AND_LOSS_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'profit_and_loss.csv'); - break; - case Scheduler::CREATE_QUOTE_ITEM_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quote_items.csv'); - break; - case Scheduler::CREATE_QUOTE_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quotes.csv'); - break; - case Scheduler::CREATE_RECURRING_INVOICE_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'recurring_invoices.csv'); - break; - case Scheduler::CREATE_TASK_REPORT: - SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'tasks.csv'); - break; + // switch ($scheduler->action_name) { + // case Scheduler::CREATE_CLIENT_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'contacts.csv'); + // break; + // case Scheduler::CREATE_CLIENT_CONTACT_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'clients.csv'); + // break; + // case Scheduler::CREATE_CREDIT_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'credits.csv'); + // break; + // case Scheduler::CREATE_DOCUMENT_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'documents.csv'); + // break; + // case Scheduler::CREATE_EXPENSE_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'expense.csv'); + // break; + // case Scheduler::CREATE_INVOICE_ITEM_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoice_items.csv'); + // break; + // case Scheduler::CREATE_INVOICE_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoices.csv'); + // break; + // case Scheduler::CREATE_PAYMENT_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'payments.csv'); + // break; + // case Scheduler::CREATE_PRODUCT_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'products.csv'); + // break; + // case Scheduler::CREATE_PROFIT_AND_LOSS_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'profit_and_loss.csv'); + // break; + // case Scheduler::CREATE_QUOTE_ITEM_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quote_items.csv'); + // break; + // case Scheduler::CREATE_QUOTE_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quotes.csv'); + // break; + // case Scheduler::CREATE_RECURRING_INVOICE_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'recurring_invoices.csv'); + // break; + // case Scheduler::CREATE_TASK_REPORT: + // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'tasks.csv'); + // break; - } + // } - $scheduler->scheduled_run = $scheduler->nextScheduledDate(); - $scheduler->save(); + // $scheduler->scheduled_run = $scheduler->nextScheduledDate(); + // $scheduler->save(); } } From b0f8e10430d0ec62ad21f2f70df9696f0805f151 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Fri, 13 Jan 2023 22:24:23 +1100 Subject: [PATCH 08/17] Clean up for the scheduler --- app/Jobs/Ninja/TaskScheduler.php | 56 ++------------------- app/Models/Scheduler.php | 41 --------------- app/Services/Scheduler/SchedulerService.php | 2 +- 3 files changed, 4 insertions(+), 95 deletions(-) diff --git a/app/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php index 402e29079392..891cc35e65f4 100644 --- a/app/Jobs/Ninja/TaskScheduler.php +++ b/app/Jobs/Ninja/TaskScheduler.php @@ -59,59 +59,9 @@ class TaskScheduler implements ShouldQueue private function doJob(Scheduler $scheduler) { - nlog("Doing job {$scheduler->action_name}"); + nlog("Doing job {$scheduler->name}"); + // + // - $company = $scheduler->company; - - // $parameters = $scheduler->parameters; - - // switch ($scheduler->action_name) { - // case Scheduler::CREATE_CLIENT_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'contacts.csv'); - // break; - // case Scheduler::CREATE_CLIENT_CONTACT_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'clients.csv'); - // break; - // case Scheduler::CREATE_CREDIT_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'credits.csv'); - // break; - // case Scheduler::CREATE_DOCUMENT_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'documents.csv'); - // break; - // case Scheduler::CREATE_EXPENSE_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'expense.csv'); - // break; - // case Scheduler::CREATE_INVOICE_ITEM_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoice_items.csv'); - // break; - // case Scheduler::CREATE_INVOICE_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'invoices.csv'); - // break; - // case Scheduler::CREATE_PAYMENT_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'payments.csv'); - // break; - // case Scheduler::CREATE_PRODUCT_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'products.csv'); - // break; - // case Scheduler::CREATE_PROFIT_AND_LOSS_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'profit_and_loss.csv'); - // break; - // case Scheduler::CREATE_QUOTE_ITEM_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quote_items.csv'); - // break; - // case Scheduler::CREATE_QUOTE_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'quotes.csv'); - // break; - // case Scheduler::CREATE_RECURRING_INVOICE_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'recurring_invoices.csv'); - // break; - // case Scheduler::CREATE_TASK_REPORT: - // SendToAdmin::dispatch($company, $parameters, $scheduler->action_class, 'tasks.csv'); - // break; - - // } - - // $scheduler->scheduled_run = $scheduler->nextScheduledDate(); - // $scheduler->save(); } } diff --git a/app/Models/Scheduler.php b/app/Models/Scheduler.php index 93d50a42657e..50c693cee6af 100644 --- a/app/Models/Scheduler.php +++ b/app/Models/Scheduler.php @@ -12,7 +12,6 @@ namespace App\Models; use App\Services\Scheduler\SchedulerService; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Carbon; @@ -58,46 +57,6 @@ class Scheduler extends BaseModel protected $appends = [ 'hashed_id', ]; - - const DAILY = 'DAY'; - - const WEEKLY = 'WEEK'; - - const BIWEEKLY = 'BIWEEKLY'; - - const MONTHLY = 'MONTH'; - - const QUARTERLY = '3MONTHS'; - - const ANNUALLY = 'YEAR'; - - const CREATE_CLIENT_REPORT = 'create_client_report'; - - const CREATE_CLIENT_CONTACT_REPORT = 'create_client_contact_report'; - - const CREATE_CREDIT_REPORT = 'create_credit_report'; - - const CREATE_DOCUMENT_REPORT = 'create_document_report'; - - const CREATE_EXPENSE_REPORT = 'create_expense_report'; - - const CREATE_INVOICE_ITEM_REPORT = 'create_invoice_item_report'; - - const CREATE_INVOICE_REPORT = 'create_invoice_report'; - - const CREATE_PAYMENT_REPORT = 'create_payment_report'; - - const CREATE_PRODUCT_REPORT = 'create_product_report'; - - const CREATE_PROFIT_AND_LOSS_REPORT = 'create_profit_and_loss_report'; - - const CREATE_QUOTE_ITEM_REPORT = 'create_quote_item_report'; - - const CREATE_QUOTE_REPORT = 'create_quote_report'; - - const CREATE_RECURRING_INVOICE_REPORT = 'create_recurring_invoice_report'; - - const CREATE_TASK_REPORT = 'create_task_report'; /** * Service entry points. diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index ff3fe469aafc..b3935ba68882 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -13,7 +13,7 @@ namespace App\Services\Scheduler; use App\Models\Scheduler; -class SchedulerServicer +class SchedulerService { public function __construct(public Scheduler $scheduler) {} From ada6210e3432b26956fd77188808e40b47fb4d47 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 Jan 2023 09:46:17 +1100 Subject: [PATCH 09/17] Working on client statement schedules --- app/Jobs/Ninja/TaskScheduler.php | 13 ++- app/Services/Scheduler/SchedulerService.php | 95 ++++++++++++++------- 2 files changed, 74 insertions(+), 34 deletions(-) diff --git a/app/Jobs/Ninja/TaskScheduler.php b/app/Jobs/Ninja/TaskScheduler.php index 891cc35e65f4..69862f8877c2 100644 --- a/app/Jobs/Ninja/TaskScheduler.php +++ b/app/Jobs/Ninja/TaskScheduler.php @@ -60,8 +60,15 @@ class TaskScheduler implements ShouldQueue private function doJob(Scheduler $scheduler) { nlog("Doing job {$scheduler->name}"); - // - // - + + try { + $scheduler->service()->runTask(); + } + catch(\Exception $e){ + nlog($e->getMessage()); + + } } + + } diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index b3935ba68882..bcaf4c95d7f8 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -11,49 +11,82 @@ namespace App\Services\Scheduler; +use App\Models\Client; use App\Models\Scheduler; +use App\Utils\Traits\MakesHash; +use Illuminate\Support\Str; class SchedulerService { + use MakesHash; + + private string $method; public function __construct(public Scheduler $scheduler) {} - public function scheduleStatement() + /** + * Called from the TaskScheduler Cron + * + * @return void + */ + public function runTask(): void { - - //Is it for one client - //Is it for all clients - //Is it for all clients excluding these clients - - //Frequency - - //show aging - //show payments - //paid/unpaid - - //When to send? 1st of month - //End of month - //This date - + $this->{$this->scheduler->template}(); } - public function scheduleReport() - { - //Report type - //same schema as ScheduleStatement + private function client_statement() + { + $query = Client::query() + ->where('company_id', $this->scheduler->company_id); + + //Email only the selected clients + if(count($this->scheduler->parameters['clients']) >= 1) + $query->where('id', $this->transformKeys($this->scheduler->parameters['clients'])); + + $query->cursor() + ->each(function ($client){ + + //work out the date range + + }); } - public function scheduleEntitySend() - { - //Entity - //Entity Id - //When - } + // public function scheduleStatement() + // { + + // //Is it for one client + // //Is it for all clients + // //Is it for all clients excluding these clients + + // //Frequency + + // //show aging + // //show payments + // //paid/unpaid + + // //When to send? 1st of month + // //End of month + // //This date + + // } - public function projectStatus() - { - //Project ID - //Tasks - task statuses - } + // public function scheduleReport() + // { + // //Report type + // //same schema as ScheduleStatement + // } + + // public function scheduleEntitySend() + // { + // //Entity + // //Entity Id + // //When + // } + + // public function projectStatus() + // { + // //Project ID + // //Tasks - task statuses + // } } \ No newline at end of file From cf9ffb05d5b65e3a0b19b45cfec85ec467a80c9c Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 Jan 2023 18:47:14 +1100 Subject: [PATCH 10/17] Tests for calculating date ranges --- app/DataMapper/Schedule/ClientStatement.php | 2 +- app/Services/Scheduler/SchedulerService.php | 69 ++++++++++----------- tests/Feature/Scheduler/SchedulerTest.php | 34 +++++++++- tests/Unit/RecurringDateTest.php | 1 + 4 files changed, 67 insertions(+), 39 deletions(-) diff --git a/app/DataMapper/Schedule/ClientStatement.php b/app/DataMapper/Schedule/ClientStatement.php index e5f51a0051e4..37ba76d081cb 100644 --- a/app/DataMapper/Schedule/ClientStatement.php +++ b/app/DataMapper/Schedule/ClientStatement.php @@ -59,7 +59,6 @@ class ClientStatement */ public string $start_date = ''; - /** * If a custom range is select for the date range then * the end_date should be supplied in Y-m-d format @@ -93,4 +92,5 @@ class ClientStatement */ public string $status = 'paid'; // paid | unpaid + } \ No newline at end of file diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index bcaf4c95d7f8..684fe282c442 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -42,51 +42,48 @@ class SchedulerService //Email only the selected clients if(count($this->scheduler->parameters['clients']) >= 1) $query->where('id', $this->transformKeys($this->scheduler->parameters['clients'])); - + + $statement_properties = $this->calculateStatementProperties(); + $query->cursor() - ->each(function ($client){ + ->each(function ($client) use($statement_properties){ //work out the date range + $pdf = $client->service()->statement($statement_properties); }); + } - // public function scheduleStatement() - // { - - // //Is it for one client - // //Is it for all clients - // //Is it for all clients excluding these clients - - // //Frequency - - // //show aging - // //show payments - // //paid/unpaid - - // //When to send? 1st of month - // //End of month - // //This date - - // } + private function calculateStatementProperties() + { + $start_end = $this->calculateStartAndEndDates(); - // public function scheduleReport() - // { - // //Report type - // //same schema as ScheduleStatement - // } + return [ + 'start_date' =>$start_end[0], + 'end_date' =>$start_end[1], + 'show_payments_table' => $this->scheduler->parameters['show_payments_table'], + 'show_aging_table' => $this->scheduler->parameters['show_aging_table'], + 'status' => $this->scheduler->status + ]; - // public function scheduleEntitySend() - // { - // //Entity - // //Entity Id - // //When - // } + } - // public function projectStatus() - // { - // //Project ID - // //Tasks - task statuses - // } + private function calculateStartAndEndDates() + { + return match ($this->scheduler->parameters['date_range']) { + 'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')], + 'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')], + 'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->format('Y-m-d')], + 'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')], + 'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')], + 'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->format('Y-m-d')], + 'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']] + }; + } + private function thisMonth() + { + + } } \ No newline at end of file diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 13c2c84c0f29..3e67bd79ea8e 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -31,7 +31,6 @@ class SchedulerTest extends TestCase use MakesHash; use MockAccountData; use WithoutEvents; - // use RefreshDatabase; protected function setUp(): void { @@ -52,6 +51,37 @@ class SchedulerTest extends TestCase $this->withoutExceptionHandling(); } + + public function testGetThisMonthRange() + { + + $this->travelTo(Carbon::parse('2023-01-14')); + + $this->assertEqualsCanonicalizing(['2023-01-01','2023-01-31'], $this->getDateRange('this_month')); + $this->assertEqualsCanonicalizing(['2023-01-01','2023-03-31'], $this->getDateRange('this_quarter')); + $this->assertEqualsCanonicalizing(['2023-01-01','2023-12-31'], $this->getDateRange('this_year')); + + $this->assertEqualsCanonicalizing(['2022-12-01','2022-12-31'], $this->getDateRange('previous_month')); + $this->assertEqualsCanonicalizing(['2022-10-01','2022-12-31'], $this->getDateRange('previous_quarter')); + $this->assertEqualsCanonicalizing(['2022-01-01','2022-12-31'], $this->getDateRange('previous_year')); + + $this->travelBack(); + + } + + private function getDateRange($range) + { + return match ($range) { + 'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')], + 'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')], + 'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->lastOfYear()->format('Y-m-d')], + 'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')], + 'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')], + 'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->lastOfYear()->format('Y-m-d')], + 'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']] + }; + } + /** * 'name' => ['bail', 'required', Rule::unique('schedulers')->where('company_id', auth()->user()->company()->id)], 'is_paused' => 'bail|sometimes|boolean', @@ -66,7 +96,7 @@ class SchedulerTest extends TestCase $data = [ 'name' => 'A test statement scheduler', 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, - 'next_run' => '2023-01-31', + 'next_run' => '2023-01-14', 'template' => 'client_statement', 'clients' => [], 'parameters' => [ diff --git a/tests/Unit/RecurringDateTest.php b/tests/Unit/RecurringDateTest.php index 8276f9a17902..e8224badb29d 100644 --- a/tests/Unit/RecurringDateTest.php +++ b/tests/Unit/RecurringDateTest.php @@ -42,4 +42,5 @@ class RecurringDateTest extends TestCase $this->assertequals($trial_ends->format('Y-m-d'), '2021-12-03'); } + } From a79e21b3cfb612f0cf19215166d2c7b1703cad10 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sat, 14 Jan 2023 22:00:22 +1100 Subject: [PATCH 11/17] Refactor to use Envelope Mailables --- app/Mail/Client/ClientStatement.php | 106 ++++++++++++++++++ app/Services/Scheduler/SchedulerService.php | 112 ++++++++++++++++++-- lang/en/texts.php | 1 + tests/Feature/Scheduler/SchedulerTest.php | 4 +- 4 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 app/Mail/Client/ClientStatement.php diff --git a/app/Mail/Client/ClientStatement.php b/app/Mail/Client/ClientStatement.php new file mode 100644 index 000000000000..5c3c3c211263 --- /dev/null +++ b/app/Mail/Client/ClientStatement.php @@ -0,0 +1,106 @@ + [], + // 'from_email' => '', + // 'from_name' => '', + // 'reply_to' => '', + // 'cc' => [], + // 'bcc' => [], + // 'subject' => ctrans('texts.your_statement'), + // 'body' => ctrans('texts.client_statement_body', ['start_date' => $this->client_start_date, 'end_date' => $this->client_end_date]), + // 'attachments' => [ + // ['name' => ctrans('texts.statement') . ".pdf", 'file' => base64_encode($pdf)], + // ] + + /** + * Create a new message instance. + * + * @return void + */ + public function __construct(public array $data){} + + /** + * Get the message envelope. + * + * @return \Illuminate\Mail\Mailables\Envelope + */ + public function envelope() + { + return new Envelope( + subject: $this->data['subject'], + tags: [$this->data['company_key']], + replyTo: $this->data['reply_to'], + from: $this->data['from'], + to: $this->data['to'], + bcc: $this->data['bcc'] + ); + } + + /** + * Get the message content definition. + * + * @return \Illuminate\Mail\Mailables\Content + */ + public function content() + { + return new Content( + view: 'email.template.client', + text: 'email.template.text', + with: [ + 'text_body' => $this->data['body'], + 'body' => $this->data['body'], + 'whitelabel' => $this->data['whitelabel'], + 'settings' => $this->data['settings'], + 'whitelabel' => $this->data['whitelabel'], + 'logo' => $this->data['logo'], + 'signature' => $this->data['signature'], + 'company' => $this->data['company'], + 'greeting' => $this->data['greeting'], + ] + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments() + { + $array_of_attachments = []; + + foreach($this->data['attachments'] as $attachment) + { + + $array_of_attachments[] = + Attachment::fromData(fn () => base64_decode($attachment['file']), $attachment['name']) + ->withMime('application/pdf'); + + } + + return $array_of_attachments; + + } +} diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index 684fe282c442..11f0f2a7ed18 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -11,17 +11,26 @@ namespace App\Services\Scheduler; +use App\Mail\Client\ClientStatement; use App\Models\Client; use App\Models\Scheduler; +use App\Utils\Ninja; +use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; +use Illuminate\Mail\Mailables\Address; +use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Str; class SchedulerService { use MakesHash; + use MakesDates; private string $method; + private Client $client; + public function __construct(public Scheduler $scheduler) {} /** @@ -43,13 +52,21 @@ class SchedulerService if(count($this->scheduler->parameters['clients']) >= 1) $query->where('id', $this->transformKeys($this->scheduler->parameters['clients'])); - $statement_properties = $this->calculateStatementProperties(); $query->cursor() - ->each(function ($client) use($statement_properties){ + ->each(function ($_client){ + + $this->client = $_client; + $statement_properties = $this->calculateStatementProperties(); //work out the date range - $pdf = $client->service()->statement($statement_properties); + $pdf = $_client->service()->statement($statement_properties); + + $mail_able_envelope = $this->buildMailableData($pdf); + + Mail::send($mail_able_envelope); + + //calculate next run dates; }); @@ -59,12 +76,15 @@ class SchedulerService { $start_end = $this->calculateStartAndEndDates(); + $this->client_start_date = $this->translateDate($start_end[0], $this->client->date_format(), $this->client->locale()); + $this->client_end_date = $this->translateDate($start_end[1], $this->client->date_format(), $this->client->locale()); + return [ 'start_date' =>$start_end[0], 'end_date' =>$start_end[1], 'show_payments_table' => $this->scheduler->parameters['show_payments_table'], 'show_aging_table' => $this->scheduler->parameters['show_aging_table'], - 'status' => $this->scheduler->status + 'status' => $this->scheduler->parameters['status'] ]; } @@ -74,16 +94,92 @@ class SchedulerService return match ($this->scheduler->parameters['date_range']) { 'this_month' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')], 'this_quarter' => [now()->firstOfQuarter()->format('Y-m-d'), now()->lastOfQuarter()->format('Y-m-d')], - 'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->format('Y-m-d')], + 'this_year' => [now()->firstOfYear()->format('Y-m-d'), now()->lastOfYear()->format('Y-m-d')], 'previous_month' => [now()->subMonth()->firstOfMonth()->format('Y-m-d'), now()->subMonth()->lastOfMonth()->format('Y-m-d')], 'previous_quarter' => [now()->subQuarter()->firstOfQuarter()->format('Y-m-d'), now()->subQuarter()->lastOfQuarter()->format('Y-m-d')], - 'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->format('Y-m-d')], - 'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']] + 'previous_year' => [now()->subYear()->firstOfYear()->format('Y-m-d'), now()->subYear()->lastOfYear()->format('Y-m-d')], + 'custom_range' => [$this->scheduler->parameters['start_date'], $this->scheduler->parameters['end_date']], + 'default' => [now()->firstOfMonth()->format('Y-m-d'), now()->lastOfMonth()->format('Y-m-d')], }; } - private function thisMonth() + private function buildMailableData($pdf) { + App::setLocale($this->client->locale()); + $primary_contact = $this->client->primary_contact()->first(); + $settings = $this->client->getMergedSettings(); + + App::forgetInstance('translator'); + $t = app('translator'); + $t->replace(Ninja::transformTranslations($settings)); + + $data = [ + 'to' => [new Address($this->client->present()->email(), $this->client->present()->name())], + 'from' => new Address($this->client->company->owner()->email, $this->client->company->owner()->name()), + 'reply_to' => [$this->buildReplyTo($settings)], + 'cc' => $this->buildCc($settings), + 'bcc' => $this->buildBcc($settings), + 'subject' => ctrans('texts.your_statement'), + 'body' => ctrans('texts.client_statement_body', ['start_date' => $this->client_start_date, 'end_date' => $this->client_end_date]), + 'attachments' => [ + ['name' => ctrans('texts.statement') . ".pdf", 'file' => base64_encode($pdf)], + ], + 'company_key' => $this->client->company->company_key, + 'settings' => $settings, + 'whitelabel' => $this->client->user->account->isPaid() ? true : false, + 'logo' => $this->client->company->present()->logo($settings), + 'signature' => $settings->email_signature, + 'company' => $this->client->company, + 'greeting' => ctrans('texts.email_salutation', ['name' => $primary_contact->present()->name()]), + ]; + + return new ClientStatement($data); } + + private function buildReplyTo($settings) + { + + $reply_to_email = str_contains($settings->reply_to_email, "@") ? $settings->reply_to_email : $this->client->company->owner()->email; + + $reply_to_name = strlen($settings->reply_to_name) > 3 ? $settings->reply_to_name : $this->client->company->owner()->present()->name(); + + return new Address($reply_to_email, $reply_to_name); + + } + + private function buildBcc($settings): array + { + $bccs = false; + $bcc_array = []; + + if (strlen($settings->bcc_email) > 1) { + + if (Ninja::isHosted() && $this->client->company->account->isPaid()) { + $bccs = array_slice(explode(',', str_replace(' ', '', $settings->bcc_email)), 0, 2); + } else { + $bccs(explode(',', str_replace(' ', '', $settings->bcc_email))); + } + } + + if(!$bccs) + return $bcc_array; + + foreach($bccs as $bcc) + { + $bcc_array[] = new Address($bcc); + } + + return $bcc_array; + + } + + private function buildCc($settings) + { + return [ + + ]; + } + + } \ No newline at end of file diff --git a/lang/en/texts.php b/lang/en/texts.php index 388d200053c7..f983eaf0238a 100644 --- a/lang/en/texts.php +++ b/lang/en/texts.php @@ -4924,6 +4924,7 @@ $LANG = array( 'action_add_to_invoice' => 'Add To Invoice', 'danger_zone' => 'Danger Zone', 'import_completed' => 'Import completed', + 'client_statement_body' => 'Your statement from :start_date to :end_date is attached.' ); diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 3e67bd79ea8e..f9eff751acf9 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -98,12 +98,12 @@ class SchedulerTest extends TestCase 'frequency_id' => RecurringInvoice::FREQUENCY_MONTHLY, 'next_run' => '2023-01-14', 'template' => 'client_statement', - 'clients' => [], 'parameters' => [ 'date_range' => 'last_month', 'show_payments_table' => true, 'show_aging_table' => true, - 'status' => 'paid' + 'status' => 'paid', + 'clients' => [], ], ]; From 5d1dc6873968ed393178413dd86a8bce923ec7ab Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 15 Jan 2023 07:46:23 +1100 Subject: [PATCH 12/17] Pass whitelabel to front end --- app/Http/Controllers/BaseController.php | 3 +++ resources/views/index/index.blade.php | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php index b5b0f249f2ea..6eb9920a6bdd 100644 --- a/app/Http/Controllers/BaseController.php +++ b/app/Http/Controllers/BaseController.php @@ -984,6 +984,9 @@ class BaseController extends Controller //pass report errors bool to front end $data['report_errors'] = Ninja::isSelfHost() ? $account->report_errors : true; + //pass whitelabel bool to front end + $data['white_label'] = Ninja::isSelfHost() ? $account->isPaid() : false; + //pass referral code to front end $data['rc'] = request()->has('rc') ? request()->input('rc') : ''; $data['build'] = request()->has('build') ? request()->input('build') : ''; diff --git a/resources/views/index/index.blade.php b/resources/views/index/index.blade.php index 2d95233ce2f3..b0dc8e5d5e34 100644 --- a/resources/views/index/index.blade.php +++ b/resources/views/index/index.blade.php @@ -1,29 +1,32 @@ - + - {{config('ninja.app_name')}} + {{ $white_label ? "" : config('ninja.app_name') }} @if(\App\Utils\Ninja::isHosted()) + + + - + - + @endif