J-04 — Booking with Card Payment (3DS + FX snapshot)
One-liner: A guest pays by card in their preferred currency, completes 3DS challenge if required, and receives a confirmation with the FX snapshot disclosed.
1. Purpose
Guest completes the same 5-step booking as J-03 but pays by card in their preferred currency, possibly going through a 3-D Secure challenge, and receives a confirmation that explicitly discloses the FX snapshot used. Outcome: reservation in confirmed state with paymentStatus = "Captured" (or "Authorized" per merchant policy), payment receipt + voucher.
2. Persona Context
- Persona: Guest.
- Surfaces: Tenant Booking Web / Tenant Booking Mobile.
- Primary BFF:
bff-tenant-booking-service. - Backing services:
booking-service,pricing-service(FX),payments-service,payment-providers-adapter(Stripe / HBL / etc.),notification-service. - Preconditions: Tenant has at least one active card payment method configured; FX feed is fresh (<= 24 h staleness).
- Trigger: At Step 4, guest selects "Card" as the payment method (instead of Cash).
3. Entry Points
| # | Entry | Notes |
|---|---|---|
| 1 | Steps 1-3 from J-03; selects Card at Step 4 | Default |
| 2 | "Cash unavailable mid-flow" auto-route from J-03 | Card pre-selected |
4. Screen-by-Screen Flow
Steps 1-3 are identical to J-03 §4.1-4.3. Steps 4-5 differ:
4.4 BookingFlowStep4PaymentCard
- Layout: Read-only review block + payment-method picker with Card pre-selected; provider's PCI-compliant payment element (Stripe Elements / equivalent) embedded; currency display with FX disclosure (e.g., "1 USD = 70.50 AFN, snapshot at 18:30 GMT"); "Pay
" CTA; cancellation policy reiterated. - Components:
BookingReviewCard,PaymentProviderElement,FxSnapshotChip,PolicyHighlightCard,Button(variant=emphasis). - Offline: Disabled; clear banner: "You need to be online to pay".
- AI: None on this step in P1; P2 may surface fraud-risk indicators (HITL only).
- Errors: See §9 — provider element raises immediate validation; declines, 3DS abandonment, FX drift handled discretely.
- Loading: "Pay" button spinner; intent creation <= 1.5 s p95; 3DS challenge handed off to provider.
- A11y: Provider element is iframe-based and inherits font tokens via provider config; field labels exposed; we wrap with our own labels and
aria-describedby. Confirm button announces state changes. - RTL: Wrapping form mirrors; provider iframe respects locale; FX chip is LTR (numeric) wrapped with
BidiTextfor safety. - Perf: Intent creation <= 1.5 s p95; 3DS hand-off opens within 1 s of decision.
- Telemetry:
frontend.booking.payment_method_selected { method: card };frontend.booking.payment_intent_created { paymentId };frontend.booking.threeds_started.
4.5 BookingFlowStep5ConfirmationCardScreen
- Layout: Same as J-03 §4.5 plus payment receipt block (last4, brand, amount in display currency + tenant currency, FX snapshot, payment provider name, transaction id).
- Components:
ConfirmationCard,PaymentReceiptBlock,VoucherDownloadButton,WalletPassButton(P2). - Offline: Reachable from local cache (saved on success); voucher cached.
- AI: None.
- Errors: Webhook delayed -> banner "Payment processing - we'll email you when finalised"; status auto-refreshes.
- Loading: Once shown, instantaneous; voucher button shows spinner during PDF generation.
- A11y: Payment receipt block is a structured group with
<dl>; amounts announced in correct locale. - RTL: Layout mirrors; transaction id LTR.
- Perf: Confirmation paint <= 500 ms after payment confirmed.
- Telemetry:
frontend.booking.confirmation_viewed { reservationId, paymentMethod: card }.
5. State Machine
6. Data Requirements
6.1 Server state
| Operation | Endpoint | Idempotency | Notes |
|---|---|---|---|
createPaymentIntent | POST /api/v1/booking/payment-intent | X-Idempotency-Key | Returns provider client secret; FX snapshot embedded |
confirmBooking (card-authorize) | POST /api/v1/booking/confirm | X-Idempotency-Key | Persists with paymentStatus = "Authorized" |
capturePayment | (server-driven on webhook) | n/a | Asynchronous capture per merchant policy |
getReservation | GET /api/v1/reservations/:id | n/a | Polled until confirmed |
getReceipt | GET /api/v1/reservations/:id/receipt | n/a | Returns receipt projection |
6.2 URL state
/book/:step(1..5);/book/4-cardand/book/4-3dsare sub-routes of step 4 for analytics; refreshing during 3DS does not lose state (provider handles).
6.3 Local persistence
- Draft preserved per
holdId(same as J-03). - Card details NEVER persisted client-side; provider holds the PCI scope.
- Receipt cached after success.
6.4 Idempotency
- Per-payment-attempt idempotency key created at intent creation; replays return same intent.
- Confirm replays return same reservation.
7. AI Behavior
n/a in Phase 1. Phase 2 may add:
- Fraud-risk indicator on the
PaymentReceiptBlock(HITL: visible to GM via desktop, not the guest). - Card-issuance friction predictor to pre-emptively suggest alternate methods.
8. Offline Behavior
- Card payment requires online; CTA disabled with banner if offline.
- 3DS requires online; if connectivity drops mid-3DS, provider returns recoverable error and we show "Connection lost - retry payment".
- Confirmation screen reachable offline from cache once payment successful.
9. Error States
| Error | Trigger | UX shown | Recovery | Telemetry |
|---|---|---|---|---|
PAYMENT_DECLINED | Issuer declined | Inline payment form error: "Card declined - try another card or pay cash"; CTA back to method picker | User retries with different card or switches to cash if enabled | frontend.booking.payment_declined { reasonCode } |
THREEDS_ABANDONED | User cancels 3DS or times out | Banner: "Verification cancelled - try again"; intent cleared, can retry | User retries | frontend.booking.threeds_abandoned |
THREEDS_FAILED | Issuer rejects | Same as declined | Same | frontend.booking.threeds_failed |
PAYMENT_PROVIDER_DEGRADED | Provider 5xx | Banner: "Payment provider degraded - please try in a few minutes or pay cash"; auto-retry once | Manual retry; switch to cash if enabled | error.surfaced { code } |
MELMASTOON.PRICING.FX_DRIFT_>0.5pct | FX moved between intent creation and confirm | Modal: "Price refreshed - please confirm"; new total shown with diff | User confirms or abandons | frontend.booking.fx_drift { delta } |
BOOKING_CONFIRM_TIMEOUT | Server-side capture pending > 10 s | Banner: "Payment is processing - we'll email you the confirmation"; status poller continues for 60 s | Webhook completes asynchronously | frontend.booking.confirm_pending |
WEBHOOK_DELAYED | Capture webhook delayed > 60 s | Confirmation screen with "Payment processing" badge + email expected | Asynchronous | frontend.booking.webhook_delayed |
10. E2E Test Gates
- Composite gate
G-WEB-1step "5-step booking (card + 3DS) -> confirmation". - Card-decline -> alternate-card retry recovery.
- 3DS abandonment recovery.
- FX drift > 0.5% requires re-confirmation.
- Webhook delayed branch shows correct UX.
11. Performance Requirements
| Metric | Target |
|---|---|
| Intent creation RTT | <= 1.5 s p95 |
| 3DS challenge open | <= 1 s after decision |
| Confirm + capture RTT (no 3DS) | <= 2 s p95 |
| Confirmation paint after success | <= 500 ms |
| Webhook delivery -> confirmation update | <= 60 s p95 |
12. Accessibility Requirements
- Provider iframe inherits typography tokens via provider config; we expose surrounding labels with
aria-describedbyfor inline errors. - Currency + FX chip is announced as a group with provenance ("Snapshot at 18:30 GMT").
- 3DS challenge is announced when launched; focus moves to challenge frame.
- Confirmation page receipt block is a
<dl>with explicit term/definition relationships.
13. Telemetry
Frontend events
frontend.booking.payment_method_selected { method: card }frontend.booking.payment_intent_created { paymentId }frontend.booking.threeds_started/_completed/_abandoned/_failedfrontend.booking.payment_declined { reasonCode }frontend.booking.fx_drift { delta }frontend.booking.confirm_pending/webhook_delayedfrontend.booking.confirmation_viewed { reservationId, paymentMethod: card }
Domain events emitted
melmastoon.payments.intent.created.v1melmastoon.payments.intent.authorized.v1melmastoon.payments.captured.v1(or.authorized.v1per policy)melmastoon.booking.created.v1melmastoon.notifications.receipt.sent.v1
14. Success Criteria
- Card payment success path completes <= 8 s p95 incl. 3DS.
- FX snapshot is visible on confirmation screen and in receipt PDF.
- Decline path retries do not double-charge (idempotency holds).
- 3DS abandonment retains hold and lets user retry.
- Webhook-delayed branch never strands the guest; confirmation arrives by email <= 5 min p95.
- Card details never appear in client logs or telemetry.