Merchant billing
@dscodotco/merchant-billing is the B2B invoicing engine that bills tenant brands for the platform. It is an append-only, effective-dated contract model, a stream of usage facts, a pure rating step that turns facts + contract into invoice lines, and a guarded collection state machine posted to the ledger.
The fee menu (contract terms)
A contract’s terms is a JSON fee menu. Every term is optional — an absent term is not billed, never billed at zero.
| Term | What it bills |
|---|---|
platform_fee_cents_monthly | A flat monthly platform fee. |
gmv_fee_bps | GMV usage fee — basis points of the period’s gross merchandise value. 120 ≈ $1,000/mo per $1M of annualized GMV (linear, cliff-free). |
per_unit_shipped_cents | Flat fee per unit shipped. |
fulfillment_fee_cents | Flat fee per fulfilled order. |
dropship_cogs_markup_bps | Dropship cost-plus — bills shipped vials at landed cost + markup. |
How facts arrive
Usage facts are captured automatically from platform events (no manual entry needed):
gmv— one per placed order (commerce.order.placed.v1), quantity = the order’s merchandise subtotal in cents. Idempotent per order.unit_shipped— one per shipped line (fulfillment.cogs.captured.v1), carrying the landedunit_cost_centsthat drives the dropship COGS line.
The monthly loop
A daily, idempotent monthly-invoice-run sweep generates + issues each tenant’s prior-month invoice and posts it to accounting; a dunning-overdue sweep moves past-grace invoices to overdue (MERCHANT_BILLING_DUNNING_GRACE_DAYS, default 30). So the loop is end-to-end: orders/ships -> facts -> monthly invoice -> collection.
Collection uses a pluggable CollectionProvider. Two adapters ship today: manual (the default when a contract names no provider) and stripe_ach — a Stripe Invoicing / ACH (us_bank_account) collection adapter (StripeAchCollectionProvider). The Stripe adapter reads its secret from STRIPE_B2B_SECRET_KEY and bills the tenant’s stripe_customer_id from the contract terms; its paid/failed webhooks are consumed via the webhooks gateway. A contract selects its provider explicitly; absent that, collection is manual.
Surfaces
-
Operators (console -> Merchant billing): create contracts, view invoices/statement, run invoicing/dunning on demand, and a revenue summary (
GET /v1/merchant-billing/admin/summary). -
Merchants (portal, the Billing panel): read-only,
fdk_credential, scoperead:own_billing. These routes live in themerchant-apimodule (composition over the billing repo), not in the billing module’s own operator surface:Method Path Purpose GET/v1/merchant/billing/invoicesList the caller’s invoices. GET/v1/merchant/billing/invoices/:idOne invoice with its line items. GET/v1/merchant/billing/statementOutstanding balance (a distinct read — its failure surfaces as an error, never a laundered $0). The recoup/breakeven tracker is a separate merchant read on the billing module itself:
GET /v1/merchant/recoup(alsoread:own_billing;config: nullon a200is the honest “not set up”, never an error).