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
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:
56
lib/commerce/api.sx
Normal file
56
lib/commerce/api.sx
Normal 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}))
|
||||||
@@ -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)")
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
73
lib/commerce/tests/api.sx
Normal 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)
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user