Skip to main content
Petanque Life

Billing & Revenue

F21.07 10 features

At a glance

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.

How it works

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.

Key capabilities

  • 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

In practice

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.

Features in this subsystem

10
ID Status Features
F21.07.01 Shipped 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 Shipped Cross-tenant invoice list with status, amount, due date, dunning stage. Filter + export to CSV and PDF. Implemented (PL-T127)
F21.07.03 Shipped Payments list — all payments across tenants. Detail shows method, provider reference, linked invoice, refund timestamps. Implemented (PL-T127)
F21.07.04 Shipped 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 Shipped 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 Shipped 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 Shipped 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 Shipped 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 Shipped 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 Shipped 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)