'boolean', 'legacy_data' => 'array', 'provider_data' => 'array', 'metadata' => 'array', 'trial_ends_at' => 'datetime', 'ends_at' => 'datetime', 'cancelled_at' => 'datetime', 'paused_at' => 'datetime', 'resumed_at' => 'datetime', 'synced_at' => 'datetime', 'last_provider_sync' => 'datetime', 'starts_at' => 'datetime', ]; /** * Accessor for total coupon discount */ protected function getTotalCouponDiscountAttribute(): float { // Use preloaded sum if available, otherwise calculate it if (array_key_exists('total_coupon_discount', $this->attributes)) { return (float) $this->attributes['total_coupon_discount']; } return $this->couponUsages()->sum('discount_amount'); } protected $dates = [ 'trial_ends_at', 'ends_at', 'cancelled_at', 'paused_at', 'resumed_at', 'synced_at', 'last_provider_sync', 'starts_at', ]; public function user() { return $this->belongsTo(User::class); } public function plan() { return $this->belongsTo(Plan::class); } public function trialExtensions() { return $this->hasMany(TrialExtension::class); } public function subscriptionChanges() { return $this->hasMany(SubscriptionChange::class); } public function couponUsages() { return $this->hasMany(CouponUsage::class); } /** * Check if subscription is active */ public function isActive(): bool { return in_array($this->status, ['active', 'trialing']) && (! $this->ends_at || $this->ends_at->isFuture()); } /** * Check if subscription is on trial */ public function isOnTrial(): bool { return $this->status === 'trialing' && $this->trial_ends_at && $this->trial_ends_at->isFuture(); } /** * Check if subscription is cancelled */ public function isCancelled(): bool { return $this->status === 'cancelled' || ($this->ends_at && $this->ends_at->isPast()); } /** * Check if subscription supports recurring payments */ public function isRecurring(): bool { return $this->plan && $this->plan->monthly_billing; } /** * Get the display name for the provider */ public function getProviderDisplayName(): string { $displayNames = [ 'stripe' => 'Stripe', 'lemon_squeezy' => 'Lemon Squeezy', 'polar' => 'Polar.sh', 'oxapay' => 'OxaPay', 'crypto' => 'Crypto', 'activation_key' => 'Activation Key', ]; return $displayNames[$this->provider] ?? ucfirst($this->provider); } /** * Get provider-specific data */ public function getProviderData(?string $key = null, $default = null) { if ($key) { return data_get($this->provider_data, $key, $default); } return $this->provider_data; } /** * Set provider-specific data */ public function setProviderData(string $key, $value): void { $data = $this->provider_data ?? []; data_set($data, $key, $value); $this->provider_data = $data; } /** * Sync subscription status with provider */ public function syncWithProvider(): bool { try { $orchestrator = app(PaymentOrchestrator::class); $result = $orchestrator->syncSubscriptionStatus($this); $this->update([ 'status' => $result['status'] ?? $this->status, 'provider_data' => array_merge($this->provider_data ?? [], $result), 'last_provider_sync' => now(), ]); Log::info('Subscription synced with provider', [ 'subscription_id' => $this->id, 'provider' => $this->provider, 'status' => $result['status'] ?? 'unknown', ]); return true; } catch (\Exception $e) { Log::error('Failed to sync subscription with provider', [ 'subscription_id' => $this->id, 'provider' => $this->provider, 'error' => $e->getMessage(), ]); return false; } } /** * Cancel the subscription */ public function cancel(string $reason = ''): bool { try { $orchestrator = app(PaymentOrchestrator::class); $result = $orchestrator->cancelSubscription($this, $reason); if ($result) { $this->update([ 'status' => 'cancelled', 'cancelled_at' => now(), 'cancellation_reason' => $reason, ]); } return $result; } catch (\Exception $e) { Log::error('Failed to cancel subscription', [ 'subscription_id' => $this->id, 'provider' => $this->provider, 'error' => $e->getMessage(), ]); return false; } } /** * Update subscription plan */ public function updatePlan(Plan $newPlan): bool { try { $orchestrator = app(PaymentOrchestrator::class); $result = $orchestrator->updateSubscription($this, $newPlan); if ($result['success']) { $this->update([ 'plan_id' => $newPlan->id, 'updated_at' => now(), ]); } return $result['success']; } catch (\Exception $e) { Log::error('Failed to update subscription plan', [ 'subscription_id' => $this->id, 'provider' => $this->provider, 'error' => $e->getMessage(), ]); return false; } } /** * Get subscription metadata */ public function getMetadata(?string $key = null, $default = null) { if ($key) { return data_get($this->metadata, $key, $default); } return $this->metadata; } /** * Set subscription metadata */ public function setMetadata(string $key, $value): void { $data = $this->metadata ?? []; data_set($data, $key, $value); $this->metadata = $data; } /** * Scope: Active subscriptions */ public function scopeActive($query) { return $query->where('status', 'active') ->where(function ($q) { $q->whereNull('ends_at')->orWhere('ends_at', '>', now()); }); } /** * Scope: Cancelled subscriptions */ public function scopeCancelled($query) { return $query->where('status', 'cancelled') ->where(function ($q) { $q->whereNull('ends_at')->orWhere('ends_at', '<=', now()); }); } /** * Scope: On trial subscriptions */ public function scopeOnTrial($query) { return $query->where('status', 'trialing') ->where('trial_ends_at', '>', now()); } /** * Scope: By provider */ public function scopeByProvider($query, string $provider) { return $query->where('provider', $provider); } /** * Scope: By user */ public function scopeByUser($query, $userId) { return $query->where('user_id', $userId); } /** * Scope: With total coupon discount */ public function scopeWithTotalCouponDiscount($query) { return $query->withSum('couponUsages as total_coupon_discount', 'discount_amount'); } /** * Extend trial period */ public function extendTrial(int $days, string $reason = '', string $extensionType = 'manual', ?User $grantedBy = null): TrialExtension { $originalEnd = $this->trial_ends_at; $newEnd = $originalEnd ? $originalEnd->copy()->addDays($days) : now()->addDays($days); $extension = $this->trialExtensions()->create([ 'user_id' => $this->user_id, 'extension_days' => $days, 'reason' => $reason, 'extension_type' => $extensionType, 'original_trial_ends_at' => $originalEnd, 'new_trial_ends_at' => $newEnd, 'granted_at' => now(), 'granted_by_admin_id' => $grantedBy?->id, ]); // Update the subscription's trial end date $this->update(['trial_ends_at' => $newEnd]); // Record the change SubscriptionChange::createRecord( $this, 'pause', "Trial extended by {$days} days", ['trial_ends_at' => $originalEnd?->format('Y-m-d H:i:s')], ['trial_ends_at' => $newEnd->format('Y-m-d H:i:s')], $reason ); return $extension; } /** * Get total trial extensions granted */ public function getTotalTrialExtensionsDays(): int { return $this->trialExtensions()->sum('extension_days'); } /** * Get latest trial extension */ public function getLatestTrialExtension(): ?TrialExtension { return $this->trialExtensions()->latest()->first(); } /** * Check if trial was extended */ public function hasExtendedTrial(): bool { return $this->trialExtensions()->exists(); } /** * Apply coupon to subscription */ public function applyCoupon(Coupon $coupon, float $amount): CouponUsage { if (! $coupon->isValid($this->user)) { throw new \Exception('Coupon is not valid for this user'); } return $coupon->applyToSubscription($this, $amount); } /** * Get total discount from coupons */ public function getTotalCouponDiscount(): float { return $this->couponUsages()->sum('discount_amount'); } /** * Record subscription change */ public function recordChange( string $changeType, string $description, ?array $oldValues = null, ?array $newValues = null, ?string $reason = null ): SubscriptionChange { return SubscriptionChange::createRecord( $this, $changeType, $description, $oldValues, $newValues, $reason ); } /** * Get pending changes */ public function getPendingChanges(): \Illuminate\Database\Eloquent\Collection { return $this->subscriptionChanges()->pending()->get(); } /** * Process pending changes */ public function processPendingChanges(): int { $pending = $this->getPendingChanges(); $processedCount = 0; foreach ($pending as $change) { $change->markAsProcessed(); $processedCount++; } return $processedCount; } }