163 lines
5.7 KiB
PHP
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]);
|
|
}
|
|
}
|
|
}
|