diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index 62d75c22d..c8836a92b 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -6523,6 +6523,79 @@ def test_elmhurst_no_insulation_cylinder_thickness_zero_takes_uninsulated_loss() assert abs(got_jan_kwh - expected_jan_kwh) < 1e-4 +def test_cylinder_with_unknown_insulation_defaults_per_table_29_age_band() -> None: + """RdSAP 10 Table 29 (PDF p.56) "Hot water cylinder insulation if not + accessible": when a cylinder is lodged but its insulation is unknown + (type absent AND no thickness — ADR-0028 records ~308/1000 17.0 certs + omitting `cylinder_insulation_type`), the age-band default applies: + bands A-F → 12 mm loose jacket, G/H → 25 mm factory, I-M → 38 mm + factory. This is the same Table 29 row the §10.7 no-water-heating + default already uses. Previously the cascade returned None (zero + storage loss), silently treating an unknown cylinder as a combi. + Distinct from the uninsulated case: a lodged 0 mm means NO insulation + (t = 0), an absent thickness means NOT ACCESSIBLE (Table 29). + """ + # Arrange — same dwelling, insulation fields both absent; the + # fixture's building part lodges construction_age_band "B" → the + # Table 29 A-F row (12 mm loose jacket). + from domain.sap10_calculator.worksheet.water_heating import ( + cylinder_storage_loss_factor_table_2, + cylinder_temperature_factor_table_2b, + cylinder_volume_factor_table_2a, + ) + + hp_main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=29, + heat_emitter_type=1, + emitter_temperature=1, + main_heating_control=2206, + main_heating_category=4, + sap_main_heating_code=None, + ) + epc = make_minimal_sap10_epc( + total_floor_area_m2=_TYPICAL_TFA_M2, + habitable_rooms_count=4, + country_code="ENG", + has_hot_water_cylinder=True, + sap_building_parts=[make_building_part()], # age band B + sap_heating=make_sap_heating( + main_heating_details=[hp_main], + water_heating_code=901, + cylinder_size=3, # Medium → 160 L + cylinder_insulation_type=None, # not lodged + cylinder_insulation_thickness_mm=None, # not lodged + cylinder_thermostat="Y", + ), + ) + expected_jan_kwh = ( + 160.0 + * cylinder_storage_loss_factor_table_2( + insulation_type="loose_jacket", thickness_mm=12.0 + ) + * cylinder_volume_factor_table_2a(160.0) + * cylinder_temperature_factor_table_2b( + has_cylinder_thermostat=True, separately_timed_dhw=True + ) + * 31 + ) + + # Act + wh_result, _ = _water_heating_worksheet_and_gains( + epc=epc, + water_efficiency_pct=1.7, + is_instantaneous=False, + primary_age="D", + pcdb_record=None, + ) + + # Assert — the Table 29 band-B default (12 mm loose jacket), not the + # zero-loss combi default and not the harsher uninsulated t=0. + assert wh_result is not None + got_jan_kwh = wh_result.solar_storage_monthly_kwh[0] + assert abs(got_jan_kwh - expected_jan_kwh) < 1e-4 + + def test_no_water_heating_default_age_a_to_f_uses_12mm_loose_jacket_per_table_29() -> None: """RdSAP 10 §10.7 + Table 29 (PDF p.55-56): when no water heating system is lodged, the default cylinder takes the age-band insulation,