commerce: provider-neutral payment-request envelope (8 tests) — Phase 5 ext
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m10s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m10s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ if [ ! -x "$SX_SERVER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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_JSON="lib/commerce/scoreboard.json"
|
||||||
OUT_MD="lib/commerce/scoreboard.md"
|
OUT_MD="lib/commerce/scoreboard.md"
|
||||||
@@ -68,6 +68,7 @@ run_suite() {
|
|||||||
(load "lib/commerce/quote.sx")
|
(load "lib/commerce/quote.sx")
|
||||||
(load "lib/commerce/ledger.sx")
|
(load "lib/commerce/ledger.sx")
|
||||||
(load "lib/commerce/order.sx")
|
(load "lib/commerce/order.sx")
|
||||||
|
(load "lib/commerce/payment.sx")
|
||||||
(load "lib/commerce/recon.sx")
|
(load "lib/commerce/recon.sx")
|
||||||
(load "lib/commerce/federation.sx")
|
(load "lib/commerce/federation.sx")
|
||||||
(load "lib/commerce/attribution.sx")
|
(load "lib/commerce/attribution.sx")
|
||||||
|
|||||||
41
lib/commerce/payment.sx
Normal file
41
lib/commerce/payment.sx
Normal file
@@ -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 <ledger total> :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)))))
|
||||||
@@ -8,12 +8,13 @@
|
|||||||
"stack": {"pass": 16, "fail": 0},
|
"stack": {"pass": 16, "fail": 0},
|
||||||
"quote": {"pass": 13, "fail": 0},
|
"quote": {"pass": 13, "fail": 0},
|
||||||
"ledger": {"pass": 20, "fail": 0},
|
"ledger": {"pass": 20, "fail": 0},
|
||||||
"order": {"pass": 21, "fail": 0},
|
"order": {"pass": 22, "fail": 0},
|
||||||
"recon": {"pass": 20, "fail": 0},
|
"recon": {"pass": 20, "fail": 0},
|
||||||
"federation": {"pass": 12, "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_fail": 0,
|
||||||
"total": 201
|
"total": 209
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ _Generated by `lib/commerce/conformance.sh`_
|
|||||||
| stack | 16 | 0 | 16 |
|
| stack | 16 | 0 | 16 |
|
||||||
| quote | 13 | 0 | 13 |
|
| quote | 13 | 0 | 13 |
|
||||||
| ledger | 20 | 0 | 20 |
|
| ledger | 20 | 0 | 20 |
|
||||||
| order | 21 | 0 | 21 |
|
| order | 22 | 0 | 22 |
|
||||||
| recon | 20 | 0 | 20 |
|
| recon | 20 | 0 | 20 |
|
||||||
| federation | 12 | 0 | 12 |
|
| federation | 12 | 0 | 12 |
|
||||||
| attribution | 16 | 0 | 16 |
|
| attribution | 16 | 0 | 16 |
|
||||||
| **Total** | **201** | **0** | **201** |
|
| payment | 7 | 0 | 7 |
|
||||||
|
| **Total** | **209** | **0** | **209** |
|
||||||
|
|||||||
@@ -65,3 +65,10 @@
|
|||||||
(commerce-test "post-restart-status" (order-status b "O3") :fulfilled)
|
(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-recon-ok" (order-recon b "O3") :ok)
|
||||||
(commerce-test "post-restart-flow-done" (order-flow-status env id3) "done")
|
(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}}))
|
||||||
|
|||||||
43
lib/commerce/tests/payment.sx
Normal file
43
lib/commerce/tests/payment.sx
Normal file
@@ -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)
|
||||||
@@ -21,7 +21,7 @@ reconciliation — all auditable via the event log.
|
|||||||
|
|
||||||
## Status (rolling)
|
## 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
|
## Ground rules
|
||||||
|
|
||||||
@@ -89,14 +89,23 @@ that unlocks the most tests per effort each iteration.
|
|||||||
flow-on-sx flow, recorded in the ledger; idempotent.
|
flow-on-sx flow, recorded in the ledger; idempotent.
|
||||||
- [ ] stock-constrained reservation — order-begin! fails (railway `fail`) when
|
- [ ] stock-constrained reservation — order-begin! fails (railway `fail`) when
|
||||||
requested qty exceeds stocko availability; reservation decrements a stock view.
|
requested qty exceeds stocko availability; reservation decrements a stock view.
|
||||||
- [ ] provider-neutral payment-request envelope — the `'payment` suspension carries
|
- [x] provider-neutral payment-request envelope — `payment.sx`: `payment-request`
|
||||||
`{:order :amount :currency :return-url}` (mirroring flow `host.sx`'s `request`
|
materialises `{:order :amount :currency :return-url}` at the IO edge (amount from
|
||||||
envelope) so any provider's host adapter can initiate payment without the engine
|
the ledger, currency/return-url host-supplied); `pending-payments` enumerates
|
||||||
knowing the vendor. SumUp/Stripe/etc. adapters stay at the IO edge (orders
|
suspended orders with their envelopes (host poller seam). Engine stays vendor-
|
||||||
service); `order-settle!(ref, amount)` remains the vendor-neutral resume seam.
|
agnostic; `order-settle!(ref, amount)` is the resume seam.
|
||||||
Keeps lib/commerce provider-agnostic; enables multi-provider support in the core.
|
|
||||||
|
|
||||||
## Progress log
|
## 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 —
|
- 2026-06-07 — `attribution.sx` (Phase 5 ext): line-level discount attribution —
|
||||||
the briefing's marquee "which line item triggered this discount?" query.
|
the briefing's marquee "which line item triggered this discount?" query.
|
||||||
`promo-lines` is the pure per-promo scope (percent/member → class lines, bundle
|
`promo-lines` is the pure per-promo scope (percent/member → class lines, bundle
|
||||||
|
|||||||
Reference in New Issue
Block a user