diff --git a/fusion_centralize_billing/models/usage.py b/fusion_centralize_billing/models/usage.py index f8e12658..63dde5eb 100644 --- a/fusion_centralize_billing/models/usage.py +++ b/fusion_centralize_billing/models/usage.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2026 Nexa Systems Inc. # License OPL-1 -from odoo import fields, models +from odoo import api, fields, models class FusionBillingUsage(models.Model): @@ -37,3 +37,24 @@ class FusionBillingUsage(models.Model): _idempotency_uniq = models.Constraint( "unique(idempotency_key)", "Usage idempotency key must be unique.", ) + + @api.model + def _record_usage(self, subscription, metric_code, quantity, period_start, period_end, idem=None): + """Upsert one aggregated usage row. Same idempotency key updates in place (no double-count).""" + metric = self.env['fusion.billing.metric'].search([('code', '=', metric_code)], limit=1) + if not metric: + raise ValueError("Unknown metric code: %s" % metric_code) + vals = { + 'subscription_id': subscription.id, + 'metric_id': metric.id, + 'period_start': period_start, + 'period_end': period_end, + 'quantity': quantity, + 'idempotency_key': idem, + } + if idem: + existing = self.search([('idempotency_key', '=', idem)], limit=1) + if existing: + existing.write({'quantity': quantity}) + return existing + return self.create(vals) diff --git a/fusion_centralize_billing/tests/__init__.py b/fusion_centralize_billing/tests/__init__.py index 23d9aa9c..07117e0d 100644 --- a/fusion_centralize_billing/tests/__init__.py +++ b/fusion_centralize_billing/tests/__init__.py @@ -1,2 +1,3 @@ from . import test_identity from . import test_charge +from . import test_usage diff --git a/fusion_centralize_billing/tests/test_usage.py b/fusion_centralize_billing/tests/test_usage.py new file mode 100644 index 00000000..10475bba --- /dev/null +++ b/fusion_centralize_billing/tests/test_usage.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase, tagged + + +@tagged('post_install', '-at_install') +class TestUsageIngestion(TransactionCase): + + def setUp(self): + super().setUp() + self.metric = self.env['fusion.billing.metric'].sudo().create( + {'name': 'CPU seconds', 'code': 'cpu_seconds', 'aggregation': 'sum'}) + self.plan = self.env['sale.subscription.plan'].sudo().create( + {'name': 'Monthly', 'billing_period_value': 1, 'billing_period_unit': 'month'}) + self.partner = self.env['res.partner'].sudo().create({'name': 'Acme'}) + self.sub = self.env['sale.order'].sudo().create({ + 'partner_id': self.partner.id, 'is_subscription': True, 'plan_id': self.plan.id, + }) + self.Usage = self.env['fusion.billing.usage'].sudo() + + def test_record_usage_creates_row(self): + u = self.Usage._record_usage( + self.sub, 'cpu_seconds', 120.0, + '2026-05-01 00:00:00', '2026-06-01 00:00:00', idem='nexacloud:cpu:sub1:2026-05-01') + self.assertEqual(u.quantity, 120.0) + self.assertEqual(u.metric_id, self.metric) + + def test_idempotent_key_updates_not_duplicates(self): + k = 'nexacloud:cpu:sub1:2026-05-01' + self.Usage._record_usage(self.sub, 'cpu_seconds', 100.0, '2026-05-01', '2026-06-01', idem=k) + self.Usage._record_usage(self.sub, 'cpu_seconds', 175.0, '2026-05-01', '2026-06-01', idem=k) + rows = self.Usage.search([('idempotency_key', '=', k)]) + self.assertEqual(len(rows), 1) # no duplicate + self.assertEqual(rows.quantity, 175.0) # last value wins for the same key