122 lines
4.1 KiB
PHP
122 lines
4.1 KiB
PHP
<?php
|
|
|
|
namespace Modules\FinancialManagement\Services;
|
|
|
|
use Modules\FinancialManagement\Enums\InvoiceStatus;
|
|
use Modules\FinancialManagement\Models\FinancialInvoice;
|
|
use Modules\FinancialManagement\Models\InvoiceLineItem;
|
|
use Modules\FinancialManagement\Models\RetentionEntry;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
|
|
class ProgressBillingService
|
|
{
|
|
/**
|
|
* Generate a progress invoice for a project based on completion %.
|
|
*/
|
|
public function generateInvoice(
|
|
Project $project,
|
|
float $currentPercentage,
|
|
float $retentionRate = 10.00,
|
|
): FinancialInvoice {
|
|
$lastBilled = (float) ($project->last_billed_percentage ?? 0);
|
|
$contractValue = (float) ($project->contract_value ?? 0);
|
|
|
|
if ($currentPercentage <= $lastBilled) {
|
|
throw new \InvalidArgumentException(
|
|
"Current progress ({$currentPercentage}%) must exceed last billed ({$lastBilled}%)"
|
|
);
|
|
}
|
|
|
|
$incrementalPercent = $currentPercentage - $lastBilled;
|
|
$subtotal = $contractValue * ($incrementalPercent / 100);
|
|
$retention = $subtotal * ($retentionRate / 100);
|
|
$totalAmount = $subtotal - $retention;
|
|
|
|
$invoice = FinancialInvoice::create([
|
|
'project_id' => $project->id,
|
|
'invoice_number' => 'INV-' . strtoupper(uniqid()),
|
|
'status' => InvoiceStatus::Draft,
|
|
'subtotal' => $subtotal,
|
|
'retention_amount' => $retention,
|
|
'total_amount' => $totalAmount,
|
|
'retention_rate' => $retentionRate,
|
|
'billed_percentage' => $currentPercentage,
|
|
'invoice_date' => now()->toDateString(),
|
|
'due_date' => now()->addDays(30)->toDateString(),
|
|
]);
|
|
|
|
// Create line item for progress billing
|
|
InvoiceLineItem::create([
|
|
'invoice_id' => $invoice->id,
|
|
'description' => "Progress billing: {$lastBilled}% → {$currentPercentage}% ({$incrementalPercent}%)",
|
|
'quantity' => 1,
|
|
'unit_price' => $subtotal,
|
|
'total' => $subtotal,
|
|
]);
|
|
|
|
return $invoice;
|
|
}
|
|
|
|
/**
|
|
* On invoice approval, create retention debit entry.
|
|
*/
|
|
public function holdRetention(FinancialInvoice $invoice): void
|
|
{
|
|
if ($invoice->retention_amount <= 0) return;
|
|
|
|
RetentionEntry::create([
|
|
'project_id' => $invoice->project_id,
|
|
'invoice_id' => $invoice->id,
|
|
'type' => 'debit',
|
|
'amount' => $invoice->retention_amount,
|
|
'description' => "Retention held on Invoice #{$invoice->invoice_number} ({$invoice->retention_rate}%)",
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* On invoice payment, update project's last billed percentage.
|
|
*/
|
|
public function recordPayment(FinancialInvoice $invoice, float $amount): void
|
|
{
|
|
$newPaid = (float) $invoice->paid_amount + $amount;
|
|
$totalAmount = (float) $invoice->total_amount;
|
|
|
|
$updates = ['paid_amount' => $newPaid];
|
|
|
|
if ($newPaid >= $totalAmount) {
|
|
$invoice->transitionTo(InvoiceStatus::Paid);
|
|
// Update project's last billed percentage
|
|
$invoice->project()->update(['last_billed_percentage' => $invoice->billed_percentage]);
|
|
} else {
|
|
$invoice->transitionTo(InvoiceStatus::PartiallyPaid);
|
|
}
|
|
|
|
$invoice->update($updates);
|
|
}
|
|
|
|
/**
|
|
* Release all retention for a project (on project completion).
|
|
*/
|
|
public function releaseRetention(Project $project): void
|
|
{
|
|
$totalRetention = RetentionEntry::where('project_id', $project->id)
|
|
->where('type', 'debit')
|
|
->sum('amount');
|
|
|
|
$alreadyCredited = RetentionEntry::where('project_id', $project->id)
|
|
->where('type', 'credit')
|
|
->sum('amount');
|
|
|
|
$remaining = $totalRetention - $alreadyCredited;
|
|
|
|
if ($remaining > 0) {
|
|
RetentionEntry::create([
|
|
'project_id' => $project->id,
|
|
'type' => 'credit',
|
|
'amount' => $remaining,
|
|
'description' => 'Retention released on project completion',
|
|
]);
|
|
}
|
|
}
|
|
}
|