mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(epc): hydrate recorded performance, RHI, and dates on read
The Baseline stage is the first consumer to read these off a persisted EPC end-to-end, surfacing three gaps that only manifest on real API data: - Only the 21.0.1 mapper copied through the recorded current-performance scalars (SAP rating, CO2, PEUI) and *no* mapper mapped the EPC band, so Lodged Performance raised for 17.x/18.0/19.0/20.0.0 certs. Overlay all four from the raw payload in `from_api_response`, once, for every schema version. - Likewise the `renewable_heat_incentive` block (baseline space/water-heating kWh) was only mapped by the 21.x paths. Gap-fill it centrally from the raw payload when a mapper left it unset. - The FE-owned `epc_property` date columns are Postgres `timestamp`s while the SQLModel mirror types them `str`, so a read hands back a `datetime` and `date.fromisoformat()` raised. Normalise via `_as_date()`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ea72ee97bf
commit
edf1003dcf
2 changed files with 129 additions and 41 deletions
|
|
@ -5,6 +5,7 @@ from decimal import ROUND_HALF_UP, Decimal
|
|||
from typing import Any, Dict, Final, List, Optional, Sequence, TypeVar, Union, cast
|
||||
from datatypes.epc.schema.helpers import from_dict
|
||||
|
||||
from datatypes.epc.domain.epc import Epc
|
||||
from datatypes.epc.domain.epc_property_data import (
|
||||
BASEMENT_WALL_CONSTRUCTION_CODE,
|
||||
Addendum,
|
||||
|
|
@ -2268,61 +2269,53 @@ class EpcPropertyDataMapper:
|
|||
if schema == "RdSAP-Schema-21.0.1":
|
||||
from datatypes.epc.schema.rdsap_schema_21_0_1 import RdSapSchema21_0_1
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_21_0_1(
|
||||
from_dict(RdSapSchema21_0_1, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_21_0_1(
|
||||
from_dict(RdSapSchema21_0_1, data)
|
||||
)
|
||||
if schema == "RdSAP-Schema-21.0.0":
|
||||
elif schema == "RdSAP-Schema-21.0.0":
|
||||
from datatypes.epc.schema.rdsap_schema_21_0_0 import RdSapSchema21_0_0
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_21_0_0(
|
||||
from_dict(RdSapSchema21_0_0, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_21_0_0(
|
||||
from_dict(RdSapSchema21_0_0, data)
|
||||
)
|
||||
if schema == "RdSAP-Schema-20.0.0":
|
||||
elif schema == "RdSAP-Schema-20.0.0":
|
||||
from datatypes.epc.schema.rdsap_schema_20_0_0 import RdSapSchema20_0_0
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_20_0_0(
|
||||
from_dict(RdSapSchema20_0_0, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_20_0_0(
|
||||
from_dict(RdSapSchema20_0_0, data)
|
||||
)
|
||||
if schema == "RdSAP-Schema-19.0":
|
||||
elif schema == "RdSAP-Schema-19.0":
|
||||
from datatypes.epc.schema.rdsap_schema_19_0 import RdSapSchema19_0
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_19_0(
|
||||
from_dict(RdSapSchema19_0, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_19_0(
|
||||
from_dict(RdSapSchema19_0, data)
|
||||
)
|
||||
if schema == "RdSAP-Schema-18.0":
|
||||
elif schema == "RdSAP-Schema-18.0":
|
||||
from datatypes.epc.schema.rdsap_schema_18_0 import RdSapSchema18_0
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_18_0(
|
||||
from_dict(RdSapSchema18_0, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_18_0(
|
||||
from_dict(RdSapSchema18_0, data)
|
||||
)
|
||||
if schema == "RdSAP-Schema-17.1":
|
||||
elif schema == "RdSAP-Schema-17.1":
|
||||
from datatypes.epc.schema.rdsap_schema_17_1 import RdSapSchema17_1
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_17_1(
|
||||
from_dict(RdSapSchema17_1, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_17_1(
|
||||
from_dict(RdSapSchema17_1, data)
|
||||
)
|
||||
if schema == "RdSAP-Schema-17.0":
|
||||
elif schema == "RdSAP-Schema-17.0":
|
||||
from datatypes.epc.schema.rdsap_schema_17_0 import RdSapSchema17_0
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_17_0(
|
||||
from_dict(RdSapSchema17_0, data)
|
||||
)
|
||||
mapped = EpcPropertyDataMapper.from_rdsap_schema_17_0(
|
||||
from_dict(RdSapSchema17_0, data)
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported EPC schema: {schema!r}")
|
||||
|
||||
raise ValueError(f"Unsupported EPC schema: {schema!r}")
|
||||
return _clear_basement_flag_when_system_built(
|
||||
_with_renewable_heat_incentive(
|
||||
_with_recorded_performance(mapped, data), data
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -2330,6 +2323,85 @@ class EpcPropertyDataMapper:
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _with_recorded_performance(
|
||||
epc: EpcPropertyData, data: Dict[str, Any]
|
||||
) -> EpcPropertyData:
|
||||
"""Overlay the recorded current-performance scalars from the raw API payload.
|
||||
|
||||
The current SAP rating, EPC band, Primary Energy Intensity
|
||||
(``energy_consumption_current``) and CO2 are top-level fields on every RdSAP
|
||||
schema response, but only a couple of the per-schema mappers copy them
|
||||
through (and none map the band). Baseline's Lodged Performance reads all four
|
||||
off the EPC, so map them here, once, for every schema version. An absent key
|
||||
leaves the mapped value untouched.
|
||||
"""
|
||||
band = data.get("current_energy_efficiency_band")
|
||||
co2 = data.get("co2_emissions_current")
|
||||
consumption = data.get("energy_consumption_current")
|
||||
rating = data.get("energy_rating_current")
|
||||
return replace(
|
||||
epc,
|
||||
current_energy_efficiency_band=(
|
||||
Epc(band) if band is not None else epc.current_energy_efficiency_band
|
||||
),
|
||||
co2_emissions_current=(
|
||||
float(co2) if co2 is not None else epc.co2_emissions_current
|
||||
),
|
||||
energy_consumption_current=(
|
||||
int(consumption)
|
||||
if consumption is not None
|
||||
else epc.energy_consumption_current
|
||||
),
|
||||
energy_rating_current=(
|
||||
int(rating) if rating is not None else epc.energy_rating_current
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _with_renewable_heat_incentive(
|
||||
epc: EpcPropertyData, data: Dict[str, Any]
|
||||
) -> EpcPropertyData:
|
||||
"""Gap-fill the RHI block (baseline space/water-heating kWh) from the raw
|
||||
payload.
|
||||
|
||||
The ``renewable_heat_incentive`` object is present on every schema response,
|
||||
but only the 21.x mappers copy it through; Baseline reads
|
||||
``space_heating_kwh`` / ``water_heating_kwh`` off it. Only fills when a mapper
|
||||
left it unset, and only when the block carries both required kWh figures —
|
||||
otherwise the EPC is returned untouched.
|
||||
"""
|
||||
if epc.renewable_heat_incentive is not None:
|
||||
return epc
|
||||
rhi = data.get("renewable_heat_incentive")
|
||||
if not isinstance(rhi, dict):
|
||||
return epc
|
||||
rhi_obj = cast(Dict[str, Any], rhi)
|
||||
space = rhi_obj.get("space_heating_existing_dwelling")
|
||||
water = rhi_obj.get("water_heating")
|
||||
if space is None or water is None:
|
||||
return epc
|
||||
return replace(
|
||||
epc,
|
||||
renewable_heat_incentive=RenewableHeatIncentive(
|
||||
space_heating_kwh=float(space),
|
||||
water_heating_kwh=float(water),
|
||||
impact_of_loft_insulation_kwh=_optional_float(
|
||||
rhi_obj.get("impact_of_loft_insulation")
|
||||
),
|
||||
impact_of_cavity_insulation_kwh=_optional_float(
|
||||
rhi_obj.get("impact_of_cavity_insulation")
|
||||
),
|
||||
impact_of_solid_wall_insulation_kwh=_optional_float(
|
||||
rhi_obj.get("impact_of_solid_wall_insulation")
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _optional_float(value: Any) -> Optional[float]:
|
||||
return float(value) if value is not None else None
|
||||
|
||||
|
||||
def _clear_basement_flag_when_system_built(
|
||||
epc: EpcPropertyData,
|
||||
) -> EpcPropertyData:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
from typing import Optional, Protocol, TypeVar
|
||||
|
||||
from sqlmodel import Session, col, delete, select
|
||||
|
|
@ -57,6 +57,24 @@ def _require(value: Optional[_T], field: str) -> _T:
|
|||
return value
|
||||
|
||||
|
||||
def _as_date(value: object) -> date:
|
||||
"""Normalise an ``epc_property`` date column value to a ``date``.
|
||||
|
||||
The FE-owned date columns (``inspection_date`` / ``completion_date`` /
|
||||
``registration_date``) are Postgres ``timestamp``s even though the SQLModel
|
||||
mirror types them ``str`` (it stores the writer's ``isoformat()`` string).
|
||||
So a read hands back a ``datetime``, while a value still in flight may be
|
||||
the ISO string — accept both.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return value.date()
|
||||
if isinstance(value, date):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
return date.fromisoformat(value)
|
||||
raise TypeError(f"unexpected inspection_date value: {value!r}")
|
||||
|
||||
|
||||
class _HasEpcPropertyId(Protocol):
|
||||
epc_property_id: int
|
||||
|
||||
|
|
@ -425,7 +443,7 @@ class EpcPostgresRepository(EpcRepository):
|
|||
|
||||
return EpcPropertyData(
|
||||
dwelling_type=p.dwelling_type,
|
||||
inspection_date=date.fromisoformat(p.inspection_date),
|
||||
inspection_date=_as_date(p.inspection_date),
|
||||
tenure=p.tenure,
|
||||
transaction_type=p.transaction_type,
|
||||
address_line_1=_require(p.address_line_1, "address_line_1"),
|
||||
|
|
@ -480,12 +498,10 @@ class EpcPostgresRepository(EpcRepository):
|
|||
pressure_test=p.pressure_test,
|
||||
language_code=p.language_code,
|
||||
completion_date=(
|
||||
date.fromisoformat(p.completion_date) if p.completion_date else None
|
||||
_as_date(p.completion_date) if p.completion_date else None
|
||||
),
|
||||
registration_date=(
|
||||
date.fromisoformat(p.registration_date)
|
||||
if p.registration_date
|
||||
else None
|
||||
_as_date(p.registration_date) if p.registration_date else None
|
||||
),
|
||||
measurement_type=p.measurement_type,
|
||||
conservatory_type=p.conservatory_type,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue