php-commerce/Models/Invoice.php
Snider 8f27fe85c3 refactor: update Tenant module imports after namespace migration
Updates all references from Core\Mod\Tenant to Core\Tenant following
the monorepo separation. The Tenant module now lives in its own package
with the simplified namespace.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:39:12 +00:00

243 lines
5.9 KiB
PHP

<?php
namespace Core\Mod\Commerce\Models;
use Core\Tenant\Models\Workspace;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Invoice model representing a billing document.
*
* @property int $id
* @property int $workspace_id
* @property int|null $order_id
* @property string $invoice_number
* @property string $status
* @property string $currency
* @property float $subtotal
* @property float $tax_amount
* @property float $discount_amount
* @property float $total
* @property float $amount_paid
* @property float $amount_due
* @property \Carbon\Carbon $issue_date
* @property \Carbon\Carbon $due_date
* @property \Carbon\Carbon|null $paid_at
* @property string|null $billing_name
* @property array|null $billing_address
* @property string|null $tax_id
* @property string|null $pdf_path
*/
class Invoice extends Model
{
use HasFactory;
protected static function newFactory(): \Core\Mod\Commerce\Database\Factories\InvoiceFactory
{
return \Core\Mod\Commerce\Database\Factories\InvoiceFactory::new();
}
protected $fillable = [
'workspace_id',
'order_id',
'payment_id',
'invoice_number',
'status',
'currency',
'subtotal',
'tax_amount',
'tax_rate',
'tax_country',
'discount_amount',
'total',
'amount_paid',
'amount_due',
'issue_date',
'due_date',
'paid_at',
'billing_name',
'billing_email',
'billing_address',
'tax_id',
'pdf_path',
'auto_charge',
'charge_attempts',
'last_charge_attempt',
'next_charge_attempt',
'metadata',
];
protected $casts = [
'subtotal' => '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);
}
}