feat(billing): idempotent usage ingestion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1
|
# License OPL-1
|
||||||
from odoo import fields, models
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
class FusionBillingUsage(models.Model):
|
class FusionBillingUsage(models.Model):
|
||||||
@@ -37,3 +37,24 @@ class FusionBillingUsage(models.Model):
|
|||||||
_idempotency_uniq = models.Constraint(
|
_idempotency_uniq = models.Constraint(
|
||||||
"unique(idempotency_key)", "Usage idempotency key must be unique.",
|
"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)
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
from . import test_identity
|
from . import test_identity
|
||||||
from . import test_charge
|
from . import test_charge
|
||||||
|
from . import test_usage
|
||||||
|
|||||||
33
fusion_centralize_billing/tests/test_usage.py
Normal file
33
fusion_centralize_billing/tests/test_usage.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user