commerce: public session API + per-line audit + checkout stub (12 tests) — Phase 1 done
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 45s

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) <noreply@anthropic.com>
This commit is contained in:
2026-06-06 23:46:51 +00:00
parent 29955831be
commit a0f3a1177e
6 changed files with 144 additions and 7 deletions

56
lib/commerce/api.sx Normal file
View File

@@ -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}))

View File

@@ -17,7 +17,7 @@ if [ ! -x "$SX_SERVER" ]; then
exit 1 exit 1
fi fi
SUITES=(catalog cart price) SUITES=(catalog cart price api)
OUT_JSON="lib/commerce/scoreboard.json" OUT_JSON="lib/commerce/scoreboard.json"
OUT_MD="lib/commerce/scoreboard.md" OUT_MD="lib/commerce/scoreboard.md"
@@ -46,6 +46,7 @@ run_suite() {
(load "lib/commerce/catalog.sx") (load "lib/commerce/catalog.sx")
(load "lib/commerce/cart.sx") (load "lib/commerce/cart.sx")
(load "lib/commerce/price.sx") (load "lib/commerce/price.sx")
(load "lib/commerce/api.sx")
(epoch 2) (epoch 2)
(eval "(define ct-pass 0)") (eval "(define ct-pass 0)")
(eval "(define ct-fail 0)") (eval "(define ct-fail 0)")

View File

@@ -2,9 +2,10 @@
"suites": { "suites": {
"catalog": {"pass": 16, "fail": 0}, "catalog": {"pass": 16, "fail": 0},
"cart": {"pass": 18, "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_fail": 0,
"total": 54 "total": 66
} }

View File

@@ -7,4 +7,5 @@ _Generated by `lib/commerce/conformance.sh`_
| catalog | 16 | 0 | 16 | | catalog | 16 | 0 | 16 |
| cart | 18 | 0 | 18 | | cart | 18 | 0 | 18 |
| price | 20 | 0 | 20 | | price | 20 | 0 | 20 |
| **Total** | **54** | **0** | **54** | | api | 12 | 0 | 12 |
| **Total** | **66** | **0** | **66** |

73
lib/commerce/tests/api.sx Normal file
View File

@@ -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)

View File

@@ -21,7 +21,7 @@ reconciliation — all auditable via the event log.
## Status (rolling) ## 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 ## 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] `catalog.sx` — product/variant/stock as facts
- [x] `cart.sx` — line items, add/remove/qty - [x] `cart.sx` — line items, add/remove/qty
- [x] `price.sx` — base pricing relation, subtotal; tax - [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) ## Phase 2 — Promotions (relational)
- [ ] promo rules: percentage, fixed, bundle, member rate - [ ] 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 - [ ] tests: webhook replay, partial refund, double-charge guard
## Progress log ## 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 - 2026-06-06 — `price.sx`: deterministic `cart-subtotal` (Σ unit×qty, variant
delta defaults 0) + jurisdiction-relational tax. `taxo` facts indexed by delta defaults 0) + jurisdiction-relational tax. `taxo` facts indexed by
(jurisdiction, product-class, customer-class)→bps, queried multidirectionally; (jurisdiction, product-class, customer-class)→bps, queried multidirectionally;