Skip to main content

Catalog History

Code lives in app/schemas/history_entry.py, app/services/history.py, app/api/v1/routes/history.py (plus the record_audit calls in app/services/plans.py and app/services/rates.py).

Plans and Rates are one-row-per-key catalog domains with no date-ranged revisions. Their audit trail is the audit_logs table: every create/update/delete writes one audit row whose payload is {"snapshot": <the row's full state after the change>} (the domain's *Read schema, model_dump(mode="json")). GET /api/v1/history reads those rows back as a history. There is no separate history table - the former history_entries table was dropped by migration b2c4d6e8f0a1_drop_history_entries.

What is read​

app/services/history.py selects audit_logs rows of the current tenant where domain matches, entity_id is set and payload has a snapshot key. Audit rows without a snapshot (for example authentication events) are never returned. Each audit row is written in the same DB transaction as the mutation it records (record_audit does no commit of its own), so a history row is only persisted alongside the change it describes. rates.py can write snapshot-only rows hidden from the general audit list (HISTORY_ONLY_TAGS).

Response item (HistoryEntryRead): id, domain ("plan" or "rate"), entity_id (the row's own id - not a FK, so it survives soft-deletion), key (denormalized business key: Rate.key / plan name), action ("create" | "update" | "delete"), snapshot, created_at.

API​

MethodPathNotes
GET/api/v1/history?domain=<domain>&key=<key>&entity_id=<uuid> - domain required, key/entity_id optional narrowing, paginated (API conventions), oldest first
GET/api/v1/plans/{plan_id}/historyPlans-only convenience route: the same rows as ?domain=plan&entity_id=<plan_id>, but addressable by id and returned newest-first. 404s only if the plan never belonged to the tenant

Access is gated by the history:read permission, which the route gate applies to every /history/* path (route-to-permission map in app/core/permissions.py). In the built-in role templates (app/core/role_templates.py) it is held by superadmin, tenant admin, operator and read-only support, not by member or customer, since the data is tenant-wide.

What it does not do​

  • No diff computation between consecutive snapshots - callers compare two rows' snapshots.
  • Not an "as of" pricing model: Plans and Rates are still one-row-per-key with no from/to; the live row always reflects the current state. See the Plans and Rates pages.
  • Subscribers and Accounts have their own date-ranged revision rows (Subscribers), not this API.
  • Services and Tax Rates no longer exist (scope).
  • No retention/pruning policy for audit_logs.