Skip to content

Commit

Permalink
Merge pull request #299 from liberu-billing/sweep/Add-Late-Fee-Config…
Browse files Browse the repository at this point in the history
…uration-and-Automated-Late-Fee-Calculation-for-Invoices

Add Late Fee Configuration and Automated Late Fee Calculation for Invoices
  • Loading branch information
curtisdelicata authored Dec 24, 2024
2 parents 7b848de + e9691d9 commit 00265bc
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 0 deletions.
79 changes: 79 additions & 0 deletions app/Models/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use App\Services\CurrencyService;
use App\Traits\HasTeam;
use Illuminate\Support\Facades\Mail;
use Carbon\Carbon;

class Invoice extends Model
{
Expand All @@ -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()
Expand Down Expand Up @@ -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;
}
}
50 changes: 50 additions & 0 deletions app/Models/LateFeeConfiguration.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@


<?php

namespace App\Models;

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

class LateFeeConfiguration extends Model
{
use HasFactory;
use HasTeam;

protected $fillable = [
'team_id',
'fee_type',
'fee_amount',
'grace_period_days',
'max_fee_amount',
'is_compound',
'frequency',
];

protected $casts = [
'is_compound' => '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'
];
}
}
16 changes: 16 additions & 0 deletions app/Services/BillingService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@


<?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('late_fee_configurations', function (Blueprint $table) {
$table->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');
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@


<?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::table('invoices', function (Blueprint $table) {
$table->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']);
});
}
};

0 comments on commit 00265bc

Please sign in to comment.