Model/backend/documents_parser/tests/test_end_to_end.py
Khalim Conn-Kowlessar 883028c89e P6.1 follow-on: unbox BuildingPartIdentifier at backend boundaries
Threads the strict BuildingPartIdentifier type (introduced in a8b443f6)
through the two remaining backend touchpoints:

- EpcBuildingPartModel.from_*: SQLModel column expects a string, so
  unbox the enum with .identifier.value before binding to the DB.
- documents_parser end-to-end tests: swap bare-string equality
  ("main" / "extension_1") for identity checks against the enum
  members (BuildingPartIdentifier.MAIN / EXTENSION_1).

Documents_parser test pack passes (105/105). No dedicated SQLModel test
covers EpcBuildingPartModel.from_*; the .value line is exercised
transitively via db_writer.py / local_runner.py in production.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:58:23 +00:00

423 lines
16 KiB
Python

import os
from datetime import date
import pytest
from backend.documents_parser.extractor import PasHubRdSapSiteNotesExtractor
from backend.documents_parser.pdf import pdf_to_text_list
from datatypes.epc.domain.epc_property_data import (
BuildingPartIdentifier,
EpcPropertyData,
InstantaneousWwhrs,
MainHeatingDetail,
SapBuildingPart,
SapEnergySource,
SapFloorDimension,
SapHeating,
SapVentilation,
SapWindow,
ShowerOutlet,
ShowerOutlets,
)
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
PDF_PATH = os.path.join(os.path.dirname(__file__), "fixtures", "PasHubSiteNotes_1.pdf")
PDF_PATH_2 = os.path.join(
os.path.dirname(__file__), "fixtures", "PasHubSiteNotes_2.pdf"
)
class TestPdfToEpcPropertyData:
@pytest.fixture
def result(self) -> EpcPropertyData:
with open(PDF_PATH, "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_full_epc_property_data(self, result: EpcPropertyData) -> None:
assert result == EpcPropertyData(
dwelling_type="Mid-terrace house",
inspection_date=date(2025, 9, 25),
tenure="Rented Social",
transaction_type="Grant-Scheme (ECO, RHI, etc.)",
roofs=[],
walls=[],
floors=[],
main_heating=[],
door_count=2,
sap_heating=SapHeating(
instantaneous_wwhrs=InstantaneousWwhrs(),
main_heating_details=[
MainHeatingDetail(
has_fghrs=False,
main_fuel_type="Mains gas",
heat_emitter_type="Radiators",
emitter_temperature="Unknown",
main_heating_control="Programmer, room thermostat and TRVs",
fan_flue_present=True,
condensing=True,
weather_compensator=False,
central_heating_pump_age_str="Unknown",
)
],
has_fixed_air_conditioning=False,
shower_outlets=ShowerOutlets(
shower_outlet=ShowerOutlet(
shower_outlet_type="Non-Electric Shower"
),
),
),
sap_windows=[
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North West",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=2.3,
window_height=1.2,
draught_proofed=True,
window_location="Main Building",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North West",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=1.0,
window_height=1.2,
draught_proofed=True,
window_location="Main Building",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North East",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=1.0,
window_height=0.9,
draught_proofed=True,
window_location="Main Building",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=1.0,
window_height=0.9,
draught_proofed=True,
window_location="Extension 1",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North East",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=1.7,
window_height=0.9,
draught_proofed=True,
window_location="Extension 1",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North West",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=2.3,
window_height=0.9,
draught_proofed=True,
window_location="Extension 1",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North West",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=1.2,
window_height=1.0,
draught_proofed=True,
window_location="Extension 1",
window_wall_type="External wall",
permanent_shutters_present=False,
),
SapWindow(
frame_material="Wooden or PVC",
glazing_gap="16 mm or more",
orientation="North East",
window_type="Window",
glazing_type="Double glazing, Unknown install date",
window_width=1.0,
window_height=0.9,
draught_proofed=True,
window_location="Extension 1",
window_wall_type="External wall",
permanent_shutters_present=False,
),
],
sap_energy_source=SapEnergySource(
mains_gas=True,
meter_type="Single",
pv_battery_count=0,
wind_turbines_count=0,
gas_smart_meter_present=True,
is_dwelling_export_capable=True,
wind_turbines_terrain_type="Suburban",
electricity_smart_meter_present=True,
),
sap_building_parts=[
SapBuildingPart(
identifier=BuildingPartIdentifier.MAIN,
construction_age_band="1950-1966",
wall_construction="Cavity",
wall_insulation_type="Filled Cavity",
wall_thickness_measured=True,
party_wall_construction="Cavity Masonry, Filled",
sap_floor_dimensions=[
SapFloorDimension(
room_height_m=2.19,
total_floor_area_m2=35.68,
party_wall_length_m=10.62,
heat_loss_perimeter_m=13.44,
floor=1,
),
SapFloorDimension(
room_height_m=2.17,
total_floor_area_m2=35.68,
party_wall_length_m=10.62,
heat_loss_perimeter_m=11.0,
floor=0,
),
],
wall_thickness_mm=310,
roof_insulation_location="Joists",
roof_insulation_thickness=100,
floor_type="Ground Floor",
floor_construction_type="Solid",
floor_insulation_type_str="As Built",
floor_u_value_known=False,
),
SapBuildingPart(
identifier=BuildingPartIdentifier.EXTENSION_1,
construction_age_band="2003-2006",
wall_construction="Cavity",
wall_insulation_type="As built",
wall_thickness_measured=True,
party_wall_construction="Cavity Masonry, Filled",
sap_floor_dimensions=[
SapFloorDimension(
room_height_m=2.0,
total_floor_area_m2=3.8,
party_wall_length_m=0.0,
heat_loss_perimeter_m=5.7,
floor=0,
),
],
wall_thickness_mm=310,
roof_insulation_location="Sloping ceiling insulation",
roof_insulation_thickness="As built",
),
],
solar_water_heating=False,
has_hot_water_cylinder=False,
has_fixed_air_conditioning=False,
wet_rooms_count=0,
extensions_count=1,
heated_rooms_count=0,
open_chimneys_count=0,
habitable_rooms_count=3,
insulated_door_count=0,
cfl_fixed_lighting_bulbs_count=1,
led_fixed_lighting_bulbs_count=0,
incandescent_fixed_lighting_bulbs_count=4,
total_floor_area_m2=75.16,
built_form="Mid-terrace",
property_type="House",
has_conservatory=False,
blocked_chimneys_count=0,
draughtproofed_door_count=2,
address_line_1="40, Abbey Place",
post_town="Crewe",
postcode="CW1 4JR",
report_reference="6EA2A86D-94CE-4792-8D49-AB495C744EDD",
number_of_storeys=2,
any_unheated_rooms=False,
waste_water_heat_recovery="None",
hydro=False,
photovoltaic_array=False,
sap_ventilation=SapVentilation(
ventilation_type="Mechanical Extract - Decentralised",
draught_lobby=False,
pressure_test="No test",
open_flues_count=0,
closed_flues_count=0,
boiler_flues_count=0,
other_flues_count=0,
extract_fans_count=0,
passive_vents_count=0,
flueless_gas_fires_count=0,
ventilation_in_pcdf_database=False,
),
)
class TestPdfToEpcPropertyDataFixture2:
@pytest.fixture
def result(self) -> EpcPropertyData:
with open(PDF_PATH_2, "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_thickness(self, result: EpcPropertyData) -> None:
assert result.sap_heating.cylinder_insulation_thickness_mm == 38
def test_cylinder_size(self, result: EpcPropertyData) -> None:
assert result.sap_heating.cylinder_size == "Normal (90-130 litres)"
def test_secondary_heating_type(self, result: EpcPropertyData) -> None:
assert result.sap_heating.secondary_heating_type == "Open fire in grate"
PDF_PATH_3 = os.path.join(
os.path.dirname(__file__), "fixtures", "PasHubSiteNotes_3.pdf"
)
class TestPdfToEpcPropertyDataFixture3:
@pytest.fixture
def result(self) -> EpcPropertyData:
with open(PDF_PATH_3, "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_immersion_heating_type(self, result: EpcPropertyData) -> None:
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"
)
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
)
def test_electric_storage_heater_fuel_type(self, result: EpcPropertyData) -> None:
assert (
result.sap_heating.main_heating_details[0].main_fuel_type == "Electricity"
)
PDF_PATH_4 = os.path.join(
os.path.dirname(__file__), "fixtures", "PasHubSiteNotes_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
PDF_PATH_5 = os.path.join(
os.path.dirname(__file__), "fixtures", "PasHubSiteNotes_5.pdf"
)
class TestPdfToEpcPropertyDataFixture5:
@pytest.fixture
def result(self) -> EpcPropertyData:
with open(PDF_PATH_5, "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_cfl_bulb_count(self, result: EpcPropertyData) -> None:
assert result.cfl_fixed_lighting_bulbs_count == 2
def test_secondary_heating_type(self, result: EpcPropertyData) -> None:
assert (
result.sap_heating.secondary_heating_type
== "Panel, convector or radiant heaters"
)
def test_electric_shower_outlet_type(self, result: EpcPropertyData) -> None:
assert result.sap_heating.shower_outlets is not None
assert (
result.sap_heating.shower_outlets.shower_outlet.shower_outlet_type
== "Electric Shower"
)
PDF_PATH_6 = os.path.join(
os.path.dirname(__file__), "fixtures", "PasHubSiteNotes_6.pdf"
)
class TestPdfToEpcPropertyDataFixture6:
@pytest.fixture
def result(self) -> EpcPropertyData:
with open(PDF_PATH_6, "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_party_wall_construction(self, result: EpcPropertyData) -> None:
assert (
result.sap_building_parts[0].party_wall_construction
== "Solid Masonry, Timber Frame, or System Built"
)