- C1/H4: rating cron only rates subs on the charge's own plan_id
- C1: _fc_rate_usage skips creating a line when amount is 0 (still updates existing)
- C2/C4: /usage authorizes each event (exists + is_subscription + linked customer)
- C3: API handlers validate input and return 4xx-shaped errors instead of raising;
controller maps status=='error' to HTTP 400
- H1: cron uses real billing window [last_invoice_date or start_date, next_invoice_date)
- H2: _aggregate uses half-open window anchored on period_start
- H3: idempotency scoped to (subscription_id, metric_id, idempotency_key)
- H5: webhook stores canonical body, signs+POSTs it verbatim, adds X-Fusion-Event-Id,
caps backoff at 2**min(attempts,10)
- H6: SSRF guard rejects non-https / localhost / private / link-local webhook_url
- M7: charge_model reduced to standard/package (dropped unimplemented graduated/volume)
- L1: currency_id required on charge + reconciliation
- L2: charge price non-negative + unit_batch positive DB constraints
Adds 17 regression tests (suite 22 -> 39, all green via fcb_test_on_trial.sh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fusion Centralized Billing (fusion_centralize_billing)
Centralized billing engine that makes this Odoo 19 Enterprise instance the single billing brain for every NexaSystems service — NexaCloud hosting, NexaDesk chat, NexaMaps API, custom apps, and memberships. It replaces Lago and absorbs NexaCloud's home-grown Stripe billing into one customer ledger and one accounting system.
Design spec:
docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.mdStatus: SCAFFOLD. Models + security + the API auth shell are in place and the module installs. The usage engine, full inbound API, and webhook processor are stubs to be implemented from the writing-plans output.
Why this module is small
We build only the metering + integration layer. Everything financial — recurring
invoicing, HST tax, proration, dunning, customer portal, credit notes, Stripe — is
native Odoo Enterprise (sale_subscription, account_accountant, payment_stripe),
already installed and running.
Design decisions (locked)
- Odoo fully replaces Lago (we build the metered-billing engine; Lago is decommissioned last).
- One unified
res.partnerper client; separate invoice per service. - Apps drive, Odoo is the billing system of record — apps call the inbound API (as they call Lago today); Odoo bills and webhooks back.
- Odoo owns the billing catalog; apps own feature entitlements (shared
plan_code). - Pilot = NexaCloud, phased dual-run cutover.
- Aggregate-push usage ingestion (periodic counters, not a raw-event firehose).
Models (fusion.billing.*)
| Model | Purpose |
|---|---|
fusion.billing.service |
One source app; bearer API key (hashed) + webhook config. |
fusion.billing.account.link |
External account id → one res.partner (identity resolution). |
fusion.billing.metric |
Billable metric + aggregation (sum/max/last/unique). |
fusion.billing.charge |
Plan + metric → included quota + overage pricing. |
fusion.billing.usage |
Aggregated per-period usage rollups (idempotent). |
fusion.billing.webhook |
Outbound lifecycle event queue (HMAC + retry). |
fusion.billing.reconciliation |
Dual-run Odoo-vs-app delta during cutover. |
Odoo 19 note (verified): a subscription is a
sale.orderwithis_subscription=True(plan_id→sale.subscription.plan). There is nosale.subscriptionmodel.fusion.billing.usage.subscription_idtherefore points atsale.order.
Inbound API
Lago-shaped REST under /api/billing/v1/*, bearer auth. Endpoints mirror NexaDesk's
existing lago-client.ts so migration is a thin client swap. /health works today;
the rest return 501 until implemented.
Relationship to fusion_api
fusion_api manages outbound provider keys (OpenAI, Maps, Twilio) + cost tracking —
i.e. COGS. This module tracks customer revenue. Complementary: feed fusion_api
cost into margin reporting; reuse its daily-rollup aggregation pattern.
Dependencies
account_accountant, sale_subscription, sale_management, payment_stripe.
Local dev
docker exec odoo-nexa-app odoo -d nexamain -u fusion_centralize_billing --stop-after-init
# tests (once added):
docker exec odoo-nexa-app odoo -d nexamain --test-enable --test-tags /fusion_centralize_billing -u fusion_centralize_billing --stop-after-init
Canadian English, CAD, HST via account.tax. New fields on native models use the x_fc_* prefix.