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