'datetime', 'paid_at' => 'datetime', 'subtotal' => 'decimal:2', 'tax' => 'decimal:2', 'tax_percent' => 'decimal:2', 'total' => 'decimal:2', 'paypal_data' => 'array', 'billing_info' => 'array', ]; // ==================== BOOT ==================== protected static function boot() { parent::boot(); static::creating(function ($invoice) { if (empty($invoice->number)) { $invoice->number = static::generateNumber(); } }); } // ==================== RELATIONSHIPS ==================== public function user(): BelongsTo { return $this->belongsTo(User::class); } public function subscription(): BelongsTo { return $this->belongsTo(Subscription::class); } // ==================== SCOPES ==================== public function scopePaid($query) { return $query->where('status', self::STATUS_PAID); } public function scopeOpen($query) { return $query->where('status', self::STATUS_OPEN); } public function scopeForUser($query, int $userId) { return $query->where('user_id', $userId); } public function scopeRecent($query) { return $query->orderBy('created_at', 'desc'); } // ==================== STATUS CHECKS ==================== public function isPaid(): bool { return $this->status === self::STATUS_PAID; } public function isOpen(): bool { return $this->status === self::STATUS_OPEN; } public function isDraft(): bool { return $this->status === self::STATUS_DRAFT; } public function isVoid(): bool { return $this->status === self::STATUS_VOID; } public function isOverdue(): bool { return $this->status === self::STATUS_OPEN && $this->due_date && $this->due_date->isPast(); } // ==================== ACCESSORS ==================== public function getStatusLabelAttribute(): string { return match ($this->status) { self::STATUS_DRAFT => 'Borrador', self::STATUS_OPEN => 'Pendiente', self::STATUS_PAID => 'Pagada', self::STATUS_VOID => 'Anulada', self::STATUS_UNCOLLECTIBLE => 'Incobrable', default => $this->status, }; } public function getStatusColorAttribute(): string { return match ($this->status) { self::STATUS_DRAFT => 'secondary', self::STATUS_OPEN => 'warning', self::STATUS_PAID => 'success', self::STATUS_VOID => 'dark', self::STATUS_UNCOLLECTIBLE => 'danger', default => 'secondary', }; } public function getFormattedTotalAttribute(): string { $symbols = [ 'EUR' => '€', 'USD' => '$', 'BRL' => 'R$', ]; $symbol = $symbols[$this->currency] ?? $this->currency; return $symbol . number_format($this->total, 2, ',', '.'); } public function getBillingReasonLabelAttribute(): string { return match ($this->billing_reason) { self::REASON_SUBSCRIPTION_CREATE => 'Nueva suscripción', self::REASON_SUBSCRIPTION_CYCLE => 'Renovación', self::REASON_SUBSCRIPTION_UPDATE => 'Cambio de plan', self::REASON_MANUAL => 'Manual', default => $this->billing_reason, }; } // ==================== METHODS ==================== /** * Generate a unique invoice number * Format: WM-YYYY-NNNNNN */ public static function generateNumber(): string { $year = now()->year; $prefix = "WM-{$year}-"; $lastInvoice = static::where('number', 'like', "{$prefix}%") ->orderBy('number', 'desc') ->first(); if ($lastInvoice) { $lastNumber = (int) substr($lastInvoice->number, strlen($prefix)); $newNumber = $lastNumber + 1; } else { $newNumber = 1; } return $prefix . str_pad($newNumber, 6, '0', STR_PAD_LEFT); } /** * Mark invoice as paid */ public function markAsPaid(?string $paypalPaymentId = null, ?string $paypalCaptureId = null): self { $this->status = self::STATUS_PAID; $this->paid_at = now(); if ($paypalPaymentId) { $this->paypal_payment_id = $paypalPaymentId; } if ($paypalCaptureId) { $this->paypal_capture_id = $paypalCaptureId; } $this->save(); return $this; } /** * Mark invoice as void */ public function markAsVoid(): self { $this->status = self::STATUS_VOID; $this->save(); return $this; } /** * Calculate and set totals */ public function calculateTotals(float $subtotal, float $taxPercent = 0): self { $this->subtotal = $subtotal; $this->tax_percent = $taxPercent; $this->tax = round($subtotal * ($taxPercent / 100), 2); $this->total = $this->subtotal + $this->tax; return $this; } /** * Create invoice for a subscription */ public static function createForSubscription( Subscription $subscription, string $billingReason = self::REASON_SUBSCRIPTION_CYCLE, ?string $description = null ): self { $plan = $subscription->plan; $user = $subscription->user; $invoice = new self([ 'user_id' => $user->id, 'subscription_id' => $subscription->id, 'status' => self::STATUS_DRAFT, 'billing_reason' => $billingReason, 'currency' => $subscription->currency, 'description' => $description ?? "{$plan->name} - " . now()->format('F Y'), 'due_date' => now()->addDays(7), ]); // Calculate with VAT (21% for EU) $invoice->calculateTotals($plan->price, 21); // Store billing info snapshot $invoice->billing_info = [ 'name' => $user->full_name, 'email' => $user->email, 'phone' => $user->full_phone, 'country' => $user->country, ]; $invoice->save(); return $invoice; } }