# fusion_centralize_billing — Session Handoff (2026-05-27) Resume point for the centralized-billing initiative. Read this first, then continue from **"Decision pending"** below. ## Where we are - **Sub-project #1 (core billing engine): DONE and on `main`** (tip `d770c0c3`, pushed to GitHub + Gitea). - 11/11 plan tasks, TDD, Opus code-reviewed; all Critical/High bugs fixed (cross-billing cron → match by `plan_id`; `/usage` authz vs IDOR; input validation → 4xx not 500; correct billing-period window; idempotency scoped to `(sub, metric, key)`; webhook sign-exact-bytes + event-id + SSRF guard). - **39 tests green on Odoo 19 Enterprise.** - Note: the 14 billing commits were rebased off the old login-audit/helpdesk stack and landed cleanly on `main`. - **`fusion_login_audit`: also landed on `main`** (2026-05-27). Its 19 commits were rebased onto `main` and the `feat/fusion-login-audit` branch was deleted. This also restored Odoo-19 rules #9–14 in `CLAUDE.md`, which had gone missing on `main` when billing landed alone (they were authored alongside login_audit and never existed on the old base). - A concurrent `feat/helpdesk-customer-followup` session still carries pre-landing copies of the billing + login_audit commits; when it merges, replay its helpdesk-only commits onto `main`. - **Reference docs (on `main`):** - Spec: `docs/superpowers/specs/2026-05-27-nexa-billing-centralized-design.md` - Core plan: `docs/superpowers/plans/2026-05-27-fusion-centralize-billing-core.md` ## Next: sub-project #2 — NexaCloud adapter + dual-run reconciliation Per spec §12, each sub-project is its own spec → plan → build cycle. #2 decomposes into four chunks (dependency order): | Chunk | What | Risk | |-------|------|------| | **2a — Mapping + importer** | Read `nexacloud` DB → create `res.partner` + `account.link`, `product.template` + subscription plans, one subscription `sale.order` per deployment | **Low** — read-only on NexaCloud, writes only into Odoo | | **2b — Usage metering wiring** | NexaCloud `usage_metering.py` pushes CPU-seconds → Odoo `/usage`; verify aggregation → draft invoice w/ quota + overage + HST | Edits NexaCloud code | | **2c — Control loop** | NexaCloud consumes Odoo's outbound webhooks (`invoice.payment_failed` → suspend via existing `network_isolation`/`throttle_checker`; `subscription.terminated` → deprovision) | Edits NexaCloud code | | **2d — Dual-run reconciliation** | `fusion.billing.reconciliation` diffs Odoo-computed vs NexaCloud-actual per customer/period for ≥ 1 cycle before any flip | Safety gate before flipping real billing | The core engine already built the *receiving* side (`/usage`, webhook engine, charge math). #2 is about **connecting NexaCloud to it and proving the numbers match before flipping.** ## Decision pending (resume here) We were in the `superpowers:brainstorming` flow for #2 and stopped at: **which slice to start with?** - **(recommended) 2a — Mapping + importer** — lowest risk, foundation for everything else. - 2d — Reconciliation first (front-load the trust mechanism). - Full #2 design as one spec, then one plan. - Just write the #2 plan, no code this session. ## Open questions to resolve before building #2 - **Spec §15 Q2 — NexaCloud billing granularity:** confirm **one subscription per deployment** (spec leans this way) vs one subscription per customer with deployment line items. - **Access / environments needed:** - Read access to the `nexacloud` DB schema (LXC 102 / its Postgres on LXC 201) to design the importer mapping. - A NexaCloud staging or safe path for 2b/2c (they edit live NexaCloud code). - Test target for the Odoo side stays the odoo-trial Enterprise sandbox. - **Resolved already:** Stripe is one account (`acct_1ShlA9IkwUB1dVox`) for everything — no account migration (spec §11 / §15 Q1). Branch strategy — land on `main`, branch new work off `main`. ## How to run / test - **Billing tests:** `bash scripts/fcb_test_on_trial.sh` from repo root → pass = `FCB_EXIT=0` (~1–2 min). Syncs the module to the odoo-trial Enterprise sandbox (Proxmox VM 316, db `trial`) and runs `--test-enable`. Local dev Odoo is Community and **cannot** install this module. ## Branch hygiene (lesson from this session) Cut each new feature branch from `main`, and land it before starting the next. For any cross-branch git surgery, use a **throwaway `git worktree`** — never switch the shared working dir's branch, because a concurrent session may be working on it. --- ## UPDATE — sub-project #2 complete (2026-05-27, later session) All four chunks of #2 are now built. The brainstorm "which slice" question resolved to 2a-first; everything else followed. **Done + on `main` in `Odoo-Modules` (fully tested on odoo-trial, suite `FCB_EXIT=0`):** - **2a — importer** (`fusion.billing.import.wizard`): read-only `psycopg2` reader split from pure-Odoo writes; users→partners+links, plans→`cpu_seconds` charge catalog (`plan_id` NULL), deployments→one **draft shadow** `sale.order` each with the flat price. Shadow-safe by construction (draft + no token + NULL `plan_id`). Idempotent, dry-run, Test-Connection guard, README runbook. - **2d — reconciliation** (`fusion.billing.reconciliation`): `_compute_reconciliation` + `_reconcile_rows` (Odoo flat+overage vs NexaCloud actual, status match/delta), reader for NexaCloud usage+invoice actuals, "Run Reconciliation" button. **Upsert key is `(service, external_subscription_id, period)`** — per subscription, so a customer with two deployments doesn't collide. - **/usage enabler**: `_api_record_usage` resolves a subscription by the source app's own id (`x_fc_nexacloud_subscription_id`) so NexaCloud can push against shadow subs. - Core-engine bug fixed in passing: `charge.price_per_unit` is now `Float(16,6)` and `_compute_billable` keeps 6-dp precision (was `Monetary`/cent-rounded → would under-bill sub-cent rates and drift from NexaCloud's 4-dp amounts). **Code-complete in `Nexa-Cloud` (feature-flagged, NOT deployed, NOT integration-tested):** - **2b — usage push**: `services/odoo_billing_client.py` + a hook in `usage_metering.py` posting cpu-seconds to Odoo `/usage`. **2c — control loop**: `routers/odoo_billing.py` (`POST /api/v1/billing/webhooks/central`, HMAC-verified) + `services/odoo_billing_integration.py` (suspend/restore/deprovision). All INERT unless `ODOO_BILLING_ENABLED`. Implemented as NEW modules + edits to clean files only — NexaCloud `main` had concurrent **Cursor uncommitted WIP** (`routers/billing.py`, `scheduler.py`, `stripe_service.py`, `models/billing.py`, …) which was deliberately not touched. Commits: `94542ec` + `956abb2` (only my files staged). **Deployment status (2026-05-27):** - **odoo-nexa (production `nexamain`): DEPLOYED** — `fusion_centralize_billing` (core + 2a + 2d) **fresh-installed** (#1 had never actually been deployed here; `DIR_ABSENT` before). `ir_module_module.state = installed`, `odoo-nexa-app` healthy. **INERT**: no `nexacloud_dsn`, all charges `plan_id` NULL (rating cron no-op), no webhooks queued (dispatch cron no-op), inbound API 401s with no key configured. Synced to `/opt/odoo/custom-addons` + `-i` via the restart-safe recipe. - **NexaCloud (prod, `vps.nexasystems.ca` / 192.168.1.250): DEPLOYED — INERT.** Did NOT use `./deploy.sh` (it `rsync --delete`s the working tree → would have shipped the concurrent **uncommitted Cursor WIP** (7 files) AND wiped the gitignored prod `.env` files). Instead deployed **surgically**: rsync of ONLY my 6 committed billing files (no `--delete`; `.env` + Cursor's files untouched), `docker compose build backend`, **boot-tested in a throwaway container** (`run --rm --no-deps backend python -c "import app.main"` → BOOT_OK) before swapping, then `up -d backend`. `nexacloud-api` healthy, `/health` OK. Feature OFF: `ODOO_BILLING_ENABLED` unset → `/billing/webhooks/central` returns 404 and no usage is pushed. Activate later by setting `ODOO_BILLING_*` in `/opt/nexacloud/.env` (+ compose env passthrough) once the Odoo side is wired. **NOTE:** Cursor's 7-file WIP remains uncommitted locally and was never deployed — when Cursor finishes, a normal `./deploy.sh` will ship it (and re-sync `.env`). **Dual-run stand-up results (2026-05-27) — STOPPED here for review, NOT flipped:** - Read-only role `odoo_billing_ro` created on nexacloud Postgres (192.168.1.50); DSN set in `ir.config_parameter` `fusion_billing.nexacloud_dsn` on nexamain. Test Connection OK (read 7 users / 232 plans / 87 subscriptions). - **Shadow import committed on nexamain**: 7 partners, 232 plan catalogs, 87 draft shadow subscriptions; 0 skipped, 0 failed. (NOTE: importer takes ALL plans/subs regardless of active status → ~464 NC-* products now in the prod ERP catalog. Consider filtering to `is_active` plans / active subscriptions, or prune the shadow records — all reversible.) - **Reconciliation pass**: 9 (sub,period) rows had real billing activity → **2 match, 7 delta**, 0 failed. The 7 deltas, MUST resolve before flipping: 1. **One-off / non-subscription invoices** (3 rows: $877.99, $872.66, $32.20) — nexacloud invoices with NULL subscription_id (fees/manual/credits); not modeled per-subscription. 2. **List-price ≠ actual-invoiced** (4 rows: Odoo $200/$50 vs actual ~$9.1x) — likely proration or NexaCloud invoicing ≠ plan list price. - **2d bug surfaced (analysis-only, not safety):** `_reconcile_rows` with an empty `subscription_external_id` matches NULL-field orders instead of skipping → spurious delta rows for the one-off invoices. Add `if not sub_ext: skip`. **Remaining before go-live (gated on infra / ops you do):** 1. Grant the read-only DSN (`fusion_billing.nexacloud_dsn`) — see the module README — then Test Connection → dry-run import → review → real import. 2. Run a dual-run cycle (Run Reconciliation), confirm all rows `match`. 3. **2c needs the Odoo side to actually EMIT** `invoice.payment_failed` / `payment_succeeded` / `subscription.terminated` webhooks with `deployment_id` in the payload — that emission isn't wired yet (it belongs to the live billing flow). The NexaCloud receiver is built to that contract; confirm the payload shape when wiring it. 4. Integration-test + deploy the NexaCloud changes (no test harness in that repo). 5. The flip: set `charge.plan_id`, attach Stripe tokens, confirm the shadow subs. Specs/plans: `specs/2026-05-27-nexacloud-billing-importer-design.md`, `specs/2026-05-27-nexacloud-reconciliation-design.md`, and the matching plans.