Files

163 lines
5.7 KiB
PHP

<?php
namespace Modules\FinancialManagement\Models;
use App\Traits\HasPublicIdentifier;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Modules\ApprovalWorkflow\Traits\HasApprovable;
use Modules\FinancialManagement\Enums\InvoiceStatus;
use Modules\ProjectManagement\Models\Project;
use Modules\ApprovalWorkflow\Models\ApprovalChain;
use Modules\FinancialManagement\Services\ProgressBillingService;
class FinancialInvoice extends Model
{
use HasApprovable, HasPublicIdentifier;
protected $table = 'financial_invoices';
protected $fillable = [
'project_id', 'invoice_number', 'status',
'subtotal', 'retention_amount', 'total_amount', 'paid_amount',
'retention_rate', 'billed_percentage',
'penalty_rate', 'penalty_amount', 'penalty_reason',
'penalty_applied_by', 'penalty_applied_at',
'invoice_date', 'due_date', 'notes',
'payment_proof_path', 'payment_proof_name', 'payment_proof_notes',
'payment_proof_submitted_by', 'payment_proof_submitted_at',
'submitted_at', 'approved_at', 'sent_at', 'paid_at',
];
protected function casts(): array
{
return [
'status' => 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]);
}
}
}