110 lines
3.4 KiB
PHP
110 lines
3.4 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',
|
|
'invoice_date', 'due_date', 'notes',
|
|
'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',
|
|
'invoice_date' => 'date',
|
|
'due_date' => 'date',
|
|
'submitted_at' => 'datetime',
|
|
'approved_at' => 'datetime',
|
|
'sent_at' => 'datetime',
|
|
'paid_at' => 'datetime',
|
|
];
|
|
}
|
|
|
|
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]);
|
|
}
|
|
}
|
|
}
|