route('app.welcome.persona'); } public function processing(Request $request): Response|RedirectResponse { if (config('trypost.self_hosted')) { return redirect()->route('app.calendar'); } $user = $request->user(); $account = $user->accountOrFail(); $sessionId = $request->string('session_id')->toString(); // Consume the checkout session once: `fromCheckout` is true only the first // time this session_id is seen, so a back-button/refresh to the success URL // can't re-fire `checkout.completed`. `Cache::add` is atomic — it returns // true only when the key didn't exist yet. $fromCheckout = filled($sessionId) && Cache::add("checkout_tracked:{$sessionId}", true, now()->addDay()); $subscriptionActive = $account->subscribed(Account::SUBSCRIPTION_NAME); $redirectToOnboarding = $user->isAccountOwner() && $account->isOnboardingOpen(); return Inertia::render('billing/Processing', [ 'subscriptionActive' => $subscriptionActive, 'fromCheckout' => $fromCheckout, 'redirectToOnboarding' => $redirectToOnboarding, 'persona' => $user->persona?->value, 'conversion' => $fromCheckout && $account->stripe_id ? fn () => $this->buildConversionData($account, $sessionId) : null, ]); } /** * @return array{value: float, currency: string, transaction_id: string}|null */ private function buildConversionData(Account $account, string $sessionId): ?array { try { $session = $account->stripe()->checkout->sessions->retrieve( $sessionId, ['expand' => ['line_items.data.price']], ); } catch (Throwable) { return null; } if (data_get($session, 'customer') !== $account->stripe_id) { return null; } $unitAmount = data_get($session, 'line_items.data.0.price.unit_amount'); $currency = data_get($session, 'line_items.data.0.price.currency'); $transactionId = data_get($session, 'id'); if (! is_int($unitAmount) || ! is_string($currency) || ! is_string($transactionId)) { return null; } return [ 'value' => $unitAmount / 100, 'currency' => strtoupper($currency), 'transaction_id' => $transactionId, ]; } public function index(Request $request): Response|RedirectResponse { if (config('trypost.self_hosted')) { return redirect()->route('app.calendar'); } $account = $request->user()->account; abort_unless($request->user()->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN); $subscription = $account->subscription(Account::SUBSCRIPTION_NAME); return Inertia::render('settings/account/Billing', [ 'hasSubscription' => $account->subscribed(Account::SUBSCRIPTION_NAME), 'onTrial' => $account->isOnTrial(), 'trialEndsAt' => $account->activeTrialEndsAt(), 'subscription' => $subscription?->only([ 'stripe_status', 'ends_at', ]), 'plan' => $account->plan, 'workspaceCount' => $account->workspaces()->count(), 'invoices' => $account->invoices()->map(fn ($invoice) => [ 'id' => $invoice->id, 'date' => $invoice->date(), 'total' => $invoice->total(), 'status' => $invoice->status, 'invoice_pdf' => $invoice->invoice_pdf, ]), 'defaultPaymentMethod' => $account->displayablePaymentMethod(), ]); } public function swapToYearly(Request $request): RedirectResponse { if (config('trypost.self_hosted')) { return redirect()->route('app.calendar'); } $account = $request->user()->account; abort_unless($request->user()->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN); abort_unless($account->subscribed(Account::SUBSCRIPTION_NAME), SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY, 'No active subscription'); $plan = $account->plan; $yearlyPriceId = $plan?->stripe_yearly_price_id; abort_if($yearlyPriceId === null, SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY, 'No annual price configured'); $subscription = $account->subscription(Account::SUBSCRIPTION_NAME); abort_if($subscription === null, SymfonyResponse::HTTP_UNPROCESSABLE_ENTITY, 'No active subscription'); if ($subscription->stripe_price === $yearlyPriceId) { return redirect()->route('app.billing.index'); } $authorization = Gate::inspect('swapPlan', [$account]); if ($authorization->denied()) { return back()->with('flash.error', $authorization->message()); } $subscription->swap($yearlyPriceId); return redirect()->route('app.billing.index') ->with('flash.success', __('billing.flash.switched_to_yearly')); } public function portal(Request $request): RedirectResponse { if (config('trypost.self_hosted')) { return redirect()->route('app.calendar'); } $account = $request->user()->account; abort_unless($request->user()->isAccountOwner(), SymfonyResponse::HTTP_FORBIDDEN); return $account->redirectToBillingPortal( route('app.billing.index') ); } }