From c57ee578de143b0bd57881803b580fbdcb3951f6 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 17 Jun 2026 01:45:42 +0000 Subject: [PATCH] fix(modelling): mirror the FE-owned property_baseline_performance columns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../property_baseline_performance_table.py | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/infrastructure/postgres/property_baseline_performance_table.py b/infrastructure/postgres/property_baseline_performance_table.py index 03906c0c..89019478 100644 --- a/infrastructure/postgres/property_baseline_performance_table.py +++ b/infrastructure/postgres/property_baseline_performance_table.py @@ -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, )