Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 57s
order.sx — reserve -> await-payment -> fulfil as a flow-on-sx flow carrying only the order-id; the SX driver services each request by appending to the persist ledger. order-begin! creates+reserves and suspends at payment; order-settle! (webhook) resumes -> fulfils, idempotent on replay (:already-settled). order-flow-restart! simulates a process restart Scheme-side and the suspended order resumes with the ledger intact. Composes all three substrates: minikanren pricing -> flow lifecycle -> persist ledger. Total 153/153 across 9 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
4.1 KiB
Plaintext
120 lines
4.1 KiB
Plaintext
;; 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)"))))
|