'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; } }