CART & CHECKOUT (storefront): the cart is a SERVER document the browser only points at — store/{sid}/carts/{cartId} — reached through two reserved storefront routes injected on every rendered page: POST ${DROP_BASE}/cart/${DROP_LOCALE} (cart state) and POST ${DROP_BASE}/checkout/${DROP_LOCALE} (turn it into an order). No store token, no prices and no stock logic ever live in the browser.
INVARIANTS — violating any of these breaks the platform contract:
- Carts are INERT: writing one touches no stock, fires no hooks, mints no customer. Stock is reserved ONLY at checkout. Never "reserve" by creating orders from cart UI — an abandoned order holds stock; an abandoned cart holds nothing and self-expires (store-configurable retention, default 30 days).
itemsis a{sku: qty}map (positive integers) against BASE product SKUs —products/{id}document ids. Atype:"group"parent's own SKU is DENIED at checkout: add-to-cart on a variant chooser page must always use the SELECTED child's id (see the variants section of readRule("commerce-products")). An ADD-ON picked on a product page (additions— gift wrap, insurance, engraving) is its own SKU: put the addition's id intoitemsas a separate line ({"sadba-rajcat": 2, "gift-wrap": 1}), never fold its price into the host's line (see the additions section of the same rule).- NEVER materialize prices client-side. The cart stores quantities; totals shown in cart UI are display-only (bind the propagated
{{price.price}}values); the authoritative pricing/stock/coupon resolution happens inside checkout on the server. - The cart id is the ONLY client-side state: keep it in localStorage and mirror items optimistically for instant UI, but treat the server response as truth. DEBOUNCE cart syncs (~1–2 s trailing) — every
setis a server write.
- CART CALLS — one route, an
actiondiscriminator.setis an UPSERT: omitidto create (the response carries the generated id — store it), send the FULL desireditemsmap (it replaces wholesale), other fields (email,note,promo,uid) merge. Everysetrefreshes the cart's expiry.getre-reads (404 = expired/checked out — clear the pointer).deletediscards.
var CART_KEY = 'drop_cart_id';
var pending;
function syncCart(items) { // items = the FULL {sku: qty} map
clearTimeout(pending);
pending = setTimeout(async function () {
var res = await fetch(window.DROP_BASE + '/cart/' + window.DROP_LOCALE, {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({action: 'set', id: localStorage.getItem(CART_KEY) || undefined, items: items}),
});
if (res.status === 404) { localStorage.removeItem(CART_KEY); return; }
if (res.ok) localStorage.setItem(CART_KEY, (await res.json()).id);
}, 1200);
}- CHECKOUT — send
{cart}plus whatever the checkout form collected (address,payment,user, …; the body is an OrderInitEntity subset —finalize/status/numberare stripped server-side). The server loads the cart, runs the real pipeline (stock preview → confirmed/denied, coupon + credit resolution, payment init) and DELETES the cart on a confirmed checkout — clear the localStorage pointer when a confirmed order comes back. Adeniedorder KEEPS the cart (fix theitems_deniedlines and retry). The payment-init payload passes through: for Stripe checkout redirect toorder.payment.service_intent.session.
var res = await fetch(window.DROP_BASE + '/checkout/' + window.DROP_LOCALE, {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({cart: localStorage.getItem(CART_KEY), payment: 'stripe_checkout', address: address}),
});
var order = await res.json();
if (res.ok && order.status === 'confirmed') {
localStorage.removeItem(CART_KEY);
if (order.payment && order.payment.service_intent && order.payment.service_intent.session) location.href = order.payment.service_intent.session;
}
if (order.status === 'denied') { /* out of stock (order.items_denied) — the cart SURVIVES; adjust items and retry */ }ANON → LOGIN MERGE: a logged-in customer's cart is keyed by their customer id server-side (identity-token calls are forced onto it). On login, get the anon cart, set its items onto the customer cart (id = the customer uid or via the identity token), then delete the anon cart and drop the localStorage pointer.
RESPONSES are JSON always (no-store): 200 the cart/order, 400 {error, message} (validation — surface message in the cart UI), 404 missing/expired cart. Bad SKUs surface at checkout as denied with the offending ids in items_denied.