- C1/H4: rating cron only rates subs on the charge's own plan_id
- C1: _fc_rate_usage skips creating a line when amount is 0 (still updates existing)
- C2/C4: /usage authorizes each event (exists + is_subscription + linked customer)
- C3: API handlers validate input and return 4xx-shaped errors instead of raising;
controller maps status=='error' to HTTP 400
- H1: cron uses real billing window [last_invoice_date or start_date, next_invoice_date)
- H2: _aggregate uses half-open window anchored on period_start
- H3: idempotency scoped to (subscription_id, metric_id, idempotency_key)
- H5: webhook stores canonical body, signs+POSTs it verbatim, adds X-Fusion-Event-Id,
caps backoff at 2**min(attempts,10)
- H6: SSRF guard rejects non-https / localhost / private / link-local webhook_url
- M7: charge_model reduced to standard/package (dropped unimplemented graduated/volume)
- L1: currency_id required on charge + reconciliation
- L2: charge price non-negative + unit_batch positive DB constraints
Adds 17 regression tests (suite 22 -> 39, all green via fcb_test_on_trial.sh).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
33 lines
1.3 KiB
Python
33 lines
1.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright 2026 Nexa Systems Inc.
|
|
# License OPL-1
|
|
from odoo import api, fields, models
|
|
|
|
|
|
class SaleOrder(models.Model):
|
|
_inherit = "sale.order"
|
|
|
|
def _fc_rate_usage(self, charge, period_start, period_end):
|
|
"""Aggregate this subscription's usage for `charge`'s metric in the period,
|
|
compute the overage amount, and upsert a matching overage order line.
|
|
Returns the amount.
|
|
|
|
A zero amount never *creates* a new line (no $0.00 overage clutter); if a
|
|
line already exists it is still updated so a dropped-to-zero overage clears.
|
|
"""
|
|
self.ensure_one()
|
|
Usage = self.env['fusion.billing.usage']
|
|
total = Usage._aggregate(self, charge.metric_id, period_start, period_end)
|
|
_overage, amount = charge._compute_billable(total)
|
|
if charge.product_id:
|
|
line = self.order_line.filtered(lambda l: l.product_id == charge.product_id)
|
|
if not line and amount == 0:
|
|
return amount
|
|
vals = {'product_uom_qty': 1, 'price_unit': amount}
|
|
if line:
|
|
line.write(vals)
|
|
else:
|
|
self.env['sale.order.line'].create(
|
|
{'order_id': self.id, 'product_id': charge.product_id.id, **vals})
|
|
return amount
|