Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
quote.sx — cart-quote composes the pipeline into a deterministic
{:subtotal :discount :tax :total :codes} with total = subtotal - discount +
tax. Explicit tax policy: tax on gross per-line amounts (discount reduces
payable, not the tax base). This quote is the value the Phase-3 order flow
carries. Total 112/112 across 7 suites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
7.3 KiB
Markdown
129 lines
7.3 KiB
Markdown
# 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` → **112/112** (7 suites: catalog, cart, price, api, promo, stack, quote) — Phases 1-2 done + priced-quote capstone
|
||
|
||
## 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
|
||
- [ ] 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 — `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.
|
||
|
||
## Blockers
|
||
(none)
|