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
| Method | Path | Notes |
|---|---|---|
| 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}/history | Plans-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.