;; lib/commerce/order.sx — order lifecycle as a durable flow-on-sx flow. ;; ;; The lifecycle (reserve -> await payment -> fulfil) is a Scheme flow running ;; in the flow-on-sx guest (lib/flow). The flow is PURE ORCHESTRATION: it ;; carries only the order-id and enforces step ordering + the suspension at the ;; payment IO boundary. All IO/state lives in SX: the SX driver here services ;; each flow request by appending to the persist ledger (ledger.sx). ;; ;; reserve -> SX appends :reserved, resumes (synchronous host effect) ;; payment -> flow stays SUSPENDED until the SumUp webhook resumes it ;; fulfil -> SX appends :fulfilled, resumes (synchronous host effect) ;; ;; Durability: the flow's replay log is plain data (flow-store-export), so a ;; suspended order survives a process restart — order-flow-restart! simulates ;; that entirely Scheme-side. Idempotency: order-settle! only resumes a flow ;; still waiting on payment, so a replayed webhook is a no-op at the flow level, ;; and order-pay is idempotent at the ledger level. ;; The flow definition (Scheme source). oid is in scope throughout the begin. (define order-flow-src "(defflow order-lifecycle (lambda (oid) (begin (request (quote reserve) oid) (request (quote payment) oid) (request (quote fulfil) oid))))") ;; Build a flow env with the order flow registered. Never returns the env from ;; an eval boundary (the env is large/cyclic — serializing it hangs). (define order-make-env (fn () (let ((env (flow-make-env))) (begin (flow-run-in env order-flow-src) env)))) ;; --- thin Scheme bridge (string-interpolated flow ops) --- (define order-flow-start (fn (env oid) (flow-run-in env (str "(flow/start order-lifecycle \"" oid "\")")))) (define order-flow-resume (fn (env id sym) (flow-run-in env (str "(flow/resume " id " (quote " sym "))")))) (define order-flow-status (fn (env id) (flow-run-in env (str "(flow/status " id ")")))) (define order-flow-result (fn (env id) (flow-run-in env (str "(flow/result " id ")")))) ;; The request kind the flow with this id is waiting on, or nil if it is not ;; suspended on a host request (done / cancelled / unknown). (define order-flow-waiting (fn (env id) (let ((reqs (flow-run-in env "(flow-host-requests)"))) (let ((mine (filter (fn (r) (= (first r) id)) reqs))) (if (empty? mine) nil (nth (first mine) 1)))))) ;; Id out of a (flow-suspended id tag) start/resume result. (define order-susp-id (fn (susp) (nth susp 1))) ;; --- high-level lifecycle (flow + ledger composed) --- ;; Create the order, start the flow, service the reserve step, and leave the ;; flow suspended at payment. Returns the flow id (needed to settle later). (define order-begin! (fn (env b oid at quote) (begin (order-create b oid at quote) (let ((id (order-susp-id (order-flow-start env oid)))) (begin (order-reserve b oid (+ at 1) {}) (order-flow-resume env id :reserved) id))))) ;; Settle a payment: record it, resume the flow past payment, service fulfil. ;; Idempotent — only acts when the flow is still waiting on payment, so a ;; replayed webhook returns :already-settled without double-charging. (define order-settle! (fn (env b id oid ref at amount) (if (= (order-flow-waiting env id) "payment") (begin (order-pay b oid ref at amount) (order-flow-resume env id :paid) (order-fulfil b oid (+ at 1) {}) (order-flow-resume env id :fulfilled) :settled) :already-settled))) ;; Simulate a process restart: export the flow store, reset the runtime, reload ;; the flow definition, reimport the store. Done entirely Scheme-side so the ;; (large) store is never marshalled across the boundary. The persist ledger is ;; a separate store and is unaffected. Suspended flows resume afterwards. (define order-flow-restart! (fn (env) (flow-run-in env (str "(begin (define _saved (flow-store-export)) " flow-reset-src " " order-flow-src " (flow-store-import! _saved) #t)"))))