ProjectR Docs
Architecture

Checkout Flow

Cart sync, checkout sessions, and order creation.

Session state machine

prepareCheckout

prepareCheckout (public action → internal) is the tail of the flow:

  1. Inventory validation (unavailable items reported back).
  2. Shopify Storefront cartCreate with cart attributes _betterAuthUserId and _checkoutSessionKey — these flow into the order's note attributes and are the authoritative order→user mapping for rewards.
  3. App-side voucher gate: a voucher-by-code lookup enforces ownership and the allowedChannels restriction; Shopify re-validates discounts at checkout (a non-applicable code only warns). The separate validateDiscount action does the exact-match code lookup with one opaque rejection message (see Shopify Integration).
  4. Insert the checkoutSessions row (prepared, 30-minute expiresAt) 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.

On this page