InvoiceStatus::class, 'subtotal' => 'decimal:2', 'retention_amount' => 'decimal:2', 'total_amount' => 'decimal:2', 'paid_amount' => 'decimal:2', 'retention_rate' => 'decimal:2', 'billed_percentage' => 'decimal:2', 'penalty_rate' => 'decimal:2', 'penalty_amount' => 'decimal:2', 'penalty_applied_at' => 'datetime', 'invoice_date' => 'date', 'due_date' => 'date', 'payment_proof_submitted_at' => 'datetime', 'submitted_at' => 'datetime', 'approved_at' => 'datetime', 'sent_at' => 'datetime', 'paid_at' => 'datetime', ]; } protected static function booted(): void { static::saved(function (FinancialInvoice $invoice) { if ($invoice->status === InvoiceStatus::Paid && (float) $invoice->retention_amount > 0) { $totalDue = (float) $invoice->retention_amount + (float) ($invoice->penalty_amount ?? 0); $credit = RetentionEntry::where([ 'project_id' => $invoice->project_id, 'invoice_id' => $invoice->id, 'type' => 'credit', ])->first(); $desc = (float) ($invoice->penalty_amount ?? 0) > 0 ? "Retention & Late Penalty Remittance Settled & Confirmed for Invoice #{$invoice->invoice_number}" : "10% Retention Remittance Settled & Confirmed for Invoice #{$invoice->invoice_number}"; if (!$credit) { RetentionEntry::create([ 'project_id' => $invoice->project_id, 'invoice_id' => $invoice->id, 'type' => 'credit', 'status' => 'paid', 'amount' => $totalDue, 'description' => $desc, ]); } elseif ((float) $credit->amount !== $totalDue) { $credit->update([ 'amount' => $totalDue, 'status' => 'paid', 'description' => $desc, ]); } } }); } public function penaltyAppliedBy(): BelongsTo { return $this->belongsTo(\App\Models\User::class, 'penalty_applied_by'); } public function paymentProofSubmittedBy(): BelongsTo { return $this->belongsTo(\App\Models\User::class, 'payment_proof_submitted_by'); } public function project(): BelongsTo { return $this->belongsTo(Project::class); } public function lineItems(): HasMany { return $this->hasMany(InvoiceLineItem::class, 'invoice_id')->orderBy('sort_order'); } public function retentionEntries(): HasMany { return $this->hasMany(RetentionEntry::class, 'invoice_id'); } public function transitionTo(InvoiceStatus $newStatus): void { $allowed = $this->status->allowedTransitions(); if (!in_array($newStatus, $allowed)) { throw new \InvalidArgumentException( "Cannot transition invoice from {$this->status->label()} to {$newStatus->label()}" ); } $updates = ['status' => $newStatus]; match ($newStatus) { InvoiceStatus::Submitted => $updates['submitted_at'] = now(), InvoiceStatus::Approved => $updates['approved_at'] = now(), InvoiceStatus::Sent => $updates['sent_at'] = now(), InvoiceStatus::Paid => $updates['paid_at'] = now(), default => null, }; $this->update($updates); } public function getBalanceDueAttribute(): float { return (float) $this->total_amount - (float) $this->paid_amount; } public function getIsOverdueAttribute(): bool { return $this->due_date && $this->due_date->isPast() && !in_array($this->status, [InvoiceStatus::Paid]); } public function onApprovalCompleted(ApprovalChain $chain): void { if ($chain->status->value === 'approved') { $this->update([ 'status' => InvoiceStatus::Approved, 'approved_at' => now(), ]); app(ProgressBillingService::class)->holdRetention($this); } elseif ($chain->status->value === 'rejected') { $this->update(['status' => InvoiceStatus::Rejected]); } } }