;; lib/commerce/ledger.sx — the order ledger as a persist event stream. ;; ;; Each order is an append-only stream "order/" in a persist backend. ;; Order state is never stored directly — it is a projection (fold) over the ;; events, so the ledger is the single source of truth and replays identically. ;; ;; Lifecycle events: ;; :created quote snapshot {:subtotal :discount :tax :total :codes ...} ;; :reserved stock reserved ;; :paid {:amount :ref} — recorded idempotently on the payment ref ;; :fulfilled order shipped/delivered ;; :cancelled / :refunded ;; ;; Idempotency: the SumUp webhook can fire twice for one payment. order-pay ;; uses persist/append-once keyed by the payment ref, so a replayed webhook ;; yields the SAME :paid event without double-recording. Reconciliation then ;; detects genuine mismatches (paid != ordered) across the whole ledger. (define order-stream (fn (order-id) (str "order/" order-id))) ;; --- writes --- (define order-create (fn (b order-id at quote) (persist/append b (order-stream order-id) :created at quote))) (define order-reserve (fn (b order-id at data) (persist/append b (order-stream order-id) :reserved at data))) ;; Idempotent on payment ref — a replayed webhook does not double-record. (define order-pay (fn (b order-id ref at amount) (persist/append-once b (order-stream order-id) ref :paid at {:amount amount :ref ref}))) (define order-fulfil (fn (b order-id at data) (persist/append b (order-stream order-id) :fulfilled at data))) (define order-cancel (fn (b order-id at reason) (persist/append b (order-stream order-id) :cancelled at {:reason reason}))) (define order-refund (fn (b order-id ref at amount) (persist/append-once b (order-stream order-id) (str "refund/" ref) :refunded at {:amount amount :ref ref}))) ;; --- reads --- (define order-events (fn (b order-id) (persist/read b (order-stream order-id)))) ;; --- projections over an event list --- (define order-status-of (fn (events) (reduce (fn (st e) (let ((t (persist/event-type e))) (cond ((= t :created) :pending) ((= t :reserved) :reserved) ((= t :paid) :paid) ((= t :fulfilled) :fulfilled) ((= t :cancelled) :cancelled) ((= t :refunded) :refunded) (:else st)))) :new events))) (define order-total-of (fn (events) (let ((created (filter (fn (e) (= (persist/event-type e) :created)) events))) (if (empty? created) 0 (get (persist/event-data (first created)) :total))))) (define order-paid-amount-of (fn (events) (reduce (fn (acc e) (if (= (persist/event-type e) :paid) (+ acc (get (persist/event-data e) :amount)) acc)) 0 events))) (define order-refunded-amount-of (fn (events) (reduce (fn (acc e) (if (= (persist/event-type e) :refunded) (+ acc (get (persist/event-data e) :amount)) acc)) 0 events))) ;; Net settled = paid - refunded. Reconciliation compares this to the order ;; total, but only once a payment exists. (define order-recon-of (fn (events) (let ((net (- (order-paid-amount-of events) (order-refunded-amount-of events))) (total (order-total-of events)) (has-paid (some (fn (e) (= (persist/event-type e) :paid)) events))) (cond ((not has-paid) :unpaid) ((= net total) :ok) ((< net total) :underpaid) (:else :overpaid))))) ;; --- backend-level helpers --- (define order-status (fn (b order-id) (order-status-of (order-events b order-id)))) (define order-total (fn (b order-id) (order-total-of (order-events b order-id)))) (define order-paid (fn (b order-id) (order-paid-amount-of (order-events b order-id)))) (define order-recon (fn (b order-id) (order-recon-of (order-events b order-id)))) (define order-ids (fn (b) (persist/backend-streams b))) ;; Streams whose net payment does not match the order total (true mismatches, ;; excluding orders that are simply not yet paid). (define ledger-mismatches (fn (b) (filter (fn (s) (let ((r (order-recon-of (persist/read b s)))) (or (= r :underpaid) (= r :overpaid)))) (persist/backend-streams b))))