diff --git a/fusion_centralize_billing/README.md b/fusion_centralize_billing/README.md index d3a3384b..ad1c0fe6 100644 --- a/fusion_centralize_billing/README.md +++ b/fusion_centralize_billing/README.md @@ -7,9 +7,9 @@ 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:** **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. +> **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 @@ -59,6 +59,42 @@ cost into margin reporting; reuse its daily-rollup aggregation pattern. `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 ''; +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:@: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 diff --git a/fusion_centralize_billing/tests/test_importer.py b/fusion_centralize_billing/tests/test_importer.py index 9ffdcf01..33f531fe 100644 --- a/fusion_centralize_billing/tests/test_importer.py +++ b/fusion_centralize_billing/tests/test_importer.py @@ -243,3 +243,9 @@ class TestImporterReadGuard(TransactionCase): wiz = self.env['fusion.billing.import.wizard'].sudo().create({'dry_run': True}) with self.assertRaises(UserError): wiz._read_nexacloud_rows() + + def test_test_connection_guards_missing_dsn(self): + self.env['ir.config_parameter'].sudo().set_param('fusion_billing.nexacloud_dsn', '') + wiz = self.env['fusion.billing.import.wizard'].sudo().create({'dry_run': True}) + with self.assertRaises(UserError): + wiz.action_test_connection() diff --git a/fusion_centralize_billing/views/import_wizard_views.xml b/fusion_centralize_billing/views/import_wizard_views.xml index b38eaf3c..5883d7a2 100644 --- a/fusion_centralize_billing/views/import_wizard_views.xml +++ b/fusion_centralize_billing/views/import_wizard_views.xml @@ -19,6 +19,8 @@