Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
promo.sx — four promo types as tagged tuples; per-promo discount is pure
integer arithmetic, but enumeration is relational: promo-discounto and
promo-applieso run forward ("which codes apply, for how much?") and backward
("which code yields this discount?"). project grounds the membero-bound promo.
applicable-promos / promo-amount-for deterministic helpers. Total 83/83.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
154 lines
3.7 KiB
Plaintext
154 lines
3.7 KiB
Plaintext
;; lib/commerce/promo.sx — promotions as relations over the cart + catalog.
|
|
;;
|
|
;; A promo is a tagged tuple; the second field is always its code:
|
|
;; (:percent code class pct-bps) pct-bps off every line of product-class
|
|
;; (:fixed code threshold amount) amount off when subtotal >= threshold
|
|
;; (:bundle code sku n) every nth unit of sku is free
|
|
;; (:member code class pct-bps) like :percent, members only
|
|
;;
|
|
;; A ruleset is a list of promo tuples. The discount a promo yields on a
|
|
;; given cart is a pure integer computation (minor units); the *enumeration*
|
|
;; of which promos apply is relational, so promo-applieso runs forward
|
|
;; ("which codes apply and for how much?") and backward ("which code yields
|
|
;; this discount?"). Stacking precedence is a separate layer (stack.sx).
|
|
|
|
(define promo-kind (fn (p) (nth p 0)))
|
|
(define promo-code (fn (p) (nth p 1)))
|
|
|
|
;; Extended price of all lines whose sku is in product-class `class`.
|
|
(define
|
|
class-extended
|
|
(fn
|
|
(ctx cart class)
|
|
(let
|
|
((cat (ctx-catalog ctx)))
|
|
(reduce
|
|
(fn
|
|
(acc l)
|
|
(if
|
|
(= (catalog-class cat (line-sku l)) class)
|
|
(+ acc (line-extended cat l))
|
|
acc))
|
|
0
|
|
cart))))
|
|
|
|
(define
|
|
sku-qty
|
|
(fn
|
|
(cart sku)
|
|
(reduce
|
|
(fn (acc l) (if (= (line-sku l) sku) (+ acc (line-qty l)) acc))
|
|
0
|
|
cart)))
|
|
|
|
;; --- per-type discount amounts (pure, integer minor units) ---
|
|
|
|
(define
|
|
percent-amount
|
|
(fn
|
|
(ctx cart p)
|
|
(apply-bps
|
|
(class-extended ctx cart (nth p 2))
|
|
(nth p 3))))
|
|
|
|
(define
|
|
fixed-amount
|
|
(fn
|
|
(ctx cart p)
|
|
(let
|
|
((sub (cart-subtotal (ctx-catalog ctx) cart)))
|
|
(if
|
|
(>= sub (nth p 2))
|
|
(min (nth p 3) sub)
|
|
0))))
|
|
|
|
(define
|
|
bundle-amount
|
|
(fn
|
|
(ctx cart p)
|
|
(let
|
|
((sku (nth p 2)) (n (nth p 3)))
|
|
(let
|
|
((free (quotient (sku-qty cart sku) n)))
|
|
(* free (catalog-price (ctx-catalog ctx) sku))))))
|
|
|
|
(define
|
|
member-amount
|
|
(fn
|
|
(ctx cart p)
|
|
(if
|
|
(= (get ctx :customer) :member)
|
|
(apply-bps
|
|
(class-extended ctx cart (nth p 2))
|
|
(nth p 3))
|
|
0)))
|
|
|
|
;; Discount this promo yields on this cart (0 if it does not apply).
|
|
(define
|
|
promo-amount
|
|
(fn
|
|
(ctx cart p)
|
|
(let
|
|
((k (promo-kind p)))
|
|
(cond
|
|
((= k :percent) (percent-amount ctx cart p))
|
|
((= k :fixed) (fixed-amount ctx cart p))
|
|
((= k :bundle) (bundle-amount ctx cart p))
|
|
((= k :member) (member-amount ctx cart p))
|
|
(:else 0)))))
|
|
|
|
;; --- relational enumeration ---
|
|
|
|
;; (code, amount) for every promo in the ruleset (amount may be 0).
|
|
(define
|
|
promo-discounto
|
|
(fn
|
|
(ctx cart ruleset code amount)
|
|
(fresh
|
|
(p)
|
|
(membero p ruleset)
|
|
(project
|
|
(p)
|
|
(== code (promo-code p))
|
|
(== amount (promo-amount ctx cart p))))))
|
|
|
|
;; (code, amount) restricted to promos that actually apply (amount > 0).
|
|
(define
|
|
promo-applieso
|
|
(fn
|
|
(ctx cart ruleset code amount)
|
|
(fresh
|
|
(p)
|
|
(membero p ruleset)
|
|
(project
|
|
(p)
|
|
(if
|
|
(> (promo-amount ctx cart p) 0)
|
|
(mk-conj
|
|
(== code (promo-code p))
|
|
(== amount (promo-amount ctx cart p)))
|
|
fail)))))
|
|
|
|
;; --- deterministic helpers ---
|
|
|
|
;; List of (list code amount) for applicable promos, in ruleset order.
|
|
(define
|
|
applicable-promos
|
|
(fn
|
|
(ctx cart ruleset)
|
|
(run*
|
|
pair
|
|
(fresh
|
|
(code amount)
|
|
(promo-applieso ctx cart ruleset code amount)
|
|
(== pair (list code amount))))))
|
|
|
|
;; Discount for one code (0 if absent / inapplicable).
|
|
(define
|
|
promo-amount-for
|
|
(fn
|
|
(ctx cart ruleset code)
|
|
(let
|
|
((rs (run 1 a (promo-applieso ctx cart ruleset code a))))
|
|
(if (empty? rs) 0 (first rs)))))
|