inmake DOCS
Docs / Generative / Commerce cart

Commerce cart

The "commerce-cart" rule fragment — composed into the CRM & Studio agents' prompts and served as drop-core://rules/commerce-cart.

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).
  • items is a {sku: qty} map (positive integers) against BASE product SKUs — products/{id} document ids. A type:"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 into items as 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 set is a server write.
  1. CART CALLS — one route, an action discriminator. set is an UPSERT: omit id to create (the response carries the generated id — store it), send the FULL desired items map (it replaces wholesale), other fields (email, note, promo, uid) merge. Every set refreshes the cart's expiry. get re-reads (404 = expired/checked out — clear the pointer). delete discards.
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);
}
  1. CHECKOUT — send {cart} plus whatever the checkout form collected (address, payment, user, …; the body is an OrderInitEntity subset — finalize/status/number are 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. A denied order KEEPS the cart (fix the items_denied lines and retry). The payment-init payload passes through: for Stripe checkout redirect to order.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.