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