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

4.9 KiB
Raw Blame History

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_idcharge.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).