# commerce-on-sx: Catalog, cart, pricing & orders on miniKanren > **DRAFT outline.** The revenue vertical. Depends on `persist-on-sx` (durable > orders) and `flow-on-sx` (checkout as a durable flow). Don't start before > persist-on-sx Phase 1 is green. rose-ash's revenue engine — market (catalog), cart (checkout), orders (SumUp payment, reconciliation) — has no SX subsystem. The hard part of commerce isn't CRUD; it's **pricing**: discounts, bundles, tax, membership rates, promotions that stack (or don't). These are relations, and a relational engine can run them in multiple directions — forward ("what's the total?") and backward ("what promo code yields this total?", "which line item triggered the discount?"). That's a miniKanren fit. Pricing/promotion rules are relational; cart and order *lifecycle* (reserve → pay → fulfil → reconcile) is a durable `flow`; the order ledger is a `persist` stream. Commerce is the first real **composition** subsystem. End-state: a catalog model, a relational pricing/promotion engine, a cart with deterministic totals, and an order lifecycle flow with payment-webhook reconciliation — all auditable via the event log. ## Status (rolling) `bash lib/commerce/conformance.sh` → **132/132** (8 suites: catalog, cart, price, api, promo, stack, quote, ledger) — Phases 1-2 done; Phase 3 ledger done ## Ground rules - **Scope:** only `lib/commerce/**` and `plans/commerce-on-sx.md`. May **import** from `lib/minikanren/`, and (once they exist) `lib/persist/` + `lib/flow/`. Do not edit substrates. - **Architecture:** prices/promotions are miniKanren relations over catalog facts; a cart total is a *deterministic* query result (first solution under a fixed rule order). Order lifecycle is a `flow` that suspends at the payment IO boundary. Money is integer minor units — never floats. - **Determinism:** promotion stacking must have explicit, tested precedence; totals must be reproducible from the cart + catalog snapshot. - **Commits:** one feature per commit. Progress log + tick boxes. ## Architecture sketch ``` Catalog + cart Total / order product(id,price,tags) {:subtotal :discounts :tax :total} │ ▲ ▼ │ lib/commerce/catalog.sx lib/commerce/price.sx — product / variant / stock facts — miniKanren pricing relations │ — promo stacking, membership rates ▼ ▲ lib/commerce/cart.sx lib/commerce/order.sx (flow + store) — line items, quantities — reserve→pay→fulfil→reconcile │ — SumUp webhook = flow resume ▼ │ lib/commerce/api.sx ── (commerce/add) (commerce/total) (commerce/checkout) ──┘ ``` ## Phase 1 — Catalog + cart + deterministic totals - [x] `catalog.sx` — product/variant/stock as facts - [x] `cart.sx` — line items, add/remove/qty - [x] `price.sx` — base pricing relation, subtotal; tax - [x] `api.sx` + tests + scoreboard + conformance.sh ## Phase 2 — Promotions (relational) - [x] promo rules: percentage, fixed, bundle, member rate - [x] explicit stacking precedence; "best price" backward query - [x] tests: stacking order, mutually-exclusive promos, member vs guest ## Phase 3 — Order lifecycle (flow + store) - [ ] order flow: reserve stock → await payment → fulfil - [ ] payment webhook resumes the suspended flow - [x] order ledger as a `persist` stream; idempotent reconciliation ## Phase 4 — Reconciliation + federation - [ ] mismatch detection (paid≠ordered) as queries over the ledger - [ ] cross-instance catalog (federated marketplace) — out-of-scope stub - [ ] tests: webhook replay, partial refund, double-charge guard ## Progress log - 2026-06-07 — `ledger.sx` (Phase 3 piece, checkbox 3): order ledger as a persist event stream "order/". Status/total/paid/recon are projections (folds) over events — ledger is the single source of truth. `order-pay`/ `order-refund` are idempotent via `persist/append-once` keyed on the payment ref, so a replayed SumUp webhook records once (no double-charge). `order-recon-of` classifies :unpaid/:ok/:underpaid/:overpaid on net (paid−refunded) vs total; `ledger-mismatches` finds genuine paid≠ordered across all streams. Verified minikanren+scheme/flow+persist all coexist in one sx_server process. ledger suite 20/20; total 132/132. Next: order flow (reserve→pay→fulfil) as a Scheme flow-on-sx flow with webhook resume (checkboxes 1-2) — needs SX↔Scheme quote marshalling. - 2026-06-07 — `quote.sx` (pricing capstone, bridges Phase 2→3): `cart-quote` composes price+promo+stacking into the deterministic priced quote `{:subtotal :discount :tax :total :codes}` with `total = subtotal - discount + tax`. Explicit tax policy: tax on GROSS per-line amounts (discount reduces payable, not tax base) — documented for the determinism contract. This quote is the value the Phase-3 order flow will carry. quote suite 13/13; total 112/112. - 2026-06-07 — `stack.sx` (**Phase 2 complete**): stacking precedence as a separate selection layer (precedence NOT in the rules, per the miniKanren design rule). Exclusivity = unordered code pairs; `valid-stackings` enumerates every legal subset of applicable promos (powerset ∖ excluded combos); `best-stacking` is the deterministic max-total-discount selection (stable on ties). `stacking-by-totalo` is the best-price backward query ("which legal stacking yields total D?"). Member vs guest falls out of applicable-promos. stack suite 16/16; total 99/99. - 2026-06-07 — `promo.sx` (Phase 2 piece 1): four promo types as tagged tuples `(:percent code class bps)`/`(:fixed code threshold amount)`/`(:bundle code sku n)`/`(:member code class bps)`. Per-promo discount is pure integer arithmetic; `promo-discounto`/`promo-applieso` enumerate (code, amount) relationally — forward ("which apply?") and backward ("which code yields 2000?" → run* over applieso). `applicable-promos`/`promo-amount-for` deterministic helpers. promo amounts via `project` to ground the membero-bound promo. promo suite 17/17; total 83/83. Next: stacking precedence + best-price (stack.sx). - 2026-06-06 — `api.sx` (**Phase 1 complete**): session facade `{:ctx :cart}` with `commerce-add`/`-remove`/`-set-qty`/`-total`/`-count`/ `-lines`, `commerce-can-add?` catalog validation, `commerce-explain` per-line audit breakdown ({:sku :variant :qty :unit :extended :tax}), and a `commerce-checkout` Phase-3 stub. api suite 12/12; total 66/66. - 2026-06-06 — `price.sx`: deterministic `cart-subtotal` (Σ unit×qty, variant delta defaults 0) + jurisdiction-relational tax. `taxo` facts indexed by (jurisdiction, product-class, customer-class)→bps, queried multidirectionally; `apply-bps` rounds half-up with integer arithmetic only. `cart-total` returns `{:subtotal :discounts :tax :total}` (discounts 0 until Phase 2), reproducible from (context, cart). `=` does structural dict equality (order-independent), so total dicts compare directly. price suite 20/20; total 54/54. - 2026-06-06 — `cart.sx`: cart as an ordered list of (sku variant qty) lines. Pure ops `cart-add` (merges same line / appends), `cart-set-qty` (0 removes), `cart-remove`, plus `cart-qty`/`cart-count`/`cart-skus`/`cart-empty?`. `cart-lineo` is the relational view (membero over the cart) — forward and backward. cart suite 18/18; total 34/34. - 2026-06-06 — `catalog.sx`: catalog snapshot (products/variants/stock as fact tuples) + multidirectional accessor relations (`producto`/`varianto`/`stocko`, derived `priceo`/`classo`/`unit-priceo`) + deterministic `catalog-price`/ `-class`/`-has?` helpers. `conformance.sh` harness + scoreboard. catalog suite 16/16. Gotcha: minikanren `run-n` macro binds `s` internally — query vars must avoid `s`; tests compare reified results with `=` (not `equal?`, which fails on reified lists). Money = integer minor units throughout. ## Phase 3 flow-integration notes (for the next iteration) Order flow = checkboxes 1-2 (reserve→pay→fulfil as a flow-on-sx flow + webhook resume). Design is settled; the remaining work is mechanical but slow to iterate. - **flow is the Scheme-on-SX guest layer**, not the SX/minikanren host. Load order: `lib/guest/{lex,reflective/env,reflective/quoting}` + `lib/scheme/{parser, eval,runtime}` + `lib/flow/{spec,store,remote,host,api}`. Confirmed it coexists with the minikanren + persist stacks in one sx_server process. - **Driver API (SX side):** `(flow-make-env)` builds the env once; `(flow-run-in env "")` evaluates a Scheme program string. Flows/driving are all Scheme: `(flow/start flow input)`, `(flow/resume id val)`, `(flow/pending)`, `(flow/status id)`, `(flow/result id)`. Host ABI (host.sx): `(request kind payload)` suspends with a typed envelope; `(flow-host-requests)` lists pending. - **Settled design:** the Scheme flow carries ONLY the order-id (a string) and is pure orchestration: `(defflow ordf (lambda (oid) (begin (request 'reserve oid) (request 'payment oid) (request 'fulfil oid))))`. All IO/ledger work stays in SX — the SX driver services each request by appending to the persist ledger (ledger.sx) and resuming with a marker. Payment stays suspended until the webhook calls flow/resume. Marshalling is trivial (just strings). - **GOTCHA (cost me a turn):** `flow-make-env` returns a large/likely-cyclic env object; returning it from `(eval "...")` makes the harness serializer hang (got exit 0 with NO epoch-2 output). NEVER return the env — wrap as `(begin (define env (flow-make-env)) :ok)`. Structure the flow suite like `lib/flow/conformance.sh`: load once, build env once, run all assertions in ONE process returning small count values. Budget a long timeout (flow's own suite uses 540s); env build is ~150s CPU and balloons under sibling-agent CPU contention. ## Blockers (none)