diff --git a/backend/documents_parser/tests/fixtures/ExampleSiteNotes_4.pdf b/backend/documents_parser/tests/fixtures/ExampleSiteNotes_4.pdf new file mode 100644 index 00000000..ea913eeb Binary files /dev/null and b/backend/documents_parser/tests/fixtures/ExampleSiteNotes_4.pdf differ diff --git a/backend/documents_parser/tests/fixtures/site_notes_example_4_text.json b/backend/documents_parser/tests/fixtures/site_notes_example_4_text.json new file mode 100644 index 00000000..d7561ded --- /dev/null +++ b/backend/documents_parser/tests/fixtures/site_notes_example_4_text.json @@ -0,0 +1,480 @@ +[ + "SMART EPC: Record of", + "Inspection & Site Notes", + "Inspection Surveyor:", + "Rebecca Mcwilliam", + "E-Mail Address:", + "rebeccamcdea@gmail.com", + "Report Reference:", + "DA1A93D9-354C-4B4B-A299-3A681231D4B5", + "Created On:", + "15 October 2025", + "Date of Inspection:", + "13 October 2025", + "Property Address:", + "18,", + "Oakfield Close,", + "Wrenbury,", + "CW5 8ET", + "Property Photo", + "Page 1", + "", + "Photo of electricity meter:", + "Photo of electricity meter:", + "RdSAP Assessment", + "General", + "Confirm you have checked for the existence of an", + "EPC before carrying out another energy assessment.", + "Yes", + "Does an EPC exist at the point of carrying out this", + "energy assessment?", + "Yes", + "Please select why another energy assessment needs", + "to be undertaken:", + "Assessor instructed to produce a new EPC upon request from building", + "owner/tenant/landlord after confirming to the requestor that a valid EPC", + "already exists", + "Inspection Date:", + "13/10/2025", + "Transaction Type:", + "None of the Above", + "Tenure:", + "Rented Social", + "Type of Property:", + "Bungalow", + "Detachment Type:", + "End-terrace", + "Number of storeys:", + "1 Storey", + "Terrain Type:", + "Suburban", + "Number of Extensions:", + "No Extensions", + "Is an electricity smart meter present?", + "Yes", + "Electric meter type:", + "Single", + "Page 2", + "", + "Photo of electricity meter:", + "Photo of electricity meter:", + "External indicators of Cavity Wall Construction:", + "External indicators of Cavity Wall Construction:", + "Is the dwelling export-capable?", + "Yes", + "Is mains gas available?", + "No", + "Select Measurements Location:", + "Internal", + "Building Construction", + "Main Building", + "Age Range:", + "1950-1966", + "Record indicators of property age:", + "local knowledge, enquiries of owner, period building features", + "Walls - Construction Type:", + "Cavity", + "Record external indicators of Cavity Construction:", + "stretcher bond, wall thickness over 270 mm", + "Walls - Insulation Type:", + "Filled Cavity", + "Record indicators of filled cavity:", + "evidence of cavity fill drill holes, Boroscope", + "Page 3", + "", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Page 4", + "", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Photo indicators of filled cavity insulation:", + "Photo wall thickness:", + "Thermal conductivity of wall insulation:", + "Unknown", + "Wall U-Value known?", + "Not Known", + "Wall thickness:", + "300 mm", + "Party wall construction type:", + "Unable to determine", + "Floor type:", + "Ground Floor", + "Floor Construction:", + "Solid", + "Floor Insulation Type:", + "As Built", + "Floor U-Value known?", + "Not Known", + "Page 5", + "", + "Loft insulation:", + "Loft insulation:", + "Building Measurements", + "Area (m2)", + "Height (m)", + "Heat Loss Perimeter (m)", + "PWL (m)", + "Main Building", + "Floor 0", + "46.08", + "2.44", + "20.49", + "6.67", + "Roof Space", + "Main Building", + "Roofs - Construction Type:", + "Pitched roof (Slates or tiles), No access", + "Identify the reason for restricted access:", + "access hatch blocked", + "Roofs - Insulation At:", + "Unknown", + "Page 6", + "", + "Loft insulation:", + "Record indicators of Cavity Wall Construction in roof", + "space:", + "No indicator of construction visible", + "Are there rooms in the roof?", + "No", + "Windows", + "Window 1", + "Window location:", + "Main Building", + "Window wall type:", + "External wall", + "Glazing Type:", + "Double glazing, Unknown install date", + "Window type:", + "Window", + "Window frame type:", + "Wooden or PVC", + "What size is the glazing gap?", + "12 mm", + "Is the window draught proofed?", + "Yes", + "Are there permanent shutters present?", + "No", + "Window height:", + "0.97 m", + "Window width:", + "1.54 m", + "Orientation:", + "North East", + "Page 7", + "", + "Photo of glazing type:", + "Photo of glazing type:", + "Photo of glazing type:", + "Photo of glazing type:", + "Window 2", + "Window location:", + "Main Building", + "Window wall type:", + "External wall", + "Glazing Type:", + "Double glazing, Unknown install date", + "Window type:", + "Window", + "Window frame type:", + "Wooden or PVC", + "What size is the glazing gap?", + "12 mm", + "Is the window draught proofed?", + "Yes", + "Are there permanent shutters present?", + "No", + "Window height:", + "1.01 m", + "Window width:", + "1.04 m", + "Orientation:", + "North East", + "Window 3", + "Window location:", + "Main Building", + "Window wall type:", + "External wall", + "Glazing Type:", + "Double glazing, Unknown install date", + "Window type:", + "Window", + "Window frame type:", + "Wooden or PVC", + "Page 8", + "", + "Photo of glazing type:", + "Photo of glazing type:", + "Photo of glazing type:", + "Photo of glazing type:", + "What size is the glazing gap?", + "12 mm", + "Is the window draught proofed?", + "Yes", + "Are there permanent shutters present?", + "No", + "Window height:", + "0.99 m", + "Window width:", + "1.07 m", + "Orientation:", + "South West", + "Window 4", + "Window location:", + "Main Building", + "Window wall type:", + "External wall", + "Glazing Type:", + "Double glazing, Unknown install date", + "Window type:", + "Window", + "Window frame type:", + "Wooden or PVC", + "What size is the glazing gap?", + "12 mm", + "Is the window draught proofed?", + "Yes", + "Are there permanent shutters present?", + "No", + "Window height:", + "1.28 m", + "Window width:", + "2.07 m", + "Orientation:", + "South", + "Page 9", + "", + "Photo of heating system:", + "Photo of heating system:", + "Photo of heating system:", + "Photo of heating system:", + "Heating & Hot Water", + "Main Heating Systems", + "Main Heating 1", + "How would you like to select the Heating System?", + "PCDF Search", + "System type:", + "Heat pump with radiators or underfloor heating", + "Product Id", + "102421", + "Manufacturer", + "Daikin Altherma", + "Model", + "EDLQ05CAV3", + "Year", + "2014 - current", + "Fuel", + "Electricity, any tariff", + "Status", + "Normal status for an actual product", + "Central heating pump age:", + "Unknown", + "MCS installed heat pump:", + "Yes", + "Controls:", + "Programmer, room thermostat and TRVs", + "Emitter:", + "Radiators", + "Emitter Temperature:", + "Unknown", + "Page 10", + "", + "Photo of heating controls:", + "Photo of heating controls:", + "Photo of heating controls:", + "Photo of heating controls:", + "Photo of heating controls:", + "Photo of heating controls:", + "Page 11", + "", + "Photo of heating controls:", + "Photo of secondary heating system", + "Secondary Heating System", + "Secondary Fuel", + "Electricity", + "Secondary System:", + "Panel, convector or radiant heaters", + "Water Heating & Cylinder", + "Water Heating Type:", + "Regular", + "Water Heating System:", + "From main heating 1", + "Page 12", + "", + "Photo of water heating system:", + "Photo of water heating system:", + "Photo of water heating system:", + "Cylinder Size:", + "Medium (131-170 litres)", + "What is the cylinder measured heat loss:", + "Not known", + "Insulation Type:", + "Factory fitted", + "Thickness:", + "50 mm", + "Page 13", + "", + "Photo of cylinder and thermostat if present:", + "Photo of cylinder and thermostat if present:", + "Photo of ventilation type:", + "Photo of ventilation type:", + "Has thermostat?", + "Yes", + "Ventilation", + "Ventilation type:", + "Mechanical Extract - Decentralised", + "Has fixed air conditioning?", + "No", + "Is the ventilation in the PCDF database?", + "No", + "Number of open flues:", + "0", + "Number of closed flues:", + "0", + "Number of boiler flues:", + "0", + "Page 14", + "", + "Photo of extract fans:", + "Number of other flues:", + "0", + "Number of extract fans:", + "1", + "Number of passive vents:", + "0", + "Number of flueless gas fires:", + "0", + "Pressure test:", + "No test", + "Is there a draught lobby?", + "No", + "Conservatories", + "Is there conservatory?", + "No conservatory", + "Renewables", + "Wind Turbines", + "Has wind turbines?", + "No", + "Solar hot water", + "Has solar hot water?", + "No", + "Photovoltaics", + "Has photovoltaic array?", + "No", + "Number of PV batteries:", + "None", + "Hydro", + "Is the dwelling connected to Hydro?", + "No", + "Room Count Elements", + "Number of habitable rooms?", + "2", + "Are any of these rooms unheated?", + "No", + "Page 15", + "", + "Photo of LED bulbs:", + "Photo of LED bulbs:", + "Photo of LED bulbs:", + "Photo of LED bulbs:", + "Photo of LED bulbs:", + "Photo of LED bulbs:", + "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", + "Is the exact number of LED and CFL bulbs known?", + "Yes", + "Number of fixed LED bulbs:", + "7", + "Page 16", + "", + "Photo of LED bulbs:", + "Photo of shower:", + "Photo of shower:", + "Number of fixed CFL bulbs:", + "0", + "Are there any waste water heat recovery systems?", + "None", + "Number of baths:", + "1", + "How many special features are there at the", + "property?", + "0", + "Showers", + "Shower 1", + "Shower outlet type:", + "Electric Shower", + "Customer Response", + "Customer present?", + "Yes", + "Customer willing to answer satisfaction survey?", + "No", + "Addendum + Related Party Disclosure", + "Addendum", + "None", + "Page 17", + "", + "General Photos:", + "General Photos:", + "External Elevations:", + "External Elevations:", + "Related party disclosure", + "No related party", + "Hard to treat cavity walls: Property has access", + "issues?", + "No", + "Hard to treat cavity walls: Property has high", + "exposure?", + "No", + "Hard to treat cavity walls: Property has narrow", + "cavities?", + "No", + "Photographs Required", + "Page 18", + "", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "Page 19", + "", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "Page 20", + "", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "External Elevations:", + "Page 21", + "", + "External Elevations:", + "External Elevations:", + "Page 22", + "", + "Additional Notes", + "Additional Notes", + "The loft hatch is a drop-down hatch with a ladder above. The hatch is broken", + "and it could not be opened. She is about to have a new door made so loft will", + "be able to be accessed at some point soon but on the day I could not.", + "Page 23", + "" +] \ No newline at end of file diff --git a/backend/documents_parser/tests/test_end_to_end.py b/backend/documents_parser/tests/test_end_to_end.py index 0c2369f0..09fcb4db 100644 --- a/backend/documents_parser/tests/test_end_to_end.py +++ b/backend/documents_parser/tests/test_end_to_end.py @@ -294,11 +294,49 @@ class TestPdfToEpcPropertyDataFixture3: assert result.sap_heating.immersion_heating_type == "Dual" def test_pv_connection(self, result: EpcPropertyData) -> None: - assert result.sap_energy_source.pv_connection == "Connected to dwellings electricity meter" + assert ( + result.sap_energy_source.pv_connection + == "Connected to dwellings electricity meter" + ) def test_photovoltaic_supply_percent_roof(self, result: EpcPropertyData) -> None: assert result.sap_energy_source.photovoltaic_supply is not None - assert result.sap_energy_source.photovoltaic_supply.none_or_no_details.percent_roof_area == 45 + assert ( + result.sap_energy_source.photovoltaic_supply.none_or_no_details.percent_roof_area + == 45 + ) def test_electric_storage_heater_fuel_type(self, result: EpcPropertyData) -> None: - assert result.sap_heating.main_heating_details[0].main_fuel_type == "Electricity" + assert ( + result.sap_heating.main_heating_details[0].main_fuel_type == "Electricity" + ) + + +PDF_PATH_4 = os.path.join( + os.path.dirname(__file__), "fixtures", "ExampleSiteNotes_4.pdf" +) + + +class TestPdfToEpcPropertyDataFixture4: + @pytest.fixture + def result(self) -> EpcPropertyData: + with open(PDF_PATH_4, "rb") as f: + pdf_bytes = f.read() + site_notes = PasHubRdSapSiteNotesExtractor( + pdf_to_text_list(pdf_bytes) + ).extract() + return EpcPropertyDataMapper.from_site_notes(site_notes) + + def test_cylinder_insulation_type(self, result: EpcPropertyData) -> None: + assert result.sap_heating.cylinder_insulation_type == "Factory fitted" + + def test_heat_pump_fuel_type(self, result: EpcPropertyData) -> None: + assert ( + result.sap_heating.main_heating_details[0].main_fuel_type == "Electricity" + ) + + def test_roof_insulation_location_unknown(self, result: EpcPropertyData) -> None: + assert result.sap_building_parts[0].roof_insulation_location == "Unknown" + + def test_roof_insulation_thickness_none(self, result: EpcPropertyData) -> None: + assert result.sap_building_parts[0].roof_insulation_thickness is None diff --git a/backend/documents_parser/tests/test_extractor.py b/backend/documents_parser/tests/test_extractor.py index fa185180..1a21007f 100644 --- a/backend/documents_parser/tests/test_extractor.py +++ b/backend/documents_parser/tests/test_extractor.py @@ -51,6 +51,11 @@ def load_text_fixture_3() -> list[str]: return json.load(f) +def load_text_fixture_4() -> list[str]: + with open(os.path.join(FIXTURES, "site_notes_example_4_text.json")) as f: + return json.load(f) + + class TestInspectionMetadata: def test_full_inspection_metadata(self) -> None: result = PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_inspection_metadata() @@ -675,3 +680,54 @@ class TestSurveyAddendum: hard_to_treat_cavity_high_exposure=False, hard_to_treat_cavity_narrow_cavities=False, ) + + +# --- fixture 4: heat pump, factory-fitted cylinder, blocked loft --- + + +class TestCylinderInsulationType: + @pytest.fixture + def hhw(self) -> HeatingAndHotWater: + return PasHubRdSapSiteNotesExtractor( + load_text_fixture_4() + ).extract_heating_and_hot_water() + + def test_insulation_type_extracted(self, hhw: HeatingAndHotWater) -> None: + assert hhw.water_heating.insulation_type == "Factory fitted" + + def test_insulation_thickness_mm(self, hhw: HeatingAndHotWater) -> None: + assert hhw.water_heating.insulation_thickness_mm == 50 + + def test_cylinder_size(self, hhw: HeatingAndHotWater) -> None: + assert hhw.water_heating.cylinder_size == "Medium (131-170 litres)" + + +class TestHeatPumpFuelExtraction: + @pytest.fixture + def hhw(self) -> HeatingAndHotWater: + return PasHubRdSapSiteNotesExtractor( + load_text_fixture_4() + ).extract_heating_and_hot_water() + + def test_fuel_raw_value(self, hhw: HeatingAndHotWater) -> None: + assert hhw.main_heating.fuel == "Electricity, any tariff" + + def test_system_type(self, hhw: HeatingAndHotWater) -> None: + assert hhw.main_heating.system_type == "Heat pump with radiators or underfloor heating" + + +class TestRoofSpaceUnknownInsulation: + @pytest.fixture + def roof_space(self) -> RoofSpace: + return PasHubRdSapSiteNotesExtractor( + load_text_fixture_4() + ).extract_roof_space() + + def test_insulation_at_unknown(self, roof_space: RoofSpace) -> None: + assert roof_space.main_building.insulation_at == "Unknown" + + def test_insulation_thickness_mm_none(self, roof_space: RoofSpace) -> None: + assert roof_space.main_building.insulation_thickness_mm is None + + def test_insulation_thickness_str_none(self, roof_space: RoofSpace) -> None: + assert roof_space.main_building.insulation_thickness is None diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index 46d6edae..e4e89586 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -61,7 +61,7 @@ class SapHeating: water_heating_fuel: Optional[int] = None # TODO: make enum? immersion_heating_type: Optional[Union[int, str]] = None # TODO: make enum? shower_outlets: Optional[ShowerOutlets] = None - cylinder_insulation_type: Optional[int] = None + cylinder_insulation_type: Optional[Union[int, str]] = None cylinder_thermostat: Optional[str] = None secondary_fuel_type: Optional[int] = None secondary_heating_type: Optional[Union[int, str]] = None # int from API; str from site notes