tenant; $user = $session->user; if (! $tenant && $user) { $tenant = $this->ensureTenant($user, $session); } if (! $tenant) { Log::warning('Checkout assignment skipped: missing tenant', ['session' => $session->id]); return; } $package = $session->package; if (! $package) { Log::warning('Checkout assignment skipped: missing package', ['session' => $session->id]); return; } $metadata = $session->provider_metadata ?? []; $consents = [ 'accepted_terms_at' => optional($session->accepted_terms_at)->toIso8601String(), 'accepted_privacy_at' => optional($session->accepted_privacy_at)->toIso8601String(), 'accepted_withdrawal_notice_at' => optional($session->accepted_withdrawal_notice_at)->toIso8601String(), 'digital_content_waiver_at' => optional($session->digital_content_waiver_at)->toIso8601String(), 'legal_version' => $session->legal_version, ]; $consents = array_filter($consents); $providerReference = $options['provider_reference'] ?? $metadata['paddle_transaction_id'] ?? null ?? $metadata['paddle_checkout_id'] ?? null ?? CheckoutSession::PROVIDER_FREE; $providerName = $options['provider'] ?? $session->provider ?? ($metadata['paddle_transaction_id'] ?? $metadata['paddle_checkout_id'] ? CheckoutSession::PROVIDER_PADDLE : null) ?? CheckoutSession::PROVIDER_FREE; $totals = $this->resolvePaddleTotals($session, $options['payload'] ?? []); $currency = $totals['currency'] ?? $session->currency ?? $package->currency ?? 'EUR'; $price = array_key_exists('total', $totals) ? $totals['total'] : (float) $session->amount_total; $purchase = PackagePurchase::updateOrCreate( [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'provider_id' => $providerReference, ], [ 'provider' => $providerName, 'price' => round($price, 2), 'type' => $package->type === 'reseller' ? 'reseller_subscription' : 'endcustomer_event', 'purchased_at' => now(), 'metadata' => array_filter([ 'payload' => $options['payload'] ?? null, 'checkout_session_id' => $session->id, 'consents' => $consents ?: null, 'paddle_totals' => $totals !== [] ? $totals : null, 'currency' => $currency, ], static fn ($value) => $value !== null && $value !== ''), ] ); if ($package->type === 'reseller') { $tenantPackage = null; if ($purchase->wasRecentlyCreated) { $tenantPackage = TenantPackage::create([ 'tenant_id' => $tenant->id, 'package_id' => $package->id, 'price' => round($price, 2), 'active' => true, 'purchased_at' => now(), 'expires_at' => null, 'used_events' => 0, ]); } } else { $tenantPackage = TenantPackage::updateOrCreate( [ 'tenant_id' => $tenant->id, 'package_id' => $package->id, ], [ 'price' => round($price, 2), 'active' => true, 'purchased_at' => now(), 'expires_at' => $this->resolveExpiry($package, $tenant), ] ); } if ($package->type !== 'reseller') { $tenant->forceFill([ 'subscription_status' => 'active', 'subscription_expires_at' => $tenantPackage->expires_at, ])->save(); } if ($user && $user->pending_purchase) { $this->activateUser($user); } if ($user) { $mailLocale = $user->preferred_locale ?? app()->getLocale(); if ($purchase->wasRecentlyCreated) { Mail::to($user) ->locale($mailLocale) ->queue(new PurchaseConfirmation($purchase)); $opsEmail = config('mail.ops_address'); if ($opsEmail) { Notification::route('mail', $opsEmail)->notify(new PurchaseCreated($purchase)); } } AbandonedCheckout::query() ->where('user_id', $user->id) ->where('package_id', $package->id) ->where('converted', false) ->update([ 'converted' => true, 'reminder_stage' => 'converted', ]); } Log::info('Checkout session assigned', [ 'session' => $session->id, 'tenant' => $tenant->id, 'package' => $package->id, 'purchase' => $purchase->id, ]); }); } protected function ensureTenant(User $user, CheckoutSession $session): ?Tenant { if ($user->tenant) { if (! $user->tenant_id) { $user->forceFill(['tenant_id' => $user->tenant->getKey()])->save(); } return $user->tenant; } $tenant = Tenant::create([ 'user_id' => $user->id, 'name' => $session->package_snapshot['name'] ?? $user->name, 'slug' => Str::slug(($user->name ?: $user->email).' '.now()->timestamp), 'email' => $user->email, 'contact_email' => $user->email, 'is_active' => true, 'is_suspended' => false, 'subscription_tier' => 'free', 'subscription_status' => 'active', 'settings' => [ 'contact_email' => $user->email, ], ]); if ($user->tenant_id !== $tenant->id) { $user->forceFill(['tenant_id' => $tenant->id])->save(); } event(new Registered($user)); return $tenant; } protected function resolveExpiry(Package $package, Tenant $tenant) { if ($package->type === 'reseller') { return null; } return now()->addYear(); } protected function activateUser(User $user): void { $user->forceFill([ 'email_verified_at' => $user->email_verified_at ?? now(), 'role' => $user->role === 'user' ? 'tenant_admin' : $user->role, 'pending_purchase' => false, ])->save(); } /** * @param array $payload * @return array{currency?: string, subtotal?: float, discount?: float, tax?: float, total?: float} */ protected function resolvePaddleTotals(CheckoutSession $session, array $payload): array { $metadataTotals = $session->provider_metadata['paddle_totals'] ?? null; if (is_array($metadataTotals) && $metadataTotals !== []) { return $metadataTotals; } $totals = Arr::get($payload, 'details.totals', Arr::get($payload, 'totals', [])); if (! is_array($totals) || $totals === []) { return []; } $currency = Arr::get($totals, 'currency_code') ?? Arr::get($payload, 'currency_code') ?? Arr::get($totals, 'currency') ?? Arr::get($payload, 'currency'); $subtotal = $this->convertMinorAmount(Arr::get($totals, 'subtotal.amount', $totals['subtotal'] ?? null)); $discount = $this->convertMinorAmount(Arr::get($totals, 'discount.amount', $totals['discount'] ?? null)); $tax = $this->convertMinorAmount(Arr::get($totals, 'tax.amount', $totals['tax'] ?? null)); $total = $this->convertMinorAmount( Arr::get( $totals, 'total.amount', $totals['total'] ?? Arr::get($totals, 'grand_total.amount', $totals['grand_total'] ?? null) ) ); return array_filter([ 'currency' => $currency ? strtoupper((string) $currency) : null, 'subtotal' => $subtotal, 'discount' => $discount, 'tax' => $tax, 'total' => $total, ], static fn ($value) => $value !== null); } protected function convertMinorAmount(mixed $value): ?float { if ($value === null || $value === '') { return null; } if (is_array($value) && isset($value['amount'])) { $value = $value['amount']; } if (! is_numeric($value)) { return null; } return round(((float) $value) / 100, 2); } }