Model/docs/migrations/property-baseline-performance-table.md
Khalim Conn-Kowlessar f179950519 feat(baseline): wire BillDerivation into the orchestrator and persist the Bill (ADR-0014)
The PropertyBaselineOrchestrator now reads the current Fuel Rates snapshot
once per batch, builds a BillDerivation, and prices each scored property's
SapResult -> EnergyBreakdown into a Bill carried on PropertyBaselinePerformance
(None only on the stub no-calculator path). The Bill is flattened onto nullable
bill_* flat columns (per-section kwh+cost, standing charges, SEG credit, total)
on the postgres table, with bill_total_annual_bill_gbp as the not-null
discriminator on read-back. Section absent from the bill stays None, not 0.

Updated all four orchestrator construction sites to inject the FuelRatesRepository
port (handler + three test sites), and the FE migration doc to reflect the
prefixed columns and that they are now populated.

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

78 lines
5.2 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.
All columns below are **nullable** (every one is `Optional[float]`, default `None`) and **FE-owned
(Drizzle)**. The `bill_` prefix is deliberate: it keeps the per-section columns from clashing with
the recorded-demand `space_heating_kwh` / `water_heating_kwh` above. The whole block is `None` for
one row together when no calculator ran (the stub path produced no `SapResult` to price); a section
absent from the bill leaves its two columns `None` (not `0` — it was not billed). `to_domain` uses
`bill_total_annual_bill_gbp IS NOT NULL` as the discriminator for "a bill was persisted".
| Column | Type | Notes |
|---|---|---|
| `bill_heating_kwh` | float, nullable | delivered fuel kWh (main + main-2 + secondary heating) |
| `bill_heating_cost_gbp` | float, nullable | priced at the heating fuel's current rate |
| `bill_hot_water_kwh` | float, nullable | |
| `bill_hot_water_cost_gbp` | float, nullable | |
| `bill_lighting_kwh` | float, nullable | |
| `bill_lighting_cost_gbp` | float, nullable | |
| `bill_appliances_kwh` | float, nullable | unregulated load — `None` until the appliances field lands on `SapResult` |
| `bill_appliances_cost_gbp` | float, nullable | |
| `bill_cooking_kwh` | float, nullable | unregulated load — `None` until `SapResult` carries it |
| `bill_cooking_cost_gbp` | float, nullable | |
| `bill_pumps_fans_kwh` | float, nullable | |
| `bill_pumps_fans_cost_gbp` | float, nullable | |
| `bill_cooling_kwh` | float, nullable | mostly absent in UK homes; carried for completeness as it affects the bill |
| `bill_cooling_cost_gbp` | float, nullable | |
| `bill_standing_charges_gbp` | float, nullable | daily standing charge × 365, once per distinct metered fuel (off-gas fuels have none) |
| `bill_seg_credit_gbp` | float, nullable | SEG export credit on PV (subtracted) |
| `bill_total_annual_bill_gbp` | float, nullable | Σ section costs + standing charges SEG; the not-null discriminator for a persisted bill |
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 now **populated**: the `PropertyBaselineOrchestrator` reads the current Fuel
Rates snapshot, builds a `BillDerivation`, and prices every scored property's `SapResult`
`EnergyBreakdown` into a `Bill` that `from_domain` flattens onto these columns. They stay `None`
together only on the stub (no-calculator) path. The appliances / cooking sections remain `None`
until those fields land on `SapResult`. The Drizzle migration creates all `bill_*` columns nullable.