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