fix(modelling): mirror the FE-owned property_baseline_performance columns

The SQLModel had drifted to a `bill_` prefix on the Bill Derivation block, but
the FE-owned Drizzle table uses unprefixed names (`heating_kwh`, `hot_water_kwh`
… `total_annual_bill_gbp`) plus a nullable `fuel_rates_period`. INSERTs failed
with UndefinedColumn. Rename the columns to mirror the live table column-for-
column (the prefix's anti-clash purpose is moot: `heating_kwh` != the recorded
`space_heating_kwh`), and add the `fuel_rates_period` column — left None until
Bill Derivation threads the snapshot period through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-17 01:45:42 +00:00
parent edf1003dcf
commit c57ee578de

View file

@ -72,26 +72,31 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
space_heating_kwh: float
water_heating_kwh: float
# The Fuel Rates snapshot period the bill was priced against (FE-owned column,
# nullable). Not yet threaded through Bill Derivation, so left None for now.
fuel_rates_period: Optional[str] = Field(default=None)
# Bill Derivation block (ADR-0014 §6). Nullable: all None when no calculator
# ran (stub path). The ``bill_`` prefix avoids clashing with the
# ran (stub path). Column names are unprefixed to mirror the FE-owned table —
# the per-section ``heating_kwh`` / ``hot_water_kwh`` do not clash with the
# recorded-demand ``space_heating_kwh`` / ``water_heating_kwh`` above.
bill_heating_kwh: Optional[float] = Field(default=None)
bill_heating_cost_gbp: Optional[float] = Field(default=None)
bill_hot_water_kwh: Optional[float] = Field(default=None)
bill_hot_water_cost_gbp: Optional[float] = Field(default=None)
bill_lighting_kwh: Optional[float] = Field(default=None)
bill_lighting_cost_gbp: Optional[float] = Field(default=None)
bill_appliances_kwh: Optional[float] = Field(default=None)
bill_appliances_cost_gbp: Optional[float] = Field(default=None)
bill_cooking_kwh: Optional[float] = Field(default=None)
bill_cooking_cost_gbp: Optional[float] = Field(default=None)
bill_pumps_fans_kwh: Optional[float] = Field(default=None)
bill_pumps_fans_cost_gbp: Optional[float] = Field(default=None)
bill_cooling_kwh: Optional[float] = Field(default=None)
bill_cooling_cost_gbp: Optional[float] = Field(default=None)
bill_standing_charges_gbp: Optional[float] = Field(default=None)
bill_seg_credit_gbp: Optional[float] = Field(default=None)
bill_total_annual_bill_gbp: Optional[float] = Field(default=None)
heating_kwh: Optional[float] = Field(default=None)
heating_cost_gbp: Optional[float] = Field(default=None)
hot_water_kwh: Optional[float] = Field(default=None)
hot_water_cost_gbp: Optional[float] = Field(default=None)
lighting_kwh: Optional[float] = Field(default=None)
lighting_cost_gbp: Optional[float] = Field(default=None)
appliances_kwh: Optional[float] = Field(default=None)
appliances_cost_gbp: Optional[float] = Field(default=None)
cooking_kwh: Optional[float] = Field(default=None)
cooking_cost_gbp: Optional[float] = Field(default=None)
pumps_fans_kwh: Optional[float] = Field(default=None)
pumps_fans_cost_gbp: Optional[float] = Field(default=None)
cooling_kwh: Optional[float] = Field(default=None)
cooling_cost_gbp: Optional[float] = Field(default=None)
standing_charges_gbp: Optional[float] = Field(default=None)
seg_credit_gbp: Optional[float] = Field(default=None)
total_annual_bill_gbp: Optional[float] = Field(default=None)
@classmethod
def from_domain(
@ -122,15 +127,15 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
return
for section, stem in _SECTION_COLUMN_STEM.items():
cost = bill.sections.get(section)
setattr(self, f"bill_{stem}_kwh", cost.kwh if cost is not None else None)
setattr(self, f"{stem}_kwh", cost.kwh if cost is not None else None)
setattr(
self,
f"bill_{stem}_cost_gbp",
f"{stem}_cost_gbp",
cost.cost_gbp if cost is not None else None,
)
self.bill_standing_charges_gbp = bill.standing_charges_gbp
self.bill_seg_credit_gbp = bill.seg_credit_gbp
self.bill_total_annual_bill_gbp = bill.total_gbp
self.standing_charges_gbp = bill.standing_charges_gbp
self.seg_credit_gbp = bill.seg_credit_gbp
self.total_annual_bill_gbp = bill.total_gbp
def to_domain(self) -> PropertyBaselinePerformance:
return PropertyBaselinePerformance(
@ -157,18 +162,18 @@ class PropertyBaselinePerformanceModel(SQLModel, table=True):
not-None discriminator: a persisted bill always sets it, so its absence
means no calculator ran and the bill was None. A section is rebuilt only
when its kWh column is not None (paired with its cost)."""
if self.bill_total_annual_bill_gbp is None:
if self.total_annual_bill_gbp is None:
return None
sections: dict[BillSection, BillSectionCost] = {}
for section, stem in _SECTION_COLUMN_STEM.items():
kwh = cast(Optional[float], getattr(self, f"bill_{stem}_kwh"))
kwh = cast(Optional[float], getattr(self, f"{stem}_kwh"))
if kwh is None:
continue
cost_gbp = cast(float, getattr(self, f"bill_{stem}_cost_gbp"))
cost_gbp = cast(float, getattr(self, f"{stem}_cost_gbp"))
sections[section] = BillSectionCost(kwh=kwh, cost_gbp=cost_gbp)
return Bill(
sections=sections,
standing_charges_gbp=cast(float, self.bill_standing_charges_gbp),
seg_credit_gbp=cast(float, self.bill_seg_credit_gbp),
total_gbp=self.bill_total_annual_bill_gbp,
standing_charges_gbp=cast(float, self.standing_charges_gbp),
seg_credit_gbp=cast(float, self.seg_credit_gbp),
total_gbp=self.total_annual_bill_gbp,
)