Passa al contenuto principale

ADR-0024 - Offers as per-destination customer rate overrides

  • Status: Accepted (2026-06-08)
  • Deciders: Massimo Bagnoli
  • Implementation tasks: TASK-518, TASK-519
  • Supersedes: ADR-0023 tariff-level offer stopgap
  • Superseded by: nessuno

Context

ADR-0023 intentionally froze the Tariffs UI on the historical offers semantics: an offer pointed a whole tariff to another tariff through tariff_id and reverts_to. That model duplicated the Future Tariff capability and did not match the section 14.5 mockup, where an Offer is a temporary override for one destination rate inside a customer tariff.

Future Tariff remains the mechanism for scheduled whole-tariff or batch rate changes. Offer must be a punctual commercial exception for a single destination and must affect both billing and pre-call admission.

Decision

offers is the canonical physical table for destination-level customer offers. It is not renamed to tariff_offers in this ADR.

An Offer targets exactly one customer tariff and one destination:

  • tariff_id: customer tariff receiving the override.
  • destination_id: destination whose base rate is overridden.
  • override_rate: final per-minute customer rate while the offer is active.
  • valid_from: inclusive start.
  • valid_to: exclusive end, nullable for indefinite validity.
  • volume_committed_min: optional commercial commitment, nullable for unlimited.
  • scope: all_customers or company_subset.
  • scope_json.company_ids: scoped customer companies; empty means all only when scope = all_customers.

Temporal overlap is forbidden per (tariff_id, destination_id, scope, scope_json) for active rows. A company-scoped offer has precedence over an all_customers offer for the same tariff, destination, and call time.

The override rate is the final customer rate for that destination. It is not multiplied by tariff time bands, because the offer represents the negotiated rate itself.

Rating integration

The billing rating path must resolve active offers before falling back to tariff_rates. For customer CDRs, the rater passes the originator company id to the tariff rating helper so company_subset scope can be evaluated. Supplier rating is unaffected.

The pre-call admission path from ADR-0025 must see the same commercial cover as billing. akira-kam-sync therefore exports active offers into customer_rate_cover alongside base customer tariff rates.

The canonical htable key order for customer admission is:

  1. Scoped offer key: {customer_tariff_id}:{company_id}:{prefix}
  2. Base key: {customer_tariff_id}:{prefix}

Kamailio reads the scoped key first when $avp(s:orig_company) is available, then falls back to the base key. akira-kam-sync emits scoped keys only for the concrete (company, prefix) pairs covered by active company_subset offers; it does not create a cross-product of all companies and prefixes.

The key is intentionally non-pure-numeric because it contains separators and the destination prefix. This satisfies the existing kamcmd htable key gotcha that pure numeric keys are unsafe.

Consequences

  • The old whole-tariff offer semantics are removed. Whole-tariff changes remain Future Tariff responsibility.
  • Existing staging offer rows may be deleted during migration because they use the removed whole-tariff semantics and have no commissioning value.
  • Billing and admission remain coherent: an in-scope offer that changes CDR rating also creates admission cover for the same customer tariff, destination prefix, and scoped company.
  • The frontend can request the current base rate for (tariff_id, destination_id) and display the negotiated override relative to that base.

References

  • ADR-0023: Tariffs UI parity on the existing tariff model.
  • ADR-0025: Rating-gated admission and LCR.
  • TASK-518: backend, migration, billing rating, and kam-sync support.
  • TASK-519: frontend modal and Tariffs UI integration.
  • packages/akira_db/src/akira_db/models/offer.py
  • apps/backend/src/akira_backend/routers/offers.py
  • apps/backend/src/akira_backend/services/tariff_rating.py
  • packages/akira_kam_sync/src/akira_kam_sync/sync_htables.py
  • infra/roles/kamailio/templates/routing.cfg.j2