From 475c6a5a50d7d5827ab90b19fe52673aa24a54ef Mon Sep 17 00:00:00 2001 From: "sweep-ai[bot]" <128439645+sweep-ai[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:10:41 +0000 Subject: [PATCH] Implement Payment Plan Feature for Flexible Invoice --- .../Resources/PaymentPlanResource.php | 76 +++++++++++++++++++ app/Models/Invoice.php | 47 ++++++++++++ app/Models/PaymentPlan.php | 40 ++++++++++ app/Services/BillingService.php | 22 +++++- app/Services/PaymentPlanService.php | 76 +++++++++++++++++++ ...1_25_000001_create_payment_plans_table.php | 39 ++++++++++ 6 files changed, 298 insertions(+), 2 deletions(-) create mode 100644 app/Filament/Resources/PaymentPlanResource.php create mode 100644 app/Models/PaymentPlan.php create mode 100644 app/Services/PaymentPlanService.php create mode 100644 database/migrations/2024_01_25_000001_create_payment_plans_table.php diff --git a/app/Filament/Resources/PaymentPlanResource.php b/app/Filament/Resources/PaymentPlanResource.php new file mode 100644 index 00000000..cb89e2ec --- /dev/null +++ b/app/Filament/Resources/PaymentPlanResource.php @@ -0,0 +1,76 @@ + + +schema([ + Forms\Components\Select::make('invoice_id') + ->relationship('invoice', 'invoice_number') + ->required(), + Forms\Components\TextInput::make('total_installments') + ->required() + ->numeric() + ->minValue(2), + Forms\Components\Select::make('frequency') + ->options([ + 'weekly' => 'Weekly', + 'monthly' => 'Monthly', + 'quarterly' => 'Quarterly', + ]) + ->required(), + Forms\Components\DatePicker::make('start_date') + ->required(), + ]); + } + + public static function table(Tables\Table $table): Tables\Table + { + return $table + ->columns([ + Tables\Columns\TextColumn::make('invoice.invoice_number'), + Tables\Columns\TextColumn::make('total_installments'), + Tables\Columns\TextColumn::make('installment_amount'), + Tables\Columns\TextColumn::make('frequency'), + Tables\Columns\TextColumn::make('status'), + ]) + ->filters([ + // + ]) + ->actions([ + Tables\Actions\EditAction::make(), + Tables\Actions\DeleteAction::make(), + ]); + } + + public static function getRelations(): array + { + return [ + // + ]; + } + + public static function getPages(): array + { + return [ + 'index' => Pages\ListPaymentPlans::route('/'), + 'create' => Pages\CreatePaymentPlan::route('/create'), + 'edit' => Pages\EditPaymentPlan::route('/{record}/edit'), + ]; + } +} \ No newline at end of file diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index eebfb0e9..2bf0ba4b 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -21,6 +21,8 @@ class Invoice extends Model 'total_amount', 'currency', 'status', + 'parent_invoice_id', + 'is_installment', ]; public function currency() @@ -33,8 +35,53 @@ public function customer() return $this->belongsTo(Customer::class); } + public function paymentPlan() + { + return $this->hasOne(PaymentPlan::class); + } + + public function parentInvoice() + { + return $this->belongsTo(Invoice::class, 'parent_invoice_id'); + } + + public function installments() + { + return $this->hasMany(Invoice::class, 'parent_invoice_id'); + } + public function sendInvoiceEmail() { Mail::to($this->customer->email)->send(new InvoiceGenerated($this)); } + + public function createPaymentPlan($totalInstallments, $frequency = 'monthly') + { + if ($this->is_installment) { + throw new \Exception('Cannot create payment plan for an installment invoice'); + } + + $installmentAmount = round($this->total_amount / $totalInstallments, 2); + $startDate = now(); + + return PaymentPlan::create([ + 'invoice_id' => $this->id, + 'total_installments' => $totalInstallments, + 'installment_amount' => $installmentAmount, + 'frequency' => $frequency, + 'start_date' => $startDate, + 'next_due_date' => $this->calculateNextDueDate($startDate, $frequency), + 'status' => 'active', + ]); + } + + private function calculateNextDueDate($date, $frequency) + { + return match($frequency) { + 'weekly' => $date->addWeek(), + 'monthly' => $date->addMonth(), + 'quarterly' => $date->addMonths(3), + default => $date->addMonth(), + }; + } } diff --git a/app/Models/PaymentPlan.php b/app/Models/PaymentPlan.php new file mode 100644 index 00000000..f5b80a41 --- /dev/null +++ b/app/Models/PaymentPlan.php @@ -0,0 +1,40 @@ + + + 'datetime', + 'next_due_date' => 'datetime', + ]; + + public function invoice() + { + return $this->belongsTo(Invoice::class); + } + + public function installments() + { + return $this->hasMany(Invoice::class, 'parent_invoice_id'); + } +} \ No newline at end of file diff --git a/app/Services/BillingService.php b/app/Services/BillingService.php index e321e74c..4d86c55e 100644 --- a/app/Services/BillingService.php +++ b/app/Services/BillingService.php @@ -16,10 +16,14 @@ class BillingService { protected $serviceProvisioningService; + protected $paymentPlanService; - public function __construct(ServiceProvisioningService $serviceProvisioningService) - { + public function __construct( + ServiceProvisioningService $serviceProvisioningService, + PaymentPlanService $paymentPlanService = null + ) { $this->serviceProvisioningService = $serviceProvisioningService; + $this->paymentPlanService = $paymentPlanService ?? new PaymentPlanService($this); } public function generateInvoice(Subscription $subscription) @@ -53,6 +57,20 @@ public function generateInvoice(Subscription $subscription) return $invoice; } + + public function setupPaymentPlan(Invoice $invoice, $totalInstallments, $frequency = 'monthly') + { + if ($invoice->paymentPlan) { + throw new \Exception('Invoice already has a payment plan'); + } + + return $invoice->createPaymentPlan($totalInstallments, $frequency); + } + + public function processPaymentPlans() + { + $this->paymentPlanService->processPaymentPlans(); + } public function convertCurrency($amount, $fromCurrency, $toCurrency) { diff --git a/app/Services/PaymentPlanService.php b/app/Services/PaymentPlanService.php new file mode 100644 index 00000000..534647a3 --- /dev/null +++ b/app/Services/PaymentPlanService.php @@ -0,0 +1,76 @@ + + +billingService = $billingService; + } + + public function createInstallmentInvoice(PaymentPlan $paymentPlan) + { + $parentInvoice = $paymentPlan->invoice; + + $installmentInvoice = Invoice::create([ + 'customer_id' => $parentInvoice->customer_id, + 'invoice_number' => $this->generateInstallmentNumber($parentInvoice), + 'issue_date' => now(), + 'due_date' => $paymentPlan->next_due_date, + 'total_amount' => $paymentPlan->installment_amount, + 'currency' => $parentInvoice->currency, + 'status' => 'pending', + 'parent_invoice_id' => $parentInvoice->id, + 'is_installment' => true, + ]); + + $paymentPlan->update([ + 'next_due_date' => $this->calculateNextDueDate( + $paymentPlan->next_due_date, + $paymentPlan->frequency + ), + ]); + + return $installmentInvoice; + } + + public function processPaymentPlans() + { + $activePlans = PaymentPlan::where('status', 'active') + ->where('next_due_date', '<=', now()) + ->get(); + + foreach ($activePlans as $plan) { + $this->createInstallmentInvoice($plan); + + if ($plan->installments->count() >= $plan->total_installments) { + $plan->update(['status' => 'completed']); + } + } + } + + private function generateInstallmentNumber(Invoice $parentInvoice) + { + $count = $parentInvoice->installments()->count() + 1; + return $parentInvoice->invoice_number . "-INST{$count}"; + } + + private function calculateNextDueDate($date, $frequency) + { + return match($frequency) { + 'weekly' => Carbon::parse($date)->addWeek(), + 'monthly' => Carbon::parse($date)->addMonth(), + 'quarterly' => Carbon::parse($date)->addMonths(3), + default => Carbon::parse($date)->addMonth(), + }; + } +} \ No newline at end of file diff --git a/database/migrations/2024_01_25_000001_create_payment_plans_table.php b/database/migrations/2024_01_25_000001_create_payment_plans_table.php new file mode 100644 index 00000000..edce6b16 --- /dev/null +++ b/database/migrations/2024_01_25_000001_create_payment_plans_table.php @@ -0,0 +1,39 @@ + + +id(); + $table->foreignId('invoice_id')->constrained()->onDelete('cascade'); + $table->integer('total_installments'); + $table->decimal('installment_amount', 10, 2); + $table->string('frequency'); + $table->timestamp('start_date'); + $table->timestamp('next_due_date'); + $table->string('status')->default('active'); + $table->timestamps(); + }); + + Schema::table('invoices', function (Blueprint $table) { + $table->foreignId('parent_invoice_id')->nullable()->constrained('invoices')->onDelete('cascade'); + $table->boolean('is_installment')->default(false); + }); + } + + public function down() + { + Schema::table('invoices', function (Blueprint $table) { + $table->dropForeign(['parent_invoice_id']); + $table->dropColumn(['parent_invoice_id', 'is_installment']); + }); + Schema::dropIfExists('payment_plans'); + } +}; \ No newline at end of file