ADR-0023 - Tariffs UI parity on existing tariff model
- Status: Accepted (2026-06-03)
- Deciders: Massimo Bagnoli
- Implementation tasks: TASK-415, TASK-416, TASK-417, TASK-418, TASK-419, TASK-420, TASK-421, TASK-422, TASK-423
- Supersedes: nessuno
- Superseded by: ADR-0024 for destination-level offer semantics
Context
The Tariffs area must align with the product documentation, especially sections 13 (Destination Codes), 14 (Tariffs), and 16 (Auto-Upload Rates Template Builder), and with the HTML mockup around the Destination Codes and Tariffs pages.
The documentation section 14.8 describes a normalized Tariffs model with:
tariffstariff_versionstariff_ratestariff_offers
The current backend model verified during TASK-415 is close but not identical:
tariffsstores the customer/supplier price-list header.tariff_ratesstores destination rates directly under a tariff, with temporalvalid_fromandvalid_to.future_tariffsstores scheduled per-destination future rate changes.offersstores temporary tariff-level overrides withtariff_idandreverts_to.- There is no
tariff_versionstable. - There is no
tariff_offerstable name; the physical table isoffers. - Rate metadata such as setup fee, increment seconds, and minimum duration is
currently serialized in
tariff_rates.notes, while the tariff header also hasbilling_increment.
The current frontend already exposes a Tariffs list and a tariff detail rates editor, but it diverges from the documented/mockup behavior in several places.
Decision
Align the Tariffs UI to the documentation and mockup while building on the
backend model that exists today. Do not introduce schema migrations for
tariff_versions or a renamed tariff_offers table in TASK-416..TASK-423.
The canonical mapping for this implementation chain is:
| UI/documentation concept | Current physical/API model |
|---|---|
| Tariff header | tariffs |
| Active rate row | tariff_rates row active at request time |
| Future tariff change | future_tariffs row |
| Future tariff version/diff | Projection comparing future_tariffs rows with current tariff_rates |
| Offer / temporary override | offers row |
| Tariff offer table | offers, not tariff_offers |
| Rate billing fields | Current API fields plus parsed tariff_rates.notes; normalize only in a successor ADR |
| Destination display | Resolve by destination_id against destinations, not by fallback #id |
Follow-up tasks must expose the documented UI behavior through facades and client-side projections where possible. A future requirement to normalize tariff versions or rate metadata requires a successor ADR before adding those schema changes.
Gap Map For TASK-416..TASK-423
Tariffs List
- The list shows
Company non disponibilebecause it usestariffs.descriptionas a company placeholder. The UI must resolve and show the linked company name where the current model can derive it. - Rate counts are fetched by loading up to 200 rates per tariff, so counts can under-report larger tariffs.
- Future counts are fetched by loading up to 200 future rows per tariff, so counts can under-report larger future schedules.
- The list must keep the customer/supplier separation from the mockup and add row actions that match the current permissions.
- Search should cover tariff and company semantics, not only tariff name and description.
Tariff Detail / General
- The General view currently exposes incorrect or confusing summary text, such
as
Updated at: openand60+60 - round 60s. - Header/subtitle metadata must match the documented tariff concepts: kind, company, currency, validity, active rates, pending future changes, and billing increment.
- The detail page must expose the expected actions: import rates CSV, export, send rate sheet for customer tariffs, add rate, add future tariff, and offer workflows as permissions allow.
- The UI must include margin/summary information where current data can support it, and mark unsupported values explicitly in follow-up task scope instead of fabricating schema.
Active Rates
- Rate rows display destination fallback values such as
#<id>because the editor resolves destination names with/api/v1/destinations?limit=200while production data can exceed that cap. destination_namemust be reliably resolved for every loaded rate, either by a backend-enriched response or by a targeted destination lookup.- The existing
RateModal/create endpoint is not wired into the tariff detail; users cannot add a single rate from the UI even thoughPOST /api/v1/tariffs/{tariff_id}/ratesexists. - Row actions must cover edit and delete where permitted.
- The displayed increment must be coherent: per-rate metadata if present, otherwise tariff-level billing increment.
- Prefix display must not depend on fetching prefixes for only the first 200 destinations.
CSV Import / Auto-Upload
- The current bulk import modal supports preview and apply, but it does not expose a documented MERGE versus REPLACE choice.
- Backend import currently behaves as an upsert of active rates; destructive replacement is not part of the current contract.
- Auto-Upload section 16 parity must expose preview, dry-run/safety messaging, merge strategy status, and clear validation errors without implying a schema migration in this ADR.
- Existing
auto_uploadservice supportsrateswithconfig.merge_strategy='upsert'only; UI copy must not claim REPLACE support until TASK-420 or a successor task implements it.
Future Tariffs
- Future changes exist as flat
future_tariffsrows, not normalizedtariff_versions. - The UI must present versioning and diff as a projection over current rates
versus pending
future_tariffs, including effective date, pending/applied status, affected rows, and rate deltas. - Approval/calendar concepts from the mockup are not represented by the current backend and must either remain read-only/projection-only or be deferred to a successor ADR.
- Applying due future tariffs uses the existing
POST /api/v1/future-tariffs/apply-duecontract.
Offers
- Backend
offersCRUD exists, but the tariff detail UI does not expose an Offers tab/workflow. - Current offers point a whole tariff to another tariff via
tariff_idandreverts_to; the mockup describes destination-level override examples. - TASK-422 must expose the current
offersmodel honestly. Destination-level offer overrides are a model divergence and require a successor ADR before schema changes.
Send Mail
- The mockup and documentation require sending rate sheets for customer tariffs.
- The current tariff detail does not expose send-mail actions.
- TASK-423 must wire the action to an existing or newly scoped backend contract without inventing hidden mail state in the frontend.
Destination Codes Integration
- Destination names, countries, and prefixes are foundational for Tariffs parity and must be resolved from the Destination Codes model rather than inferred from rate IDs.
- Longest-prefix behavior remains a backend/rating concern; Tariffs UI should display the destination/prefix state consistently with section 13.
Consequences
- TASK-416..TASK-423 can proceed without blocking on database normalization.
- UI labels and actions must make current backend capabilities clear and avoid
silently fabricating
tariff_versionsbehavior. - Backend API enrichment is allowed where it preserves existing tables.
- Schema normalization for tariff versions, destination-level offers, or first-class per-rate billing fields is out of scope for this ADR and requires a successor ADR.
linked_company_id/linked_company_nameon the tariffs list endpoint is a PROJECTION DERIVED fromoriginators.customer_rate_id/company_idfor customer tariffs andterminators.provider_rate_id/supplier_company_idfor supplier tariffs. The tariff header keeps no direct FK to companies. A successor ADR is required only if a nominal single owner becomes necessary (for example, as default recipient for Send Mail workflows). Decided 2026-06-04 by lead architect during TASK-418 follow-up.- When more than one company is linked to the same tariff, the list endpoint
exposes
linked_company_countand the UI renders "primary + N" instead of picking an arbitrary first-by-id row. "Primary" is the linked company with the lowest case-insensitive display label, which is deterministic and independent ofcompanies.id. - TASK-423 Send Mail remains preview-only for the audit batch. Real customer
tariff email sending is a follow-up feature, not part of the current
parity stopgap. The canonical follow-up contract is pinned here:
- Add nullable
companies.rates_emailandcompanies.rates_email_subjectwith an additive Alembic migration. Recipient fallback order isrates_email, thenfinance_contact_email, thenprimary_contact_email. - Expose
GET /api/v1/tariffs/{id}/send-mail/previewfor read-only preview andPOST /api/v1/tariffs/{id}/send-mailfor the explicit user-triggered side-effect. The write action requires the new RBAC permissiontariffs.send_mail, nottariffs.write. - Persist sends in a dedicated
tariff_mail_logstable withtariff_id,company_id,sent_at,sent_by,recipient,subject,status, anderror. The UI exposes at leastsent_at,recipient,status, andsent_by. - Use the existing Email Templates module for the body with slug
tariff-rates-customer. Do not add a company body field and do not hard-code the backend body. The subject defaults from the template and can be overridden bycompanies.rates_email_subject; wildcards are{name},{company}, and{profile}. - No successor ADR is required for this feature because it is an additive contract under ADR-0023. Decided 2026-06-04 by lead architect during TASK-423 closure.
- Add nullable
Destination-level offers superseded by ADR-0024
Per architect decision on TASK-422 (2026-06-04), destination-level offer
overrides are explicitly postponed to successor ADR-0024 with a dedicated
migration. TASK-422 freezes the UI on the current tariff-level offers model
(tariff_id promo + reverts_to base, with reason/budget_cap serialized
as metadata inside description).
ADR-0024 landed with TASK-518 and supersedes this stopgap for offers. From
TASK-518 onward, offers is a per-destination customer rate override model
with tariff_id, destination_id, override_rate, validity window,
volume_committed_min, and scoped customer support. Future Tariff remains the
whole-tariff/batch change mechanism.
Canonical target schema for the future tariff_offers first-class table
(to be formalized in ADR-0024, not in this ADR):
tariff_id(base tariff, FK)destination_id(FK)ratebilling_increment_firstbilling_increment_nextvalid_fromvalid_toreason(enum:promo|commercial|anti_competitive|dispute_comp|custom)budget_capbudget_consumedis_active
Constraint: no temporal overlap allowed on (tariff_id, destination_id)
across active offer rows (exclusion constraint on the window
[valid_from, valid_to)).
The list endpoint GET /api/v1/offers supports filtering by reverts_to
(base tariff) in addition to the existing tariff_id (promo tariff) filter,
to align with the rating resolver semantics (Offer.reverts_to == customer_tariff_id). This is an API affordance only; the underlying schema
remains the current offers table until ADR-0024 lands.
References
- TASK-415: ADR and parity audit.
- TASK-416..TASK-423: follow-up implementation chain.
Akira_System_Architettura_v5_15_TOC.md: sections 13, 14, and 16 outline.akira-mockup.html: Destination Codes and Tariffs list/detail mockup.packages/akira_db/src/akira_db/models/tariff.pypackages/akira_db/src/akira_db/models/tariff_rate.pypackages/akira_db/src/akira_db/models/future_tariff.pypackages/akira_db/src/akira_db/models/offer.pyapps/backend/src/akira_backend/routers/tariffs.pyapps/backend/src/akira_backend/routers/tariff_rates.pyapps/backend/src/akira_backend/routers/offers.pyapps/frontend/src/features/tariffs/TariffList.tsxapps/frontend/src/features/tariffs/TariffRatesEditor.tsx