Checkout Flow
Cart sync, checkout sessions, and order creation.
Session state machine
prepareCheckout
prepareCheckout (public action → internal) is the tail of the flow:
- Inventory validation (unavailable items reported back).
- Shopify Storefront
cartCreatewith cart attributes_betterAuthUserIdand_checkoutSessionKey— these flow into the order's note attributes and are the authoritative order→user mapping for rewards. - App-side voucher gate: a voucher-by-code lookup enforces ownership and the
allowedChannelsrestriction; Shopify re-validates discounts at checkout (a non-applicable code only warns). The separatevalidateDiscountaction does the exact-match code lookup with one opaque rejection message (see Shopify Integration). - Insert the
checkoutSessionsrow (prepared, 30-minuteexpiresAt) and schedule its expiry job.
Rate limits: prepareCheckout is limited to 15/hour per user; preload-source sessions skip the limit.
No loyalty reservation
Checkout holds nothing from the loyalty program. Points were removed on 2026-10-16, and with them the prepareCheckout reservation and its release on abandon or expiry. Expiring or abandoning a session only closes it in the funnel. The abandon, expire and force-expire mutations still return reservationReleased for older clients, and it is always false. Old session rows may still carry loyaltyPointsReserved and loyaltyReservationStatus until migrations/removePoints:removePointsData unsets them (see Loyalty).
A stamp is earned after the order lands, from the subtotal, through the Order to Rewards workflow. The mobile bag and checkout sheet preview it with the same minimum-spend rule.
The OCC lesson
createCheckoutSessionInternal deliberately does not patch the userCarts doc with the checkout URL. That write shared a hot document with the cart-edit stream and caused hundreds of OCC conflicts in production. The session row itself is the checkout reference — read it from checkoutSessions, never from the cart.