From 413e4a00014aa0fae99073359aaf7312fd8612a7 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Sun, 3 Aug 2025 17:08:03 +1000 Subject: [PATCH] Tests for payment schedules --- app/Services/Scheduler/PaymentSchedule.php | 41 +++- app/Services/Scheduler/SchedulerService.php | 8 +- tests/Feature/Scheduler/SchedulerTest.php | 244 ++++++++++++++++++-- 3 files changed, 262 insertions(+), 31 deletions(-) diff --git a/app/Services/Scheduler/PaymentSchedule.php b/app/Services/Scheduler/PaymentSchedule.php index 103e06d5c2..54a94f96fa 100644 --- a/app/Services/Scheduler/PaymentSchedule.php +++ b/app/Services/Scheduler/PaymentSchedule.php @@ -29,8 +29,8 @@ class PaymentSchedule { $invoice = Invoice::find($this->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){ + // Needs to be draft, partial or sent AND not deleted + if(!$invoice || !in_array($invoice->status_id, [Invoice::STATUS_DRAFT, Invoice::STATUS_PARTIAL, Invoice::STATUS_SENT]) || $invoice->is_deleted){ $this->scheduler->forceDelete(); return; } @@ -43,12 +43,10 @@ class PaymentSchedule $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){ @@ -56,22 +54,41 @@ class PaymentSchedule return; } - $amount = max($next_schedule['amount'], ($next_schedule['percentage'] * $invoice->amount)); - $amount += $invoice->partial; - + nlog($next_schedule); + + if($next_schedule['is_amount']){ + nlog("is an amount"); + $amount = $next_schedule['amount']; + } + else{ + $amount = round(($next_schedule['amount']/100)*$invoice->amount, 2); + } + $amount = min($amount, $invoice->amount); + 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'); + nlog("amount to add: {$amount}"); + nlog("invoice partial before: {$invoice->partial}"); + $invoice->partial += $amount; + $invoice->partial_due_date = $next_schedule['date']; + $invoice->due_date = Carbon::parse($next_schedule['date'])->addDay()->format('Y-m-d'); $invoice->save(); + + nlog("invoice partial after: {$invoice->partial}"); + if($this->scheduler->parameters['auto_bill']){ - $invoice->service()->autoBill(); + + try{ + $invoice->service()->autoBill(); + } + catch(\Throwable $e){ + nlog("Error auto-billing invoice {$invoice->id}: {$e->getMessage()}"); + } } else{ $invoice->service()->sendEmail(); @@ -79,7 +96,7 @@ class PaymentSchedule $total_schedules = count($schedule); - if($total_schedules >= $schedule_index + 1){ + if(isset($schedule[$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); diff --git a/app/Services/Scheduler/SchedulerService.php b/app/Services/Scheduler/SchedulerService.php index dad9e927bc..8ed2ea7a73 100644 --- a/app/Services/Scheduler/SchedulerService.php +++ b/app/Services/Scheduler/SchedulerService.php @@ -13,8 +13,10 @@ namespace App\Services\Scheduler; use App\Models\Scheduler; -use App\Utils\Traits\MakesDates; use App\Utils\Traits\MakesHash; +use App\Utils\Traits\MakesDates; +use App\Services\Scheduler\PaymentSchedule; + class SchedulerService { use MakesHash; @@ -56,6 +58,10 @@ class SchedulerService (new InvoiceOutstandingTasksService($this->scheduler))->run(); } + private function payment_schedule() + { + (new PaymentSchedule($this->scheduler))->run(); + } /** * Sets the next run date of the scheduled task diff --git a/tests/Feature/Scheduler/SchedulerTest.php b/tests/Feature/Scheduler/SchedulerTest.php index 4067b6af7d..db8c5f7a4d 100644 --- a/tests/Feature/Scheduler/SchedulerTest.php +++ b/tests/Feature/Scheduler/SchedulerTest.php @@ -60,9 +60,233 @@ class SchedulerTest extends TestCase ThrottleRequests::class ); - // $this->withoutExceptionHandling(); } + public function testPaymentScheduleCalculationsIsAmountWithAutoBill() + { + $settings = $this->company->settings; + $settings->use_credits_payment = 'off'; + $settings->use_unapplied_payment = 'off'; + $this->company->settings = $settings; + $this->company->save(); + + \App\Models\Credit::where('client_id', $this->client->id)->delete(); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(30)->format('Y-m-d'), + 'partial' => 0, + 'partial_due_date' => null, + 'amount' => 300.00, + 'balance' => 300.00, + 'status_id' => Invoice::STATUS_SENT, + ]); + + + $data = [ + 'name' => 'A test payment schedule scheduler', + 'frequency_id' => 0, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'payment_schedule', + 'parameters' => [ + 'invoice_id' => $invoice->hashed_id, + 'auto_bill' => true, + 'schedule' => [ + [ + 'id' => 1, + 'date' => now()->format('Y-m-d'), + 'amount' => 40, + 'is_amount' => true, + ], + [ + 'id' => 2, + 'date' => now()->addDays(30)->format('Y-m-d'), + 'amount' => 60.00, + 'is_amount' => true, + ] + ], + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $scheduler = Scheduler::find($this->decodePrimaryKey($arr['data']['id'])); + + $this->assertNotNull($scheduler); + + $scheduler->service()->runTask(); + + $invoice = $invoice->fresh(); + + $this->assertEquals(40, $invoice->partial); + $this->assertEquals(now()->format('Y-m-d'), $invoice->partial_due_date->format('Y-m-d')); + + $scheduler = $scheduler->fresh(); + + $this->assertEquals(now()->addDays(30)->format('Y-m-d'), $scheduler->next_run->format('Y-m-d')); + + $this->travelTo(now()->addDays(30)); + + $scheduler->service()->runTask(); + + $invoice = $invoice->fresh(); + + $this->assertEquals(100, $invoice->partial); + $this->assertEquals(now()->format('Y-m-d'), $invoice->partial_due_date->format('Y-m-d')); + + $this->travelBack(); + } + + + public function testPaymentScheduleCalculationsIsAmount() + { + $settings = $this->company->settings; + $settings->use_credits_payment = 'off'; + $settings->use_unapplied_payment = 'off'; + $this->company->settings = $settings; + $this->company->save(); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(30)->format('Y-m-d'), + 'partial' => 0, + 'partial_due_date' => null, + 'amount' => 300.00, + 'balance' => 300.00, + 'status_id' => Invoice::STATUS_SENT, + ]); + + + $data = [ + 'name' => 'A test payment schedule scheduler', + 'frequency_id' => 0, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'payment_schedule', + 'parameters' => [ + 'invoice_id' => $invoice->hashed_id, + 'auto_bill' => false, + 'schedule' => [ + [ + 'id' => 1, + 'date' => now()->format('Y-m-d'), + 'amount' => 40, + 'is_amount' => true, + ], + [ + 'id' => 2, + 'date' => now()->addDays(30)->format('Y-m-d'), + 'amount' => 60.00, + 'is_amount' => true, + ] + ], + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $scheduler = Scheduler::find($this->decodePrimaryKey($arr['data']['id'])); + + $this->assertNotNull($scheduler); + + $scheduler->service()->runTask(); + + $invoice = $invoice->fresh(); + + $this->assertEquals(40, $invoice->partial); + $this->assertEquals(now()->format('Y-m-d'), $invoice->partial_due_date->format('Y-m-d')); + + $scheduler = $scheduler->fresh(); + + $this->assertEquals(now()->addDays(30)->format('Y-m-d'), $scheduler->next_run->format('Y-m-d')); + } + + public function testPaymentScheduleCalculationsIsPercentage() + { + $settings = $this->company->settings; + $settings->use_credits_payment = 'off'; + $settings->use_unapplied_payment = 'off'; + $this->company->settings = $settings; + $this->company->save(); + + $invoice = Invoice::factory()->create([ + 'company_id' => $this->company->id, + 'user_id' => $this->user->id, + 'client_id' => $this->client->id, + 'date' => now()->format('Y-m-d'), + 'due_date' => now()->addDays(30)->format('Y-m-d'), + 'partial' => 0, + 'partial_due_date' => null, + 'amount' => 300.00, + 'balance' => 300.00, + 'status_id' => Invoice::STATUS_SENT, + ]); + + + $data = [ + 'name' => 'A test payment schedule scheduler', + 'frequency_id' => 0, + 'next_run' => now()->format('Y-m-d'), + 'template' => 'payment_schedule', + 'parameters' => [ + 'invoice_id' => $invoice->hashed_id, + 'auto_bill' => false, + 'schedule' => [ + [ + 'id' => 1, + 'date' => now()->format('Y-m-d'), + 'amount' => 40, + 'is_amount' => false, + ], + [ + 'id' => 2, + 'date' => now()->addDays(30)->format('Y-m-d'), + 'amount' => 60.00, + 'is_amount' => false, + ] + ], + ], + ]; + + $response = $this->withHeaders([ + 'X-API-SECRET' => config('ninja.api_secret'), + 'X-API-TOKEN' => $this->token, + ])->postJson('/api/v1/task_schedulers', $data); + + $response->assertStatus(200); + + $arr = $response->json(); + + $scheduler = Scheduler::find($this->decodePrimaryKey($arr['data']['id'])); + + $this->assertNotNull($scheduler); + + $scheduler->service()->runTask(); + + $invoice = $invoice->fresh(); + + $this->assertEquals(120, $invoice->partial); + $this->assertEquals(now()->format('Y-m-d'), $invoice->partial_due_date->format('Y-m-d')); + } public function testDuplicateInvoicePaymentSchedule() { @@ -103,23 +327,7 @@ class SchedulerTest extends TestCase ], ]; - // $data = [ - // 'schedule' => [ - // [ - // 'id' => 1, - // 'date' => now()->format('Y-m-d'), - // 'amount' => 40, - // 'is_amount' => false, - // ], - // [ - // 'id' => 2, - // 'date' => now()->addDays(30)->format('Y-m-d'), - // 'amount' => 60.00, - // 'is_amount' => false, - // ] - // ], - // 'auto_bill' => true, - // ]; +