Add action_test_connection — a read-only connectivity/schema check that reports source row counts and imports nothing, the safe first step before a dry-run. Wire a "Test Connection" button on the wizard. Document the end-to-end run in the README: least-privilege read-only DB role SQL, the fusion_billing.nexacloud_dsn system parameter (libpq DSN = NexaCloud's URL minus +asyncpg), and the Test → dry-run → real-run flow. Refresh the stale SCAFFOLD status. 53/53 green on odoo-trial.
107 lines
5.1 KiB
Markdown
107 lines
5.1 KiB
Markdown
# 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.md`](../docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.md)
|
|
>
|
|
> **Status:** Core engine (sub-project #1) and the **NexaCloud importer (sub-project #2a)**
|
|
> are implemented and tested on odoo-trial Enterprise. 2b (usage wiring), 2c (control loop),
|
|
> and 2d (reconciliation) are pending.
|
|
|
|
## 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)
|
|
|
|
1. Odoo fully replaces Lago (we build the metered-billing engine; Lago is decommissioned last).
|
|
2. One unified `res.partner` per client; **separate invoice per service**.
|
|
3. **Apps drive**, Odoo is the billing system of record — apps call the inbound API (as they call Lago today); Odoo bills and webhooks back.
|
|
4. Odoo owns the **billing catalog**; apps own **feature entitlements** (shared `plan_code`).
|
|
5. Pilot = **NexaCloud**, phased dual-run cutover.
|
|
6. **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.order` with `is_subscription=True`
|
|
> (`plan_id` → `sale.subscription.plan`). There is **no** `sale.subscription` model.
|
|
> `fusion.billing.usage.subscription_id` therefore points at `sale.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`.
|
|
|
|
## Running the NexaCloud import (2a)
|
|
|
|
Exposed as **Fusion Billing → Import from NexaCloud** (a wizard). It runs entirely
|
|
read-only against NexaCloud, and everything it creates in Odoo is shadow-safe (draft
|
|
subscriptions, no payment token, charges with NULL `plan_id`) so it cannot charge or post
|
|
during the dual-run.
|
|
|
|
**1. Create a least-privilege read-only role in the NexaCloud Postgres (LXC 201):**
|
|
|
|
```sql
|
|
CREATE ROLE odoo_billing_ro WITH LOGIN PASSWORD '<choose-a-strong-password>';
|
|
GRANT CONNECT ON DATABASE nexacloud TO odoo_billing_ro;
|
|
GRANT USAGE ON SCHEMA public TO odoo_billing_ro;
|
|
GRANT SELECT ON users, plans, subscriptions, deployments TO odoo_billing_ro;
|
|
```
|
|
|
|
**2. Point Odoo at it** via the system parameter (Settings → Technical → System Parameters,
|
|
or odoo-shell). psycopg2 wants a **libpq DSN** — i.e. NexaCloud's SQLAlchemy URL *without*
|
|
`+asyncpg`:
|
|
|
|
```
|
|
key: fusion_billing.nexacloud_dsn
|
|
value: postgresql://odoo_billing_ro:<password>@<lxc201-host>:5432/nexacloud
|
|
```
|
|
|
|
(Odoo on nexa / VM 315 must have a network route to the LXC 201 Postgres port.)
|
|
|
|
**3. Validate → dry-run → run for real:**
|
|
|
|
- **Test Connection** — confirms reachability + schema and reports row counts; imports nothing.
|
|
- **Run Import** with **Dry run** ticked — computes the whole import inside a rolled-back
|
|
savepoint and reports created / updated / **skipped** / **failed** counts; writes nothing.
|
|
A red/amber banner flags any failures — investigate them before proceeding.
|
|
- Untick **Dry run** and **Run Import** to persist the shadow copy. Re-running is safe and
|
|
idempotent (upserts, never duplicates).
|
|
|
|
## Local dev
|
|
|
|
```bash
|
|
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.
|