From e9691d9960832e9a0a3e338525d490dc79309aef 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:28:56 +0000 Subject: [PATCH] Add Late Fee Configuration and Automated Late Fee Calculation --- app/Models/Invoice.php | 79 +++++++++++++++++++ app/Models/LateFeeConfiguration.php | 50 ++++++++++++ app/Services/BillingService.php | 16 ++++ ...1_create_late_fee_configurations_table.php | 30 +++++++ ...000002_add_late_fee_fields_to_invoices.php | 25 ++++++ 5 files changed, 200 insertions(+) create mode 100644 app/Models/LateFeeConfiguration.php create mode 100644 database/migrations/2024_01_10_000001_create_late_fee_configurations_table.php create mode 100644 database/migrations/2024_01_10_000002_add_late_fee_fields_to_invoices.php diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 7814e589..97ac84ef 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -8,6 +8,7 @@ use App\Services\CurrencyService; use App\Traits\HasTeam; use Illuminate\Support\Facades\Mail; +use Carbon\Carbon; class Invoice extends Model { @@ -27,11 +28,15 @@ class Invoice extends Model 'discount_id', 'discount_amount', 'invoice_template_id', + 'late_fee_amount', + 'last_late_fee_date', ]; protected $casts = [ 'issue_date' => 'datetime', 'due_date' => 'datetime', + 'last_late_fee_date' => 'datetime', + 'late_fee_amount' => 'decimal:2', ]; public function currency() @@ -125,4 +130,78 @@ public function getFormattedAmount() { return number_format($this->total_amount, 2) . ' ' . $this->currency; } + + public function isOverdue() + { + return $this->status === 'pending' && $this->due_date->isPast(); + } + + public function calculateLateFee() + { + if (!$this->isOverdue()) { + return 0; + } + + $config = LateFeeConfiguration::where('team_id', $this->team_id)->first(); + if (!$config) { + return 0; + } + + // Check grace period + $daysOverdue = $this->due_date->diffInDays(now()); + if ($daysOverdue <= $config->grace_period_days) { + return 0; + } + + $baseAmount = $config->is_compound ? + ($this->total_amount + $this->late_fee_amount) : + $this->total_amount; + + $fee = $config->fee_type === 'percentage' ? + ($baseAmount * ($config->fee_amount / 100)) : + $config->fee_amount; + + // Apply frequency rules + if ($this->last_late_fee_date) { + $daysSinceLastFee = $this->last_late_fee_date->diffInDays(now()); + $fee = match($config->frequency) { + 'one-time' => 0, + 'daily' => $daysSinceLastFee >= 1 ? $fee : 0, + 'weekly' => $daysSinceLastFee >= 7 ? $fee : 0, + 'monthly' => $daysSinceLastFee >= 30 ? $fee : 0, + default => 0, + }; + } + + // Check max fee amount + if ($config->max_fee_amount) { + $totalFees = $this->late_fee_amount + $fee; + if ($totalFees > $config->max_fee_amount) { + $fee = max(0, $config->max_fee_amount - $this->late_fee_amount); + } + } + + return round($fee, 2); + } + + public function applyLateFee() + { + $fee = $this->calculateLateFee(); + if ($fee > 0) { + $this->late_fee_amount += $fee; + $this->last_late_fee_date = now(); + $this->save(); + } + return $fee; + } + + public function getTotalWithLateFeeAttribute() + { + return $this->final_total + $this->late_fee_amount; + } + + public function getFormattedTotalWithLateFeeAttribute() + { + return number_format($this->total_with_late_fee, 2) . ' ' . $this->currency; + } } diff --git a/app/Models/LateFeeConfiguration.php b/app/Models/LateFeeConfiguration.php new file mode 100644 index 00000000..32979ac6 --- /dev/null +++ b/app/Models/LateFeeConfiguration.php @@ -0,0 +1,50 @@ + + + 'boolean', + 'fee_amount' => 'decimal:2', + 'max_fee_amount' => 'decimal:2', + 'grace_period_days' => 'integer', + ]; + + public static function getFrequencyOptions() + { + return [ + 'one-time' => 'One Time', + 'daily' => 'Daily', + 'weekly' => 'Weekly', + 'monthly' => 'Monthly' + ]; + } + + public static function getFeeTypeOptions() + { + return [ + 'percentage' => 'Percentage', + 'fixed' => 'Fixed Amount' + ]; + } +} \ No newline at end of file diff --git a/app/Services/BillingService.php b/app/Services/BillingService.php index ac0cba44..487f070d 100644 --- a/app/Services/BillingService.php +++ b/app/Services/BillingService.php @@ -205,12 +205,28 @@ public function sendOverdueReminders() ->get(); foreach ($overdueInvoices as $invoice) { + // Apply late fee + $invoice->applyLateFee(); + // Send overdue reminder email $this->sendOverdueReminderEmail($invoice); + + // Suspend service if applicable $this->serviceProvisioningService->manageService($invoice->subscription, 'suspend'); } } + public function processLateFees() + { + $pendingInvoices = Invoice::where('status', 'pending') + ->where('due_date', '<', Carbon::now()) + ->get(); + + foreach ($pendingInvoices as $invoice) { + $invoice->applyLateFee(); + } + } + private function sendOverdueReminderEmail(Invoice $invoice) { $customer = $invoice->customer; diff --git a/database/migrations/2024_01_10_000001_create_late_fee_configurations_table.php b/database/migrations/2024_01_10_000001_create_late_fee_configurations_table.php new file mode 100644 index 00000000..a7833b92 --- /dev/null +++ b/database/migrations/2024_01_10_000001_create_late_fee_configurations_table.php @@ -0,0 +1,30 @@ + + +id(); + $table->foreignId('team_id')->constrained()->onDelete('cascade'); + $table->enum('fee_type', ['percentage', 'fixed']); + $table->decimal('fee_amount', 10, 2); + $table->integer('grace_period_days')->default(0); + $table->decimal('max_fee_amount', 10, 2)->nullable(); + $table->boolean('is_compound')->default(false); + $table->enum('frequency', ['one-time', 'daily', 'weekly', 'monthly'])->default('one-time'); + $table->timestamps(); + }); + } + + public function down() + { + Schema::dropIfExists('late_fee_configurations'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_10_000002_add_late_fee_fields_to_invoices.php b/database/migrations/2024_01_10_000002_add_late_fee_fields_to_invoices.php new file mode 100644 index 00000000..54bf9958 --- /dev/null +++ b/database/migrations/2024_01_10_000002_add_late_fee_fields_to_invoices.php @@ -0,0 +1,25 @@ + + +decimal('late_fee_amount', 10, 2)->default(0); + $table->timestamp('last_late_fee_date')->nullable(); + }); + } + + public function down() + { + Schema::table('invoices', function (Blueprint $table) { + $table->dropColumn(['late_fee_amount', 'last_late_fee_date']); + }); + } +}; \ No newline at end of file