4.9 KiB
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_secondscharge 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, whereflat= the shadow subscription's plan-product lineprice_unit(the imported flat price), andoverage=charge._compute_billable(cpu_seconds)[1]for the period's CPU usage, withcpu_seconds = Σ usage_records.cpu_hours × 3600(the 2a unit convention).
delta=odoo_amount − external_amount.status=matchifabs(delta) ≤ tolerance(default $0.01, configurable), elsedelta.
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 shadowsale.order(byx_fc_nexacloud_subscription_id), itsflat(plan-lineprice_unit) and itscharge(byx_fc_nexacloud_plan_id→charge.plan_code), call_compute_reconciliation, and upsert onefusion.billing.reconciliationrow keyed by(service_id, partner_id, period). Returns a summary{match, delta, skipped, failed}._read_reconciliation_rows(period=None)— read-onlypsycopg2(reuses the 2a DSN): per subscription+period,Σ usage_records.cpu_hoursand 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-zerodelta/failedcount 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 todelta; tolerance boundary._reconcile_rows: creates one recon row per subscription;matchvsdeltaset correctly; re-run upserts (no duplicate); a row for an unknown subscription/charge lands inskipped/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 isdeltawith the correct signeddelta. - Re-running a period upserts (no duplicate rows).
- Full suite green on odoo-trial (
FCB_EXIT=0).