SubscriptionCreated::class, 'updated' => SubscriptionUpdated::class, ]; protected $fillable = [ 'workspace_id', 'workspace_package_id', 'gateway', 'gateway_subscription_id', 'gateway_customer_id', 'gateway_price_id', 'status', 'billing_cycle', 'current_period_start', 'current_period_end', 'trial_ends_at', 'cancel_at_period_end', 'cancelled_at', 'cancellation_reason', 'ended_at', 'paused_at', 'pause_count', 'metadata', ]; protected $casts = [ 'current_period_start' => 'datetime', 'current_period_end' => 'datetime', 'trial_ends_at' => 'datetime', 'cancel_at_period_end' => 'boolean', 'cancelled_at' => 'datetime', 'ended_at' => 'datetime', 'paused_at' => 'datetime', 'pause_count' => 'integer', 'metadata' => 'array', ]; // Relationships public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function workspacePackage(): BelongsTo { return $this->belongsTo(WorkspacePackage::class); } public function usageRecords(): HasMany { return $this->hasMany(SubscriptionUsage::class); } public function usageEvents(): HasMany { return $this->hasMany(UsageEvent::class); } // Status helpers public function isActive(): bool { return $this->status === 'active'; } public function isTrialing(): bool { return $this->status === 'trialing'; } public function isPastDue(): bool { return $this->status === 'past_due'; } public function isPaused(): bool { return $this->status === 'paused'; } public function isCancelled(): bool { return $this->status === 'cancelled'; } public function isIncomplete(): bool { return $this->status === 'incomplete'; } /** * Check if the subscription can be paused (hasn't exceeded max pause cycles). */ public function canPause(): bool { if (! config('commerce.subscriptions.allow_pause', true)) { return false; } $maxPauseCycles = config('commerce.subscriptions.max_pause_cycles', 3); return ($this->pause_count ?? 0) < $maxPauseCycles; } /** * Get the number of remaining pause cycles. */ public function remainingPauseCycles(): int { $maxPauseCycles = config('commerce.subscriptions.max_pause_cycles', 3); return max(0, $maxPauseCycles - ($this->pause_count ?? 0)); } public function isValid(): bool { return in_array($this->status, ['active', 'trialing', 'past_due']); } public function onTrial(): bool { return $this->trial_ends_at && $this->trial_ends_at->isFuture(); } public function onGracePeriod(): bool { return $this->cancel_at_period_end && $this->current_period_end->isFuture(); } public function hasEnded(): bool { return $this->ended_at !== null; } // Period helpers public function daysUntilRenewal(): int { return max(0, now()->diffInDays($this->current_period_end, false)); } public function isRenewingSoon(int $days = 7): bool { return $this->daysUntilRenewal() <= $days; } // Actions public function cancel(bool $immediately = false): void { if ($immediately) { $this->update([ 'status' => 'cancelled', 'cancelled_at' => now(), 'ended_at' => now(), ]); } else { $this->update([ 'cancel_at_period_end' => true, 'cancelled_at' => now(), ]); } } public function resume(): void { $this->update([ 'cancel_at_period_end' => false, 'cancelled_at' => null, ]); } public function pause(): void { $this->update(['status' => 'paused']); } public function markPastDue(): void { $this->update(['status' => 'past_due']); } public function renew(\Carbon\Carbon $periodStart, \Carbon\Carbon $periodEnd): void { $this->update([ 'status' => 'active', 'current_period_start' => $periodStart, 'current_period_end' => $periodEnd, ]); } // Scopes public function scopeActive($query) { return $query->whereIn('status', ['active', 'trialing']); } public function scopeValid($query) { return $query->whereIn('status', ['active', 'trialing', 'past_due']); } public function scopeForWorkspace($query, int $workspaceId) { return $query->where('workspace_id', $workspaceId); } public function scopeForGateway($query, string $gateway) { return $query->where('gateway', $gateway); } public function scopeExpiringSoon($query, int $days = 7) { return $query->where('current_period_end', '<=', now()->addDays($days)) ->where('current_period_end', '>', now()); } public function getActivitylogOptions(): LogOptions { return LogOptions::defaults() ->logOnly(['status', 'cancel_at_period_end', 'cancelled_at', 'paused_at']) ->logOnlyDirty() ->dontSubmitEmptyLogs() ->setDescriptionForEvent(fn (string $eventName) => "Subscription {$eventName}"); } }