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', ]); } } }