webmoney/backend/app/Models/Invoice.php
marcoitaloesp-ai 0adb5c889f
feat(subscriptions): sistema de assinaturas SaaS v1.49.0
- Criar tabela plans com Free, Pro Monthly, Pro Annual
- Criar tabela subscriptions com status e integração PayPal
- Criar tabela invoices com numeração sequencial WM-YYYY-NNNNNN
- Models: Plan, Subscription, Invoice com helpers
- User: hasActiveSubscription(), onTrial(), currentPlan(), etc.
- API: GET /api/plans (público)
- Seeder: PlansSeeder com 3 planos base
- Fase 2 do roadmap SaaS concluída
2025-12-17 10:46:34 +00:00

287 lines
7.4 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Invoice extends Model
{
use HasFactory;
const STATUS_DRAFT = 'draft';
const STATUS_OPEN = 'open';
const STATUS_PAID = 'paid';
const STATUS_VOID = 'void';
const STATUS_UNCOLLECTIBLE = 'uncollectible';
const REASON_SUBSCRIPTION_CREATE = 'subscription_create';
const REASON_SUBSCRIPTION_CYCLE = 'subscription_cycle';
const REASON_SUBSCRIPTION_UPDATE = 'subscription_update';
const REASON_MANUAL = 'manual';
protected $fillable = [
'user_id',
'subscription_id',
'number',
'status',
'billing_reason',
'currency',
'subtotal',
'tax',
'tax_percent',
'total',
'description',
'due_date',
'paid_at',
'paypal_payment_id',
'paypal_capture_id',
'paypal_data',
'pdf_path',
'billing_info',
];
protected $casts = [
'due_date' => '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;
}
}