Creates a Stripe Checkout session for the currently authenticated user to purchase the Pro plan. The user's Auth0 ID is passed to Stripe for identification in webhooks.
(current_user: AuthUser, body: CheckoutRequest = None)
| 304 | summary="Create Stripe Checkout Session for New Subscription" |
| 305 | ) |
| 306 | async def create_checkout_session(current_user: AuthUser, body: CheckoutRequest = None): |
| 307 | """ |
| 308 | Creates a Stripe Checkout session for the currently authenticated user to |
| 309 | purchase the Pro plan. The user's Auth0 ID is passed to Stripe for |
| 310 | identification in webhooks. |
| 311 | """ |
| 312 | base_url = get_base_url(body.return_base_url if body else None) |
| 313 | |
| 314 | has_sub, existing_id, provider, active_customer_id = _get_subscription_from_metadata(current_user) |
| 315 | if has_sub: |
| 316 | if provider == "apple": |
| 317 | raise HTTPException( |
| 318 | status_code=400, |
| 319 | detail="You have an active Apple subscription. Please cancel it in iOS Settings before purchasing via Stripe." |
| 320 | ) |
| 321 | elif provider == "stripe" and active_customer_id: |
| 322 | portal = stripe.billing_portal.Session.create( |
| 323 | customer=active_customer_id, |
| 324 | return_url=f"{base_url}/refresh", |
| 325 | ) |
| 326 | return {"url": portal.url, "redirect": "portal"} |
| 327 | |
| 328 | try: |
| 329 | # Get or create Stripe customer with Auth0 email to prevent Link email mismatch |
| 330 | customer_id = get_or_create_stripe_customer(current_user) |
| 331 | |
| 332 | # Check if user has already used a free trial |
| 333 | had_trial = False |
| 334 | if customer_id: |
| 335 | try: |
| 336 | past_subs = stripe.Subscription.list(customer=customer_id, status='all', limit=100) |
| 337 | had_trial = any(sub.trial_end is not None for sub in past_subs.data) |
| 338 | except Exception as e: |
| 339 | logger.warning(f"Failed to check trial history for {current_user.id}: {e}") |
| 340 | |
| 341 | # Check ghost customers — accounts that were deleted and re-registered with the same email |
| 342 | if not had_trial and hasattr(current_user, 'email') and current_user.email: |
| 343 | try: |
| 344 | email_hash = hashlib.sha256(current_user.email.lower().encode()).hexdigest() |
| 345 | ghost_customers = stripe.Customer.list(email=f"{email_hash}@deleted.invalid", limit=100) |
| 346 | for ghost in ghost_customers.data: |
| 347 | past_subs = stripe.Subscription.list(customer=ghost.id, status='all', limit=100) |
| 348 | if any(sub.trial_end is not None for sub in past_subs.data): |
| 349 | had_trial = True |
| 350 | break |
| 351 | except Exception as e: |
| 352 | logger.warning(f"Failed to check ghost trial history for {current_user.id}: {e}") |
| 353 | |
| 354 | checkout_params = { |
| 355 | "line_items": [{"price": PRO_PRICE_ID, "quantity": 1}], |
| 356 | "mode": "subscription", |
| 357 | "client_reference_id": current_user.id, |
| 358 | "success_url": f"{base_url}/upgrade-success", |
| 359 | "cancel_url": base_url, |
| 360 | "allow_promotion_codes": True, |
| 361 | } |
| 362 | |
| 363 | if not had_trial: |
nothing calls this directly
no test coverage detected