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
|
||||
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)")
|
||||
|
||||
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},
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -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** |
|
||||
|
||||
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)
|
||||
Reference in New Issue
Block a user