;; 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)))))