- Removido README.md padrão do Laravel (backend) - Removidos scripts de deploy (não mais necessários) - Atualizado copilot-instructions.md para novo fluxo - Adicionada documentação de auditoria do servidor - Sincronizado código de produção com repositório Novo workflow: - Trabalhamos diretamente em /root/webmoney (symlink para /var/www/webmoney) - Mudanças PHP são instantâneas - Mudanças React requerem 'npm run build' - Commit após validação funcional
323 lines
8.3 KiB
PHP
Executable File
323 lines
8.3 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Carbon\Carbon;
|
|
|
|
class Subscription extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
const STATUS_PENDING = 'pending';
|
|
const STATUS_TRIALING = 'trialing';
|
|
const STATUS_ACTIVE = 'active';
|
|
const STATUS_PAST_DUE = 'past_due';
|
|
const STATUS_CANCELED = 'canceled';
|
|
const STATUS_EXPIRED = 'expired';
|
|
|
|
protected $fillable = [
|
|
'user_id',
|
|
'plan_id',
|
|
'status',
|
|
'trial_ends_at',
|
|
'current_period_start',
|
|
'current_period_end',
|
|
'canceled_at',
|
|
'ends_at',
|
|
'cancel_reason',
|
|
'paypal_subscription_id',
|
|
'paypal_status',
|
|
'paypal_data',
|
|
'price_paid',
|
|
'currency',
|
|
];
|
|
|
|
protected $casts = [
|
|
'trial_ends_at' => 'datetime',
|
|
'current_period_start' => 'datetime',
|
|
'current_period_end' => 'datetime',
|
|
'canceled_at' => 'datetime',
|
|
'ends_at' => 'datetime',
|
|
'paypal_data' => 'array',
|
|
'price_paid' => 'decimal:2',
|
|
];
|
|
|
|
// ==================== RELATIONSHIPS ====================
|
|
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
public function plan(): BelongsTo
|
|
{
|
|
return $this->belongsTo(Plan::class);
|
|
}
|
|
|
|
public function invoices(): HasMany
|
|
{
|
|
return $this->hasMany(Invoice::class);
|
|
}
|
|
|
|
// ==================== SCOPES ====================
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->whereIn('status', [self::STATUS_TRIALING, self::STATUS_ACTIVE]);
|
|
}
|
|
|
|
public function scopeTrialing($query)
|
|
{
|
|
return $query->where('status', self::STATUS_TRIALING);
|
|
}
|
|
|
|
public function scopeCanceled($query)
|
|
{
|
|
return $query->where('status', self::STATUS_CANCELED);
|
|
}
|
|
|
|
public function scopeExpired($query)
|
|
{
|
|
return $query->where('status', self::STATUS_EXPIRED);
|
|
}
|
|
|
|
public function scopeForUser($query, int $userId)
|
|
{
|
|
return $query->where('user_id', $userId);
|
|
}
|
|
|
|
// ==================== STATUS CHECKS ====================
|
|
|
|
public function isActive(): bool
|
|
{
|
|
return in_array($this->status, [self::STATUS_TRIALING, self::STATUS_ACTIVE]);
|
|
}
|
|
|
|
public function isTrialing(): bool
|
|
{
|
|
return $this->status === self::STATUS_TRIALING;
|
|
}
|
|
|
|
public function isCanceled(): bool
|
|
{
|
|
return $this->status === self::STATUS_CANCELED;
|
|
}
|
|
|
|
public function isExpired(): bool
|
|
{
|
|
return $this->status === self::STATUS_EXPIRED;
|
|
}
|
|
|
|
public function isPastDue(): bool
|
|
{
|
|
return $this->status === self::STATUS_PAST_DUE;
|
|
}
|
|
|
|
public function onGracePeriod(): bool
|
|
{
|
|
return $this->canceled_at !== null
|
|
&& $this->ends_at !== null
|
|
&& $this->ends_at->isFuture();
|
|
}
|
|
|
|
public function hasTrialEnded(): bool
|
|
{
|
|
return $this->trial_ends_at !== null
|
|
&& $this->trial_ends_at->isPast();
|
|
}
|
|
|
|
public function isOnTrial(): bool
|
|
{
|
|
return $this->status === self::STATUS_TRIALING
|
|
&& $this->trial_ends_at !== null
|
|
&& $this->trial_ends_at->isFuture();
|
|
}
|
|
|
|
// ==================== ACCESSORS ====================
|
|
|
|
public function getDaysUntilTrialEndsAttribute(): ?int
|
|
{
|
|
if (!$this->trial_ends_at) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->trial_ends_at->isPast()) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) now()->diffInDays($this->trial_ends_at);
|
|
}
|
|
|
|
public function getDaysUntilRenewalAttribute(): ?int
|
|
{
|
|
if (!$this->current_period_end) {
|
|
return null;
|
|
}
|
|
|
|
if ($this->current_period_end->isPast()) {
|
|
return 0;
|
|
}
|
|
|
|
return (int) now()->diffInDays($this->current_period_end);
|
|
}
|
|
|
|
public function getStatusLabelAttribute(): string
|
|
{
|
|
return match ($this->status) {
|
|
self::STATUS_TRIALING => 'En prueba',
|
|
self::STATUS_ACTIVE => 'Activa',
|
|
self::STATUS_PAST_DUE => 'Pago pendiente',
|
|
self::STATUS_CANCELED => 'Cancelada',
|
|
self::STATUS_EXPIRED => 'Expirada',
|
|
default => $this->status,
|
|
};
|
|
}
|
|
|
|
public function getStatusColorAttribute(): string
|
|
{
|
|
return match ($this->status) {
|
|
self::STATUS_TRIALING => 'info',
|
|
self::STATUS_ACTIVE => 'success',
|
|
self::STATUS_PAST_DUE => 'warning',
|
|
self::STATUS_CANCELED => 'secondary',
|
|
self::STATUS_EXPIRED => 'danger',
|
|
default => 'secondary',
|
|
};
|
|
}
|
|
|
|
// ==================== METHODS ====================
|
|
|
|
/**
|
|
* Start a trial for this subscription
|
|
*/
|
|
public function startTrial(int $days): self
|
|
{
|
|
$this->status = self::STATUS_TRIALING;
|
|
$this->trial_ends_at = now()->addDays($days);
|
|
$this->current_period_start = now();
|
|
$this->current_period_end = now()->addDays($days);
|
|
$this->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Activate the subscription (after trial or payment)
|
|
*/
|
|
public function activate(?Carbon $periodEnd = null): self
|
|
{
|
|
$this->status = self::STATUS_ACTIVE;
|
|
$this->trial_ends_at = null;
|
|
$this->current_period_start = now();
|
|
|
|
if ($periodEnd) {
|
|
$this->current_period_end = $periodEnd;
|
|
} elseif ($this->plan->billing_period === 'monthly') {
|
|
$this->current_period_end = now()->addMonth();
|
|
} elseif ($this->plan->billing_period === 'annual') {
|
|
$this->current_period_end = now()->addYear();
|
|
}
|
|
|
|
$this->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Cancel the subscription
|
|
*/
|
|
public function cancel(?string $reason = null, bool $immediately = false): self
|
|
{
|
|
$this->canceled_at = now();
|
|
$this->cancel_reason = $reason;
|
|
|
|
if ($immediately) {
|
|
$this->status = self::STATUS_CANCELED;
|
|
$this->ends_at = now();
|
|
} else {
|
|
// Keep active until period ends (grace period)
|
|
$this->ends_at = $this->current_period_end;
|
|
}
|
|
|
|
$this->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Mark as expired
|
|
*/
|
|
public function markAsExpired(): self
|
|
{
|
|
$this->status = self::STATUS_EXPIRED;
|
|
$this->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Renew the subscription for another period
|
|
*/
|
|
public function renew(): self
|
|
{
|
|
$this->current_period_start = $this->current_period_end ?? now();
|
|
|
|
if ($this->plan->billing_period === 'monthly') {
|
|
$this->current_period_end = $this->current_period_start->copy()->addMonth();
|
|
} elseif ($this->plan->billing_period === 'annual') {
|
|
$this->current_period_end = $this->current_period_start->copy()->addYear();
|
|
}
|
|
|
|
$this->status = self::STATUS_ACTIVE;
|
|
$this->canceled_at = null;
|
|
$this->ends_at = null;
|
|
$this->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Swap to a different plan
|
|
*/
|
|
public function swap(Plan $newPlan): self
|
|
{
|
|
$this->plan_id = $newPlan->id;
|
|
$this->price_paid = $newPlan->price;
|
|
$this->save();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Create a subscription for a user
|
|
*/
|
|
public static function createForUser(User $user, Plan $plan): self
|
|
{
|
|
$subscription = new self([
|
|
'user_id' => $user->id,
|
|
'plan_id' => $plan->id,
|
|
'status' => self::STATUS_TRIALING,
|
|
'price_paid' => $plan->price,
|
|
'currency' => $plan->currency,
|
|
]);
|
|
|
|
if ($plan->is_free) {
|
|
$subscription->status = self::STATUS_ACTIVE;
|
|
$subscription->current_period_start = now();
|
|
$subscription->current_period_end = null; // Never expires
|
|
} elseif ($plan->has_trial) {
|
|
$subscription->trial_ends_at = now()->addDays($plan->trial_days);
|
|
$subscription->current_period_start = now();
|
|
$subscription->current_period_end = now()->addDays($plan->trial_days);
|
|
}
|
|
|
|
$subscription->save();
|
|
|
|
return $subscription;
|
|
}
|
|
}
|