From 9a1422f6d75e701f9a33e1f728276aebf77ce383 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 14 Jul 2026 21:31:08 +0000 Subject: [PATCH] =?UTF-8?q?Map=20surveyed=20PasHub=20PV=20arrays=20into=20?= =?UTF-8?q?the=20Appendix=20M=20photovoltaic=5Farrays=20input=20?= =?UTF-8?q?=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../epc/domain/tests/test_from_site_notes.py | 48 +++++++++++++++++++ .../epc/surveys/pashub_rdsap_site_notes.py | 15 ++++++ 2 files changed, 63 insertions(+) diff --git a/datatypes/epc/domain/tests/test_from_site_notes.py b/datatypes/epc/domain/tests/test_from_site_notes.py index af90f142f..5d0c165c0 100644 --- a/datatypes/epc/domain/tests/test_from_site_notes.py +++ b/datatypes/epc/domain/tests/test_from_site_notes.py @@ -74,6 +74,54 @@ class TestRoofConstructionTypeCoding: assert extension_part.roof_construction_type == "Flat" +class TestPhotovoltaicArrayMapping: + """A surveyed per-system PV block ("PV 1: kWp value: 2.43 kWp", pitch 30°, + orientation "South East", overshading "None or very little") must reach + `sap_energy_source.photovoltaic_arrays` — the ONLY field the calculator's + Appendix M generation path reads. Dropping it silently zeroes the PV credit + (issue #1590 bug 1; fixture 507639151843 under-rates by 13.6 SAP). + """ + + def test_surveyed_pv_array_reaches_photovoltaic_arrays(self) -> None: + # Arrange — the survey's PV block: 2.43 kWp, 30°, SE, no overshading. + data = load("pashub_rdsap_site_notes_example1.json") + data["renewables"]["photovoltaic_array"] = True + data["renewables"]["pv_arrays"] = [ + { + "kwp": 2.43, + "pitch_degrees": 30, + "orientation": "South East", + "overshading": "None or very little", + } + ] + survey = from_dict(PasHubRdSapSiteNotes, data) + + # Act + result = EpcPropertyDataMapper.from_site_notes(survey) + + # Assert — SAP codes: pitch 30° → 2, "South East" → octant 4, + # "None or very little" → 1 (ZPV=1.0). + arrays = result.sap_energy_source.photovoltaic_arrays + assert arrays is not None + assert len(arrays) == 1 + assert arrays[0].peak_power == 2.43 + assert arrays[0].pitch == 2 + assert arrays[0].orientation == 4 + assert arrays[0].overshading == 1 + + def test_no_pv_block_keeps_arrays_absent(self) -> None: + # Arrange — the fixture as lodged (no PV). + survey = from_dict( + PasHubRdSapSiteNotes, load("pashub_rdsap_site_notes_example1.json") + ) + + # Act + result = EpcPropertyDataMapper.from_site_notes(survey) + + # Assert + assert result.sap_energy_source.photovoltaic_arrays is None + + class TestRoofInsulationNoneCoding: """A surveyed roof-space "Insulation At: None" is the assessor explicitly recording ZERO loft insulation — `from_site_notes` must lodge an explicit diff --git a/datatypes/epc/surveys/pashub_rdsap_site_notes.py b/datatypes/epc/surveys/pashub_rdsap_site_notes.py index 583eb228d..ba629f41e 100644 --- a/datatypes/epc/surveys/pashub_rdsap_site_notes.py +++ b/datatypes/epc/surveys/pashub_rdsap_site_notes.py @@ -222,6 +222,19 @@ class Conservatories: has_conservatory: bool +@dataclass +class PvArrayDetail: + """One surveyed PV array from the Renewables section's per-system block + ("PV {n}: kWp value / Photovoltaic Pitch / Orientation / Overshading"). + Raw survey labels for orientation/overshading — coding to SAP ints happens + at the mapper boundary (ADR-0015).""" + + kwp: Optional[float] = None + pitch_degrees: Optional[int] = None + orientation: Optional[str] = None + overshading: Optional[str] = None + + @dataclass class Renewables: wind_turbines: bool @@ -231,6 +244,8 @@ class Renewables: hydro: bool pv_connection: Optional[str] = None percent_roof_covered_pv: Optional[int] = None + pv_diverter_present: Optional[bool] = None + pv_arrays: Optional[List[PvArrayDetail]] = None @dataclass