diff --git a/backend/documents_parser/extractor.py b/backend/documents_parser/extractor.py index 71555395..47022c55 100644 --- a/backend/documents_parser/extractor.py +++ b/backend/documents_parser/extractor.py @@ -66,9 +66,11 @@ class PasHubRdSapSiteNotesExtractor: val = self._get_in(list_to_process, key) return val is not None and val.lower() != "not known" - def _wall_thickness_in(self, list_to_process: List[str]) -> int: + def _wall_thickness_in(self, list_to_process: List[str]) -> Optional[int]: val = self._get_in(list_to_process, "Wall thickness:") - return int(val.split()[0]) if val else 0 + if not val or val.split()[0].lower() == "unmeasurable": + return None + return int(val.split()[0]) def _section(self, start: str, end: str) -> List[str]: try: diff --git a/backend/documents_parser/tests/test_extractor.py b/backend/documents_parser/tests/test_extractor.py index 9e7eaffd..be577f1b 100644 --- a/backend/documents_parser/tests/test_extractor.py +++ b/backend/documents_parser/tests/test_extractor.py @@ -802,6 +802,26 @@ class TestExtractNoPropertyPhoto: assert result.general.number_of_extensions == 2 +class TestWallThicknessExtraction: + def _extractor(self) -> PasHubRdSapSiteNotesExtractor: + return PasHubRdSapSiteNotesExtractor([]) + + def test_numeric_value_returns_int(self) -> None: + assert self._extractor()._wall_thickness_in(["Wall thickness:", "310 mm"]) == 310 + + def test_unmeasurable_returns_none(self) -> None: + assert self._extractor()._wall_thickness_in(["Wall thickness:", "Unmeasurable"]) is None + + def test_unmeasurable_lowercase_returns_none(self) -> None: + assert self._extractor()._wall_thickness_in(["Wall thickness:", "unmeasurable"]) is None + + def test_unmeasurable_uppercase_returns_none(self) -> None: + assert self._extractor()._wall_thickness_in(["Wall thickness:", "UNMEASURABLE"]) is None + + def test_missing_field_returns_none(self) -> None: + assert self._extractor()._wall_thickness_in([]) is None + + class TestSolidMasonryPartyWall: @pytest.fixture def bc(self) -> BuildingConstruction: diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 1ce4c73c..054b951f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1581,7 +1581,7 @@ def _map_main_building_part( construction_age_band=_extract_age_band(main.age_range), wall_construction=main.walls_construction_type, wall_insulation_type=main.walls_insulation_type, - wall_thickness_measured=main.wall_thickness_mm > 0, + wall_thickness_measured=main.wall_thickness_mm is not None, party_wall_construction=main.party_wall_construction_type, sap_floor_dimensions=_map_floor_dimensions(measurements.main_building.floors), wall_thickness_mm=main.wall_thickness_mm, @@ -1605,7 +1605,7 @@ def _map_extension_building_part( construction_age_band=_extract_age_band(ext_c.age_range), wall_construction=ext_c.walls_construction_type, wall_insulation_type=ext_c.walls_insulation_type, - wall_thickness_measured=ext_c.wall_thickness_mm > 0, + wall_thickness_measured=ext_c.wall_thickness_mm is not None, party_wall_construction=ext_c.party_wall_construction_type, sap_floor_dimensions=_map_floor_dimensions(ext_m.floors), wall_thickness_mm=ext_c.wall_thickness_mm, diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index ff25933c..ae1dbb3b 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -694,3 +694,21 @@ class TestFromSiteNotesMiscTopLevel: def test_photovoltaic_array(self, result: EpcPropertyData) -> None: # renewables.photovoltaic_array: false assert result.photovoltaic_array is False + + +class TestUnmeasurableWallThickness: + """wall_thickness_mm=None in site notes → wall_thickness_measured=False in domain.""" + + @pytest.fixture + def result(self) -> EpcPropertyData: + survey = from_dict( + PasHubRdSapSiteNotes, + load("pashub_rdsap_site_notes_example_unmeasurable_wall.json"), + ) + return EpcPropertyDataMapper.from_site_notes(survey) + + def test_wall_thickness_measured_is_false(self, result: EpcPropertyData) -> None: + assert result.sap_building_parts[0].wall_thickness_measured is False + + def test_wall_thickness_mm_is_none(self, result: EpcPropertyData) -> None: + assert result.sap_building_parts[0].wall_thickness_mm is None diff --git a/datatypes/epc/surveys/pashub_rdsap_site_notes.py b/datatypes/epc/surveys/pashub_rdsap_site_notes.py index 73a58aa6..583eb228 100644 --- a/datatypes/epc/surveys/pashub_rdsap_site_notes.py +++ b/datatypes/epc/surveys/pashub_rdsap_site_notes.py @@ -44,7 +44,7 @@ class MainBuildingConstruction: walls_insulation_type: str thermal_conductivity_of_wall_insulation: str wall_u_value_known: bool - wall_thickness_mm: int + wall_thickness_mm: Optional[int] party_wall_construction_type: str filled_cavity_indicators: Optional[str] = None @@ -59,7 +59,7 @@ class ExtensionConstruction: walls_insulation_type: str thermal_conductivity_of_wall_insulation: str wall_u_value_known: bool - wall_thickness_mm: int + wall_thickness_mm: Optional[int] party_wall_construction_type: str filled_cavity_indicators: Optional[str] = None diff --git a/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example_unmeasurable_wall.json b/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example_unmeasurable_wall.json new file mode 100644 index 00000000..261160f4 --- /dev/null +++ b/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example_unmeasurable_wall.json @@ -0,0 +1,190 @@ +{ + "inspection_metadata": { + "inspection_surveyor": "test", + "email_address": "test@test.com", + "report_reference": "49D422A9-0779-44DD-9665-464D35DFF1A8", + "created_on": "2026-03-31", + "date_of_inspection": "2026-03-31", + "property_address": "1, Test Street, Test Town, Test County, TE1 1ST" + }, + "general": { + "epc_checked_before_assessment": true, + "epc_exists_at_point_of_assessment": false, + "inspection_date": "2026-03-31", + "transaction_type": "None of the Above", + "tenure": "Rented Social", + "property_type": "House", + "detachment_type": "Mid-terrace", + "number_of_storeys": 2, + "terrain_type": "Suburban", + "number_of_extensions": 0, + "electricity_smart_meter": true, + "electric_meter_type": "Single", + "dwelling_export_capable": true, + "mains_gas_available": true, + "gas_smart_meter": true, + "gas_meter_accessible": true, + "measurements_location": "Internal" + }, + "building_construction": { + "main_building": { + "age_range": "I: 1996 - 2002", + "age_indicators": "local knowledge", + "walls_construction_type": "Cavity", + "cavity_construction_indicators": "stretcher bond", + "walls_insulation_type": "As built", + "thermal_conductivity_of_wall_insulation": "Unknown", + "wall_u_value_known": false, + "wall_thickness_mm": null, + "party_wall_construction_type": "Cavity Masonry, Unfilled" + }, + "floor": { + "floor_type": "Ground Floor", + "floor_construction": "Suspended, not timber", + "floor_insulation_type": "As Built", + "floor_u_value_known": false + } + }, + "building_measurements": { + "main_building": { + "floors": [ + { + "name": "Floor 1", + "area_m2": 24.78, + "height_m": 2.37, + "heat_loss_perimeter_m": 14.21, + "pwl_m": 6.15 + }, + { + "name": "Floor 0", + "area_m2": 24.78, + "height_m": 2.35, + "heat_loss_perimeter_m": 14.21, + "pwl_m": 6.15 + } + ] + } + }, + "roof_space": { + "main_building": { + "construction_type": "Pitched roof (Slates or tiles), Access to loft", + "insulation_at": "Joists", + "roof_u_value_known": false, + "insulation_thickness_mm": 100, + "cavity_wall_construction_indicators": "No indicator of construction visible", + "rooms_in_roof": false + } + }, + "windows": [ + { + "id": 1, + "location": "Main Building", + "wall_type": "External wall", + "glazing_type": "Double glazing, Unknown install date", + "window_type": "Window", + "frame_type": "Wooden or PVC", + "glazing_gap": "16 mm or more", + "draught_proofed": true, + "permanent_shutters": false, + "height_m": 1.36, + "width_m": 1.0, + "orientation": "South East" + } + ], + "heating_and_hot_water": { + "main_heating": { + "selection_method": "PCDF Search", + "system_type": "Boiler with radiators or underfloor heating", + "product_id": 18400, + "manufacturer": "Vaillant", + "model": "ecoFIT sustain 415", + "orig_manufacturer": "Vaillant", + "fuel": "Mains gas", + "summer_efficiency": 0, + "type": "Regular", + "condensing": true, + "year": "2018 - current", + "mount": "Wall", + "open_flue": "Room-sealed", + "fan_assist": true, + "status": "Normal status for an actual product", + "central_heating_pump_age": "Unknown", + "controls": "Programmer, room thermostat and TRVs", + "flue_gas_heat_recovery_system": false, + "weather_compensator": false, + "emitter": "Radiators", + "emitter_temperature": "Unknown" + }, + "secondary_heating": { + "secondary_fuel": "No Secondary Heating" + }, + "water_heating": { + "type": "Regular", + "system": "From main heating 1", + "cylinder_size": "Normal (90-130 litres)", + "cylinder_measured_heat_loss": "Not known", + "insulation_type": "Factory fitted", + "insulation_thickness_mm": 12, + "has_thermostat": true + } + }, + "ventilation": { + "ventilation_type": "Natural", + "has_fixed_air_conditioning": false, + "number_of_open_flues": 0, + "number_of_closed_flues": 0, + "number_of_boiler_flues": 0, + "number_of_other_flues": 0, + "number_of_extract_fans": 2, + "number_of_passive_vents": 0, + "number_of_flueless_gas_fires": 0, + "pressure_test": "No test", + "draught_lobby": false + }, + "conservatories": { + "has_conservatory": false + }, + "renewables": { + "wind_turbines": false, + "solar_hot_water": false, + "photovoltaic_array": false, + "number_of_pv_batteries": 0, + "hydro": false + }, + "room_count_elements": { + "number_of_habitable_rooms": 2, + "any_unheated_rooms": true, + "number_of_heated_rooms": 0, + "number_of_external_doors": 2, + "number_of_insulated_external_doors": 0, + "number_of_draughtproofed_external_doors": 2, + "number_of_open_chimneys": 0, + "number_of_blocked_chimneys": 0, + "number_of_fixed_incandescent_bulbs": 0, + "exact_led_cfl_count_known": true, + "number_of_fixed_led_bulbs": 5, + "number_of_fixed_cfl_bulbs": 4, + "waste_water_heat_recovery": "None" + }, + "water_use": { + "number_of_baths": 1, + "number_of_special_features": 0, + "showers": [ + { + "id": 1, + "outlet_type": "Non-Electric Shower" + } + ] + }, + "customer_response": { + "customer_present": true, + "willing_to_answer_satisfaction_survey": false + }, + "addendum": { + "addendum": "None", + "related_party_disclosure": "No related party", + "hard_to_treat_cavity_access_issues": false, + "hard_to_treat_cavity_high_exposure": false, + "hard_to_treat_cavity_narrow_cavities": false + } +}