fix(billing): resolve code-review findings (authz, cross-billing, validation, webhook integrity)
- 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>
This commit is contained in:
@@ -43,23 +43,30 @@ class FusionBillingCharge(models.Model):
|
||||
charge_model = fields.Selection(
|
||||
[
|
||||
("standard", "Standard (per unit)"),
|
||||
("graduated", "Graduated"),
|
||||
("package", "Package"),
|
||||
("volume", "Volume"),
|
||||
],
|
||||
default="standard", required=True,
|
||||
)
|
||||
currency_id = fields.Many2one(
|
||||
"res.currency", default=lambda self: self.env.company.currency_id,
|
||||
"res.currency", required=True,
|
||||
default=lambda self: self.env.company.currency_id,
|
||||
)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
_price_non_negative = models.Constraint(
|
||||
"CHECK (price_per_unit >= 0)", "Overage price per unit cannot be negative.",
|
||||
)
|
||||
_unit_batch_positive = models.Constraint(
|
||||
"CHECK (unit_batch > 0)", "Unit batch must be greater than zero.",
|
||||
)
|
||||
|
||||
def _compute_billable(self, total_quantity):
|
||||
"""Return (overage_units, amount) for total period usage under this charge.
|
||||
|
||||
- overage_units = usage above included_quota (never negative)
|
||||
- 'standard'/'package'/'volume': priced per `unit_batch` block, partial block rounds up.
|
||||
(graduated tiers are out of scope for the core; treated as 'standard'.)
|
||||
- 'standard': price the overage in (rounded-up) `unit_batch` blocks.
|
||||
- 'package': price whole packages over the RAW quantity (quota ignored for
|
||||
package counting); a partial package rounds up.
|
||||
"""
|
||||
self.ensure_one()
|
||||
overage = max(0.0, (total_quantity or 0.0) - (self.included_quota or 0.0))
|
||||
@@ -68,6 +75,6 @@ class FusionBillingCharge(models.Model):
|
||||
# whole packages over the RAW quantity (quota ignored for package counting)
|
||||
blocks = math.ceil((total_quantity or 0.0) / batch) if total_quantity else 0
|
||||
return overage, round(blocks * (self.price_per_unit or 0.0), 2)
|
||||
# standard / volume / graduated-fallback: price the overage in (rounded-up) batches
|
||||
# standard: price the overage in (rounded-up) batches
|
||||
blocks = math.ceil(overage / batch) if overage > 0 else 0
|
||||
return overage, round(blocks * (self.price_per_unit or 0.0), 2)
|
||||
|
||||
Reference in New Issue
Block a user