Passa al contenuto principale

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:

  • tariffs
  • tariff_versions
  • tariff_rates
  • tariff_offers

The current backend model verified during TASK-415 is close but not identical:

  • tariffs stores the customer/supplier price-list header.
  • tariff_rates stores destination rates directly under a tariff, with temporal valid_from and valid_to.
  • future_tariffs stores scheduled per-destination future rate changes.
  • offers stores temporary tariff-level overrides with tariff_id and reverts_to.
  • There is no tariff_versions table.
  • There is no tariff_offers table name; the physical table is offers.
  • Rate metadata such as setup fee, increment seconds, and minimum duration is currently serialized in tariff_rates.notes, while the tariff header also has billing_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 conceptCurrent physical/API model
Tariff headertariffs
Active rate rowtariff_rates row active at request time
Future tariff changefuture_tariffs row
Future tariff version/diffProjection comparing future_tariffs rows with current tariff_rates
Offer / temporary overrideoffers row
Tariff offer tableoffers, not tariff_offers
Rate billing fieldsCurrent API fields plus parsed tariff_rates.notes; normalize only in a successor ADR
Destination displayResolve 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 disponibile because it uses tariffs.description as 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: open and 60+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=200 while production data can exceed that cap.
  • destination_name must 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 though POST /api/v1/tariffs/{tariff_id}/rates exists.
  • 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_upload service supports rates with config.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_tariffs rows, not normalized tariff_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-due contract.

Offers​

  • Backend offers CRUD exists, but the tariff detail UI does not expose an Offers tab/workflow.
  • Current offers point a whole tariff to another tariff via tariff_id and reverts_to; the mockup describes destination-level override examples.
  • TASK-422 must expose the current offers model 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_versions behavior.
  • 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_name on the tariffs list endpoint is a PROJECTION DERIVED from originators.customer_rate_id/company_id for customer tariffs and terminators.provider_rate_id/supplier_company_id for 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_count and 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 of companies.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_email and companies.rates_email_subject with an additive Alembic migration. Recipient fallback order is rates_email, then finance_contact_email, then primary_contact_email.
    • Expose GET /api/v1/tariffs/{id}/send-mail/preview for read-only preview and POST /api/v1/tariffs/{id}/send-mail for the explicit user-triggered side-effect. The write action requires the new RBAC permission tariffs.send_mail, not tariffs.write.
    • Persist sends in a dedicated tariff_mail_logs table with tariff_id, company_id, sent_at, sent_by, recipient, subject, status, and error. The UI exposes at least sent_at, recipient, status, and sent_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 by companies.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.

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)
  • rate
  • billing_increment_first
  • billing_increment_next
  • valid_from
  • valid_to
  • reason (enum: promo | commercial | anti_competitive | dispute_comp | custom)
  • budget_cap
  • budget_consumed
  • is_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.py
  • packages/akira_db/src/akira_db/models/tariff_rate.py
  • packages/akira_db/src/akira_db/models/future_tariff.py
  • packages/akira_db/src/akira_db/models/offer.py
  • apps/backend/src/akira_backend/routers/tariffs.py
  • apps/backend/src/akira_backend/routers/tariff_rates.py
  • apps/backend/src/akira_backend/routers/offers.py
  • apps/frontend/src/features/tariffs/TariffList.tsx
  • apps/frontend/src/features/tariffs/TariffRatesEditor.tsx