Commerce / proposed next steps
draft · 2 Jul 2026

What's left between a contract and the first dollar

A map of the remaining build, pieced together from the context docs and a pass over the code on master. Two asks: does this ordering look right, and which pieces should I pick up — especially on the testing / actually-using-the-API side.

Every "exists / missing" claim below was checked against the code as of today. Happy to be corrected on anything — this is me building a picture, not asserting one.

the through-line

Contract → money, and where the chain breaks today

The docs describe one pipeline: a contract edit fans out through the outbox to OpenMeter (subscription → usage → invoice), which drives billing status and kicks off settlement in Modern Treasury. Here's the state of each link:

working partial / stubbed missing open decision
Contract + snapshot
create, add/remove product, status transitions
Outbox events
only contract.created is ever written
Relay / publisher
nothing consumes the outbox at all
OpenMeter subscription
provisioned on activation; never updated after
Usage invoice
OpenMeter per docs — but an MT-backed invoices module landed
billing_status
inert free-form string; no state machine
Settlement
disbursement journey works; three gaps below
Seller cash-out
payout API works standalone; not stitched in
now — proposed order

Seven concrete pieces of work

Ordered by dependency: 1–2 unblock everything downstream, 3–4 finish the contract→billing seam, 5–7 close the gaps in the payout path. 5–7 don't depend on 1–4 and could be picked up in parallel.

  1. 1

    Build the outbox relay / publisher

    missing infra

    The outbox table is written to but nothing reads it — no poller, no scheduler dependency, nothing ever sets published_at. A relay that reads unpublished rows in order, dispatches to subscribers, and marks rows published only when downstream effects succeed (idempotent retries keyed on event_id).

    Why first: every downstream projection (OpenMeter sync, invoice handling) waits on this. It's the first background process in the API.

  2. 2

    Emit outbox events on all contract mutations

    partial contracts

    Only contract.created exists today. Product add/remove and status transitions have TODO markers where events should be emitted, and a ContractUpdatedEvent class exists but is never used. Payloads need enough state (product/pricing deltas) for subscription sync.

    Pairs with #1 — the relay is useless with nothing to relay, and vice versa. No dependency between them; could be one person's first two PRs or two people in parallel.

  3. 3

    Finish contract → OpenMeter subscription sync

    partial openmeter

    Provisioning already landed: activating a contract creates the OM subscription synchronously and stores the provider refs. What's missing: syncing anything after activation (product/pricing changes, suspend/terminate → subscription update/cancel), moving the projection onto the relay instead of an inline call, and retry on provisioning failure (currently just logged).

    Depends on: #1 and #2.

  4. 4

    Invoice ingestion → billing_status

    decision needed openmeter / invoices

    Consume the invoice lifecycle (issued / paid / past-due) to drive the contract's billing_status machine — today it's an inert free-form string. On paid, hand off to settlement kickoff.

    Open decision first: the context docs say OpenMeter owns invoicing, but a Modern Treasury-backed invoices module recently landed — and it currently has no link to contracts and doesn't touch OpenMeter. Which system cuts the usage invoice? This changes the shape of the whole task (OM webhooks vs. feeding the MT invoices module from OM usage data).
  5. 5

    Commit-balance ledger step (AVAILABLE → COMMITTED)

    missing ledger

    Step 2 of the documented payout happy path. The disbursement currently drains the buyer's AVAILABLE balance directly; the COMMITTED / COMMITTED_DOWNPAYMENT accounts are provisioned but nothing ever posts to them. Add the commit recipe and release from COMMITTED in the journey.

  6. 6

    Seasoning window for holding release

    stubbed ledger

    Seller funds are supposed to sit in Pay-In Holding for a seasoning period; today the release fires immediately (notBefore: new Date(), marked "follow up" in the code). The delayed-release machinery already honors notBefore — only the delay value is stubbed. Small task, real correctness win.

  7. 7

    Wire seller cash-out into the settlement journey

    unwired payments

    Payout creation works standalone (step 7 of the happy path), but it isn't stitched into the disbursement journey after holding release. Needs a small design choice: automatic cash-out on release vs. seller-initiated.

later — bigger arcs

Epics parked behind the list above

Contract model completion

The doc specifies an event-sourced edit log; today it's snapshot-based, with only 4 of ~16 operations implemented (no financing, no bitemporality, no auto-renew scheduler, service_status inert). Needs an architecture decision: build the edit log, or amend the doc.

Remaining funds-flow scenarios

Partner/referral splits, tax, hosting fees + funding requests, free trials (NOTIONAL accounts), downtime credits, crypto slippage — everything in the funds-flow doc beyond bank pay-in and the seller-disbursement happy path.

Airwallex card & FX pay-ins

Not started — enum members are commented out across the schema. Card pay-ins and the one accrual flow (settlement receivable). Deferred until the bank path is solid.

Availability log / DCIM ingestion

Device-health events → availability log → metering and downtime credits. Blocked on a design pass — depends on what Brokkr exposes.

Reconciliation pipeline

ACH returns, failed payment orders, and settlement anomalies currently just alert Sentry ("no reconciliation pipeline yet" in two places). Auto-matching, retries, and a manual-review queue. Cross-cutting; grows with the flows.

where I could plug in

Testing & actually using the API

You mentioned the team needs help testing / really exercising the API. Here's what I've done so far and where I think that effort is most useful next:

Done: a scenario-driven testing UI

An internal client app (apps/testing-ui) that drives the API as a platform operator: customer onboarding, wallets, settlements (with leg polling), and payouts.

Honest question: is this shape useful to the team, or should the effort go into scripted journeys instead? Happy to hear "kill it" too.

Be the first user of each new flow

As each item above lands, write the end-to-end journey that proves it — the way hurl/ covers onboard→fund and settle→payout today. E.g. once #5–7 land: one journey doing fund → commit → settle → season → cash out, asserting ledger balances at each step.

Grow the hurl journeys into a real E2E suite

Per-actor flows that run against a local API and could run in CI against sandbox. Immediate candidates: a journey for the OpenMeter path (activate → usage → subscription), failure-path journeys (ACH return, failed payout), and idempotency checks.

Feed back what testing finds

Rough edges, API ergonomics, missing validations, doc gaps — filed as small focused PRs or notes rather than a pile of complaints. Testing is only useful if the findings land somewhere.

What I'd like from you

  1. Does the ordering above match your mental model? Anything mis-prioritized or already in flight elsewhere?
  2. The invoicing decision (item 4): OpenMeter or Modern Treasury as the invoice source — who makes that call, and can we make it soon?
  3. Which of items 1–7 should I pick up — and is the testing work above the right way for me to be useful in parallel?
  4. Is the testing UI worth continuing to invest in, or would you rather see everything as scripted hurl journeys?