Skip to content

Cost Snapshot

A point-in-time aggregate of fleet contract costs. One row per (date, currency). Snapshots are write-once telemetry — there's no "edit a snapshot" workflow, and we never delete a snapshot when a contract changes.

Fields

Field Type Notes
snapshot_date date The date this snapshot represents.
currency char(3) ISO 4217. Multiple currencies on the same date = multiple rows.
monthly_burn Decimal(14,2) Sum of monthly_cost across active contracts in this currency.
renewal_90d Decimal(14,2) Total renewal cost in the 90-day forward window from snapshot_date.
active_contract_count int Number of active contracts in this currency on the snapshot date.
coverage_gap_count int (nullable) Devices with no direct contract assignment. Stored on the alphabetically-first per-date snapshot only — null on others — since gap count is currency-agnostic.

Natural key

(snapshot_date, currency) — set via natural_key_field_names.

Constraints

  • UniqueConstraint(snapshot_date, currency) — prevents accidental duplicate snapshots on the same day.

Indexes

  • (-snapshot_date, currency) — speeds up the time-series queries the Cost History page runs.

Why no FK to Contract?

A snapshot is a fleet aggregate, not a per-contract row. Linking to live contracts would mean deleting a contract destroys the historical record of its spend — exactly wrong, since "we used to spend $X with that vendor" is the value proposition. Snapshots are immutable historical facts, decoupled from current Contract state.

Why subclass BaseModel rather than PrimaryModel?

Snapshots don't need ChangeLog / Tags / Relationships / dynamic groups. Operators don't tag a snapshot or relate it to a Device. BaseModel gives the UUID PK + natural-key machinery, which is the minimum useful surface.

Read-only API

The /api/plugins/contracts/cost-snapshots/ endpoint exposes list + retrieve only — POST / PATCH / DELETE return 405 Method Not Allowed. The viewset composes DRF's ListModelMixin + RetrieveModelMixin + GenericViewSet directly, so write methods aren't even routable. See External Interactions — REST API.

Captured by

The Capture cost history snapshot Job (run weekly). The Job calls cost.take_snapshot() which update_or_creates one row per (date, currency).

Extras features enabled

graphql. (No webhooks — snapshots are telemetry, not user-managed; no custom_fields — operator-tagged metadata doesn't fit the immutable-record model.)