ReferenceMerchant billing

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.

TermWhat it bills
platform_fee_cents_monthlyA flat monthly platform fee.
gmv_fee_bpsGMV 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_centsFlat fee per unit shipped.
fulfillment_fee_centsFlat fee per fulfilled order.
dropship_cogs_markup_bpsDropship 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 landed unit_cost_cents that 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, scope read:own_billing. These routes live in the merchant-api module (composition over the billing repo), not in the billing module’s own operator surface:

    MethodPathPurpose
    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 (also read:own_billing; config: null on a 200 is the honest “not set up”, never an error).