From a0f3a1177e4af425c55db7b982afd513b8c2c933 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 6 Jun 2026 23:46:51 +0000 Subject: [PATCH] =?UTF-8?q?commerce:=20public=20session=20API=20+=20per-li?= =?UTF-8?q?ne=20audit=20+=20checkout=20stub=20(12=20tests)=20=E2=80=94=20P?= =?UTF-8?q?hase=201=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit api.sx — session facade {:ctx :cart}: commerce-add/remove/set-qty/total/ count/lines, commerce-can-add? catalog validation, commerce-explain per-line audit breakdown, commerce-checkout Phase-3 stub. Completes Phase 1 (catalog + cart + deterministic totals). Total 66/66 across 4 suites. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/commerce/api.sx | 56 +++++++++++++++++++++++++++ lib/commerce/conformance.sh | 3 +- lib/commerce/scoreboard.json | 7 ++-- lib/commerce/scoreboard.md | 3 +- lib/commerce/tests/api.sx | 73 ++++++++++++++++++++++++++++++++++++ plans/commerce-on-sx.md | 9 ++++- 6 files changed, 144 insertions(+), 7 deletions(-) create mode 100644 lib/commerce/api.sx create mode 100644 lib/commerce/tests/api.sx diff --git a/lib/commerce/api.sx b/lib/commerce/api.sx new file mode 100644 index 00000000..b662103a --- /dev/null +++ b/lib/commerce/api.sx @@ -0,0 +1,56 @@ +;; lib/commerce/api.sx — public commerce surface. +;; +;; A session bundles a pricing context with a cart: {:ctx CTX :cart CART}. +;; All operations are pure and return a new session. The total and the +;; per-line breakdown are deterministic functions of (ctx, cart). +;; +;; commerce-checkout is a Phase-3 stub — the order lifecycle is a durable +;; flow that suspends at the SumUp payment boundary. + +(define commerce-session (fn (ctx) {:cart empty-cart :ctx ctx})) + +(define commerce-ctx (fn (sess) (get sess :ctx))) +(define commerce-cart (fn (sess) (get sess :cart))) +(define commerce-lines (fn (sess) (cart-lines (get sess :cart)))) +(define commerce-count (fn (sess) (cart-count (get sess :cart)))) + +(define + commerce-add + (fn + (sess sku variant qty) + (assoc sess :cart (cart-add (get sess :cart) sku variant qty)))) + +(define + commerce-remove + (fn + (sess sku variant) + (assoc sess :cart (cart-remove (get sess :cart) sku variant)))) + +(define + commerce-set-qty + (fn + (sess sku variant qty) + (assoc sess :cart (cart-set-qty (get sess :cart) sku variant qty)))) + +;; True when the sku exists in the session's catalog snapshot. +(define + commerce-can-add? + (fn (sess sku) (catalog-has? (ctx-catalog (get sess :ctx)) sku))) + +(define + commerce-total + (fn (sess) (cart-total (get sess :ctx) (get sess :cart)))) + +;; Per-line audit breakdown — the "which line contributed what" view. +(define + line-detail + (fn (ctx line) (let ((cat (ctx-catalog ctx))) {:sku (line-sku line) :unit (line-unit-price cat (line-sku line) (line-variant line)) :qty (line-qty line) :variant (line-variant line) :extended (line-extended cat line) :tax (line-tax ctx line)}))) + +(define + commerce-explain + (fn + (sess) + (map (fn (l) (line-detail (get sess :ctx) l)) (get sess :cart)))) + +;; Phase 3 — order lifecycle flow (reserve -> pay -> fulfil) lands here. +(define commerce-checkout (fn (sess) {:note "order lifecycle flow lands in Phase 3" :phase 3 :status :not-implemented})) diff --git a/lib/commerce/conformance.sh b/lib/commerce/conformance.sh index a0975685..d88466dd 100755 --- a/lib/commerce/conformance.sh +++ b/lib/commerce/conformance.sh @@ -17,7 +17,7 @@ if [ ! -x "$SX_SERVER" ]; then exit 1 fi -SUITES=(catalog cart price) +SUITES=(catalog cart price api) OUT_JSON="lib/commerce/scoreboard.json" OUT_MD="lib/commerce/scoreboard.md" @@ -46,6 +46,7 @@ run_suite() { (load "lib/commerce/catalog.sx") (load "lib/commerce/cart.sx") (load "lib/commerce/price.sx") +(load "lib/commerce/api.sx") (epoch 2) (eval "(define ct-pass 0)") (eval "(define ct-fail 0)") diff --git a/lib/commerce/scoreboard.json b/lib/commerce/scoreboard.json index 3fba68ed..c30f641e 100644 --- a/lib/commerce/scoreboard.json +++ b/lib/commerce/scoreboard.json @@ -2,9 +2,10 @@ "suites": { "catalog": {"pass": 16, "fail": 0}, "cart": {"pass": 18, "fail": 0}, - "price": {"pass": 20, "fail": 0} + "price": {"pass": 20, "fail": 0}, + "api": {"pass": 12, "fail": 0} }, - "total_pass": 54, + "total_pass": 66, "total_fail": 0, - "total": 54 + "total": 66 } diff --git a/lib/commerce/scoreboard.md b/lib/commerce/scoreboard.md index 95bb8501..bca6e9ae 100644 --- a/lib/commerce/scoreboard.md +++ b/lib/commerce/scoreboard.md @@ -7,4 +7,5 @@ _Generated by `lib/commerce/conformance.sh`_ | catalog | 16 | 0 | 16 | | cart | 18 | 0 | 18 | | price | 20 | 0 | 20 | -| **Total** | **54** | **0** | **54** | +| api | 12 | 0 | 12 | +| **Total** | **66** | **0** | **66** | diff --git a/lib/commerce/tests/api.sx b/lib/commerce/tests/api.sx new file mode 100644 index 00000000..5b64f5d3 --- /dev/null +++ b/lib/commerce/tests/api.sx @@ -0,0 +1,73 @@ +;; lib/commerce/tests/api.sx — public commerce session surface. +;; Uses (commerce-test name got expected) provided by conformance.sh. + +(define + acat + (make-catalog + (list + (list "widget" 1000 :standard) + (list "book" 800 :zero-rated)) + (list (list "widget" :small -200)) + (list))) + +(define + arules + (list + (list :uk :standard :guest 2000) + (list :uk :zero-rated :guest 0))) + +(define actx (make-pricing-context acat arules :uk :guest)) +(define sess0 (commerce-session actx)) + +;; --- empty session --- + +(commerce-test "new-session-empty" (commerce-cart sess0) empty-cart) +(commerce-test "new-count" (commerce-count sess0) 0) +(commerce-test "new-total" (commerce-total sess0) {:subtotal 0 :discounts 0 :total 0 :tax 0}) + +;; --- add + total --- + +(define + sess1 + (commerce-add + (commerce-add sess0 "widget" :small 2) + "book" + :none 1)) + +(commerce-test "add-count" (commerce-count sess1) 3) +(commerce-test + "add-lines" + (commerce-lines sess1) + (list (list "widget" :small 2) (list "book" :none 1))) +(commerce-test "add-total" (commerce-total sess1) {:subtotal 2400 :discounts 0 :total 2720 :tax 320}) + +;; --- mutate --- + +(commerce-test + "set-qty" + (commerce-lines (commerce-set-qty sess1 "widget" :small 1)) + (list (list "widget" :small 1) (list "book" :none 1))) + +(commerce-test + "remove" + (commerce-lines (commerce-remove sess1 "book" :none)) + (list (list "widget" :small 2))) + +;; --- validation --- + +(commerce-test "can-add-yes" (commerce-can-add? sess0 "widget") true) +(commerce-test "can-add-no" (commerce-can-add? sess0 "ghost") false) + +;; --- audit breakdown --- + +(commerce-test + "explain" + (commerce-explain sess1) + (list {:sku "widget" :unit 800 :qty 2 :variant :small :extended 1600 :tax 320} {:sku "book" :unit 800 :qty 1 :variant :none :extended 800 :tax 0})) + +;; --- checkout stub --- + +(commerce-test + "checkout-stub" + (get (commerce-checkout sess1) :status) + :not-implemented) diff --git a/plans/commerce-on-sx.md b/plans/commerce-on-sx.md index 58125180..66d306a7 100644 --- a/plans/commerce-on-sx.md +++ b/plans/commerce-on-sx.md @@ -21,7 +21,7 @@ reconciliation — all auditable via the event log. ## Status (rolling) -`bash lib/commerce/conformance.sh` → **54/54** (3 suites: catalog, cart, price) +`bash lib/commerce/conformance.sh` → **66/66** (4 suites: catalog, cart, price, api) — Phase 1 done ## Ground rules @@ -58,7 +58,7 @@ lib/commerce/api.sx ── (commerce/add) (commerce/total) (commerce/checkout) - [x] `catalog.sx` — product/variant/stock as facts - [x] `cart.sx` — line items, add/remove/qty - [x] `price.sx` — base pricing relation, subtotal; tax -- [ ] `api.sx` + tests + scoreboard + conformance.sh +- [x] `api.sx` + tests + scoreboard + conformance.sh ## Phase 2 — Promotions (relational) - [ ] promo rules: percentage, fixed, bundle, member rate @@ -76,6 +76,11 @@ lib/commerce/api.sx ── (commerce/add) (commerce/total) (commerce/checkout) - [ ] tests: webhook replay, partial refund, double-charge guard ## Progress log +- 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;