Merge loops/commerce into architecture: commerce-on-sx revenue vertical
Pricing/promotions/reconciliation as miniKanren relations, order lifecycle as a flow-on-sx durable flow, order ledger as a persist event stream. Base roadmap (Phases 1-4) + Phase 5 extensions (line-level attribution, provider-neutral payment envelope, time-windowed promos, discount-aware tax, stock-constrained reservation, refund-as-flow) + end-to-end composition proof. 297/297 across 18 suites (bash lib/commerce/conformance.sh).
This commit is contained in:
@@ -21,7 +21,7 @@ reconciliation — all auditable via the event log.
|
||||
|
||||
## Status (rolling)
|
||||
|
||||
`bash lib/commerce/conformance.sh` → **0/0** (not yet started)
|
||||
`bash lib/commerce/conformance.sh` → **297/297** (18 suites; + integration) — **roadmap + Phase 5 backlog + e2e composition proof complete**
|
||||
|
||||
## Ground rules
|
||||
|
||||
@@ -55,28 +55,223 @@ lib/commerce/api.sx ── (commerce/add) (commerce/total) (commerce/checkout)
|
||||
```
|
||||
|
||||
## Phase 1 — Catalog + cart + deterministic totals
|
||||
- [ ] `catalog.sx` — product/variant/stock as facts
|
||||
- [ ] `cart.sx` — line items, add/remove/qty
|
||||
- [ ] `price.sx` — base pricing relation, subtotal; tax
|
||||
- [ ] `api.sx` + tests + scoreboard + conformance.sh
|
||||
- [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)
|
||||
- [ ] promo rules: percentage, fixed, bundle, member rate
|
||||
- [ ] explicit stacking precedence; "best price" backward query
|
||||
- [ ] tests: stacking order, mutually-exclusive promos, member vs guest
|
||||
- [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
|
||||
- [x] order flow: reserve stock → await payment → fulfil
|
||||
- [x] 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
|
||||
- [x] mismatch detection (paid≠ordered) as queries over the ledger
|
||||
- [x] cross-instance catalog (federated marketplace) — out-of-scope stub
|
||||
- [x] tests: webhook replay, partial refund, double-charge guard
|
||||
|
||||
## Phase 5 — Extensions (backlog; base roadmap complete) — **ALL DONE (278/278)**
|
||||
Thesis-aligned deepenings of the relational/composition showcase. Pick the one
|
||||
that unlocks the most tests per effort each iteration.
|
||||
- [x] line-level discount attribution — "which line item triggered this discount?"
|
||||
as a backward miniKanren query (`attribution.sx`: `promo-toucheso` relation,
|
||||
`lines-for-code`/`codes-for-line` both directions, `order-level-codes` for fixed).
|
||||
- [x] time-windowed promotions — `window.sx`: windowed promo `(promo from until)`,
|
||||
`active-ruleset`/`active-codes`/`windowed-quote` gate by datetime; feeds the
|
||||
existing promo/stack/quote pipeline unchanged. Determinism preserved.
|
||||
- [x] discount-aware tax policy — `nettax.sx`: `cart-quote-net` taxes the net
|
||||
(post-discount) base; `allocate-discount` spreads the basket discount across
|
||||
lines by extended share with largest-remainder so per-line shares sum exactly.
|
||||
- [x] refund as a flow — `refund.sx`: refund lifecycle (request → approve →
|
||||
settle) as a second flow-on-sx flow with two suspension points; idempotent
|
||||
settle, reject path, ledger-recorded; reuses order.sx flow helpers.
|
||||
- [x] stock-constrained reservation — `stock.sx`: `can-reserve?`/`reserve-check`
|
||||
precondition (host gates order-begin! on it, keeping the flow pure);
|
||||
`reservation-shortfalls` detail; `effective-available` nets out concurrent
|
||||
reservations; `sufficient-stocko` relational availability query.
|
||||
- [x] provider-neutral payment-request envelope — `payment.sx`: `payment-request`
|
||||
materialises `{:order :amount :currency :return-url}` at the IO edge (amount from
|
||||
the ledger, currency/return-url host-supplied); `pending-payments` enumerates
|
||||
suspended orders with their envelopes (host poller seam). Engine stays vendor-
|
||||
agnostic; `order-settle!(ref, amount)` is the resume seam.
|
||||
|
||||
## Progress log
|
||||
(loop fills this in)
|
||||
- 2026-06-07 — `tests/integration.sx` (hardening): end-to-end composition proof —
|
||||
one narrative across every module (catalog → stock check → quote[promo+stack+tax]
|
||||
→ attribution → order flow → payment envelope → settle → recon → refund flow →
|
||||
ledger mismatch) asserting the seams tie together with consistent numbers
|
||||
(subtotal 2400, discount 210, tax 320, total 2510; settle→:ok; refund 510→
|
||||
:underpaid; mismatch flagged). Proves the three-substrate composition. One env
|
||||
with both order+refund flows. integration suite 19/19; total 297/297 (18 suites).
|
||||
- 2026-06-07 — `refund.sx` (**Phase 5 backlog complete**): refund lifecycle as a
|
||||
second flow-on-sx flow `(lambda (oid) (begin (request 'approve oid) (request
|
||||
'settle oid)))` — two suspension points (approval = human/policy decision,
|
||||
settle = provider). `refund-begin!` records :refund-requested and suspends at
|
||||
approval; `refund-approve!` advances to settle; `refund-settle!` records
|
||||
:refunded (idempotent) and completes; `refund-reject!` records :refund-rejected
|
||||
and cancels the flow. Only :refunded moves the books, so requested/rejected
|
||||
refunds leave recon unchanged. Reuses order.sx flow helpers. refund suite
|
||||
20/20; total 278/278 (17 suites). NB: conformance now has two env-building
|
||||
suites (order, refund) — each builds the ~150s flow env in its own process.
|
||||
- 2026-06-07 — `stock.sx` (Phase 5 ext): stock-constrained reservation. Design
|
||||
choice: reservation is a precondition the host checks BEFORE order-begin!
|
||||
(validate → begin), keeping the order flow pure orchestration. `available-stock`
|
||||
reads the catalog stock facts; `can-reserve?`/`reserve-check`/
|
||||
`reservation-shortfalls` gate a cart; `effective-available`/`line-reservable-with?`
|
||||
net out concurrent reservations (no over-reserve); `sufficient-stocko` is the
|
||||
multidirectional availability query. Only refund-as-flow remains in the
|
||||
backlog. stock suite 19/19; total 258/258 (16 suites).
|
||||
- 2026-06-07 — `nettax.sx` (Phase 5 ext): discount-aware tax — the alternative to
|
||||
quote.sx's gross-tax default. `cart-quote-net` taxes the NET (post-discount)
|
||||
base. `allocate-discount` spreads the basket-level discount across lines in
|
||||
proportion to extended price with a deterministic largest-remainder pass so
|
||||
per-line shares sum EXACTLY to the discount; each line is then taxed on its net
|
||||
at its class rate. Both policies reproducible from inputs; pick per jurisdiction.
|
||||
nettax suite 11/11; total 239/239 (15 suites).
|
||||
- 2026-06-07 — `window.sx` (Phase 5 ext): time-windowed promotions. A validity
|
||||
window is kept SEPARATE from the promo tuple — windowed promo `(promo from
|
||||
until)` (inclusive int timestamps, nil = open bound). `active-ruleset` filters
|
||||
to the plain promos live at `at` and feeds the existing promo/stack/quote
|
||||
pipeline unchanged (promo.sx untouched); `active-promoo`/`active-codes` is the
|
||||
backward "which codes are live at T?" query; `windowed-quote` is the
|
||||
datetime-aware quote (deterministic in `at`). window suite 19/19; total 228/228.
|
||||
- 2026-06-07 — `payment.sx` (Phase 5 ext, the item the user asked about):
|
||||
provider-neutral payment-request envelope, materialised at the IO edge from the
|
||||
ledger amount + host-supplied currency/return-url — keeps lib/commerce vendor-
|
||||
agnostic (SumUp/Stripe adapters live in the orders service). `payment-request`
|
||||
builds the `{:order :amount :currency :return-url}` envelope; `pending-payments`
|
||||
is the host-poller seam listing suspended orders + their envelopes. Gotcha: a
|
||||
Scheme **string** carried as a flow payload round-trips back to SX wrapped as
|
||||
`{:scm-string "..."}` (numbers come back clean) — unwrap via `scm->string`
|
||||
before using it as the oid. payment suite 7/7 + 1 order-suite integration test;
|
||||
total 209/209 (13 suites).
|
||||
- 2026-06-07 — `attribution.sx` (Phase 5 ext): line-level discount attribution —
|
||||
the briefing's marquee "which line item triggered this discount?" query.
|
||||
`promo-lines` is the pure per-promo scope (percent/member → class lines, bundle
|
||||
→ sku lines, fixed → order-level/none); `promo-toucheso` relates (code, line)
|
||||
for applying promos, run forward (`lines-for-code`) and backward
|
||||
(`codes-for-line`). `order-level-codes` lists applying fixed promos; predicate
|
||||
`line-touched-by?`. Additive — promo.sx amounts unchanged. attribution suite
|
||||
16/16; total 201/201 (12 suites).
|
||||
- 2026-06-07 — `recon.sx` + `federation.sx` (**Phase 4 complete — roadmap done**).
|
||||
`recon.sx`: reconciliation as relational queries over the ledger. Per-order
|
||||
summary tuples (id total paid refunded net status); `recon-statuso`/`neto`/
|
||||
`mismatcho` are miniKanren relations, so "which orders are overpaid?",
|
||||
"settled to net N?" are backward `run*` queries. Helpers: overpaid/underpaid/
|
||||
settled/unpaid-orders, mismatched-orders, orders-with-net, ledger-discrepancy.
|
||||
Tests cover double-charge guard (two refs → :overpaid), partial refund (net <
|
||||
total → :underpaid), webhook replay (same ref twice → single :paid, :ok). 20/20.
|
||||
`federation.sx` (out-of-scope stub): a federated catalog is the UNION of each
|
||||
instance's product facts, so the SAME relations query cross-instance —
|
||||
`fed-producto`/`fed-priceo`, `instances-with-sku`, `sku-offers`, deterministic
|
||||
`cheapest-offer`. In-process mock, no real network/ActivityPub. 12/12.
|
||||
Total 185/185 across 11 suites.
|
||||
- 2026-06-07 — `order.sx` (**Phase 3 complete**, checkboxes 1-2): order lifecycle
|
||||
as a flow-on-sx flow `(lambda (oid) (begin (request 'reserve oid) (request
|
||||
'payment oid) (request 'fulfil oid)))` — pure orchestration carrying only the
|
||||
order-id; the SX driver services each request by appending to the persist
|
||||
ledger. `order-begin!` creates+reserves and leaves the flow SUSPENDED at
|
||||
payment; `order-settle!` (the webhook) resumes → fulfils, and is idempotent
|
||||
(only acts while waiting on payment, so a replayed webhook → :already-settled).
|
||||
`order-flow-restart!` simulates a process restart entirely Scheme-side
|
||||
(export→reset→reload→import) and the suspended order resumes correctly
|
||||
afterwards with the persist ledger intact. Composes all three substrates
|
||||
(minikanren pricing → flow lifecycle → persist ledger). order suite 21/21;
|
||||
total 153/153. Gotchas: flow ids start at 1; never return flow-make-env across
|
||||
the eval boundary (serializer hangs on the cyclic env); guest Scheme rejects
|
||||
`:ok` keyword as a value — use `#t`. Flow env build ~150s CPU; order suite runs
|
||||
single-process with timeout 560.
|
||||
- 2026-06-07 — `ledger.sx` (Phase 3 piece, checkbox 3): order ledger as a
|
||||
persist event stream "order/<id>". 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 "<scheme-src>")` 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
|
||||
(loop fills this in)
|
||||
(none)
|
||||
|
||||
Reference in New Issue
Block a user