Parent PRD
#317
What to build
A customer paying with a 3D Secure-required card (e.g. Stripe test card 4000 0038 0000 0200) completes the checkout through Stripe's 3DS challenge and ends with a paid order, exactly as in Slice 1's happy path.
The vertical user journey:
- Customer reaches /checkout (anonymous or account, no subscription gate). They fill the form, select shipping, enter card details.
- They click "Place Order".
EPStripePayment captures a confirmation token and calls placeOrder.
- Server runs
createCartPaymentIntent({ confirm: true, confirmation_token }). Stripe responds with requires_action because of 3DS.
- Server's
handlePay detects the requires_action status, stores the action data into session.payment.actionData, leaves session.status as open, leaves session.payment.status as requires_action. No order is created at this point. Returns the session.
- Client-side
EPStripePayment inspects the response, calls stripe.handleNextAction({ clientSecret }). Stripe's 3DS modal/redirect flow handles the bank challenge.
- On success, client calls a new
resumePayment refAction, which POSTs /api/checkout/sessions/current/resume-payment.
- Server's new
handleResumePayment re-reads the PI from EP. If it's now succeeded, runs the same tail as Slice 1: checkoutApi → confirmOrder → cart cleanup. If still pending or failed, returns the appropriate error.
- Customer sees order confirmation; the cart is empty. Same end state as a non-3DS Slice 1 checkout.
This slice also retires handleConfirm (legacy from the old session model) by aliasing it to handleResumePayment for backwards compatibility, then deprecating it. useCheckoutSession.confirmPayment() is marked deprecated and forwards to resumePayment.
Cuts through every layer:
- Package — handler.
handlePay gains a requires_action branch: stores action data, returns early without creating an order. New handleResumePayment handler completes the post-3DS flow. handleConfirm aliased to handleResumePayment and marked deprecated.
- Package — adapter.
Cart Payment Intent Adapter extended to map the requires_action response into a PaymentAdapterResult with the action data passed through.
- Package — component.
EPStripePayment inspects the placeOrder response. On requires_action, runs stripe.handleNextAction({ clientSecret }) (where the client secret is in the action data). On Stripe success, calls resumePayment refAction. On Stripe failure or user cancel, surfaces the error and leaves the session retryable.
- Package — session provider.
EPCheckoutSessionProvider exposes a new resumePayment refAction. confirmPayment refAction kept and aliased to resumePayment for backwards compatibility.
- Host — routes. New route file
app/api/checkout/sessions/current/resume-payment/route.ts mounting handleResumePayment.
- Tests. Unit tests for the
requires_action mapping in the adapter. Handler tests for the requires_action branch and handleResumePayment (success, still-pending, failed sub-cases). Integration test for /resume-payment end-to-end. Component test for EPStripePayment's 3DS handling, including failure and cancel cases.
See parent PRD §Implementation Decisions → "Single-shot checkout flow" and the 3DS notes in §Further Notes.
Acceptance criteria
Blocked by
User stories addressed
Reference by number from the parent PRD:
- User story 9
- User story 23
- User story 24
Parent PRD
#317
What to build
A customer paying with a 3D Secure-required card (e.g. Stripe test card
4000 0038 0000 0200) completes the checkout through Stripe's 3DS challenge and ends with a paid order, exactly as in Slice 1's happy path.The vertical user journey:
EPStripePaymentcaptures a confirmation token and callsplaceOrder.createCartPaymentIntent({ confirm: true, confirmation_token }). Stripe responds withrequires_actionbecause of 3DS.handlePaydetects therequires_actionstatus, stores the action data intosession.payment.actionData, leavessession.statusasopen, leavessession.payment.statusasrequires_action. No order is created at this point. Returns the session.EPStripePaymentinspects the response, callsstripe.handleNextAction({ clientSecret }). Stripe's 3DS modal/redirect flow handles the bank challenge.resumePaymentrefAction, which POSTs/api/checkout/sessions/current/resume-payment.handleResumePaymentre-reads the PI from EP. If it's nowsucceeded, runs the same tail as Slice 1:checkoutApi→confirmOrder→ cart cleanup. If still pending or failed, returns the appropriate error.This slice also retires
handleConfirm(legacy from the old session model) by aliasing it tohandleResumePaymentfor backwards compatibility, then deprecating it.useCheckoutSession.confirmPayment()is marked deprecated and forwards toresumePayment.Cuts through every layer:
handlePaygains arequires_actionbranch: stores action data, returns early without creating an order. NewhandleResumePaymenthandler completes the post-3DS flow.handleConfirmaliased tohandleResumePaymentand marked deprecated.Cart Payment Intent Adapterextended to map therequires_actionresponse into aPaymentAdapterResultwith the action data passed through.EPStripePaymentinspects theplaceOrderresponse. Onrequires_action, runsstripe.handleNextAction({ clientSecret })(where the client secret is in the action data). On Stripe success, callsresumePaymentrefAction. On Stripe failure or user cancel, surfaces the error and leaves the session retryable.EPCheckoutSessionProviderexposes a newresumePaymentrefAction.confirmPaymentrefAction kept and aliased toresumePaymentfor backwards compatibility.app/api/checkout/sessions/current/resume-payment/route.tsmountinghandleResumePayment.requires_actionmapping in the adapter. Handler tests for therequires_actionbranch andhandleResumePayment(success, still-pending, failed sub-cases). Integration test for/resume-paymentend-to-end. Component test forEPStripePayment's 3DS handling, including failure and cancel cases.See parent PRD §Implementation Decisions → "Single-shot checkout flow" and the 3DS notes in §Further Notes.
Acceptance criteria
handlePayreturns HTTP 200 with a session inpayment.status === "requires_action"when EP responds withrequires_action. No EP order is created at this point.session.payment.actionDatacarries the data needed forstripe.handleNextAction(typically aclient_secret).handleResumePaymentre-reads the PaymentIntent status from EP and proceeds withcheckoutApi→confirmOrder→ cart cleanup when the PI issucceeded.handleResumePaymentreturns a typed error and leaves the session retryable when the PI is stillrequires_action,requires_payment_method, or otherwise not succeeded.EPStripePaymentautomatically runsstripe.handleNextActionwhen theplaceOrderresponse carriesrequires_action, then calls theresumePaymentrefAction.EPStripePaymentsurfaces the error to the designer (via$ctx.stripePaymentData.error) when the user cancels the 3DS challenge or it fails.POST /api/checkout/sessions/current/resume-paymentis mounted and routes through the standard request adapter.useCheckoutSession.confirmPayment()continues to work (aliased toresumePayment) and emits a deprecation warning in development builds.4000 0038 0000 0200, sees the 3DS challenge modal, completes it successfully, and lands on the order confirmation; the resulting EP order is paid; the cart is empty.Blocked by
User stories addressed
Reference by number from the parent PRD: