From eb7e6be14782c1ed4673b78917592559c702188c Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 7 Jun 2026 11:04:16 +0000 Subject: [PATCH] =?UTF-8?q?commerce:=20provider-neutral=20payment-request?= =?UTF-8?q?=20envelope=20(8=20tests)=20=E2=80=94=20Phase=205=20ext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit payment.sx — payment-request materialises {:order :amount :currency :return-url} at the IO edge (amount from the ledger, currency/return-url host-supplied), so lib/commerce stays vendor-agnostic; SumUp/Stripe adapters live in the orders service and order-settle!(ref, amount) is the resume seam. pending-payments enumerates suspended orders + envelopes (host poller seam). Gotcha handled: a Scheme string flow-payload round-trips back wrapped as {:scm-string ...} — unwrapped via scm->string. Total 209/209 across 13 suites. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/commerce/conformance.sh | 3 ++- lib/commerce/payment.sx | 41 +++++++++++++++++++++++++++++++++ lib/commerce/scoreboard.json | 9 ++++---- lib/commerce/scoreboard.md | 5 ++-- lib/commerce/tests/order.sx | 7 ++++++ lib/commerce/tests/payment.sx | 43 +++++++++++++++++++++++++++++++++++ plans/commerce-on-sx.md | 23 +++++++++++++------ 7 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 lib/commerce/payment.sx create mode 100644 lib/commerce/tests/payment.sx diff --git a/lib/commerce/conformance.sh b/lib/commerce/conformance.sh index c457b3ea..161a4a64 100755 --- a/lib/commerce/conformance.sh +++ b/lib/commerce/conformance.sh @@ -17,7 +17,7 @@ if [ ! -x "$SX_SERVER" ]; then exit 1 fi -SUITES=(catalog cart price api promo stack quote ledger order recon federation attribution) +SUITES=(catalog cart price api promo stack quote ledger order recon federation attribution payment) OUT_JSON="lib/commerce/scoreboard.json" OUT_MD="lib/commerce/scoreboard.md" @@ -68,6 +68,7 @@ run_suite() { (load "lib/commerce/quote.sx") (load "lib/commerce/ledger.sx") (load "lib/commerce/order.sx") +(load "lib/commerce/payment.sx") (load "lib/commerce/recon.sx") (load "lib/commerce/federation.sx") (load "lib/commerce/attribution.sx") diff --git a/lib/commerce/payment.sx b/lib/commerce/payment.sx new file mode 100644 index 00000000..dcbb26f1 --- /dev/null +++ b/lib/commerce/payment.sx @@ -0,0 +1,41 @@ +;; lib/commerce/payment.sx — provider-neutral payment-request envelope. +;; +;; The order flow (order.sx) suspends on `(request 'payment oid)` — it carries +;; ONLY the order-id and calls no provider. This layer materialises, at the IO +;; edge, the envelope a provider adapter needs to initiate payment: +;; +;; {:order oid :amount :currency C :return-url U} +;; +;; amount comes from the ledger (the :created quote total); currency + return-url +;; are host/provider config (legitimately host-supplied). The engine stays +;; vendor-agnostic: SumUp/Stripe/etc. adapters consume this envelope, and +;; order-settle!(ref, amount) is the vendor-neutral resume seam. No provider +;; SDK, HTTP, or webhook parsing lives here — that is the orders service's job. + +(define payment-request (fn (b oid currency return-url) {:order oid :amount (order-total b oid) :return-url return-url :currency currency})) + +(define payment-request-order (fn (pr) (get pr :order))) +(define payment-request-amount (fn (pr) (get pr :amount))) +(define payment-request-currency (fn (pr) (get pr :currency))) +(define payment-request-return-url (fn (pr) (get pr :return-url))) + +;; A Scheme string carried as a flow payload round-trips back to SX wrapped as +;; {:scm-string "..."}; unwrap it to the bare order-id. +(define + scm->string + (fn + (v) + (if (and (dict? v) (has-key? v :scm-string)) (get v :scm-string) v))) + +;; Host poller seam: every order currently suspended awaiting payment, each with +;; its envelope. A provider adapter iterates these, initiates payment, and later +;; calls order-settle! when the webhook arrives. Needs the flow env. +(define + pending-payments + (fn + (env b currency return-url) + (let + ((reqs (flow-run-in env "(flow-host-requests)"))) + (map + (fn (r) {:id (first r) :request (payment-request b (scm->string (nth r 2)) currency return-url)}) + (filter (fn (r) (= (nth r 1) "payment")) reqs))))) diff --git a/lib/commerce/scoreboard.json b/lib/commerce/scoreboard.json index 6f635a12..eb9fa90f 100644 --- a/lib/commerce/scoreboard.json +++ b/lib/commerce/scoreboard.json @@ -8,12 +8,13 @@ "stack": {"pass": 16, "fail": 0}, "quote": {"pass": 13, "fail": 0}, "ledger": {"pass": 20, "fail": 0}, - "order": {"pass": 21, "fail": 0}, + "order": {"pass": 22, "fail": 0}, "recon": {"pass": 20, "fail": 0}, "federation": {"pass": 12, "fail": 0}, - "attribution": {"pass": 16, "fail": 0} + "attribution": {"pass": 16, "fail": 0}, + "payment": {"pass": 7, "fail": 0} }, - "total_pass": 201, + "total_pass": 209, "total_fail": 0, - "total": 201 + "total": 209 } diff --git a/lib/commerce/scoreboard.md b/lib/commerce/scoreboard.md index 965df57f..ec38636c 100644 --- a/lib/commerce/scoreboard.md +++ b/lib/commerce/scoreboard.md @@ -12,8 +12,9 @@ _Generated by `lib/commerce/conformance.sh`_ | stack | 16 | 0 | 16 | | quote | 13 | 0 | 13 | | ledger | 20 | 0 | 20 | -| order | 21 | 0 | 21 | +| order | 22 | 0 | 22 | | recon | 20 | 0 | 20 | | federation | 12 | 0 | 12 | | attribution | 16 | 0 | 16 | -| **Total** | **201** | **0** | **201** | +| payment | 7 | 0 | 7 | +| **Total** | **209** | **0** | **209** | diff --git a/lib/commerce/tests/order.sx b/lib/commerce/tests/order.sx index 79b1bb7f..7d4f80b0 100644 --- a/lib/commerce/tests/order.sx +++ b/lib/commerce/tests/order.sx @@ -65,3 +65,10 @@ (commerce-test "post-restart-status" (order-status b "O3") :fulfilled) (commerce-test "post-restart-recon-ok" (order-recon b "O3") :ok) (commerce-test "post-restart-flow-done" (order-flow-status env id3) "done") + +;; --- payment-request envelope (provider-neutral) for the still-suspended O2 --- + +(commerce-test + "pending-payments-lists-suspended" + (pending-payments env b :GBP "https://shop/return") + (list {:id id2 :request {:order "O2" :amount 1200 :return-url "https://shop/return" :currency :GBP}})) diff --git a/lib/commerce/tests/payment.sx b/lib/commerce/tests/payment.sx new file mode 100644 index 00000000..f72cf150 --- /dev/null +++ b/lib/commerce/tests/payment.sx @@ -0,0 +1,43 @@ +;; lib/commerce/tests/payment.sx — provider-neutral payment-request envelope. +;; Uses (commerce-test name got expected) provided by conformance.sh. +;; Envelope construction is ledger-only (no flow env); pending-payments (which +;; needs the flow env) is exercised in the order suite. + +(define q1 {:codes (list) :subtotal 1000 :discount 0 :total 1200 :tax 200}) +(define q2 {:codes (list) :subtotal 5000 :discount 500 :total 4500 :tax 0}) + +(define b (persist/mem-backend)) +(define _c1 (order-create b "P1" 1 q1)) +(define _c2 (order-create b "P2" 1 q2)) + +(commerce-test + "envelope" + (payment-request b "P1" :GBP "https://shop/return") + {:order "P1" :amount 1200 :return-url "https://shop/return" :currency :GBP}) + +(commerce-test + "envelope-amount" + (payment-request-amount (payment-request b "P1" :GBP "x")) + 1200) +(commerce-test + "envelope-currency" + (payment-request-currency (payment-request b "P1" :GBP "x")) + :GBP) +(commerce-test + "envelope-order" + (payment-request-order (payment-request b "P1" :GBP "x")) + "P1") +(commerce-test + "envelope-return-url" + (payment-request-return-url (payment-request b "P1" :GBP "https://r")) + "https://r") + +;; amount tracks the ledger total, currency is per-call (provider/instance config) +(commerce-test + "envelope-amount-2" + (payment-request-amount (payment-request b "P2" :EUR "x")) + 4500) +(commerce-test + "envelope-currency-2" + (payment-request-currency (payment-request b "P2" :EUR "x")) + :EUR) diff --git a/plans/commerce-on-sx.md b/plans/commerce-on-sx.md index a9ea0089..e4cdf920 100644 --- a/plans/commerce-on-sx.md +++ b/plans/commerce-on-sx.md @@ -21,7 +21,7 @@ reconciliation — all auditable via the event log. ## Status (rolling) -`bash lib/commerce/conformance.sh` → **201/201** (12 suites; + attribution) — **roadmap complete; Phase 5 extensions in progress** +`bash lib/commerce/conformance.sh` → **209/209** (13 suites; + payment) — **roadmap complete; Phase 5 extensions in progress** ## Ground rules @@ -89,14 +89,23 @@ that unlocks the most tests per effort each iteration. flow-on-sx flow, recorded in the ledger; idempotent. - [ ] stock-constrained reservation — order-begin! fails (railway `fail`) when requested qty exceeds stocko availability; reservation decrements a stock view. -- [ ] provider-neutral payment-request envelope — the `'payment` suspension carries - `{:order :amount :currency :return-url}` (mirroring flow `host.sx`'s `request` - envelope) so any provider's host adapter can initiate payment without the engine - knowing the vendor. SumUp/Stripe/etc. adapters stay at the IO edge (orders - service); `order-settle!(ref, amount)` remains the vendor-neutral resume seam. - Keeps lib/commerce provider-agnostic; enables multi-provider support in the core. +- [x] provider-neutral payment-request envelope — `payment.sx`: `payment-request` + materialises `{:order :amount :currency :return-url}` at the IO edge (amount from + the ledger, currency/return-url host-supplied); `pending-payments` enumerates + suspended orders with their envelopes (host poller seam). Engine stays vendor- + agnostic; `order-settle!(ref, amount)` is the resume seam. ## Progress log +- 2026-06-07 — `payment.sx` (Phase 5 ext, the item the user asked about): + provider-neutral payment-request envelope, materialised at the IO edge from the + ledger amount + host-supplied currency/return-url — keeps lib/commerce vendor- + agnostic (SumUp/Stripe adapters live in the orders service). `payment-request` + builds the `{:order :amount :currency :return-url}` envelope; `pending-payments` + is the host-poller seam listing suspended orders + their envelopes. Gotcha: a + Scheme **string** carried as a flow payload round-trips back to SX wrapped as + `{:scm-string "..."}` (numbers come back clean) — unwrap via `scm->string` + before using it as the oid. payment suite 7/7 + 1 order-suite integration test; + total 209/209 (13 suites). - 2026-06-07 — `attribution.sx` (Phase 5 ext): line-level discount attribution — the briefing's marquee "which line item triggered this discount?" query. `promo-lines` is the pure per-promo scope (percent/member → class lines, bundle