'decimal:2', 'tax_amount' => 'decimal:2', 'discount_amount' => 'decimal:2', 'total' => 'decimal:2', 'amount_paid' => 'decimal:2', 'amount_due' => 'decimal:2', 'issue_date' => 'date', 'due_date' => 'date', 'paid_at' => 'datetime', 'billing_address' => 'array', 'auto_charge' => 'boolean', 'charge_attempts' => 'integer', 'last_charge_attempt' => 'datetime', 'next_charge_attempt' => 'datetime', 'metadata' => 'array', ]; // Accessors for compatibility /** * Get the issued_at attribute (alias for issue_date). */ public function getIssuedAtAttribute(): ?\Carbon\Carbon { return $this->issue_date; } // Relationships public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function order(): BelongsTo { return $this->belongsTo(Order::class); } public function items(): HasMany { return $this->hasMany(InvoiceItem::class); } public function payment(): BelongsTo { return $this->belongsTo(Payment::class); } public function payments(): HasMany { return $this->hasMany(Payment::class); } // Status helpers public function isDraft(): bool { return $this->status === 'draft'; } public function isSent(): bool { return $this->status === 'sent'; } public function isPaid(): bool { return $this->status === 'paid'; } public function isPending(): bool { return in_array($this->status, ['draft', 'sent', 'pending']); } public function isOverdue(): bool { return $this->status === 'overdue' || ($this->isPending() && $this->due_date && $this->due_date->isPast()); } public function isVoid(): bool { return $this->status === 'void'; } // Actions public function markAsPaid(?Payment $payment = null): void { $data = [ 'status' => 'paid', 'paid_at' => now(), 'amount_paid' => $this->total, 'amount_due' => 0, ]; if ($payment) { $data['payment_id'] = $payment->id; } $this->update($data); } public function markAsVoid(): void { $this->update(['status' => 'void']); } public function send(): void { $this->update(['status' => 'sent']); } // Scopes public function scopePaid($query) { return $query->where('status', 'paid'); } public function scopeUnpaid($query) { return $query->whereIn('status', ['draft', 'sent', 'pending', 'overdue']); } public function scopePending($query) { return $query->whereIn('status', ['draft', 'sent', 'pending']); } public function scopeOverdue($query) { return $query->where(function ($q) { $q->where('status', 'overdue') ->orWhere(function ($q2) { $q2->whereIn('status', ['draft', 'sent', 'pending']) ->where('due_date', '<', now()); }); }); } public function scopeForWorkspace($query, int $workspaceId) { return $query->where('workspace_id', $workspaceId); } // Invoice number generation public static function generateInvoiceNumber(): string { $prefix = config('commerce.billing.invoice_prefix', 'INV-'); $year = now()->format('Y'); // Get the last invoice number for this year $lastInvoice = static::where('invoice_number', 'like', "{$prefix}{$year}-%") ->orderByDesc('id') ->first(); if ($lastInvoice) { $lastNumber = (int) substr($lastInvoice->invoice_number, -4); $nextNumber = $lastNumber + 1; } else { $nextNumber = config('commerce.billing.invoice_start_number', 1000); } return sprintf('%s%s-%04d', $prefix, $year, $nextNumber); } }