Model/docs/migrations/property-baseline-performance-table.md
Khalim Conn-Kowlessar 2c8c299fde docs(migration): add the Bill Derivation block to the property_baseline table (ADR-0014)
Slice 5b: update the FE-owned migration spec so the other repo can create the
bill columns in parallel.

- Bill block: per-section delivered kWh + cost (heating, hot water, lighting,
  appliances, cooking, pumps/fans, cooling) + standing_charges_gbp,
  seg_credit_gbp, total_annual_bill_gbp, fuel_rates_period.
- space_heating_kwh / water_heating_kwh (RHI recorded demand) marked SUPERSEDED
  by heating_kwh / hot_water_kwh (calculator delivered fuel); kept until the bill
  populates, then dropped.
- Cooling section kept (mostly 0 but affects the bill, cheap to store).
- Records the calculator-load-bearing posture (effective_* may differ from
  lodged_* for pre-10.2) and that columns are defined now / populated when the
  SapResult->EnergyBreakdown adapter + BillDerivation wiring land.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:13:23 +00:00

71 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `property_baseline_performance` table — FE-owned migration
**Context:** Slice 6 (Hestia-Homes/Model#1135) of the `ara_first_run` rebuild. The
`PropertyBaselineOrchestrator` establishes a Property's **Baseline Performance** (ADR-0004) and persists it
via a new `PropertyBaselineRepository` port. This is a brand-new table — no predecessor.
Per ADR-0004's amendment, the lodged/effective pair does **not** land on `property_details_epc`
(which is being retired as too coupled to the legacy EPC-API schema). It lands here, as its own
aggregate's table.
The SQLModel row is defined in `infrastructure/postgres/` so the ephemeral-Postgres tests build it
via `SQLModel.metadata.create_all`. The **production migration is FE-owned (Drizzle ORM)** — a
straight lift-and-shift of the columns below.
## `property_baseline_performance` — one row per Property
| Column | Type | Notes |
|---|---|---|
| `id` | serial PK | |
| `property_id` | int, FK → `property.id`, **unique** | one Baseline Performance per Property |
| `lodged_sap_score` | int | Lodged Performance — gov register, off the Effective EPC |
| `lodged_epc_band` | text | the `Epc` enum, stored as its string value (e.g. `"C"`) |
| `lodged_co2_emissions_t_per_yr` | float | tonnes CO₂/yr (whole dwelling) |
| `lodged_primary_energy_intensity_kwh_per_m2_yr` | int | PEUI (kWh/m²/yr); **not** "heat demand" — see CONTEXT.md |
| `effective_sap_score` | int | Effective Performance — what modelling scored against |
| `effective_epc_band` | text | |
| `effective_co2_emissions_t_per_yr` | float | tonnes CO₂/yr (whole dwelling) |
| `effective_primary_energy_intensity_kwh_per_m2_yr` | int | kWh/m²/yr |
| `rebaseline_reason` | text | `none` \| `pre_sap10` \| `physical_state_changed` \| `both` |
| `space_heating_kwh` | float | EPC `renewable_heat_incentive` recorded demand. **Superseded** by `heating_kwh` (delivered) when the bill block populates; kept until then to avoid an empty-kWh gap, dropped in the population slice. |
| `water_heating_kwh` | float | EPC `renewable_heat_incentive`; **superseded** by `hot_water_kwh`. |
### Bill block (ADR-0014) — the energy bill, composed per section
Produced by **Bill Derivation**: the calculator's **delivered** kWh per end use priced at current
**Fuel Rates** (a committed snapshot, not SAP's standardised prices), per section + the total.
Per-section kWh is *delivered fuel* (demand ÷ efficiency — what the household pays for), distinct
from the recorded-demand `space_heating_kwh`/`water_heating_kwh` above which it supersedes.
| Column | Type | Notes |
|---|---|---|
| `fuel_rates_period` | text | which Fuel Rates snapshot priced this bill (e.g. `"2026-04 to 2026-06"`) — provenance |
| `heating_kwh` | float | delivered fuel kWh (main + secondary heating) |
| `heating_cost_gbp` | float | priced at the heating fuel's current rate |
| `hot_water_kwh` | float | |
| `hot_water_cost_gbp` | float | |
| `lighting_kwh` | float | |
| `lighting_cost_gbp` | float | |
| `appliances_kwh` | float | unregulated load — **0 until the appliances/cooking fields land on `SapResult`** (ADR-0014 TODO) |
| `appliances_cost_gbp` | float | |
| `cooking_kwh` | float | unregulated load — 0 until `SapResult` carries it |
| `cooking_cost_gbp` | float | |
| `pumps_fans_kwh` | float | |
| `pumps_fans_cost_gbp` | float | |
| `cooling_kwh` | float | mostly 0 in UK homes; carried for completeness as it affects the bill |
| `cooling_cost_gbp` | float | |
| `standing_charges_gbp` | float | daily standing charge × 365, once per distinct metered fuel (off-gas fuels have none) |
| `seg_credit_gbp` | float | SEG export credit on PV (subtracted) |
| `total_annual_bill_gbp` | float | Σ section costs + standing charges SEG |
The calculator is **load-bearing** (ADR-0013 amendment): for `sap_version < 10.2` the `effective_*`
columns hold the calculator's output (so `effective_* != lodged_*` legitimately); at/above 10.2 they
mirror the lodged figures and divergence is logged. A cert the calculator cannot score aborts the
batch rather than persisting a wrong row.
### Population timing
The bill columns are **defined now so the FE can create them**, but are populated only once the
`SapResult``EnergyBreakdown` adapter + `BillDerivation` wiring land (gated on the appliances /
cooking `SapResult` fields). Until then the SQLModel mirror in `infrastructure/postgres/` adds these
columns as nullable; the Drizzle migration can create them nullable in parallel.