diff --git a/app/DataMapper/Schedule/PaymentSchedule.php b/app/DataMapper/Schedule/PaymentSchedule.php new file mode 100644 index 0000000000..e5805370da --- /dev/null +++ b/app/DataMapper/Schedule/PaymentSchedule.php @@ -0,0 +1,37 @@ + string, + * 'amount' => float, + * 'percentage' => float + * ) + */ + public array $schedule = []; + + /** + * The invoice id + * + * @var string + */ + public string $invoice_id = ''; + + /** + * Whether to auto bill the invoice + * + * @var bool + */ + public bool $auto_bill = false; +} \ No newline at end of file diff --git a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php index c9994b9475..6249bef1dc 100644 --- a/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/StoreSchedulerRequest.php @@ -72,6 +72,12 @@ class StoreSchedulerRequest extends Request 'parameters.status' => ['bail','sometimes', 'nullable', 'string'], 'parameters.include_project_tasks' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'], 'parameters.auto_send' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'], + 'parameters.invoice_id' => ['bail','sometimes', 'string', 'required_if:template,payment_schedule'], + 'parameters.auto_bill' => ['bail','sometimes', 'boolean', 'required_if:template,payment_schedule'], + 'parameters.schedule' => ['bail','sometimes', 'array', 'required_if:template,payment_schedule'], + 'parameters.schedule.*.date' => ['bail','sometimes', 'date:Y-m-d'], + 'parameters.schedule.*.amount' => ['bail','sometimes', 'numeric'], + 'parameters.schedule.*.percentage' => ['bail','sometimes', 'numeric'], ]; return $rules; diff --git a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php index e38b21f9de..e04340614c 100644 --- a/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php +++ b/app/Http/Requests/TaskScheduler/UpdateSchedulerRequest.php @@ -72,6 +72,12 @@ class UpdateSchedulerRequest extends Request 'parameters.status' => ['bail','sometimes', 'nullable', 'string'], 'parameters.include_project_tasks' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'], 'parameters.auto_send' => ['bail','sometimes', 'boolean', 'required_if:template,invoice_outstanding_tasks'], + 'parameters.invoice_id' => ['bail','sometimes', 'string', 'required_if:template,payment_schedule'], + 'parameters.auto_bill' => ['bail','sometimes', 'boolean', 'required_if:template,payment_schedule'], + 'parameters.schedule' => ['bail','sometimes', 'array', 'required_if:template,payment_schedule','min:1'], + 'parameters.schedule.*.date' => ['bail','sometimes', 'date:Y-m-d'], + 'parameters.schedule.*.amount' => ['bail','sometimes', 'numeric'], + 'parameters.schedule.*.percentage' => ['bail','sometimes', 'numeric'], ]; return $rules; diff --git a/app/Services/Scheduler/PaymentSchedule.php b/app/Services/Scheduler/PaymentSchedule.php new file mode 100644 index 0000000000..507e61ea8b --- /dev/null +++ b/app/Services/Scheduler/PaymentSchedule.php @@ -0,0 +1,93 @@ +decodePrimaryKey($this->scheduler->parameters['invoice_id'])); + + // Needs to be draft, partial or paid AND not deleted + if(!$invoice ||!in_array($invoice->status_id, [Invoice::STATUS_DRAFT, Invoice::STATUS_PARTIAL, Invoice::STATUS_PAID]) || $invoice->is_deleted){ + $this->scheduler->forceDelete(); + return; + } + + $invoice = $invoice->service()->markSent()->save(); + + $offset = $invoice->company->timezone_offset(); + $schedule = $this->scheduler->parameters['schedule']; + $schedule_index = 0; + $next_schedule = false; + + foreach($schedule as $key =>$item){ + + if(now()->startOfDay()->eq(Carbon::parse($item['date'])->subSeconds($offset)->startOfDay())){ + $next_schedule = $item; + $schedule_index = $key; + } + + } + + if(!$next_schedule){ + $this->scheduler->forceDelete(); + return; + } + + $amount = max($next_schedule['amount'], ($next_schedule['percentage'] * $invoice->amount)); + $amount += $invoice->partial; + + + if($amount > $invoice->balance){ + $amount = $invoice->balance; + } + + $invoice->partial = $amount; + $invoice->partial_due_date = $item['date']; + $invoice->due_date = Carbon::parse($item['date'])->addDay()->format('Y-m-d'); + + $invoice->save(); + + if($this->scheduler->parameters['auto_bill']){ + $invoice->service()->autoBill(); + } + else{ + $invoice->service()->sendEmail(); + } + + $total_schedules = count($schedule); + + if($total_schedules >= $schedule_index + 1){ + $next_run = $schedule[$schedule_index + 1]['date']; + $this->scheduler->next_run_client = $next_run; + $this->scheduler->next_run = Carbon::parse($next_run)->addSeconds($offset); + $this->scheduler->save(); + } + else { + $this->scheduler->forceDelete(); + } + } +} diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 54939a8799..18bec8e653 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -61,6 +61,48 @@ class SchedulerTest extends TestCase // $this->withoutExceptionHandling(); } + public function testPaymentSchedule() + { + $data = [ + [ + 'date' => now()->format('Y-m-d'), + 'amount' => 100, + 'percentage' => 100, + ], + [ + 'date' => now()->addDays(1)->format('Y-m-d'), + 'amount' => 100, + 'percentage' => 100, + ], + [ + 'date' => now()->addDays(2)->format('Y-m-d'), + 'amount' => 100, + 'percentage' => 100, + ], + ]; + + $offset = -3600; + + $next_schedule = collect($data)->first(function ($item) use ($offset){ + return now()->startOfDay()->eq(Carbon::parse($item['date'])->subSeconds($offset)->startOfDay()); + }); + + $this->assertNotNull($next_schedule); + + $this->assertEquals($next_schedule['date'], now()->format('Y-m-d')); + + $this->travelTo(now()->addDays(1)); + + $next_schedule = collect($data)->first(function ($item) use ($offset) { + return now()->startOfDay()->eq(Carbon::parse($item['date'])->subSeconds($offset)->startOfDay()); + }); + + $this->assertNotNull($next_schedule); + + $this->assertEquals($next_schedule['date'], now()->format('Y-m-d')); + + } + public function testInvoiceOutstandingTasks() {