Skip to main content

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

#EntryNotes
1Steps 1-3 from J-03; selects Card at Step 4Default
2"Cash unavailable mid-flow" auto-route from J-03Card 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 BidiText for 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

OperationEndpointIdempotencyNotes
createPaymentIntentPOST /api/v1/booking/payment-intentX-Idempotency-KeyReturns provider client secret; FX snapshot embedded
confirmBooking (card-authorize)POST /api/v1/booking/confirmX-Idempotency-KeyPersists with paymentStatus = "Authorized"
capturePayment(server-driven on webhook)n/aAsynchronous capture per merchant policy
getReservationGET /api/v1/reservations/:idn/aPolled until confirmed
getReceiptGET /api/v1/reservations/:id/receiptn/aReturns receipt projection

6.2 URL state

  • /book/:step (1..5); /book/4-card and /book/4-3ds are 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

ErrorTriggerUX shownRecoveryTelemetry
PAYMENT_DECLINEDIssuer declinedInline payment form error: "Card declined - try another card or pay cash"; CTA back to method pickerUser retries with different card or switches to cash if enabledfrontend.booking.payment_declined { reasonCode }
THREEDS_ABANDONEDUser cancels 3DS or times outBanner: "Verification cancelled - try again"; intent cleared, can retryUser retriesfrontend.booking.threeds_abandoned
THREEDS_FAILEDIssuer rejectsSame as declinedSamefrontend.booking.threeds_failed
PAYMENT_PROVIDER_DEGRADEDProvider 5xxBanner: "Payment provider degraded - please try in a few minutes or pay cash"; auto-retry onceManual retry; switch to cash if enablederror.surfaced { code }
MELMASTOON.PRICING.FX_DRIFT_>0.5pctFX moved between intent creation and confirmModal: "Price refreshed - please confirm"; new total shown with diffUser confirms or abandonsfrontend.booking.fx_drift { delta }
BOOKING_CONFIRM_TIMEOUTServer-side capture pending > 10 sBanner: "Payment is processing - we'll email you the confirmation"; status poller continues for 60 sWebhook completes asynchronouslyfrontend.booking.confirm_pending
WEBHOOK_DELAYEDCapture webhook delayed > 60 sConfirmation screen with "Payment processing" badge + email expectedAsynchronousfrontend.booking.webhook_delayed

10. E2E Test Gates

  • Composite gate G-WEB-1 step "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

MetricTarget
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-describedby for 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 / _failed
  • frontend.booking.payment_declined { reasonCode }
  • frontend.booking.fx_drift { delta }
  • frontend.booking.confirm_pending / webhook_delayed
  • frontend.booking.confirmation_viewed { reservationId, paymentMethod: card }

Domain events emitted

  • melmastoon.payments.intent.created.v1
  • melmastoon.payments.intent.authorized.v1
  • melmastoon.payments.captured.v1 (or .authorized.v1 per policy)
  • melmastoon.booking.created.v1
  • melmastoon.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.

References