{"record":{"id":"e4d3060c5931906f","repo":"TryGhost/Ghost","slug":"latest-stripe-checkout-session-does-not-include-a-e4d306","errorCode":null,"errorMessage":"Latest Stripe checkout session does not include a success URL","messagePattern":"Latest Stripe checkout session does not include a success URL","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"e2e/helpers/playwright/flows/offers.ts","lineNumber":66,"sourceCode":"}\n\nexport async function completeOfferSignupViaPortal(page: Page, stripe: StripeTestService, opts?: {emailAddress?: string; name?: string}): Promise<{emailAddress: string; name: string}> {\n    const offerPage = new PortalOfferPage(page);\n    const emailAddress = opts?.emailAddress ?? `test${faker.string.uuid()}@ghost.org`;\n    const name = opts?.name ?? faker.person.fullName();\n\n    await offerPage.fillAndSubmit(emailAddress, name);\n    await offerPage.continueIfVisible();\n\n    const fakeCheckoutPage = new FakeStripeCheckoutPage(page);\n    await fakeCheckoutPage.waitUntilLoaded();\n    await stripe.completeLatestSubscriptionCheckout({name});\n\n    const latestCheckoutSession = stripe.getCheckoutSessions().at(-1);\n    const successUrl = latestCheckoutSession?.response.success_url;\n\n    if (!successUrl) {\n        throw new Error('Latest Stripe checkout session does not include a success URL');\n    }\n\n    await page.goto(successUrl);\n\n    return {emailAddress, name};\n}\n\nexport async function redeemOfferViaPortal(page: Page, stripe: StripeTestService, opts?: {\n    emailAddress?: string;\n    name?: string;\n}): Promise<{\n    accountPage: PortalAccountPage;\n    emailAddress: string;\n    name: string;\n    subscription: MemberSubscription;\n}> {\n    const membersService = new MembersService(page.request);\n    const {emailAddress, name} = await completeOfferSignupViaPortal(page, stripe, opts);","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/TryGhost/Ghost/blob/47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe/e2e/helpers/playwright/flows/offers.ts#L48-L84","documentation":"Identical guard to the donations flow, thrown inside `redeemOfferViaStripeCheckout` after `stripe.completeLatestSubscriptionCheckout`. The offer-redeeming subscription checkout must yield a `success_url` so the browser can navigate to the confirmation. Failure means no session was captured or the captured session omits `success_url`.","triggerScenarios":"The offer redemption flow submits `FakeStripeCheckoutPage`, then `completeLatestSubscriptionCheckout` runs, but `getCheckoutSessions()` is empty or the last session's `response.success_url` is missing.","commonSituations":"Offer was not created with a valid Stripe price/product, so no real checkout session is generated; the fake Stripe server webhook for `checkout.session.completed` did not register the session; subscription checkout path differs from one-off payment path and the mock did not record it.","solutions":["Verify the offer is backed by a synced Stripe tier/product (see the tiers sync flow) before redemption.","Confirm `fakeCheckoutPage.waitUntilLoaded()` returned and `completeLatestSubscriptionCheckout` recorded a session.","Inspect `stripe.getCheckoutSessions().length` to determine whether the issue is zero sessions or a missing field.","Check the Stripe test service is capturing subscription-mode checkouts, not just payment-mode."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"const sessions = stripe.getCheckoutSessions();\nif (!sessions.length || !sessions.at(-1)?.response?.success_url) {\n    throw new Error(`Offer checkout not recorded (${sessions.length} sessions)`);\n}","typeGuard":"function isCheckoutSessionWithSuccessUrl(s: unknown): s is {response: {success_url: string}} {\n    return !!s && typeof (s as any).response?.success_url === 'string';\n}","tryCatchPattern":null,"preventionTips":["Sync the offer's tier to Stripe before attempting redemption.","Verify the fake checkout page fully loaded before calling `completeLatestSubscriptionCheckout`.","Confirm the Stripe mock captures subscription-mode (not just payment-mode) checkouts."],"tags":["stripe","e2e","checkout","offers","subscriptions"],"backgroundTag":null,"analyzedSha":"47d8b0e2ad2fd4757d3bc45f46c3ac165ff8a1fe","analyzedAt":"2026-08-13T01:25:26.651Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}