Appearance
Entity: consumption
Entity Type: Database table (TimescaleDB hypertable, partitioned by slotAt)
Description: One calculated value for one gauge, one 15-minute interval and one data source. Consumption is derived data: it is computed from reading values and from invoice amounts, and can be rebuilt from them at any time. Values from different sources are stored side by side as parallel series and never overwrite one another — a gauge read both remotely and manually carries two series over the same intervals, and the series a consumer sees is chosen at read time (see Consumption Aggregation & Effective Consumption).
The same table carries money: a kind of costWithVat or costWithoutVat stores an amount spread from an invoice over the intervals that invoice covers, in the same shape as a quantity, so that summaries and comparisons read both from one place.
Business rule. For one gauge, one interval can hold exactly one value of the same kind, measured quantity, flow direction, tariff band, source and — for values derived from an invoice — the same invoice. An invoice and the credit note that corrects it cover the same period and are stored side by side, so the invoice is part of the row's identity; for values from readings and from virtual gauges it is null.
Data Attributes Table
| Attribute Name | Description | Data Type | Default Value | Required (= Nullable) | Unique | Format | Validations | Index | Example |
|---|---|---|---|---|---|---|---|---|---|
| id | Surrogate primary key. Combined with slotAt into a composite primary key, because slotAt is the hypertable partition column. | UUID | Generated in code (app layer) | Yes | Part of composite PK | UUID v7 | - | Primary Key (composite with slotAt) | 018f6e2a-1234-7abc-9def-0123456789ab |
| tenantId | Tenant this record belongs to. Carried for consistency with the rest of the domain model; on a hypertable it is also the predicate the application-level tenant guard applies, since row-level security is not available here. | UUID | - | Yes | No | UUID v7 | Foreign Key → tenant; must exist | name: idx_consumption_tenantId, type: btree | 018fa51f-fda1-79f4-8461-2cb8f1cabc10 |
| gaugeId | Gauge the value belongs to. Loose reference only — no foreign key, matching reading, because the hypertable is written at ingestion volume and the gauge lives in another bounded context. | UUID | - | Yes | Part of composite unique | UUID v7 | No database-level foreign key — existence enforced at application level. | name: idx_consumption_gauge_slot_at, type: btree | 018f6e2a-… |
| slotAt | Start of the 15-minute interval the value belongs to, in UTC. Hypertable partition column. A value always describes the whole interval, never an instant. | Timestamp with time zone | - | Yes | Part of composite unique | ISO 8601 — YYYY-MM-DDTHH:mm:ss.SSSZ | Must be aligned to a quarter hour (00, 15, 30, 45 minutes, zero seconds). | Hypertable partition key; part of composite unique index | 2026-01-15T09:15:00Z |
| kind | Whether the value is a quantity or an amount of money. | Enum | - | Yes | Part of composite unique | Enum - ConsumptionKind | An entry from the enum. | Part of composite unique index | consumption |
| type | Measured quantity. Required when kind is consumption; null for money, which is not a measured quantity. | Enum | - | No | Part of composite unique | Enum - ReadingType | Must be null when kind is costWithVat or costWithoutVat. | Part of composite unique index | energy |
| direction | Flow direction, from the building's point of view. null where the quantity has no direction. | Enum | - | No | Part of composite unique | Enum - GaugeDirection | - | Part of composite unique index | import |
| tariff | Tariff band the value falls in. null where the gauge has no tariff split. | Enum | - | No | Part of composite unique | Enum - ReadingTariff | - | Part of composite unique index | high |
| source | Where the value came from: a remote series, a manual reading, an invoice, or a virtual-gauge calculation. Never mixed within one series. | Enum | - | Yes | Part of composite unique | Enum - ReadingSource | An entry from the enum. calculated is reserved for virtual gauges. | Part of composite unique index | remote |
| value | The calculated value for the interval, in the unit named by unit. Exact decimal rather than floating point, so that a money series sums back to its invoice to the last hundredth. | Decimal | - | Yes | No | numeric(18,6) | Money rows carry at most two decimal places. Quantities may be negative (a correction, or an export series); money may be negative on a credit note. | - | 12.480000 |
| unit | Unit the value is stored in — the canonical unit for the quantity (kWh for energy, m³ for volume, kW for power) or the currency for money. Not part of the row's identity: a value is stored once and converted for display. | Enum | - | Yes | No | Enum - Unit | Must belong to the unit family implied by kind and type. | - | kWh |
| invoiceId | Invoice the value was derived from. Set on every row whose source is invoice and on every money row; null otherwise. Loose reference, as for gaugeId. Part of the row's identity, so that an invoice and a credit note over the same period do not overwrite each other. | UUID | null | No | Part of composite unique | UUID v7 | Must be set when source is invoice or kind is a money kind; must be null otherwise — see invoice. | name: idx_consumption_invoice_id, type: btree | 018f7b31-… |
| createdAt | Timestamp of record creation. | Timestamp with time zone | now() — set in code | Yes | No | ISO 8601 — YYYY-MM-DDTHH:mm:ss.SSSZ | Cannot be null. | - | 2026-01-16T02:11:00Z |
| updatedAt | Timestamp of the last recalculation that changed this value. | Timestamp with time zone | now() — set in code | Yes | No | ISO 8601 — YYYY-MM-DDTHH:mm:ss.SSSZ | Cannot be null. | - | 2026-03-02T08:40:00Z |
| createdBy | Actor that wrote the value. Always a system actor — consumption is never entered by hand. | String | - | Yes | No | type:actor — e.g. system:consumption-calculation | Non-empty. | - | system:consumption-calculation |
| updatedBy | Actor that last recalculated the value. | String | - | Yes | No | type:actor | Non-empty. | - | system:consumption-recalculation |
Indexes
| Name | Columns | Type | Why |
|---|---|---|---|
uq_consumption_slot | gaugeId, slotAt, kind, type, direction, tariff, source, invoiceId — nulls not distinct | Unique | Enforces the business rule above and makes recalculation an upsert rather than a delete-and-insert. |
idx_consumption_gauge_slot_at | gaugeId, slotAt DESC | btree | The read path of every chart, report and summary. |
idx_consumption_invoice_id | invoiceId | btree | Recalculating or removing the series of one corrected invoice. |
idx_consumption_tenantId | tenantId | btree | Supports the application-level tenant predicate. |
Soft deletion
This entity has no deletedAt. A value is either recomputed in place or, where its input disappeared, removed by the recalculation that owns that gauge and period — there is no user-facing delete, and an interval with no input carries no row at all. An absent row means "not measured", which is not the same as a stored zero.
Audited fields
Not carried in an audit trail. Every row is machine-written and reproducible from its inputs, and the trail that matters is the one on those inputs: readings are audited by the reading action log, invoices by their own trail. createdBy and updatedBy name the calculation that wrote the row, which is what an investigation of a suspicious figure needs.
Not registered for entityName resolution — not an audited entity; a machine-written time-series row identified by its gaugeId/interval scope.
Row-level security
None at the database level. Like reading, this table is a TimescaleDB hypertable with compression, and row-level security is not usable together with it; tenant isolation is enforced by the same application-level hypertable guard, which requires the tenantId predicate on every statement.
Retention
None. Retention applies to the raw inputs only; the 15-minute series and their aggregates are kept indefinitely, so historical consumption stays reconstructable after the readings behind it have been thinned.