arch-atlas

CheckoutController

controllers/checkout_controller.dart · Notifier<CheckoutState>

domain

Overview

Drives the entire checkout pipeline. Holds a CartService + ProvisionService directly (no DI). Each step (init, address, shipping, payment) mutates CheckoutState via copyWith and exposes a 'shallow-cart safeguard' that refetches the full cart if Medusa returns a stripped response.

User Flow

  1. initCheckout: createPhysicalCart → addItemToCart × N → getCart → savePendingCheckout → fetchPaymentProviders + fetchShippingOptions → ProvisionService.getProvisionQuestions/Policy.
  2. updateShippingAddress: updateCart with shipping_address + email, then fetchShippingOptions for that address.
  3. setShippingOption / setPaymentProvider: store choice in state (paymentProvider does NOT call backend).
  4. applyPromoCode / removePromoCode: deferred to CartService; re-fetch full cart on shallow response.
  5. createPaymentSession: createPaymentCollection → updatePaymentCollectionAmount if the cart total drifted → createPaymentSession(providerId) → pull clientSecret out of payment_sessions list.
  6. confirmStripePayment: validate required questions → updateCart with booking_answers → init+present PaymentSheet → completeCart → addOrderToHistory → clear local cart entries → clearPendingCheckout.

Cases & Edge Cases

  • Shallow-cart safeguard repeats after every mutating call (updateCart, addShippingMethod, applyPromoCode, removePromoCode) because Medusa sometimes returns the cart without items inlined; we detect by 'previous had items, response has none' and refetch.
  • Payment-collection amount drift is reconciled by comparing the existing collection amount with (cart.total * 100) cents and PATCH-ing if needed.
  • Booking-question validation runs IN confirmStripePayment, not earlier — required because answers can change up until the moment the user taps Pay.
  • StripeException.FailureCode.Canceled is treated as a soft outcome (snack 'Payment Cancelled') rather than an error toast.
  • PaymentSheet flow is wrapped in try/catch with a Stripe-specific branch — non-Stripe errors fall through to a generic 'Error: $e' snack.
  • TODO in updateShippingAddress: email is hard-coded to 'user@example.com'; this currently breaks email receipts for production users.

Code References

  • lib/src/features/shared/checkout/controllers/checkout_controller.dart:16 // CheckoutController
  • lib/src/features/shared/checkout/controllers/checkout_controller.dart:27 // initCheckout
  • lib/src/features/shared/checkout/controllers/checkout_controller.dart:243 // createPaymentSession (amount sync)
  • lib/src/features/shared/checkout/controllers/checkout_controller.dart:308 // confirmStripePayment
  • lib/src/features/shared/checkout/controllers/checkout_controller.dart:437 // checkoutControllerProvider

API Calls

Empty — fill this in later. Open the manifest and add content here.

Notes

Side-effect-heavy method (confirmStripePayment) takes BuildContext and a callback — keep it untestable in isolation; tests are integration-only for now.