diff --git a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py index f73bf104..6280ccc8 100644 --- a/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/cert_to_inputs.py @@ -698,11 +698,15 @@ def _pv_generation_kwh_per_yr(epc: EpcPropertyData) -> float: return total_kwp * _PV_ANNUAL_YIELD_KWH_PER_KWP -def _pv_export_credit_gbp_per_kwh(prices: PriceTable) -> float: - """PV cost credit per kWh generated. SAP 10.2 Table 12 code 60 (PV - export to grid) — 5.59 p/kWh on the spec table, 13.19 p/kWh under - cert calibration (legacy unit prices).""" - return prices.unit_price_p_per_kwh(_PV_EXPORT_TARIFF_CODE) * _PENCE_TO_GBP +def _pv_export_credit_gbp_per_kwh() -> float: + """PV cost credit per kWh generated. Per ADR-0010 §10 the rating + cascade uses RdSAP10 Table 32 prices; code 60 (PV export to grid) + = 13.19 p/kWh (same as standard electricity — PV gen displaces + grid imports at the standard rate). The legacy SAP 10.2 Table 12 + value (5.59 p/kWh) is no longer the target and is intentionally + not read here, so the CalculatorInputs boundary reports the same + rate _fuel_cost applies internally.""" + return table_32_unit_price_p_per_kwh(_PV_EXPORT_TARIFF_CODE) * _PENCE_TO_GBP def _other_fuel_cost_gbp_per_kwh( @@ -2178,7 +2182,7 @@ def cert_to_inputs( _STANDARD_ELECTRICITY_FUEL_CODE, ), pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc), - pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(prices), + pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(), secondary_heating_fraction=secondary_fraction_value, secondary_heating_efficiency=secondary_efficiency_value, energy_requirements=energy_requirements_result, diff --git a/packages/domain/src/domain/sap/rdsap/tests/fixtures/golden/2130-1033-4050-5007-8395.json b/packages/domain/src/domain/sap/rdsap/tests/fixtures/golden/2130-1033-4050-5007-8395.json new file mode 100644 index 00000000..cf294791 --- /dev/null +++ b/packages/domain/src/domain/sap/rdsap/tests/fixtures/golden/2130-1033-4050-5007-8395.json @@ -0,0 +1,453 @@ +{ + "uprn": 100030334057, + "roofs": [ + { + "description": "Pitched, 300 mm loft insulation", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + { + "description": "Pitched, 100 mm loft insulation", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "walls": [ + { + "description": "Solid brick, as built, no insulation (assumed)", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 1 + }, + { + "description": "Solid brick, with internal insulation", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "Suspended, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + { + "description": "Solid, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 3, + "window": { + "description": "Fully double glazed", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + "addendum": { + "addendum_numbers": [ + 8 + ] + }, + "lighting": { + "description": "Excellent lighting efficiency", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "DE22 3RW", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "DERBY", + "psv_count": 0, + "built_form": 3, + "created_at": "2025-07-24 16:36:27", + "door_count": 2, + "region_code": 6, + "report_type": 2, + "sap_heating": { + "number_baths": 1, + "cylinder_size": 1, + "shower_outlets": [ + { + "shower_outlet": { + "shower_wwhrs": 1, + "shower_outlet_type": 1 + } + } + ], + "number_baths_wwhrs": 0, + "water_heating_code": 901, + "water_heating_fuel": 26, + "cylinder_thermostat": "N", + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "Y", + "heat_emitter_type": 1, + "emitter_temperature": 1, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17505 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 10.2, + "sap_windows": [ + { + "pvc_frame": "true", + "glazing_gap": "16+", + "orientation": 8, + "window_type": 1, + "glazing_type": 3, + "window_width": { + "value": 0.95, + "quantity": "m" + }, + "window_height": { + "value": 1.75, + "quantity": "m" + }, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "glazing_gap": "16+", + "orientation": 8, + "window_type": 1, + "glazing_type": 3, + "window_width": { + "value": 0.95, + "quantity": "m" + }, + "window_height": { + "value": 1.7, + "quantity": "m" + }, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "glazing_gap": "16+", + "orientation": 4, + "window_type": 1, + "glazing_type": 3, + "window_width": { + "value": 1, + "quantity": "m" + }, + "window_height": { + "value": 1.7, + "quantity": "m" + }, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "glazing_gap": "16+", + "orientation": 4, + "window_type": 1, + "glazing_type": 3, + "window_width": { + "value": 0.95, + "quantity": "m" + }, + "window_height": { + "value": 1.7, + "quantity": "m" + }, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "glazing_gap": "16+", + "orientation": 6, + "window_type": 1, + "glazing_type": 3, + "window_width": { + "value": 0.7, + "quantity": "m" + }, + "window_height": { + "value": 0.9, + "quantity": "m" + }, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "glazing_gap": "16+", + "orientation": 6, + "window_type": 1, + "glazing_type": 3, + "window_width": { + "value": 0.65, + "quantity": "m" + }, + "window_height": { + "value": 0.5, + "quantity": "m" + }, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + } + ], + "schema_type": "RdSAP-Schema-21.0.1", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "(not tested)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "dwelling_type": "End-terrace house", + "language_code": 1, + "pressure_test": 4, + "property_type": 0, + "address_line_1": "5 Lynton Street", + "assessment_type": "RdSAP", + "completion_date": "2025-07-24", + "inspection_date": "2025-07-18", + "extensions_count": 1, + "measurement_type": 1, + "open_flues_count": 0, + "total_floor_area": 64, + "transaction_type": 1, + "conservatory_type": 1, + "heated_room_count": 4, + "other_flues_count": 0, + "registration_date": "2025-07-24", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "pv_connection": 2, + "photovoltaic_supply": [ + [ + { + "pitch": 2, + "peak_power": 2.04, + "orientation": 4, + "overshading": 1 + } + ], + [ + { + "pitch": 2, + "peak_power": 2.04, + "orientation": 8, + "overshading": 2 + } + ] + ], + "wind_turbines_count": 0, + "gas_smart_meter_present": "false", + "is_dwelling_export_capable": "true", + "wind_turbines_terrain_type": 2, + "electricity_smart_meter_present": "true" + }, + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "closed_flues_count": 0, + "extract_fans_count": 1, + "lzc_energy_sources": [ + 11 + ], + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "wall_thickness": 230, + "floor_heat_loss": 7, + "roof_construction": 4, + "wall_construction": 3, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.64, + "floor_insulation": 1, + "total_floor_area": 28.2, + "party_wall_length": 8.27, + "floor_construction": 2, + "heat_loss_perimeter": 13.38 + }, + { + "floor": 1, + "room_height": 2.58, + "total_floor_area": 28.2, + "party_wall_length": 8.27, + "heat_loss_perimeter": 15.09 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "B", + "party_wall_construction": 1, + "wall_thickness_measured": "Y", + "roof_insulation_location": 2, + "roof_insulation_thickness": "300mm", + "wall_insulation_thickness_measured": 100 + }, + { + "identifier": "Extension 1", + "wall_dry_lined": "N", + "floor_heat_loss": 7, + "roof_construction": 5, + "wall_construction": 3, + "building_part_number": 2, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.5, + "floor_insulation": 1, + "total_floor_area": 7.21, + "party_wall_length": 0, + "floor_construction": 1, + "heat_loss_perimeter": 9.88 + } + ], + "wall_insulation_type": 3, + "construction_age_band": "B", + "party_wall_construction": "NA", + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "100mm", + "wall_insulation_thickness": "measured", + "wall_insulation_thickness_measured": 100, + "wall_insulation_thermal_conductivity": 1 + } + ], + "boilers_flues_count": 0, + "open_chimneys_count": 0, + "solar_water_heating": "N", + "habitable_room_count": 4, + "heating_cost_current": { + "value": 939, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 2.7, + "energy_rating_average": 60, + "energy_rating_current": 82, + "lighting_cost_current": { + "value": 45, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "blocked_chimneys_count": 0, + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 591, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 161, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "schema_version_current": "LIG-21.0", + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 290, + "indicative_cost": "\u00a37,500 - \u00a311,000", + "improvement_type": "Q", + "improvement_details": { + "improvement_number": 7 + }, + "improvement_category": 5, + "energy_performance_rating": 91, + "environmental_impact_rating": 76 + }, + { + "sequence": 2, + "typical_saving": 58, + "indicative_cost": "\u00a35,000 - \u00a310,000", + "improvement_type": "W1", + "improvement_details": { + "improvement_number": 57 + }, + "improvement_category": 5, + "energy_performance_rating": 92, + "environmental_impact_rating": 79 + } + ], + "co2_emissions_potential": 1.6, + "energy_rating_potential": 92, + "lighting_cost_potential": { + "value": 45, + "currency": "GBP" + }, + "schema_version_original": "LIG-21.0", + "hot_water_cost_potential": { + "value": 161, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 2166.19, + "space_heating_existing_dwelling": 10128.81 + }, + "draughtproofed_door_count": 2, + "energy_consumption_current": 232, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "10.2.2.0", + "energy_consumption_potential": 138, + "environmental_impact_current": 65, + "cfl_fixed_lighting_bulbs_count": 0, + "current_energy_efficiency_band": "B", + "environmental_impact_potential": 79, + "led_fixed_lighting_bulbs_count": 9, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "A", + "co2_emissions_current_per_floor_area": 43, + "incandescent_fixed_lighting_bulbs_count": 0 +} diff --git a/packages/domain/src/domain/sap/rdsap/tests/test_cert_to_inputs.py b/packages/domain/src/domain/sap/rdsap/tests/test_cert_to_inputs.py index 77270322..bc61fa86 100644 --- a/packages/domain/src/domain/sap/rdsap/tests/test_cert_to_inputs.py +++ b/packages/domain/src/domain/sap/rdsap/tests/test_cert_to_inputs.py @@ -394,6 +394,23 @@ def test_calculator_always_uses_uk_average_weather_for_rating() -> None: assert inputs_default.region == 0 +def test_pv_export_credit_input_reports_rdsap10_table_32_rate() -> None: + # Arrange — per ADR-0010 the rating cascade uses RdSAP10 Table 32 + # prices (code 60 = 13.19 p/kWh for PV export). The CalculatorInputs + # boundary must report the same rate _fuel_cost applies internally + # so synthetic consumers (and the calculator's fallback path) don't + # see a different number from what the cascade produces. Previously + # this read from SAP10.2 Table 12 (5.59 p/kWh), leaving the input + # boundary out of step with the cascade. + epc = _typical_semi_detached_epc() + + # Act + inputs = cert_to_inputs(epc) + + # Assert + assert abs(inputs.pv_export_credit_gbp_per_kwh - 0.1319) <= 1e-6 + + def test_open_chimneys_raise_infiltration_ach() -> None: # Arrange — Direction check: chimneys add Table 2.1 volume to the # infiltration calc, so an otherwise identical dwelling with 2 open diff --git a/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py b/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py index 8ee7077d..347b5850 100644 --- a/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py +++ b/packages/domain/src/domain/sap/rdsap/tests/test_golden_fixtures.py @@ -132,6 +132,26 @@ _EXPECTATIONS: tuple[_GoldenExpectation, ...] = ( expected_co2_resid_tonnes_per_yr=-0.2234, notes="Semi-detached, TFA 102, age C, gas PCDB-listed.", ), + _GoldenExpectation( + cert_number="2130-1033-4050-5007-8395", + actual_sap=82, + expected_sap_resid=+8, + expected_pe_resid_kwh_per_m2=-61.2533, + expected_co2_resid_tonnes_per_yr=+0.1856, + notes=( + "End-terrace + 1 extension, TFA 64, gas combi PCDB index 17505, " + "postcode DE22 (PCDB Table 172 match), PV: 2× 2.04 kWp arrays " + "(SE + NE). Cert scored under SAP10.2 software (Table 12 PV " + "export = 5.59 p/kWh, £194 PV credit → SAP 82). Our calc " + "targets RdSAP10 (Table 32 PV export = 13.19 p/kWh, £457 PV " + "credit → SAP 90) per ADR-0010. The +8 SAP / −61 PE residual " + "is spec-version drift, not a code bug — both calcs are " + "internally consistent against their own price table. The PE " + "residual is amplified because PV generation also offsets PE " + "via inputs.other_primary_factor; that offset scales with the " + "PV gen kWh, not the export-credit price." + ), + ), _GoldenExpectation( cert_number="0390-2254-6420-2126-5561", actual_sap=65, @@ -221,6 +241,7 @@ _PCDB_CHAIN_EXPECTATIONS: tuple[tuple[str, int, float], ...] = ( ("0300-2747-7640-2526-2135", 17992, None), # gas PCDB-listed ("8135-1728-8500-0511-3296", 17702, None), # gas PCDB-listed ("0390-2254-6420-2126-5561", 18119, None), # LN12 gas combi PCDB-listed + ("2130-1033-4050-5007-8395", 17505, None), # DE22 gas combi PCDB-listed + PV )