Files
GSB-Construction/Modules/FinancialManagement/app/Services/ProgressBillingService.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',
]);
}
}
}