;; lib/events/ticket.sx — paid-ticket contract between events and commerce. ;; ;; A paid booking spans two subsystems. events does NOT import commerce; instead ;; this module defines the CONTRACT — the two messages on the wire — and the ;; events-side orchestration over provisional holds (booking.sx). commerce ;; imports these shapes; the dependency only points one way. ;; ;; checkout-request events -> commerce "take payment for this seat" ;; {:kind :events.checkout :occ-key :actor :amount :currency :ref} ;; ;; payment-result commerce -> events "here's how payment went" ;; {:kind :events.payment :occ-key :actor :ref :status} ;; :status ∈ :paid | :failed | :expired ;; ;; Flow: ev/request-ticket! places a capacity-safe HOLD (reserving the seat so ;; it can't be oversold while payment pends) and returns a checkout-request to ;; hand to commerce. When commerce reports back, ev/settle-payment! confirms the ;; hold on :paid or releases it otherwise. Settlement is idempotent — an ;; at-least-once redelivery of the same result is safe. `ref` is the opaque ;; correlation/idempotency id; occ-key + actor locate the hold, so settlement ;; needs no side table. ;; ---- contract: checkout request (events -> commerce) ---- (define ev/checkout-request (fn (occ-key actor amount currency ref) {:actor actor :amount amount :kind :events.checkout :ref ref :currency currency :occ-key occ-key})) (define ev/checkout-request? (fn (m) (and (dict? m) (= (get m :kind) :events.checkout)))) (define ev/req-occ-key (fn (r) (get r :occ-key))) (define ev/req-actor (fn (r) (get r :actor))) (define ev/req-amount (fn (r) (get r :amount))) (define ev/req-currency (fn (r) (get r :currency))) (define ev/req-ref (fn (r) (get r :ref))) ;; ---- contract: payment result (commerce -> events) ---- (define ev/payment-result (fn (occ-key actor ref status) {:actor actor :kind :events.payment :status status :ref ref :occ-key occ-key})) (define ev/payment-result? (fn (m) (and (dict? m) (= (get m :kind) :events.payment)))) (define ev/result-occ-key (fn (r) (get r :occ-key))) (define ev/result-actor (fn (r) (get r :actor))) (define ev/result-ref (fn (r) (get r :ref))) (define ev/result-status (fn (r) (get r :status))) (define ev/payment-paid (fn (occ-key actor ref) (ev/payment-result occ-key actor ref :paid))) (define ev/payment-failed (fn (occ-key actor ref) (ev/payment-result occ-key actor ref :failed))) (define ev/payment-expired (fn (occ-key actor ref) (ev/payment-result occ-key actor ref :expired))) ;; ---- orchestration ---- ;; Begin a paid booking: place a capacity-safe hold and, if reserved, return a ;; checkout-request for commerce. :full when no seat; :already when the actor ;; already holds/booked this occurrence (no duplicate request). (define ev/request-ticket! (fn (b occ-key capacity actor amount currency ref) (let ((h (ev/hold! b occ-key capacity actor))) (cond ((= (get h :status) :held) {:seat (get h :seat) :request (ev/checkout-request occ-key actor amount currency ref) :status :awaiting-payment}) ((= (get h :status) :already) {:seat (get h :seat) :status :already}) (else {:capacity capacity :status :full}))))) ;; Settle a payment result from commerce. :paid confirms the hold; :failed / ;; :expired release it. Idempotent: a redelivered :paid stays :confirmed, a ;; redelivered release is a :noop. If a :paid arrives for a hold that is already ;; gone (released/expired first), returns :paid-but-no-hold so the caller can ;; trigger a refund. (define ev/settle-payment! (fn (b result) (let ((occ-key (ev/result-occ-key result)) (actor (ev/result-actor result)) (ref (ev/result-ref result))) (if (= (ev/result-status result) :paid) (let ((c (ev/confirm! b occ-key actor))) (cond ((= (get c :status) :confirmed) {:actor actor :status :confirmed :ref ref}) ((= (get c :status) :already-confirmed) {:actor actor :status :confirmed :ref ref}) (else {:actor actor :status :paid-but-no-hold :ref ref}))) (let ((r (ev/release! b occ-key actor))) (if (= (get r :status) :released) {:actor actor :status :released :ref ref} {:actor actor :status :noop :ref ref}))))))