diff --git a/backend/documents_parser/tests/fixtures/Summary_000890.pdf b/backend/documents_parser/tests/fixtures/Summary_000890.pdf new file mode 100644 index 00000000..9d5afcea Binary files /dev/null and b/backend/documents_parser/tests/fixtures/Summary_000890.pdf differ diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index 60f28581..164063c6 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -73,6 +73,7 @@ _SUMMARY_000902_PDF = _FIXTURES / "Summary_000902.pdf" # cert 9418 _SUMMARY_000889_PDF = _FIXTURES / "Summary_000889.pdf" # cert 2536 (Normal cylinder) _SUMMARY_000884_PDF = _FIXTURES / "Summary_000884.pdf" # cert 9421 (Normal cylinder) _SUMMARY_000910_PDF = _FIXTURES / "Summary_000910.pdf" # cert 0036 (Flat, party wall U=0) +_SUMMARY_000890_PDF = _FIXTURES / "Summary_000890.pdf" # cert 7800 (two electric showers) # GOV.UK EPB API JSON for cert 001479 — the API-path counterpart of the # Summary_001479.pdf fixture. Together they drive the API ≡ Summary @@ -945,6 +946,28 @@ def test_summary_mapper_raises_on_unmapped_glazing_type_label() -> None: assert excinfo.value.value == "Quintuple glazed with helium" +def test_summary_7800_two_electric_showers_count_as_two_not_one() -> None: + # Arrange — cert 7800-1501-0922-7127-3563's Summary §16 lodges TWO + # instantaneous electric showers ("Shower 01" + "Shower 11", both + # `outlet_type='Electric shower'`). Pre-Slice S0380.19 the mapper + # hardcoded `electric_shower_count = 1 if has_electric_shower else + # None`, losing the multiplicity. Cascade-equivalent on this cert: + # Appendix J eq J16 (N_ES,per_outlet = N_shower / N_outlets) and + # eq J18 (Σ_j E_ES,j) yield the same (64a) value for 1 vs 2 outlets + # when there are no mixer outlets, so the SAP delta is unchanged + # — but the lodged multiplicity is now surfaced for any future + # cascade consumer that needs it. + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000890_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert — both lodged electric showers surface on the EPC. + assert epc.sap_heating.electric_shower_count == 2 + assert epc.sap_heating.mixer_shower_count == 0 + + def test_summary_0036_flat_unknown_party_wall_routes_to_u_zero() -> None: # Arrange — cert 0036-6325-1100-0063-1226 is a "Flat, Mid-Terrace" # whose Summary lodges party_wall_type='U Unable to determine'. diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index edb814e0..68818ee6 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3564,12 +3564,19 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating: # Shower-outlet classification: SAP10.2 Appendix J routes electric # showers via §J line 64a (their own kWh stream) and treats mixer # showers as drawing from the HW system. The Summary PDF lodges - # outlet_type as 'Electric shower' or 'Non-electric shower' — set - # the explicit counts so the cascade doesn't default mixer=1 on - # electric-only dwellings (000487). - has_electric_shower = any( - s.outlet_type == "Electric shower" - for s in survey.baths_and_showers.showers + # outlet_type as 'Electric shower' or 'Non-electric shower' — count + # each outlet by type so the cascade sees the actual lodged number + # (cert 7800-1501-0922-7127-3563 has 2 electric showers; the + # previous hardcoded `1 if has_electric_shower else None` lost the + # multiplicity, and `None` left the cascade defaulting to mixer=1 + # on electric-only dwellings). + electric_shower_count_from_survey = sum( + 1 for s in survey.baths_and_showers.showers + if s.outlet_type == "Electric shower" + ) + mixer_shower_count_from_survey = sum( + 1 for s in survey.baths_and_showers.showers + if s.outlet_type != "Electric shower" ) # Water heating fuel: Summary §15 "Water Heating Fuel Type" lodges # the fuel name as a string ("Mains gas", "Electricity", ...). Map @@ -3635,24 +3642,12 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating: mh.secondary_heating_sap_code, ), number_baths=survey.baths_and_showers.number_of_baths, - # Zero-shower lodgings resolve to explicit 0 (not None) so the - # cascade doesn't default-assume a mixer — same disposition - # the API path received in slice 102f-prep.8 ("API mapper - # resolves shower_outlets=None → 0 mixers") on cohort cert - # 2225. Non-zero shower lodgings keep the hand-built-fixture - # convention (None for non-electric → cascade derives count - # from `shower_outlets` instead) so the boiler-cohort parity - # tests in this file stay GREEN. - electric_shower_count=( - 0 - if not survey.baths_and_showers.showers - else (1 if has_electric_shower else None) - ), - mixer_shower_count=( - 0 - if not survey.baths_and_showers.showers - else (0 if has_electric_shower else None) - ), + # Both counts derived by tallying the lodged shower list. Zero- + # shower lodgings resolve to (0, 0) — the API path's slice 102f- + # prep.8 disposition for cert 2225. Multi-shower lodgings surface + # the lodged multiplicity (cert 7800: 2 electric showers). + electric_shower_count=electric_shower_count_from_survey, + mixer_shower_count=mixer_shower_count_from_survey, ) diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py index dca08d79..c53c9625 100644 --- a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py +++ b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py @@ -221,6 +221,12 @@ def build_epc() -> EpcPropertyData: epc.sap_heating.shower_outlets = ShowerOutlets( shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"), ) + # Slice S0380.19: Elmhurst mapper now counts shower outlets by + # type (electric vs mixer) instead of the previous hardcoded + # 0/1/None sentinels. Cohort cert 000474 lodges 1 non-electric + # (mixer) outlet → electric=0, mixer=1. + epc.sap_heating.electric_shower_count = 0 + epc.sap_heating.mixer_shower_count = 1 # Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst # mapper as the str dual-encoding that internal_gains.py reads. # `make_main_heating_detail` doesn't expose the str kwarg, so set diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000477.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000477.py index b366b0d3..95a5d25f 100644 --- a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000477.py +++ b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000477.py @@ -188,6 +188,9 @@ def build_epc() -> EpcPropertyData: epc.sap_heating.shower_outlets = ShowerOutlets( shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"), ) + # Slice S0380.19: counted shower outlets (was: None/None sentinels). + epc.sap_heating.electric_shower_count = 0 + epc.sap_heating.mixer_shower_count = 1 # Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst # mapper as the str dual-encoding that internal_gains.py reads. # `make_main_heating_detail` doesn't expose the str kwarg, so set diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000480.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000480.py index 59ebb8f9..bf0462df 100644 --- a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000480.py +++ b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000480.py @@ -238,6 +238,9 @@ def build_epc() -> EpcPropertyData: epc.sap_heating.shower_outlets = ShowerOutlets( shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"), ) + # Slice S0380.19: counted shower outlets (was: None/None sentinels). + epc.sap_heating.electric_shower_count = 0 + epc.sap_heating.mixer_shower_count = 1 # Summary §14 "Heat pump age: Unknown" — surfaced by the Elmhurst # mapper as the str dual-encoding that internal_gains.py reads. epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown" diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000490.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000490.py index 50a37631..80980f7c 100644 --- a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000490.py +++ b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000490.py @@ -190,6 +190,9 @@ def build_epc() -> EpcPropertyData: epc.sap_heating.shower_outlets = ShowerOutlets( shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"), ) + # Slice S0380.19: counted shower outlets (was: None/None sentinels). + epc.sap_heating.electric_shower_count = 0 + epc.sap_heating.mixer_shower_count = 1 epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown" return epc diff --git a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000516.py b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000516.py index 15533dbf..732ddbde 100644 --- a/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000516.py +++ b/domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000516.py @@ -214,6 +214,9 @@ def build_epc() -> EpcPropertyData: epc.sap_heating.shower_outlets = ShowerOutlets( shower_outlet=ShowerOutlet(shower_outlet_type="Non-electric shower"), ) + # Slice S0380.19: counted shower outlets (was: None/None sentinels). + epc.sap_heating.electric_shower_count = 0 + epc.sap_heating.mixer_shower_count = 1 epc.sap_heating.main_heating_details[0].central_heating_pump_age_str = "Unknown" return epc