Skip to content

Commit

Permalink
Implement Payment Plan Feature for Flexible Invoice
Browse files Browse the repository at this point in the history
  • Loading branch information
sweep-ai[bot] authored Dec 24, 2024
1 parent 9edcbbd commit 475c6a5
Show file tree
Hide file tree
Showing 6 changed files with 298 additions and 2 deletions.
76 changes: 76 additions & 0 deletions app/Filament/Resources/PaymentPlanResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@


<?php

namespace App\Filament\Resources;

use App\Filament\Resources\PaymentPlanResource\Pages;
use App\Models\PaymentPlan;
use Filament\Forms;
use Filament\Resources\Resource;
use Filament\Tables;

class PaymentPlanResource extends Resource
{
protected static ?string $model = PaymentPlan::class;

protected static ?string $navigationIcon = 'heroicon-o-calendar';

public static function form(Forms\Form $form): Forms\Form
{
return $form
->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'),
];
}
}
47 changes: 47 additions & 0 deletions app/Models/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class Invoice extends Model
'total_amount',
'currency',
'status',
'parent_invoice_id',
'is_installment',
];

public function currency()
Expand All @@ -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(),
};
}
}
40 changes: 40 additions & 0 deletions app/Models/PaymentPlan.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Traits\HasTeam;

class PaymentPlan extends Model
{
use HasFactory;
use HasTeam;

protected $fillable = [
'invoice_id',
'total_installments',
'installment_amount',
'frequency',
'start_date',
'next_due_date',
'status',
];

protected $casts = [
'start_date' => 'datetime',
'next_due_date' => 'datetime',
];

public function invoice()
{
return $this->belongsTo(Invoice::class);
}

public function installments()
{
return $this->hasMany(Invoice::class, 'parent_invoice_id');
}
}
22 changes: 20 additions & 2 deletions app/Services/BillingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down
76 changes: 76 additions & 0 deletions app/Services/PaymentPlanService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@


<?php

namespace App\Services;

use App\Models\Invoice;
use App\Models\PaymentPlan;
use Carbon\Carbon;

class PaymentPlanService
{
protected $billingService;

public function __construct(BillingService $billingService)
{
$this->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(),
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::create('payment_plans', function (Blueprint $table) {
$table->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');
}
};

0 comments on commit 475c6a5

Please sign in to comment.