From 212d62e8358eaee6f722b90d295660074d08537c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 16:28:10 +0000 Subject: [PATCH] =?UTF-8?q?Map=20to=20new=20dataclasses=20from=20LBWF=20ob?= =?UTF-8?q?jects=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/aspect_type.py | 9 + backend/condition/domain/asset_condition.py | 1 - backend/condition/domain/element.py | 18 +- .../domain/mapping/lbwf/lbwf_element_map.py | 351 ++++++++++++++++++ .../domain/mapping/lbwf/lbwf_mapper.py | 25 +- .../tests/mapping/test_lbwf_mapper.py | 36 +- 6 files changed, 417 insertions(+), 23 deletions(-) create mode 100644 backend/condition/domain/mapping/lbwf/lbwf_element_map.py diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 45d0f24b..0f9a406a 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -5,6 +5,7 @@ class AspectType(str, Enum): MATERIAL = "material" CONDITION = "condition" TYPE = "type" + AREA = "area" CONFIGURATION = "configuration" PRESENCE = "presence" RISK = "risk" @@ -18,3 +19,11 @@ class AspectType(str, Enum): CLADDING = "cladding" CATEGORY = "category" QUANTITY = "quantity" + ADEQUACY = "adequacy" + RATING = "rating" + STRATEGY = "strategy" + EXTENT = "extent" + DISTRIBUTION = "distribution" + STRUCTURE = "structure" + COVERING = "covering" + FIRE_RATING = "fire_rating" diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index cd57d9ff..1b157a6b 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -21,7 +21,6 @@ class AssetCondition: renewal_year: Optional[int] = None element_instance: Optional[int] = None - element_location: Optional[str] = None source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index d9698ddf..e082bd4f 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -6,7 +6,7 @@ class Element(str, Enum): # ====================== # PROPERTY / GENERAL # ====================== - PROPERTY_TYPE = "property_type" + PROPERTY = "property" PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" PROPERTY_CLASSIFICATION = "property_classification" PROPERTY_AGE_BAND = "property_age_band" @@ -14,18 +14,15 @@ class Element(str, Enum): FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" + QUALITY_STANDARD = "quality_standard" # ====================== # EXTERNAL – ROOF # ====================== - ROOF_COVERING = "roof_covering" - ROOF_STRUCTURE = "roof_structure" - ROOF_CHIMNEY = "roof_chimney" - ROOF_FASCIA = "roof_fascia" - ROOF_SOFFIT = "roof_soffit" + ROOF = "roof" RAINWATER_GOODS = "rainwater_goods" - ROOF_PORCH_CANOPY = "roof_porch_canopy" LOFT_INSULATION = "loft_insulation" + PORCH_CANOPY = "porch_canopy" # ====================== # EXTERNAL – WALLS @@ -35,13 +32,14 @@ class Element(str, Enum): # ====================== # EXTERNAL – WINDOWS # ====================== - WINDOWS = "windows" + EXTERNAL_WINDOWS = "external_windows" COMMUNAL_WINDOWS = "communal_windows" SECONDARY_GLAZING = "secondary_glazing" # ====================== # EXTERNAL – DOORS # ====================== + EXTERNAL_DOOR = "external_door" FRONT_DOOR = "front_door" REAR_DOOR = "rear_door" STORE_DOOR = "store_door" @@ -84,6 +82,9 @@ class Element(str, Enum): HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" PROGRAMMABLE_HEATING = "programmable_heating" + HEATING_SYSTEM = "heating_system" + BOILER_FUEL = "boiler_fuel" + WATER_HEATING = "water_heating" # ====================== # INTERNAL – ELECTRICS / FIRE @@ -94,6 +95,7 @@ class Element(str, Enum): HEAT_DETECTION = "heat_detection" CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" + FIRE_RISK_ASSESSMENT = "fire" # ====================== # COMMUNAL SYSTEMS diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py new file mode 100644 index 00000000..6927e2fd --- /dev/null +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -0,0 +1,351 @@ +from dataclasses import dataclass +from typing import Optional + +from backend.condition.domain.element import Element +from backend.condition.domain.aspect_type import AspectType + + +@dataclass(frozen=True) +class LbwfElementMapping: + element: Element + aspect_type: AspectType + element_instance: Optional[int] = None + + +LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { + # ========================================================== + # PROPERTY / GENERAL + # ========================================================== + "AHR_CAT": LbwfElementMapping( + element=Element.ACCESSIBLE_HOUSING_REGISTER, + aspect_type=AspectType.CATEGORY, + ), + "ASSETSAREA": LbwfElementMapping( + element=Element.PROPERTY, + aspect_type=AspectType.AREA, + ), + # "DECNTHMINC": LbwfElementMapping( + # element=Element.DECENT_HOMES, + # aspect_type=AspectType.INCLUSION, + # ), # Ignore this one + "QUALITYSTD": LbwfElementMapping( + element=Element.QUALITY_STANDARD, + aspect_type=AspectType.TYPE, + ), + "EXTSTOREY": LbwfElementMapping( + element=Element.PROPERTY, + aspect_type=AspectType.CONFIGURATION, + ), + "FLVL": LbwfElementMapping( + element=Element.FLOOR_LEVEL_FRONT_DOOR, + aspect_type=AspectType.LOCATION, + ), + # ========================================================== + # ASBESTOS (NON-HHSRS RECORD) + # ========================================================== + "ASBESTOS": LbwfElementMapping( + element=Element.ASBESTOS, + aspect_type=AspectType.PRESENCE, + ), + # ========================================================== + # INTERNAL – BATHROOMS & KITCHENS + # ========================================================== + "INTBTHRLOC": LbwfElementMapping( + element=Element.BATHROOM, + aspect_type=AspectType.LOCATION, + ), + "INTBTHADEQ": LbwfElementMapping( + element=Element.BATHROOM, + aspect_type=AspectType.ADEQUACY, + ), + "INTKITADEQ": LbwfElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.ADEQUACY, + ), + "INTCKRLOC": LbwfElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.LOCATION, + ), + # ========================================================== + # INTERNAL – HEATING + # ========================================================== + "INTCHEXTNT": LbwfElementMapping( + element=Element.HEATING_EXTENT, + aspect_type=AspectType.CONFIGURATION, + ), + "INTCHDIST": LbwfElementMapping( + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, + ), + "INTCHBLR": LbwfElementMapping( + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, + ), + # ========================================================== + # INTERNAL – FIRE + # ========================================================== + "FRARISKRTG": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.RATING, + ), + "FRATYPE": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.TYPE, + ), + "FRAEVACSTR": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.STRATEGY, + ), + "INTSMKDET": LbwfElementMapping( + element=Element.SMOKE_DETECTION, + aspect_type=AspectType.PRESENCE, + ), + "INTCHEXTNT": LbwfElementMapping( + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.EXTENT, + ), + # ========================================================== + # HEATING & SERVICES + # ========================================================== + "INTBOILERF": LbwfElementMapping( + element=Element.BOILER_FUEL, + aspect_type=AspectType.TYPE, + ), + "INTHTDISYS": LbwfElementMapping( + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.DISTRIBUTION, + ), + "INTWTRHTNG": LbwfElementMapping( + element=Element.WATER_HEATING, + aspect_type=AspectType.TYPE, + ), + # ========================================================== + # EXTERNAL – WALLS (INSTANCED) + # ========================================================== + "EXTWALLSTR": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.STRUCTURE, + element_instance=1, + ), + "EXTWALLFN1": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=1, + ), + "EXTWALLFN2": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=2, + ), + "EXTWALLINS": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, + ), + "EXTWALLSPL": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.CONDITION, + ), + # ========================================================== + # EXTERNAL – ROOFS (INSTANCED) + # ========================================================== + "EXTRFSTR1": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=1, + ), + "EXTRFSTR2": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=2, + ), + "EXTRFSTR3": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=3, + ), + "EXTROOF1": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=1, + ), + "EXTROOF2": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=2, + ), + "EXTROOF3": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=3, + ), + # ========================================================== + # EXTERNAL – DOORS & WINDOWS + # ========================================================== + "INTFRDOOR": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + ), + "INTFRDRFRR": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.FIRE_RATING, + ), + "EXTBKSDDR1": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + "EXTBKSDDR2": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + "INTWDWTYPE": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + ), + "EXTWNDWS1": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + "EXTWNDWS2": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + # ========================================================== + # HHSRS – PHYSIOLOGICAL REQUIREMENTS + # ========================================================== + "HHSRSDAMP": LbwfElementMapping( + element=Element.HHSRS_DAMP_AND_MOULD, + aspect_type=AspectType.RISK, + ), + "HHSRSCOLD": LbwfElementMapping( + element=Element.HHSRS_EXCESS_COLD, + aspect_type=AspectType.RISK, + ), + "HHSRSHEAT": LbwfElementMapping( + element=Element.HHSRS_EXCESS_HEAT, + aspect_type=AspectType.RISK, + ), + "HHSRSASB": LbwfElementMapping( + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PSYCHOLOGICAL REQUIREMENTS + # ========================================================== + "HHSRSCROWD": LbwfElementMapping( + element=Element.HHSRS_CROWDING_AND_SPACE, + aspect_type=AspectType.RISK, + ), + "HHSRSENTRY": LbwfElementMapping( + element=Element.HHSRS_ENTRY_BY_INTRUDERS, + aspect_type=AspectType.RISK, + ), + "HHSRSENTRP": LbwfElementMapping( # collision / entrapment + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, + ), + "HHSRSLIGHT": LbwfElementMapping( + element=Element.HHSRS_LIGHTING, + aspect_type=AspectType.RISK, + ), + "HHSRSNOISE": LbwfElementMapping( + element=Element.HHSRS_NOISE, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST INFECTION + # ========================================================== + "HHSRSDOMES": LbwfElementMapping( + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + aspect_type=AspectType.RISK, + ), + "HHSRSFOOD": LbwfElementMapping( + element=Element.HHSRS_FOOD_SAFETY, + aspect_type=AspectType.RISK, + ), + "HHSRSPERS": LbwfElementMapping( + element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, + aspect_type=AspectType.RISK, + ), + "HHSRSWATER": LbwfElementMapping( + element=Element.HHSRS_WATER_SUPPLY, + aspect_type=AspectType.RISK, + ), + "HHSRSFBATH": LbwfElementMapping( + element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + aspect_type=AspectType.RISK, + ), + "HHSRSPOSI": LbwfElementMapping( + element=Element.HHSRS_SURFACES_MOULD, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST ACCIDENTS + # ========================================================== + "HHSRSFLEVE": LbwfElementMapping( + element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, + aspect_type=AspectType.RISK, + ), + "HHSRSFSTAI": LbwfElementMapping( + element=Element.HHSRS_FALLS_ON_STAIRS, + aspect_type=AspectType.RISK, + ), + "HHSRSFBETW": LbwfElementMapping( + element=Element.HHSRS_FALLS_BETWEEN_LEVELS, + aspect_type=AspectType.RISK, + ), + "HHSRSELEC": LbwfElementMapping( + element=Element.HHSRS_ELECTRICAL_HAZARDS, + aspect_type=AspectType.RISK, + ), + "HHSRSFIRE": LbwfElementMapping( + element=Element.HHSRS_FIRE, + aspect_type=AspectType.RISK, + ), + "HHSRSFLAME": LbwfElementMapping( + element=Element.HHSRS_FLAMES_HOT_SURFACES, + aspect_type=AspectType.RISK, + ), + "HHSRSEXPLO": LbwfElementMapping( + element=Element.HHSRS_EXPLOSION, + aspect_type=AspectType.RISK, + ), + "HHSRSSTRUC": LbwfElementMapping( + element=Element.HHSRS_STRUCTURAL_COLLAPSE, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST POLLUTION + # ========================================================== + "HHSRSCO": LbwfElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), + "HHSRSFUEL": LbwfElementMapping( + element=Element.HHSRS_UNSAFE_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSNO2": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSSO2": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSLEAD": LbwfElementMapping( + element=Element.HHSRS_LEAD, + aspect_type=AspectType.RISK, + ), + "HHSRSRADIA": LbwfElementMapping( + element=Element.HHSRS_RADIATION, + aspect_type=AspectType.RISK, + ), + "HHSRSBIOC": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), +} diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index dcd1d748..01f48a35 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,7 +1,11 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.element import Element +from backend.condition.domain.mapping.lbwf.lbwf_element_map import ( + LbwfElementMapping, + LBWF_ELEMENT_MAP, +) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -26,7 +30,9 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element: LbwfElement = LbwfMapper._map_element(raw_asset.element_code) + element_mapping: LbwfElementMapping = LbwfMapper._map_element( + raw_asset.element_code + ) except: logger.warning( f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" @@ -36,22 +42,25 @@ class LbwfMapper(Mapper): mapped_assets.append( AssetCondition( uprn=uprn, - element=element, - condition_description=raw_asset.attribute_code_description, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.attribute_code_description, quantity=raw_asset.quantity, + install_date=raw_asset.install_date, renewal_year=LbwfMapper._calculate_renewal_year( raw_asset, survey_year ), - source=raw_asset.element_comments, - install_date=raw_asset.install_date, + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=raw_asset.element_comments, ) ) return mapped_assets @staticmethod - def _map_element(lbwf_element_code: LbwfAssetCondition) -> LbwfElement: - return LbwfElement[lbwf_element_code] + def _map_element(lbwf_element_code: str) -> LbwfElementMapping: + return LBWF_ELEMENT_MAP[lbwf_element_code] @staticmethod def _calculate_renewal_year( diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index f4266ac4..918b6fea 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -181,9 +181,30 @@ def test_lbwf_mapper_maps_house(): prop_sub_type="TERRACED", element_group="ASSETS", element_code="EXTWALLFN1", + element_code_description="Wall Finish 1 in External Area", + attribute_code="SMTHRENDER", + attribute_code_description="Render or Pebbledash in External Area", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=date(2009, 4, 1), + remaining_life=26, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="EXTWALLFN2", element_code_description="Wall Finish 2 in External Area", attribute_code="SMTHRENDER", - attribute_code_description="Smooth Render Wall Finish 1 in External Area", + attribute_code_description="Smooth Render Wall Finish 2 in External Area", element_date_value=None, element_numerical_value=None, element_text_value=None, @@ -256,8 +277,8 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HEATING_EXTENT, - aspect_type=AspectType.CONFIGURATION, + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", quantity=1, @@ -281,7 +302,7 @@ def test_lbwf_mapper_maps_house(): element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, - value="Render or Pebbledash", + value="Render or Pebbledash in External Area", quantity=1, renewal_year=2052, install_date=date(2009, 4, 1), @@ -292,7 +313,7 @@ def test_lbwf_mapper_maps_house(): element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, - value="Smooth Render Wall Finish 1 in External Area", + value="Smooth Render Wall Finish 2 in External Area", quantity=1, renewal_year=2052, install_date=date(2009, 4, 1), @@ -306,4 +327,7 @@ def test_lbwf_mapper_maps_house(): ) # assert - assert actual_assets == expected_assets + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}"