# Sub-project #2d — NexaCloud Dual-Run Reconciliation (Design) - **Date:** 2026-05-27 - **Status:** Design (proceeding straight to build — approach determined by parent spec §10) - **Module:** `fusion_centralize_billing` (Odoo 19 Enterprise; tested on odoo-trial) - **Parent:** Sub-project #2. Depends on **2a** (the importer creates the shadow subscriptions + the `cpu_seconds` charge catalog this reconciles against). - **Model already exists:** `fusion.billing.reconciliation` (`service_id`, `partner_id`, `period`, `odoo_amount`, `external_amount`, `delta`, `status` ∈ match/delta/resolved, `note`). ## 1. Goal Prove, for ≥ 1 billing cycle, that Odoo's billing engine computes the **same charge** as NexaCloud already does — per subscription, per period — before any real billing is flipped. Read-only against NexaCloud; writes only `fusion.billing.reconciliation` rows in Odoo. ## 2. What gets compared For each imported shadow subscription and period: - **`external_amount`** = NexaCloud's **actual** pre-tax charge for that subscription+period (the NexaCloud invoice **subtotal**, i.e. flat plan + its own metered overage, before HST). - **`odoo_amount`** = what **Odoo would charge** for the same period: `flat + overage`, where - `flat` = the shadow subscription's plan-product line `price_unit` (the imported flat price), and - `overage` = `charge._compute_billable(cpu_seconds)[1]` for the period's CPU usage, with `cpu_seconds = Σ usage_records.cpu_hours × 3600` (the 2a unit convention). - **`delta`** = `odoo_amount − external_amount`. - **`status`** = `match` if `abs(delta) ≤ tolerance` (default $0.01, configurable), else `delta`. Comparing **pre-tax subtotals** keeps it apples-to-apples — HST is native Odoo and not what we're validating; the metered math + catalog mapping is. ## 3. Architecture (mirrors 2a: pure compute split from the read) - **`_compute_reconciliation(flat_amount, charge, cpu_seconds, external_amount, tolerance)`** → `(odoo_amount, delta, status)`. Pure, deterministic, unit-tested with fixtures. This is the reconciliation core. - **`_reconcile_rows(rows, tolerance=0.01)`** — pure Odoo: for each input row `{subscription_external_id, period, cpu_seconds, external_amount}`, resolve the shadow `sale.order` (by `x_fc_nexacloud_subscription_id`), its `flat` (plan-line `price_unit`) and its `charge` (by `x_fc_nexacloud_plan_id` → `charge.plan_code`), call `_compute_reconciliation`, and **upsert** one `fusion.billing.reconciliation` row keyed by `(service_id, partner_id, period)`. Returns a summary `{match, delta, skipped, failed}`. - **`_read_reconciliation_rows(period=None)`** — read-only `psycopg2` (reuses the 2a DSN): per subscription+period, `Σ usage_records.cpu_hours` and the NexaCloud invoice **subtotal**. Integration glue (validated manually, like 2a's reader); not unit-tested against a foreign DB. - **Trigger:** a button on the existing import wizard (**“Run Reconciliation”**) and a model method suitable for an `ir.cron`. A non-zero `delta`/`failed` count is surfaced loudly (banner + ERROR log), same as the importer. ## 4. 2a amendment (small, required) Add **`x_fc_nexacloud_plan_id`** (`Char`) to `sale.order` and set it in the importer's `_import_subscription` (from `subscription.plan_id`). Reconciliation needs sub → plan → charge, and parsing it out of the product `default_code` would be fragile. ## 5. Idempotency / re-runnability Reconciliation rows upsert on `(service_id, partner_id, period)`, so re-running a period updates its row rather than duplicating — the dual-run is run every cycle. ## 6. Shadow-safety Reconciliation is pure measurement: it reads NexaCloud and writes only `fusion.billing.reconciliation`. It never touches subscriptions, invoices, payments, or the charge catalog, so the 2a shadow guarantees are untouched. ## 7. Testing `TransactionCase` on odoo-trial with fixtures: - `_compute_reconciliation`: under-quota match; overage match; a real delta flips status to `delta`; tolerance boundary. - `_reconcile_rows`: creates one recon row per subscription; `match` vs `delta` set correctly; re-run upserts (no duplicate); a row for an unknown subscription/charge lands in `skipped`/`failed`, not a crash. - amendment: importer sets `x_fc_nexacloud_plan_id`. ## 8. Out of scope - The **flip** (set `charge.plan_id`, attach tokens, confirm subs) — happens once deltas are within tolerance for ≥ 1 cycle; not automated here. - Reading NexaCloud RAM/disk/bandwidth (CPU is the only metered-overage metric in v1, per 2a). - A reconciliation dashboard/report view beyond the list of `fusion.billing.reconciliation`. ## 9. Success criteria - For fixture data where Odoo's math equals NexaCloud's, every row is `match`; where it diverges beyond tolerance, the row is `delta` with the correct signed `delta`. - Re-running a period upserts (no duplicate rows). - Full suite green on odoo-trial (`FCB_EXIT=0`).