Files
Odoo-Modules/docs/superpowers/2026-05-27-fusion-billing-session-handoff.md

10 KiB
Raw Blame History

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 #914 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 (~12 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): DEPLOYEDfusion_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 --deletes 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.