From a5144a925ced4bfa8f6892befc429dbb8e8fb76c Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Wed, 27 May 2026 14:37:30 -0400 Subject: [PATCH] feat(billing): /usage resolves subscription by source app id (enables 2b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _api_record_usage now resolves the target subscription via the source app's own id (x_fc_nexacloud_subscription_id, scoped to the service) before falling back to a direct Odoo sale.order id. This is what lets NexaCloud push usage against the shadow subscriptions the importer created from NexaCloud UUIDs — closing the flip-day mapping gap the review flagged. Authz unchanged (partner must be linked to the service). --- fusion_centralize_billing/models/service.py | 27 +++++++++++++++---- .../tests/test_importer.py | 23 ++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/fusion_centralize_billing/models/service.py b/fusion_centralize_billing/models/service.py index 29f21970..d56f1aa3 100644 --- a/fusion_centralize_billing/models/service.py +++ b/fusion_centralize_billing/models/service.py @@ -109,6 +109,27 @@ class FusionBillingService(models.Model): self, ext, name=payload.get('name'), email=payload.get('email')) return {'status': 'ok', 'partner_id': link.partner_id.id, 'external_id': ext} + def _fc_resolve_subscription(self, external_ref): + """Resolve the subscription sale.order a usage event targets. + + Prefer the source app's OWN id (``x_fc_nexacloud_subscription_id`` scoped to this + service) so apps reference their own ids — this is what lets NexaCloud push usage + against shadow subscriptions the importer created from its UUIDs. Falls back to a + direct Odoo ``sale.order`` id for live-created subs (post-flip). Authorization is + still enforced by the caller (partner must be linked to this service).""" + self.ensure_one() + SaleOrder = self.env['sale.order'] + sub = SaleOrder.search([ + ('x_fc_nexacloud_subscription_id', '=', str(external_ref)), + ('x_fc_billing_service_id', '=', self.id), + ], limit=1) + if sub: + return sub + try: + return SaleOrder.browse(int(external_ref)) + except (TypeError, ValueError): + return SaleOrder + def _api_record_usage(self, payload): """Ingest a batch of usage events. @@ -139,15 +160,11 @@ class FusionBillingService(models.Model): 'period_start', 'period_end'): if ev.get(key) in (None, ''): return {'status': 'error', 'error': 'missing %s' % key} - try: - sub_id = int(ev['subscription_external_id']) - except (TypeError, ValueError): - return {'status': 'error', 'error': 'invalid subscription_external_id'} try: quantity = float(ev['quantity']) except (TypeError, ValueError): return {'status': 'error', 'error': 'invalid quantity'} - sub = self.env['sale.order'].browse(sub_id) + sub = self._fc_resolve_subscription(ev['subscription_external_id']) if not sub.exists() or not sub.is_subscription \ or sub.partner_id not in linked_partners: return {'status': 'error', 'error': 'unknown subscription'} diff --git a/fusion_centralize_billing/tests/test_importer.py b/fusion_centralize_billing/tests/test_importer.py index c36d47bb..51934ef3 100644 --- a/fusion_centralize_billing/tests/test_importer.py +++ b/fusion_centralize_billing/tests/test_importer.py @@ -254,3 +254,26 @@ class TestImporterReadGuard(TransactionCase): wiz = self.env['fusion.billing.import.wizard'].sudo().create({'dry_run': True}) with self.assertRaises(UserError): wiz.action_test_connection() + + +@tagged('post_install', '-at_install') +class TestUsageApiSourceId(TransactionCase): + """The /usage API must resolve a subscription by NexaCloud's OWN id, so usage can be + pushed against shadow subs the importer created from UUIDs (the flip-day gap).""" + + def setUp(self): + super().setUp() + self.env['fusion.billing.import.wizard'].sudo()._import_rows(_fixture()) + self.service = self.env['fusion.billing.service'].search([('code', '=', 'nexacloud')]) + + def test_record_usage_resolves_by_nexacloud_subscription_id(self): + res = self.service._api_record_usage({'events': [{ + 'subscription_external_id': 's-1', # NexaCloud UUID, not the Odoo id + 'metric_code': 'cpu_seconds', 'quantity': 3600.0, + 'period_start': '2026-05-01', 'period_end': '2026-06-01', + 'idempotency_key': 'nc:s-1:2026-05'}]}) + self.assertEqual(res['status'], 'ok') + self.assertEqual(res['accepted'], 1) + sub = self.env['sale.order'].search([('x_fc_nexacloud_subscription_id', '=', 's-1')]) + usage = self.env['fusion.billing.usage'].search([('subscription_id', '=', sub.id)]) + self.assertEqual(usage.quantity, 3600.0)