diff --git a/backend/ecmk_fetcher/processor.py b/backend/ecmk_fetcher/processor.py index 2f122080..5852bb79 100644 --- a/backend/ecmk_fetcher/processor.py +++ b/backend/ecmk_fetcher/processor.py @@ -42,8 +42,8 @@ logger = setup_logger() def run_job() -> None: - username: str = "" # TODO: get from github secrets - password: str = "" + username: str = "joseph.westwood@domna.homes" # TODO: get from github secrets + password: str = "DomnaTest123!" property_list_file: str = ( "hubspot-crm-exports-southern-ra-lite-programme-3103-2026-03-31-2.xlsx" diff --git a/datatypes/epc/surveys/__init__.py b/datatypes/epc/surveys/__init__.py new file mode 100644 index 00000000..b71034ba --- /dev/null +++ b/datatypes/epc/surveys/__init__.py @@ -0,0 +1,3 @@ +from .pashub_rdsap_site_notes import PasHubRdSapSiteNotes + +__all__ = ["PasHubRdSapSiteNotes"] diff --git a/datatypes/epc/surveys/pashub_rdsap_site_notes.py b/datatypes/epc/surveys/pashub_rdsap_site_notes.py new file mode 100644 index 00000000..c5b3dbe4 --- /dev/null +++ b/datatypes/epc/surveys/pashub_rdsap_site_notes.py @@ -0,0 +1,291 @@ +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class InspectionMetadata: + inspection_surveyor: str + email_address: str + report_reference: str + created_on: str + date_of_inspection: str + property_address: str + property_photo: Optional[bool] = None + + +@dataclass +class General: + epc_checked_before_assessment: bool + epc_exists_at_point_of_assessment: bool + inspection_date: str + transaction_type: str + tenure: str + property_type: str + detachment_type: str + number_of_storeys: int + terrain_type: str + number_of_extensions: int + electricity_smart_meter: bool + electric_meter_type: str + dwelling_export_capable: bool + mains_gas_available: bool + gas_smart_meter: bool + gas_meter_accessible: bool + measurements_location: str + + +@dataclass +class MainBuildingConstruction: + age_range: str + age_indicators: str + walls_construction_type: str + cavity_construction_indicators: str + walls_insulation_type: str + thermal_conductivity_of_wall_insulation: str + wall_u_value_known: bool + wall_thickness_mm: int + party_wall_construction_type: str + filled_cavity_indicators: Optional[str] = None + + +@dataclass +class ExtensionConstruction: + id: int + age_range: str + age_indicators: str + walls_construction_type: str + cavity_construction_indicators: str + walls_insulation_type: str + thermal_conductivity_of_wall_insulation: str + wall_u_value_known: bool + wall_thickness_mm: int + party_wall_construction_type: str + filled_cavity_indicators: Optional[str] = None + + +@dataclass +class FloorConstruction: + floor_type: str + floor_construction: str + floor_insulation_type: str + floor_u_value_known: bool + + +@dataclass +class BuildingConstruction: + main_building: MainBuildingConstruction + floor: FloorConstruction + extensions: Optional[List[ExtensionConstruction]] = None + + +@dataclass +class FloorMeasurement: + name: str + area_m2: float + height_m: float + heat_loss_perimeter_m: float + pwl_m: float + + +@dataclass +class MainBuildingMeasurements: + floors: List[FloorMeasurement] + + +@dataclass +class ExtensionMeasurements: + id: int + floors: List[FloorMeasurement] + + +@dataclass +class BuildingMeasurements: + main_building: MainBuildingMeasurements + extensions: Optional[List[ExtensionMeasurements]] = None + + +@dataclass +class RoofSpaceDetail: + construction_type: str + insulation_at: str + roof_u_value_known: bool + cavity_wall_construction_indicators: str + rooms_in_roof: bool + # Numeric thickness (mm) when known; string (e.g. "As built") when not measured + insulation_thickness_mm: Optional[int] = None + insulation_thickness: Optional[str] = None + + +@dataclass +class ExtensionRoofSpace: + id: int + construction_type: str + insulation_at: str + roof_u_value_known: bool + cavity_wall_construction_indicators: str + rooms_in_roof: bool + insulation_thickness_mm: Optional[int] = None + insulation_thickness: Optional[str] = None + + +@dataclass +class RoofSpace: + main_building: RoofSpaceDetail + extensions: Optional[List[ExtensionRoofSpace]] = None + + +@dataclass +class Window: + id: int + location: str + wall_type: str + glazing_type: str + window_type: str + frame_type: str + glazing_gap: str + draught_proofed: bool + permanent_shutters: bool + height_m: float + width_m: float + orientation: str + + +@dataclass +class MainHeating: + selection_method: str + system_type: str + product_id: int + manufacturer: str + model: str + orig_manufacturer: str + fuel: str + summer_efficiency: float + type: str + condensing: bool + year: str + mount: str + open_flue: str + fan_assist: bool + status: str + central_heating_pump_age: str + controls: str + flue_gas_heat_recovery_system: bool + weather_compensator: bool + emitter: str + emitter_temperature: str + + +@dataclass +class SecondaryHeating: + secondary_fuel: str + + +@dataclass +class WaterHeating: + type: str + system: str + cylinder_size: str + cylinder_measured_heat_loss: Optional[str] = None + insulation_type: Optional[str] = None + insulation_thickness_mm: Optional[int] = None + has_thermostat: Optional[bool] = None + + +@dataclass +class HeatingAndHotWater: + main_heating: MainHeating + secondary_heating: SecondaryHeating + water_heating: WaterHeating + + +@dataclass +class Ventilation: + ventilation_type: str + has_fixed_air_conditioning: bool + number_of_open_flues: int + number_of_closed_flues: int + number_of_boiler_flues: int + number_of_other_flues: int + number_of_extract_fans: int + number_of_passive_vents: int + number_of_flueless_gas_fires: int + pressure_test: str + draught_lobby: bool + ventilation_in_pcdf_database: Optional[bool] = None + + +@dataclass +class Conservatories: + has_conservatory: bool + + +@dataclass +class Renewables: + wind_turbines: bool + solar_hot_water: bool + photovoltaic_array: bool + number_of_pv_batteries: int + hydro: bool + + +@dataclass +class RoomCountElements: + number_of_habitable_rooms: int + any_unheated_rooms: bool + number_of_external_doors: int + number_of_insulated_external_doors: int + number_of_draughtproofed_external_doors: int + number_of_open_chimneys: int + number_of_blocked_chimneys: int + number_of_fixed_incandescent_bulbs: int + exact_led_cfl_count_known: bool + number_of_fixed_led_bulbs: int + number_of_fixed_cfl_bulbs: int + waste_water_heat_recovery: str + number_of_heated_rooms: Optional[int] = None + + +@dataclass +class Shower: + id: int + outlet_type: str + + +@dataclass +class WaterUse: + number_of_baths: int + number_of_special_features: int + showers: List[Shower] + + +@dataclass +class CustomerResponse: + customer_present: bool + willing_to_answer_satisfaction_survey: bool + + +@dataclass +class SurveyAddendum: + addendum: str + related_party_disclosure: str + hard_to_treat_cavity_access_issues: bool + hard_to_treat_cavity_high_exposure: bool + hard_to_treat_cavity_narrow_cavities: bool + + +@dataclass +class PasHubRdSapSiteNotes: + inspection_metadata: InspectionMetadata + general: General + building_construction: BuildingConstruction + building_measurements: BuildingMeasurements + roof_space: RoofSpace + windows: List[Window] + heating_and_hot_water: HeatingAndHotWater + ventilation: Ventilation + conservatories: Conservatories + renewables: Renewables + room_count_elements: RoomCountElements + water_use: WaterUse + customer_response: CustomerResponse + addendum: SurveyAddendum diff --git a/datatypes/epc/surveys/tests/__init__.py b/datatypes/epc/surveys/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example1.json b/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example1.json new file mode 100644 index 00000000..b5772e24 --- /dev/null +++ b/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example1.json @@ -0,0 +1,232 @@ +{ + "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": "test" + }, + "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": 280, + "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" + }, + { + "id": 2, + "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.33, + "width_m": 0.96, + "orientation": "South East" + }, + { + "id": 3, + "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.04, + "width_m": 0.96, + "orientation": "North West" + }, + { + "id": 4, + "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.02, + "width_m": 0.97, + "orientation": "North West" + } + ], + "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": "PV Recommended", + "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 + } +} diff --git a/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example2.json b/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example2.json new file mode 100644 index 00000000..1d9c38f5 --- /dev/null +++ b/datatypes/epc/surveys/tests/fixtures/pashub_rdsap_site_notes_example2.json @@ -0,0 +1,330 @@ +{ + "inspection_metadata": { + "inspection_surveyor": "test", + "email_address": "test@test.com", + "report_reference": "6EA2A86D-94CE-4792-8D49-AB495C744EDD", + "created_on": "2025-11-10", + "date_of_inspection": "2025-09-25", + "property_address": "test", + "property_photo": true + }, + "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" + }, + "building_construction": { + "main_building": { + "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" + }, + "extensions": [ + { + "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" + } + ], + "floor": { + "floor_type": "Ground Floor", + "floor_construction": "Solid", + "floor_insulation_type": "As Built", + "floor_u_value_known": false + } + }, + "building_measurements": { + "main_building": { + "floors": [ + { + "name": "Floor 1", + "area_m2": 35.68, + "height_m": 2.19, + "heat_loss_perimeter_m": 13.44, + "pwl_m": 10.62 + }, + { + "name": "Floor 0", + "area_m2": 35.68, + "height_m": 2.17, + "heat_loss_perimeter_m": 11.0, + "pwl_m": 10.62 + } + ] + }, + "extensions": [ + { + "id": 1, + "floors": [ + { + "name": "Floor 0", + "area_m2": 3.8, + "height_m": 2.0, + "heat_loss_perimeter_m": 5.7, + "pwl_m": 0.0 + } + ] + } + ] + }, + "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": "cavity visible in roof space", + "rooms_in_roof": false + }, + "extensions": [ + { + "id": 1, + "construction_type": "Pitched roof, Sloping ceiling", + "insulation_at": "Sloping ceiling insulation", + "roof_u_value_known": false, + "insulation_thickness": "As built", + "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.2, + "width_m": 2.3, + "orientation": "North West" + }, + { + "id": 2, + "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": 1.0, + "orientation": "North West" + }, + { + "id": 3, + "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": 0.9, + "width_m": 1.0, + "orientation": "North East" + }, + { + "id": 4, + "location": "Extension 1", + "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": 0.9, + "width_m": 1.0, + "orientation": "North" + }, + { + "id": 5, + "location": "Extension 1", + "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": 0.9, + "width_m": 1.7, + "orientation": "North East" + }, + { + "id": 6, + "location": "Extension 1", + "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": 0.9, + "width_m": 2.3, + "orientation": "North West" + }, + { + "id": 7, + "location": "Extension 1", + "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.0, + "width_m": 1.2, + "orientation": "North West" + }, + { + "id": 8, + "location": "Extension 1", + "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": 0.9, + "width_m": 1.0, + "orientation": "North East" + } + ], + "heating_and_hot_water": { + "main_heating": { + "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, + "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": { + "secondary_fuel": "No Secondary Heating" + }, + "water_heating": { + "type": "Regular", + "system": "From main heating 1", + "cylinder_size": "No Cylinder", + "cylinder_measured_heat_loss": null, + "insulation_type": null, + "insulation_thickness_mm": null, + "has_thermostat": null + } + }, + "ventilation": { + "ventilation_type": "Mechanical Extract - Decentralised", + "ventilation_in_pcdf_database": false, + "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 + }, + "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": 3, + "any_unheated_rooms": false, + "number_of_heated_rooms": null, + "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": 4, + "exact_led_cfl_count_known": true, + "number_of_fixed_led_bulbs": 0, + "number_of_fixed_cfl_bulbs": 1, + "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 + } +} diff --git a/datatypes/epc/surveys/tests/test_pashub_rdsap_site_notes_loading.py b/datatypes/epc/surveys/tests/test_pashub_rdsap_site_notes_loading.py new file mode 100644 index 00000000..3c6e6622 --- /dev/null +++ b/datatypes/epc/surveys/tests/test_pashub_rdsap_site_notes_loading.py @@ -0,0 +1,362 @@ +import json +import os +from typing import Any, Dict + +import pytest + +from datatypes.epc.schema.tests.helpers import from_dict +from datatypes.epc.surveys.pashub_rdsap_site_notes import ( + ExtensionConstruction, + ExtensionMeasurements, + ExtensionRoofSpace, + PasHubRdSapSiteNotes, +) + +FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures") + + +def load(filename: str) -> Dict[str, Any]: + with open(os.path.join(FIXTURES, filename)) as f: + return json.load(f) # type: ignore[no-any-return] + + +class TestExample1: + """No extensions; regular boiler with hot water cylinder; natural ventilation.""" + + @pytest.fixture + def survey(self) -> PasHubRdSapSiteNotes: + return from_dict(PasHubRdSapSiteNotes, load("example1.json")) + + # --- inspection_metadata --- + + def test_report_reference(self, survey: PasHubRdSapSiteNotes) -> None: + assert ( + survey.inspection_metadata.report_reference + == "49D422A9-0779-44DD-9665-464D35DFF1A8" + ) + + def test_created_on(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.inspection_metadata.created_on == "2026-03-31" + + def test_property_photo_absent(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.inspection_metadata.property_photo is None + + # --- general --- + + def test_transaction_type(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.general.transaction_type == "None of the Above" + + def test_number_of_extensions(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.general.number_of_extensions == 0 + + def test_smart_meters(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.general.electricity_smart_meter is True + assert survey.general.gas_smart_meter is True + + # --- building_construction --- + + def test_main_building_wall_thickness(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_construction.main_building.wall_thickness_mm == 280 + + def test_main_building_walls_insulation_type( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert ( + survey.building_construction.main_building.walls_insulation_type + == "As built" + ) + + def test_filled_cavity_indicators_absent( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert ( + survey.building_construction.main_building.filled_cavity_indicators is None + ) + + def test_no_extensions_in_construction(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_construction.extensions is None + + def test_floor_construction(self, survey: PasHubRdSapSiteNotes) -> None: + assert ( + survey.building_construction.floor.floor_construction + == "Suspended, not timber" + ) + + # --- building_measurements --- + + def test_main_building_floor_count(self, survey: PasHubRdSapSiteNotes) -> None: + assert len(survey.building_measurements.main_building.floors) == 2 + + def test_floor_area(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_measurements.main_building.floors[0].area_m2 == 24.78 + + def test_no_extension_measurements(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_measurements.extensions is None + + # --- roof_space --- + + def test_roof_insulation_thickness_mm(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.roof_space.main_building.insulation_thickness_mm == 100 + + def test_roof_insulation_thickness_string_absent( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert survey.roof_space.main_building.insulation_thickness is None + + def test_no_extension_roof_spaces(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.roof_space.extensions is None + + def test_rooms_in_roof_false(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.roof_space.main_building.rooms_in_roof is False + + # --- windows --- + + def test_window_count(self, survey: PasHubRdSapSiteNotes) -> None: + assert len(survey.windows) == 4 + + def test_window_dimensions(self, survey: PasHubRdSapSiteNotes) -> None: + w = survey.windows[0] + assert w.height_m == 1.36 + assert w.width_m == 1.0 + + def test_window_orientation(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.windows[0].orientation == "South East" + assert survey.windows[2].orientation == "North West" + + def test_window_glazing_gap(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.windows[0].glazing_gap == "16 mm or more" + + # --- heating_and_hot_water --- + + def test_main_heating_manufacturer(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.heating_and_hot_water.main_heating.manufacturer == "Vaillant" + + def test_main_heating_model(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.heating_and_hot_water.main_heating.model == "ecoFIT sustain 415" + + def test_main_heating_product_id(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.heating_and_hot_water.main_heating.product_id == 18400 + + def test_main_heating_type_regular(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.heating_and_hot_water.main_heating.type == "Regular" + + def test_water_heating_cylinder_present(self, survey: PasHubRdSapSiteNotes) -> None: + wh = survey.heating_and_hot_water.water_heating + assert wh.cylinder_size == "Normal (90-130 litres)" + assert wh.insulation_type == "Factory fitted" + assert wh.insulation_thickness_mm == 12 + assert wh.has_thermostat is True + + def test_secondary_heating_none(self, survey: PasHubRdSapSiteNotes) -> None: + assert ( + survey.heating_and_hot_water.secondary_heating.secondary_fuel + == "No Secondary Heating" + ) + + # --- ventilation --- + + def test_ventilation_type(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.ventilation.ventilation_type == "Natural" + + def test_ventilation_pcdf_absent(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.ventilation.ventilation_in_pcdf_database is None + + def test_extract_fans(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.ventilation.number_of_extract_fans == 2 + + # --- room_count_elements --- + + def test_habitable_rooms(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_habitable_rooms == 2 + + def test_heated_rooms_zero(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_heated_rooms == 0 + + def test_led_bulbs(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_fixed_led_bulbs == 5 + + def test_cfl_bulbs(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_fixed_cfl_bulbs == 4 + + # --- water_use --- + + def test_shower_outlet_type(self, survey: PasHubRdSapSiteNotes) -> None: + assert len(survey.water_use.showers) == 1 + assert survey.water_use.showers[0].outlet_type == "Non-Electric Shower" + + # --- addendum --- + + def test_addendum_value(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.addendum.addendum == "PV Recommended" + + def test_related_party_disclosure(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.addendum.related_party_disclosure == "No related party" + + def test_hard_to_treat_flags_false(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.addendum.hard_to_treat_cavity_access_issues is False + assert survey.addendum.hard_to_treat_cavity_high_exposure is False + assert survey.addendum.hard_to_treat_cavity_narrow_cavities is False + + +class TestExample2: + """With extensions; combi boiler (no cylinder); mechanical extract ventilation.""" + + @pytest.fixture + def survey(self) -> PasHubRdSapSiteNotes: + return from_dict(PasHubRdSapSiteNotes, load("example2.json")) + + # --- inspection_metadata --- + + def test_report_reference(self, survey: PasHubRdSapSiteNotes) -> None: + assert ( + survey.inspection_metadata.report_reference + == "6EA2A86D-94CE-4792-8D49-AB495C744EDD" + ) + + def test_property_photo_present(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.inspection_metadata.property_photo is True + + def test_created_on_differs_from_inspection_date( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert survey.inspection_metadata.created_on == "2025-11-10" + assert survey.inspection_metadata.date_of_inspection == "2025-09-25" + + # --- general --- + + def test_number_of_extensions(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.general.number_of_extensions == 1 + + def test_transaction_type(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.general.transaction_type == "Grant-Scheme (ECO, RHI, etc.)" + + # --- building_construction --- + + def test_main_building_filled_cavity_indicators( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert ( + survey.building_construction.main_building.filled_cavity_indicators + == "evidence of cavity fill drill holes" + ) + + def test_extension_construction_present(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_construction.extensions is not None + assert len(survey.building_construction.extensions) == 1 + + def test_extension_construction_type(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_construction.extensions is not None + ext: ExtensionConstruction = survey.building_construction.extensions[0] + assert ext.id == 1 + assert ext.walls_insulation_type == "As built" + assert ext.wall_thickness_mm == 310 + + def test_extension_no_filled_cavity_indicators( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert survey.building_construction.extensions is not None + assert ( + survey.building_construction.extensions[0].filled_cavity_indicators is None + ) + + # --- building_measurements --- + + def test_main_building_floor_count(self, survey: PasHubRdSapSiteNotes) -> None: + assert len(survey.building_measurements.main_building.floors) == 2 + + def test_extension_measurements_present(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_measurements.extensions is not None + assert len(survey.building_measurements.extensions) == 1 + + def test_extension_floor_area(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.building_measurements.extensions is not None + ext: ExtensionMeasurements = survey.building_measurements.extensions[0] + assert ext.id == 1 + assert len(ext.floors) == 1 + assert ext.floors[0].area_m2 == 3.8 + + # --- roof_space --- + + def test_main_roof_insulation_thickness_mm( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert survey.roof_space.main_building.insulation_thickness_mm == 100 + + def test_extension_roof_spaces_present(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.roof_space.extensions is not None + assert len(survey.roof_space.extensions) == 1 + + def test_extension_roof_uses_string_thickness( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert survey.roof_space.extensions is not None + ext: ExtensionRoofSpace = survey.roof_space.extensions[0] + assert ext.insulation_thickness == "As built" + assert ext.insulation_thickness_mm is None + + def test_extension_roof_construction_type( + self, survey: PasHubRdSapSiteNotes + ) -> None: + assert survey.roof_space.extensions is not None + assert ( + survey.roof_space.extensions[0].construction_type + == "Pitched roof, Sloping ceiling" + ) + + # --- windows --- + + def test_window_count(self, survey: PasHubRdSapSiteNotes) -> None: + assert len(survey.windows) == 8 + + def test_extension_windows(self, survey: PasHubRdSapSiteNotes) -> None: + extension_windows = [w for w in survey.windows if w.location == "Extension 1"] + assert len(extension_windows) == 5 + + def test_window_ids_sequential(self, survey: PasHubRdSapSiteNotes) -> None: + ids = [w.id for w in survey.windows] + assert ids == list(range(1, 9)) + + # --- heating_and_hot_water --- + + def test_main_heating_type_combi(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.heating_and_hot_water.main_heating.type == "Combi" + + def test_main_heating_model(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.heating_and_hot_water.main_heating.model == "ecoTEC pro 28" + + def test_water_heating_no_cylinder(self, survey: PasHubRdSapSiteNotes) -> None: + wh = survey.heating_and_hot_water.water_heating + assert wh.cylinder_size == "No Cylinder" + assert wh.cylinder_measured_heat_loss is None + assert wh.insulation_type is None + assert wh.insulation_thickness_mm is None + assert wh.has_thermostat is None + + # --- ventilation --- + + def test_ventilation_type(self, survey: PasHubRdSapSiteNotes) -> None: + assert ( + survey.ventilation.ventilation_type == "Mechanical Extract - Decentralised" + ) + + def test_ventilation_in_pcdf_database(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.ventilation.ventilation_in_pcdf_database is False + + def test_no_extract_fans_for_mev(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.ventilation.number_of_extract_fans == 0 + + # --- room_count_elements --- + + def test_habitable_rooms(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_habitable_rooms == 3 + + def test_heated_rooms_null(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_heated_rooms is None + + def test_incandescent_bulbs(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.room_count_elements.number_of_fixed_incandescent_bulbs == 4 + + # --- addendum --- + + def test_addendum_none(self, survey: PasHubRdSapSiteNotes) -> None: + assert survey.addendum.addendum == "None" diff --git a/pytest.ini b/pytest.ini index 4a5327c1..c22c8296 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,6 @@ pythonpath = . log_cli = true log_cli_level = INFO addopts = --cov-report term-missing --cov=etl/epc --cov=recommendations --cov=backend --cov=etl/epc_clean --cov=etl/spatial -testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests +testpaths = recommendations/tests backend/tests etl/epc/tests etl/epc_clean/tests etl/spatial/tests backend/condition/tests backend/address2UPRN/tests backend/onboarders/tests backend/categorisation/tests backend/export/tests etl/hubspot/tests backend/hubspot_trigger_orchestrator/tests datatypes/epc/schema/tests datatypes/epc/surveys/tests markers = integration: mark a test as an integration test