From 57066a9ed050874fc50f8e3ed200aa89e03e43bc Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 00:26:21 +0000 Subject: [PATCH] commerce: composed priced quote (price+promo+stacking) (13 tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- lib/commerce/conformance.sh | 3 +- lib/commerce/quote.sx | 36 ++++++++++++ lib/commerce/scoreboard.json | 7 ++- lib/commerce/scoreboard.md | 3 +- lib/commerce/tests/quote.sx | 108 +++++++++++++++++++++++++++++++++++ plans/commerce-on-sx.md | 9 ++- 6 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 lib/commerce/quote.sx create mode 100644 lib/commerce/tests/quote.sx diff --git a/lib/commerce/conformance.sh b/lib/commerce/conformance.sh index cae06db6..5ca015d7 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 api promo stack) +SUITES=(catalog cart price api promo stack quote) OUT_JSON="lib/commerce/scoreboard.json" OUT_MD="lib/commerce/scoreboard.md" @@ -49,6 +49,7 @@ run_suite() { (load "lib/commerce/api.sx") (load "lib/commerce/promo.sx") (load "lib/commerce/stack.sx") +(load "lib/commerce/quote.sx") (epoch 2) (eval "(define ct-pass 0)") (eval "(define ct-fail 0)") diff --git a/lib/commerce/quote.sx b/lib/commerce/quote.sx new file mode 100644 index 00000000..9d70e661 --- /dev/null +++ b/lib/commerce/quote.sx @@ -0,0 +1,36 @@ +;; lib/commerce/quote.sx — the final priced quote: price + promo + stacking. +;; +;; A quote is the deterministic composition of the pricing pipeline for a +;; (context, cart, ruleset, exclusions) tuple: +;; {:subtotal S :discount D :tax T :total (S - D + T) :codes (...)} +;; +;; Tax policy (explicit, for the determinism contract): tax is computed on the +;; GROSS per-line amounts (pre-discount), via price.sx cart-tax. The best +;; promo stacking reduces the payable total but not the tax base. Same inputs +;; always yield the same quote — this is the value the order flow carries. + +(define + cart-quote + (fn + (ctx cart ruleset exclusions) + (let + ((cat (ctx-catalog ctx))) + (let + ((sub (cart-subtotal cat cart)) + (disc (best-promo-discount ctx cart ruleset exclusions)) + (tax (cart-tax ctx cart)) + (codes (best-promo-codes ctx cart ruleset exclusions))) + {:codes codes :subtotal sub :discount disc :total (+ (- sub disc) tax) :tax tax})))) + +(define quote-subtotal (fn (q) (get q :subtotal))) +(define quote-discount (fn (q) (get q :discount))) +(define quote-tax (fn (q) (get q :tax))) +(define quote-total (fn (q) (get q :total))) +(define quote-codes (fn (q) (get q :codes))) + +;; Session-level convenience (a session is {:ctx :cart}). +(define + session-quote + (fn + (sess ruleset exclusions) + (cart-quote (get sess :ctx) (get sess :cart) ruleset exclusions))) diff --git a/lib/commerce/scoreboard.json b/lib/commerce/scoreboard.json index e7b435ac..75850718 100644 --- a/lib/commerce/scoreboard.json +++ b/lib/commerce/scoreboard.json @@ -5,9 +5,10 @@ "price": {"pass": 20, "fail": 0}, "api": {"pass": 12, "fail": 0}, "promo": {"pass": 17, "fail": 0}, - "stack": {"pass": 16, "fail": 0} + "stack": {"pass": 16, "fail": 0}, + "quote": {"pass": 13, "fail": 0} }, - "total_pass": 99, + "total_pass": 112, "total_fail": 0, - "total": 99 + "total": 112 } diff --git a/lib/commerce/scoreboard.md b/lib/commerce/scoreboard.md index b1009f24..889ada5f 100644 --- a/lib/commerce/scoreboard.md +++ b/lib/commerce/scoreboard.md @@ -10,4 +10,5 @@ _Generated by `lib/commerce/conformance.sh`_ | api | 12 | 0 | 12 | | promo | 17 | 0 | 17 | | stack | 16 | 0 | 16 | -| **Total** | **99** | **0** | **99** | +| quote | 13 | 0 | 13 | +| **Total** | **112** | **0** | **112** | diff --git a/lib/commerce/tests/quote.sx b/lib/commerce/tests/quote.sx new file mode 100644 index 00000000..7a0cedd6 --- /dev/null +++ b/lib/commerce/tests/quote.sx @@ -0,0 +1,108 @@ +;; lib/commerce/tests/quote.sx — composed priced quote (price+promo+stacking). +;; Uses (commerce-test name got expected) provided by conformance.sh. + +(define + pcat + (make-catalog + (list + (list "widget" 1000 :standard) + (list "book" 800 :zero-rated) + (list "tea" 1000 :reduced)) + (list) + (list))) + +(define + tax-rules + (list + (list :uk :standard :guest 2000) + (list :uk :reduced :guest 500) + (list :uk :zero-rated :guest 0) + (list :uk :standard :member 2000) + (list :uk :reduced :member 500) + (list :uk :zero-rated :member 0))) + +(define gctx (make-pricing-context pcat tax-rules :uk :guest)) +(define mctx (make-pricing-context pcat tax-rules :uk :member)) + +(define + cart + (list + (list "widget" :none 3) + (list "book" :none 1) + (list "tea" :none 6))) + +(define + ruleset + (list + (list :percent "TEN" :standard 1000) + (list :percent "TWENTY" :standard 2000) + (list :fixed "FIVER" 5000 500) + (list :bundle "B3T" "tea" 3) + (list :member "MEM" :standard 2500))) + +(define + exclusions + (list (list "TEN" "TWENTY") (list "TEN" "MEM") (list "TWENTY" "MEM"))) + +;; subtotal: 3000 + 800 + 6000 = 9800 +;; tax (gross): widget 600 + tea 300 + book 0 = 900 +;; guest discount: TWENTY 600 + FIVER 500 + B3T 2000 = 3100 +;; guest total: 9800 - 3100 + 900 = 7600 + +(define gq (cart-quote gctx cart ruleset exclusions)) + +(commerce-test "quote-subtotal" (quote-subtotal gq) 9800) +(commerce-test "quote-tax" (quote-tax gq) 900) +(commerce-test "quote-discount-guest" (quote-discount gq) 3100) +(commerce-test "quote-total-guest" (quote-total gq) 7600) +(commerce-test + "quote-codes-guest" + (quote-codes gq) + (list "TWENTY" "FIVER" "B3T")) + +(commerce-test "quote-full-guest" gq {:codes (list "TWENTY" "FIVER" "B3T") :subtotal 9800 :discount 3100 :total 7600 :tax 900}) + +;; member discount: MEM 750 + FIVER 500 + B3T 2000 = 3250 +;; member total: 9800 - 3250 + 900 = 7450 +(define mq (cart-quote mctx cart ruleset exclusions)) + +(commerce-test "quote-discount-member" (quote-discount mq) 3250) +(commerce-test "quote-total-member" (quote-total mq) 7450) +(commerce-test + "quote-codes-member" + (quote-codes mq) + (list "FIVER" "B3T" "MEM")) + +;; --- determinism: same inputs, identical quote --- + +(commerce-test + "quote-deterministic" + (= + (cart-quote gctx cart ruleset exclusions) + (cart-quote gctx cart ruleset exclusions)) + true) + +;; --- no promos: discount 0, total = subtotal + tax --- + +(commerce-test + "quote-no-promos" + (cart-quote gctx cart (list) (list)) + {:codes (list) :subtotal 9800 :discount 0 :total 10700 :tax 900}) + +;; --- empty cart --- + +(commerce-test + "quote-empty" + (cart-quote gctx empty-cart ruleset exclusions) + {:codes (list) :subtotal 0 :discount 0 :total 0 :tax 0}) + +;; --- session convenience --- + +(define + sess + (commerce-add (commerce-session gctx) "widget" :none 3)) + +(commerce-test + "session-quote" + (quote-total (session-quote sess ruleset exclusions)) + 3000) diff --git a/plans/commerce-on-sx.md b/plans/commerce-on-sx.md index ee341f99..65f9af3a 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` → **99/99** (6 suites: catalog, cart, price, api, promo, stack) — Phases 1-2 done +`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 @@ -76,6 +76,13 @@ lib/commerce/api.sx ── (commerce/add) (commerce/total) (commerce/checkout) - [ ] 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