feat(billing): /usage resolves subscription by source app id (enables 2b)
_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).
This commit is contained in:
@@ -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'}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user