Billing & Revenue
En bref
Financial oversight across every tenant: revenue dashboard with MRR/ARR/runway/churn, cross-tenant invoice and payment lists, an unmatched-payments inbox with fuzzy invoice suggestions, two-step idempotent refunds, subscription lifecycle with plan history, churn analysis, dunning override, platform-fee reporting, and end-to-end reconciliation against the gateway.
Comment ça fonctionne
Billing & Revenue is the CFO-grade view that complements the tenant-facing billing surface. The revenue dashboard renders MRR/ARR, net and gross revenue, and platform fees per plan, country, and tenant on a rolling 12-month trend with a 5-minute cache; LTV:CAC and runway derive from the same source. Cross-tenant lists for invoices and payments expose status, amount, due date, dunning stage, method, provider reference, linked invoice, and refund timestamps; both export to CSV and PDF.
The unmatched-payments inbox at `GET /sys/payments/unmatched` reads `UnmatchedBankTransfer` rows and decorates each with up to five fuzzy invoice suggestions ranked by OCR text, amount, tenant, due window, and counterparty. `/assign` posts the matching `Payment` plus an `AccountingEntry` and recomputes invoice status; `/ignore` parks the row. Both mutations require `sys_finance`, fresh-auth, and a reason. Refunds are deliberately two-step: `/refund/preview` calculates the impact, then `/refund` executes — idempotent on `(payment_id, amount_cents, actor)` so replays return the original refund rather than double-charging.
The orchestration calls the gateway, posts the counter `AccountingEntry`, transitions the invoice to `partially_refunded` or `refunded`, and optionally enqueues a `notify_tenant` job. Subscription management offers a cross-tenant list and detail with a plan-history timeline; `/change-plan`, `/pause`, `/cancel`, and `/extend-trial` each enforce sys_finance + fresh-auth + reason and persist a per-subscription `plan_history` log. The churn report aggregates `SubscriptionCancellation` and legacy `Subscription(status=CANCELLED)` rows in a window, bucketed by reason / plan / country, with revenue impact locked at cancellation time via `Subscription.amount_eur_cents`.
The dunning override writes `override_path` (`freeze` or `custom`) and `override_custom_stages` directly on the `DunningCase` so the worker reads it as a single source of truth. Platform-fee and reconciliation reports compare gateway-side data against internal `AccountingEntry` rows for finance close.
Capacités clés
- CFO dashboard: MRR/ARR, gross/net revenue, platform fees, runway, LTV:CAC, churn, 12-month trend
- Cross-tenant invoice and payment lists with CSV/PDF export
- Unmatched-payments inbox with fuzzy ranked suggestions and reason-gated assign/ignore
- Two-step preview-then-execute refund, idempotent on `(payment_id, amount, actor)`
- Subscription lifecycle: change-plan, pause, cancel (now/period-end), trial-extend (≤30d)
- Plan-history timeline persisted per subscription
- Churn report bucketed by reason / plan / country with revenue impact locked at cancellation
- Dunning override (freeze or custom stages) read by the worker as single source of truth
- Platform-fee report by tenant or plan; reconciliation view per provider
En pratique
A CFO opens the revenue dashboard on Monday morning, confirms MRR is up 4 percent, and notices runway has lengthened to 21 months. He flips to the unmatched-payments inbox and finds three SEK transfers from yesterday. The top suggestion for each shows a 0.96 OCR match and the right tenant; he clicks `Assign`, completes fresh-auth, and the invoices flip to paid with new `AccountingEntry` rows.
A federation then asks for a partial refund on a duplicate charge; he opens the payment, runs `/refund/preview`, confirms the figure, executes `/refund`, and the gateway call plus counter entry plus `notify_tenant` job complete in one round trip. Idempotency means an accidental retry returns the original refund instead of charging twice.
Fonctionnalités de ce sous-système
10| ID | Status | Fonctionnalités |
|---|---|---|
| F21.07.01 | Livré | Revenue dashboard — MRR/ARR, net revenue, gross revenue, platform fees, per plan / country / tenant. Rolling 12-month trend, 5-minute cache. ✅ PL-T127 |
| F21.07.02 | Livré | Cross-tenant invoice list with status, amount, due date, dunning stage. Filter + export to CSV and PDF. Implemented (PL-T127) |
| F21.07.03 | Livré | Payments list — all payments across tenants. Detail shows method, provider reference, linked invoice, refund timestamps. Implemented (PL-T127) |
| F21.07.04 | Livré | Unmatched payments inbox — GET /sys/payments/unmatched returns rows from UnmatchedBankTransfer decorated with up to 5 fuzzy invoice suggestions (OCR/amount/tenant/due-window/counterparty). POST /sys/payments/unmatched/{id}/assign posts the matching Payment + AccountingEntry and recomputes invoice status; /ignore parks the row. Both mutations require sys_finance + fresh-auth + reason. ✅ PL-T127b |
| F21.07.05 | Livré | Refund workflow — two-step POST /sys/payments/{id}/refund/preview then POST /sys/payments/{id}/refund. Idempotent on (payment_id, amount_cents, actor) (replays return original refund). Orchestrates gateway call → counter AccountingEntry → invoice status transition (partially_refunded / refunded) → optional notify_tenant job. sys_finance + fresh-auth + reason. Implemented (PL-T127b) |
| F21.07.06 | Livré | Subscription management — GET /sys/subscriptions cross-tenant list + detail with plan-history timeline; POST /sys/subscriptions/{id}/{change-plan,pause,cancel,extend-trial} for tier/amount/proration, until-date pause, now/period_end cancel, ≤30-day trial extension. Plan history persisted as audit-derived timeline + per-subscription plan_history log. All mutations sys_finance + fresh-auth + reason. Implemented (PL-T127b) |
| F21.07.07 | Livré | Churn report — GET /sys/billing/churn?from&to aggregates SubscriptionCancellation + legacy Subscription(status=CANCELLED) rows in a window, bucketed by reason / plan / country. Revenue impact locked at cancellation via Subscription.amount_eur_cents. sys_finance. ✅ PL-T127c |
| F21.07.08 | Livré | Dunning override — GET /sys/tenants/{id}/dunning + POST .../dunning/override + POST .../dunning/resume. Persists override_path (freeze / custom) + override_custom_stages directly on DunningCase; worker reads this as its single source of truth. sys_finance + fresh-auth + reason. Implemented (PL-T127c) |
| F21.07.09 | Livré | Platform-fee report — GET /sys/billing/platform-fees?from&to&group_by=tenant\ country sums Invoice(issuer_type=platform) into gross/paid/outstanding totals per group + invoice rows. GET /sys/billing/platform-fees.csv exports the same payload. sys_finance. | Implemented (PL-T127c) |
| F21.07.10 | Livré | Reconciliation view — GET /sys/billing/reconciliation?provider=stripe\ bankgirot&from&to compares provider-ledger totals (Stripe Balance / Bankgirot settlement) to internal AccountingEntry sums, flags drift above tolerance (100 cents). 1 h in-process cache, 503 when provider credentials are missing. sys_finance. | Implemented (PL-T127c) |