commerce: composed priced quote (price+promo+stacking) (13 tests)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 34s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SUITES=(catalog cart price api promo stack)
|
SUITES=(catalog cart price api promo stack quote)
|
||||||
|
|
||||||
OUT_JSON="lib/commerce/scoreboard.json"
|
OUT_JSON="lib/commerce/scoreboard.json"
|
||||||
OUT_MD="lib/commerce/scoreboard.md"
|
OUT_MD="lib/commerce/scoreboard.md"
|
||||||
@@ -49,6 +49,7 @@ run_suite() {
|
|||||||
(load "lib/commerce/api.sx")
|
(load "lib/commerce/api.sx")
|
||||||
(load "lib/commerce/promo.sx")
|
(load "lib/commerce/promo.sx")
|
||||||
(load "lib/commerce/stack.sx")
|
(load "lib/commerce/stack.sx")
|
||||||
|
(load "lib/commerce/quote.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)")
|
||||||
|
|||||||
36
lib/commerce/quote.sx
Normal file
36
lib/commerce/quote.sx
Normal file
@@ -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)))
|
||||||
@@ -5,9 +5,10 @@
|
|||||||
"price": {"pass": 20, "fail": 0},
|
"price": {"pass": 20, "fail": 0},
|
||||||
"api": {"pass": 12, "fail": 0},
|
"api": {"pass": 12, "fail": 0},
|
||||||
"promo": {"pass": 17, "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_fail": 0,
|
||||||
"total": 99
|
"total": 112
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ _Generated by `lib/commerce/conformance.sh`_
|
|||||||
| api | 12 | 0 | 12 |
|
| api | 12 | 0 | 12 |
|
||||||
| promo | 17 | 0 | 17 |
|
| promo | 17 | 0 | 17 |
|
||||||
| stack | 16 | 0 | 16 |
|
| stack | 16 | 0 | 16 |
|
||||||
| **Total** | **99** | **0** | **99** |
|
| quote | 13 | 0 | 13 |
|
||||||
|
| **Total** | **112** | **0** | **112** |
|
||||||
|
|||||||
108
lib/commerce/tests/quote.sx
Normal file
108
lib/commerce/tests/quote.sx
Normal file
@@ -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)
|
||||||
@@ -21,7 +21,7 @@ reconciliation — all auditable via the event log.
|
|||||||
|
|
||||||
## Status (rolling)
|
## 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
|
## Ground rules
|
||||||
|
|
||||||
@@ -76,6 +76,13 @@ 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-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
|
- 2026-06-07 — `stack.sx` (**Phase 2 complete**): stacking precedence as a
|
||||||
separate selection layer (precedence NOT in the rules, per the miniKanren
|
separate selection layer (precedence NOT in the rules, per the miniKanren
|
||||||
design rule). Exclusivity = unordered code pairs; `valid-stackings` enumerates
|
design rule). Exclusivity = unordered code pairs; `valid-stackings` enumerates
|
||||||
|
|||||||
Reference in New Issue
Block a user