sessionTtlMinutes = $sessionTtlMinutes ?? (int) config('checkout.session_ttl_minutes', 30); $this->historyRetention = $historyRetention ?? (int) config('checkout.status_history_max', 25); } public function createOrResume(?User $user, Package $package, array $context = []): CheckoutSession { return DB::transaction(function () use ($user, $package, $context) { $existing = $this->findActiveSession($user, $package); if ($existing) { $this->refreshExpiration($existing); return $existing; } $session = new CheckoutSession(); $session->id = (string) Str::uuid(); $session->status = CheckoutSession::STATUS_DRAFT; $session->provider = CheckoutSession::PROVIDER_NONE; $session->user()->associate($user); $session->tenant()->associate($context['tenant'] ?? null); $session->package()->associate($package); $session->package_snapshot = $this->packageSnapshot($package); $session->currency = Arr::get($session->package_snapshot, 'currency', 'EUR'); $session->amount_subtotal = Arr::get($session->package_snapshot, 'price', 0); $session->amount_total = Arr::get($session->package_snapshot, 'price', 0); $session->locale = $context['locale'] ?? app()->getLocale(); $session->expires_at = now()->addMinutes($this->sessionTtlMinutes); $session->status_history = []; $this->appendStatus($session, CheckoutSession::STATUS_DRAFT, 'session_created'); $session->save(); return $session; }); } public function updatePackage(CheckoutSession $session, Package $package): CheckoutSession { return DB::transaction(function () use ($session, $package) { $session->package()->associate($package); $session->package_snapshot = $this->packageSnapshot($package); $session->amount_subtotal = Arr::get($session->package_snapshot, 'price', 0); $session->amount_total = Arr::get($session->package_snapshot, 'price', 0); $session->provider = CheckoutSession::PROVIDER_NONE; $session->status = CheckoutSession::STATUS_DRAFT; $session->stripe_payment_intent_id = null; $session->stripe_customer_id = null; $session->stripe_subscription_id = null; $session->paypal_order_id = null; $session->paypal_subscription_id = null; $session->provider_metadata = []; $session->failure_reason = null; $session->expires_at = now()->addMinutes($this->sessionTtlMinutes); $this->appendStatus($session, CheckoutSession::STATUS_DRAFT, 'package_switched'); $session->save(); return $session; }); } public function selectProvider(CheckoutSession $session, string $provider): CheckoutSession { $provider = strtolower($provider); if (! in_array($provider, [CheckoutSession::PROVIDER_STRIPE, CheckoutSession::PROVIDER_PAYPAL, CheckoutSession::PROVIDER_FREE], true)) { throw new RuntimeException("Unsupported checkout provider [{$provider}]"); } $session->provider = $provider; $session->status = $provider === CheckoutSession::PROVIDER_FREE ? CheckoutSession::STATUS_PROCESSING : CheckoutSession::STATUS_AWAITING_METHOD; $session->failure_reason = null; $session->expires_at = now()->addMinutes($this->sessionTtlMinutes); $this->appendStatus($session, $session->status, 'provider_selected'); $session->save(); return $session; } public function markRequiresCustomerAction(CheckoutSession $session, string $reason = null): CheckoutSession { $session->status = CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION; $session->failure_reason = $reason; $this->appendStatus($session, CheckoutSession::STATUS_REQUIRES_CUSTOMER_ACTION, $reason ?? 'requires_action'); $session->save(); return $session; } public function markProcessing(CheckoutSession $session, array $metadata = []): CheckoutSession { $session->status = CheckoutSession::STATUS_PROCESSING; if (! empty($metadata)) { $session->provider_metadata = array_merge($session->provider_metadata ?? [], $metadata); } $session->failure_reason = null; $this->appendStatus($session, CheckoutSession::STATUS_PROCESSING, 'processing'); $session->save(); return $session; } public function markCompleted(CheckoutSession $session, ?CarbonInterface $completedAt = null): CheckoutSession { $session->status = CheckoutSession::STATUS_COMPLETED; $session->completed_at = $completedAt ?? now(); $session->failure_reason = null; $this->appendStatus($session, CheckoutSession::STATUS_COMPLETED, 'completed'); $session->save(); return $session; } public function markFailed(CheckoutSession $session, string $reason): CheckoutSession { $session->status = CheckoutSession::STATUS_FAILED; $session->failure_reason = $reason; $this->appendStatus($session, CheckoutSession::STATUS_FAILED, $reason); $session->save(); return $session; } public function cancel(CheckoutSession $session, string $reason = 'cancelled'): CheckoutSession { $session->status = CheckoutSession::STATUS_CANCELLED; $session->failure_reason = $reason; $this->appendStatus($session, CheckoutSession::STATUS_CANCELLED, $reason); $session->save(); return $session; } public function refreshExpiration(CheckoutSession $session): CheckoutSession { $session->expires_at = now()->addMinutes($this->sessionTtlMinutes); $session->save(); return $session; } public function attachTenant(CheckoutSession $session, Tenant $tenant): CheckoutSession { $session->tenant()->associate($tenant); $session->save(); return $session; } protected function appendStatus(CheckoutSession $session, string $status, ?string $reason = null): void { $history = $session->status_history ?? []; $history[] = [ 'status' => $status, 'reason' => $reason, 'at' => now()->toIso8601String(), ]; if (count($history) > $this->historyRetention) { $history = array_slice($history, -1 * $this->historyRetention); } $session->status_history = $history; } protected function packageSnapshot(Package $package): array { return [ 'id' => $package->getKey(), 'name' => $package->name, 'type' => $package->type, 'price' => (float) $package->price, 'currency' => $package->currency ?? 'EUR', 'features' => $package->features, 'limits' => $package->limits, ]; } protected function findActiveSession(?User $user, Package $package): ?CheckoutSession { if (! $user) { return null; } return CheckoutSession::query() ->where('user_id', $user->getKey()) ->where('package_id', $package->getKey()) ->active() ->orderByDesc('created_at') ->first(); } }