import json import os import pytest from backend.documents_parser.extractor import PasHubRdSapSiteNotesExtractor from datatypes.epc.surveys.pashub_rdsap_site_notes import ( BuildingConstruction, BuildingMeasurements, ExtensionConstruction, ExtensionMeasurements, ExtensionRoofSpace, FloorConstruction, FloorMeasurement, General, HeatingAndHotWater, MainBuildingConstruction, MainBuildingMeasurements, MainHeating, PasHubRdSapSiteNotes, RoofSpace, RoofSpaceDetail, SecondaryHeating, Ventilation, WaterHeating, ) FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") def load_text_fixture() -> list[str]: with open(os.path.join(FIXTURES, "site_notes_example_text.json")) as f: return json.load(f) class TestGeneral: @pytest.fixture def general(self) -> General: return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_general() def test_epc_checked_before_assessment(self, general: General) -> None: assert general.epc_checked_before_assessment is True def test_epc_exists_at_point_of_assessment(self, general: General) -> None: assert general.epc_exists_at_point_of_assessment is False def test_inspection_date(self, general: General) -> None: assert general.inspection_date == "2025-09-25" def test_transaction_type(self, general: General) -> None: assert general.transaction_type == "Grant-Scheme (ECO, RHI, etc.)" def test_tenure(self, general: General) -> None: assert general.tenure == "Rented Social" def test_property_type(self, general: General) -> None: assert general.property_type == "House" def test_detachment_type(self, general: General) -> None: assert general.detachment_type == "Mid-terrace" def test_number_of_storeys(self, general: General) -> None: assert general.number_of_storeys == 2 def test_number_of_extensions(self, general: General) -> None: assert general.number_of_extensions == 1 def test_electricity_smart_meter(self, general: General) -> None: assert general.electricity_smart_meter is True def test_mains_gas_available(self, general: General) -> None: assert general.mains_gas_available is True def test_measurements_location(self, general: General) -> None: assert general.measurements_location == "Internal" def test_full_general(self, general: General) -> None: assert general == General( epc_checked_before_assessment=True, epc_exists_at_point_of_assessment=False, inspection_date="2025-09-25", transaction_type="Grant-Scheme (ECO, RHI, etc.)", tenure="Rented Social", property_type="House", detachment_type="Mid-terrace", number_of_storeys=2, terrain_type="Suburban", number_of_extensions=1, 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", ) class TestBuildingConstruction: @pytest.fixture def construction(self) -> BuildingConstruction: return PasHubRdSapSiteNotesExtractor( load_text_fixture() ).extract_building_construction() def test_main_building_wall_u_value_known_is_false( self, construction: BuildingConstruction ) -> None: assert construction.main_building.wall_u_value_known is False def test_main_building_wall_thickness_mm( self, construction: BuildingConstruction ) -> None: assert construction.main_building.wall_thickness_mm == 310 def test_main_building_filled_cavity_indicators_present( self, construction: BuildingConstruction ) -> None: assert ( construction.main_building.filled_cavity_indicators == "evidence of cavity fill drill holes" ) def test_extension_filled_cavity_indicators_absent( self, construction: BuildingConstruction ) -> None: assert construction.extensions is not None assert construction.extensions[0].filled_cavity_indicators is None def test_one_extension(self, construction: BuildingConstruction) -> None: assert construction.extensions is not None assert len(construction.extensions) == 1 def test_extension_id(self, construction: BuildingConstruction) -> None: assert construction.extensions is not None assert construction.extensions[0].id == 1 def test_full_building_construction( self, construction: BuildingConstruction ) -> None: assert construction == BuildingConstruction( main_building=MainBuildingConstruction( age_range="1950-1966", age_indicators="local knowledge, enquiries of owner", walls_construction_type="Cavity", cavity_construction_indicators="wall thickness over 270 mm", walls_insulation_type="Filled Cavity", filled_cavity_indicators="evidence of cavity fill drill holes", thermal_conductivity_of_wall_insulation="Unknown", wall_u_value_known=False, wall_thickness_mm=310, party_wall_construction_type="Cavity Masonry, Filled", ), floor=FloorConstruction( floor_type="Ground Floor", floor_construction="Solid", floor_insulation_type="As Built", floor_u_value_known=False, ), extensions=[ ExtensionConstruction( id=1, age_range="2003-2006", age_indicators="local knowledge, enquiries of owner", walls_construction_type="Cavity", cavity_construction_indicators="wall thickness over 270 mm", walls_insulation_type="As built", thermal_conductivity_of_wall_insulation="Unknown", wall_u_value_known=False, wall_thickness_mm=310, party_wall_construction_type="Cavity Masonry, Filled", filled_cavity_indicators=None, ) ], ) class TestBuildingMeasurements: @pytest.fixture def measurements(self) -> BuildingMeasurements: return PasHubRdSapSiteNotesExtractor( load_text_fixture() ).extract_building_measurements() def test_main_building_has_two_floors( self, measurements: BuildingMeasurements ) -> None: assert len(measurements.main_building.floors) == 2 def test_main_building_floor_area( self, measurements: BuildingMeasurements ) -> None: assert measurements.main_building.floors[0].area_m2 == 35.68 def test_integer_token_parses_to_float( self, measurements: BuildingMeasurements ) -> None: # "11" in the PDF (no decimal) should parse to 11.0 assert measurements.main_building.floors[1].heat_loss_perimeter_m == 11.0 def test_extension_measurements_present( self, measurements: BuildingMeasurements ) -> None: assert measurements.extensions is not None assert len(measurements.extensions) == 1 def test_extension_id(self, measurements: BuildingMeasurements) -> None: assert measurements.extensions is not None assert measurements.extensions[0].id == 1 def test_full_building_measurements( self, measurements: BuildingMeasurements ) -> None: assert measurements == BuildingMeasurements( main_building=MainBuildingMeasurements( floors=[ FloorMeasurement( name="Floor 1", area_m2=35.68, height_m=2.19, heat_loss_perimeter_m=13.44, pwl_m=10.62, ), FloorMeasurement( name="Floor 0", area_m2=35.68, height_m=2.17, heat_loss_perimeter_m=11.0, pwl_m=10.62, ), ] ), extensions=[ ExtensionMeasurements( id=1, floors=[ FloorMeasurement( name="Floor 0", area_m2=3.8, height_m=2.0, heat_loss_perimeter_m=5.7, pwl_m=0.0, ) ], ) ], ) class TestRoofSpace: @pytest.fixture def roof_space(self) -> RoofSpace: return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_roof_space() def test_main_building_insulation_thickness_mm( self, roof_space: RoofSpace ) -> None: assert roof_space.main_building.insulation_thickness_mm == 100 def test_main_building_insulation_thickness_string_absent( self, roof_space: RoofSpace ) -> None: assert roof_space.main_building.insulation_thickness is None def test_main_building_rooms_in_roof(self, roof_space: RoofSpace) -> None: assert roof_space.main_building.rooms_in_roof is False def test_main_building_roof_u_value_known(self, roof_space: RoofSpace) -> None: assert roof_space.main_building.roof_u_value_known is False def test_extension_uses_string_thickness(self, roof_space: RoofSpace) -> None: assert roof_space.extensions is not None assert roof_space.extensions[0].insulation_thickness == "As built" assert roof_space.extensions[0].insulation_thickness_mm is None def test_full_roof_space(self, roof_space: RoofSpace) -> None: assert roof_space == RoofSpace( main_building=RoofSpaceDetail( construction_type="Pitched roof (Slates or tiles), Access to loft", insulation_at="Joists", roof_u_value_known=False, cavity_wall_construction_indicators="cavity visible in roof space", rooms_in_roof=False, insulation_thickness_mm=100, insulation_thickness=None, ), extensions=[ ExtensionRoofSpace( id=1, construction_type="Pitched roof, Sloping ceiling", insulation_at="Sloping ceiling insulation", roof_u_value_known=False, cavity_wall_construction_indicators="No indicator of construction visible", rooms_in_roof=False, insulation_thickness_mm=None, insulation_thickness="As built", ) ], ) class TestWindows: @pytest.fixture def windows(self) -> list: return PasHubRdSapSiteNotesExtractor(load_text_fixture()).extract_windows() def test_window_count(self, windows: list) -> None: assert len(windows) == 8 def test_ids_are_sequential(self, windows: list) -> None: assert [w.id for w in windows] == list(range(1, 9)) def test_first_window_location(self, windows: list) -> None: assert windows[0].location == "Main Building" def test_extension_window_location(self, windows: list) -> None: assert windows[3].location == "Extension 1" def test_height_parses_to_float(self, windows: list) -> None: assert windows[0].height_m == 1.2 def test_draught_proofed_true(self, windows: list) -> None: assert windows[0].draught_proofed is True def test_permanent_shutters_false(self, windows: list) -> None: assert windows[0].permanent_shutters is False def test_first_window_full(self, windows: list) -> None: from datatypes.epc.surveys.pashub_rdsap_site_notes import Window assert windows[0] == Window( 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.2, width_m=2.3, orientation="North West", ) class TestHeatingAndHotWater: @pytest.fixture def hhw(self) -> HeatingAndHotWater: return PasHubRdSapSiteNotesExtractor( load_text_fixture() ).extract_heating_and_hot_water() def test_product_id_parses_to_int(self, hhw: HeatingAndHotWater) -> None: assert hhw.main_heating.product_id == 16839 def test_summer_efficiency_parses_to_float(self, hhw: HeatingAndHotWater) -> None: assert hhw.main_heating.summer_efficiency == 0.0 def test_condensing_true(self, hhw: HeatingAndHotWater) -> None: assert hhw.main_heating.condensing is True def test_fghrs_false(self, hhw: HeatingAndHotWater) -> None: # multi-line label assert hhw.main_heating.flue_gas_heat_recovery_system is False def test_secondary_fuel(self, hhw: HeatingAndHotWater) -> None: assert hhw.secondary_heating.secondary_fuel == "No Secondary Heating" def test_water_heating_no_cylinder(self, hhw: HeatingAndHotWater) -> None: assert hhw.water_heating.cylinder_size == "No Cylinder" assert hhw.water_heating.insulation_type is None assert hhw.water_heating.has_thermostat is None def test_full_heating_and_hot_water(self, hhw: HeatingAndHotWater) -> None: assert hhw == HeatingAndHotWater( main_heating=MainHeating( selection_method="PCDF Search", system_type="Boiler with radiators or underfloor heating", product_id=16839, manufacturer="Vaillant", model="ecoTEC pro 28", orig_manufacturer="Vaillant", fuel="Mains gas", summer_efficiency=0.0, type="Combi", condensing=True, year="2005 - 2015", 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=SecondaryHeating( secondary_fuel="No Secondary Heating", ), water_heating=WaterHeating( type="Regular", system="From main heating 1", cylinder_size="No Cylinder", cylinder_measured_heat_loss=None, insulation_type=None, insulation_thickness_mm=None, has_thermostat=None, ), ) class TestVentilation: @pytest.fixture def ventilation(self) -> Ventilation: return PasHubRdSapSiteNotesExtractor( load_text_fixture() ).extract_ventilation() def test_ventilation_type(self, ventilation: Ventilation) -> None: assert ventilation.ventilation_type == "Mechanical Extract - Decentralised" def test_number_of_open_flues(self, ventilation: Ventilation) -> None: assert ventilation.number_of_open_flues == 0 def test_ventilation_in_pcdf_database(self, ventilation: Ventilation) -> None: assert ventilation.ventilation_in_pcdf_database is False def test_full_ventilation(self, ventilation: Ventilation) -> None: assert ventilation == Ventilation( ventilation_type="Mechanical Extract - Decentralised", 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=0, number_of_passive_vents=0, number_of_flueless_gas_fires=0, pressure_test="No test", draught_lobby=False, ventilation_in_pcdf_database=False, )