ledger.sx — each order is an append-only persist stream "order/<id>"; status/total/paid/recon are folds over events (ledger = 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. order-recon-of classifies unpaid/ok/underpaid/overpaid on net vs total; ledger-mismatches finds genuine paid != ordered across streams. minikanren+scheme/flow+persist verified coexisting in one process. Total 132/132 across 8 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8.1 KiB
commerce-on-sx: Catalog, cart, pricing & orders on miniKanren
DRAFT outline. The revenue vertical. Depends on
persist-on-sx(durable orders) andflow-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/**andplans/commerce-on-sx.md. May import fromlib/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
flowthat 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
catalog.sx— product/variant/stock as factscart.sx— line items, add/remove/qtyprice.sx— base pricing relation, subtotal; taxapi.sx+ tests + scoreboard + conformance.sh
Phase 2 — Promotions (relational)
- promo rules: percentage, fixed, bundle, member rate
- explicit stacking precedence; "best price" backward query
- 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
- order ledger as a
persiststream; 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-refundare idempotent viapersist/append-oncekeyed on the payment ref, so a replayed SumUp webhook records once (no double-charge).order-recon-ofclassifies :unpaid/:ok/:underpaid/:overpaid on net (paid−refunded) vs total;ledger-mismatchesfinds 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-quotecomposes 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-stackingsenumerates every legal subset of applicable promos (powerset ∖ excluded combos);best-stackingis the deterministic max-total-discount selection (stable on ties).stacking-by-totalois 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-appliesoenumerate (code, amount) relationally — forward ("which apply?") and backward ("which code yields 2000?" → run* over applieso).applicable-promos/promo-amount-fordeterministic helpers. promo amounts viaprojectto 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}withcommerce-add/-remove/-set-qty/-total/-count/-lines,commerce-can-add?catalog validation,commerce-explainper-line audit breakdown ({:sku :variant :qty :unit :extended :tax}), and acommerce-checkoutPhase-3 stub. api suite 12/12; total 66/66. - 2026-06-06 —
price.sx: deterministiccart-subtotal(Σ unit×qty, variant delta defaults 0) + jurisdiction-relational tax.taxofacts indexed by (jurisdiction, product-class, customer-class)→bps, queried multidirectionally;apply-bpsrounds half-up with integer arithmetic only.cart-totalreturns{: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 opscart-add(merges same line / appends),cart-set-qty(0 removes),cart-remove, pluscart-qty/cart-count/cart-skus/cart-empty?.cart-lineois 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, derivedpriceo/classo/unit-priceo) + deterministiccatalog-price/-class/-has?helpers.conformance.shharness + scoreboard. catalog suite 16/16. Gotcha: minikanrenrun-nmacro bindssinternally — query vars must avoids; tests compare reified results with=(notequal?, which fails on reified lists). Money = integer minor units throughout.
Blockers
(none)