Files
Odoo-Modules/docs/superpowers/specs/2026-05-27-nexacloud-reconciliation-design.md

90 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`).