import pytest import pandas as pd from backend.Funding import Funding, EligibilityCaveats from backend.tests.test_data.innovation_measure_fixtures import innovation_scenarios from backend.tests.test_data.pre_heating_scenarios import pre_main_heating_scenarios from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes from etl.epc_clean.epc_attributes.MainFuelAttributes import MainFuelAttributes @pytest.fixture def mock_project_scores_matrix(): data = [] floor_segments = ["0-72", "73-97", "98-199", "200"] bands = [ "Low_G", "High_G", "Low_F", "High_F", "Low_E", "High_E", "Low_D", "High_D", "Low_C", "High_C", "Low_B", "High_B", "Low_A", "High_A" ] cost = 50.0 for floor in floor_segments: for start in bands: for finish in bands: if start != finish: # skip identical start/finish (no SAP movement) data.append({ "Floor Area Segment": floor, "Starting Band": start, "Finishing Band": finish, "Cost Savings": cost }) cost += 5.0 # increment to create variety return pd.DataFrame(data) @pytest.fixture def mock_partial_scores_matrix(): df = pd.read_csv("backend/tests/test_data/ECO4_Partial_Project_Scores_Matrix_v6.csv") df.columns = ['Measure category', 'Measure_Type', 'Pre_Main_Heating_Source', 'Post_Main_Heating_Source', 'Total Floor Area Band', 'Starting Band', 'Average Treatable Factor', 'Cost Savings', 'SAP Savings'] return df @pytest.fixture def mock_whlg_postcodes(): return pd.DataFrame([{"Postcode": "ab12cd"}]) @pytest.fixture def mock_mainheating(): return { 'original_description': 'Electric storage heaters', 'has_radiators': False, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False, 'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': True, 'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, 'has_community_scheme': False, 'has_ground_source_heat_pump': False, 'has_no_system_present': False, 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric': True, 'has_mains_gas': False, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False, "has_electric_heat_pumps": False, "has_micro-cogeneration": False, 'has_mineral_and_wood': False, "has_dual_fuel_appliance": False } @pytest.fixture def mock_main_fuel(): return { 'original_description': 'Electricity: electricity, unspecified tariff', 'fuel_type': 'electricity', 'tariff_type': 'unspecified tariff', 'is_community': False, 'no_individual_heating_or_community_network': False, 'complex_fuel_type': None, } @pytest.fixture def mock_mainheat_energy_eff(): return "Average" ### ------------------------- ### PRIVATE RENTED SECTOR (PRS) ### ------------------------- def test_eco4_prs_eligible_with_swi( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) # The property is: # 1) private, # 2) EPC E # 3) is getting a solid was measure # so it's eligible for ECO4 measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] funding.check_funding( measures=measures, starting_sap=50, # EPC E ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible assert EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME in funding.eco4_eligibility_caveats def test_eco4_prs_not_eligible_high_epc( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """Should NOT be eligible if EPC is too high (C or above).""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] funding.check_funding( measures=measures, starting_sap=72, # EPC C (too high) ending_sap=75, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible def test_gbis_prs_general_eligibility( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """PRS EPC D–G & council tax band A–D should trigger GBIS general route.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] funding.check_funding( measures=measures, starting_sap=65, # EPC D ending_sap=70, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="A", is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.gbis_eligible def test_gbis_prs_low_income_caveat( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """PRS EPC D–G should flag low-income caveat when low-income route is used.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] funding.check_funding( measures=measures, starting_sap=60, # EPC D ending_sap=70, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", is_partial=False, existing_li_thickness=0, current_wall_uvalue=2, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.gbis_eligible assert EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME in funding.gbis_eligibility_caveats ### ------------------------- ### SOCIAL HOUSING ### ------------------------- def test_eco4_sh_epc_e_eligible( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC E social housing should be ECO4 eligible without innovation.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] funding.check_funding( measures=measures, starting_sap=50, # EPC E ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible def test_eco4_sh_epc_d_requires_innovation( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC D social housing should require an innovation measure.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] funding.check_funding( measures=measures, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats # Test with an innovation measure funding2 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures2 = [{"type": "internal_wall_insulation", "is_innovation": True, "innovation_uplift": 0.25}] funding2.check_funding( measures=measures2, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding2.eco4_eligible assert not funding2.eco4_eligibility_caveats # Test with innovation solar. If the measure is solar, we need to have an eligible heating system. # If we don't have an eligible heating system in place, we need to have one as part of the measure # package # THIS SHOULD NOT BE ELIGIBLE funding3 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures3 = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}] funding3.check_funding( measures=measures3, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding3.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding3.eco4_eligibility_caveats # Test with innovation solar and ASHP. This should be eligible funding4 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures4 = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, ] funding4.check_funding( measures=measures4, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Air source heat pump, radiators", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding4.eco4_eligible assert not funding4.eco4_eligibility_caveats # Test with innovation solar, a non-eligible heating system but a heating upgrade funding5 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures5 = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "high_heat_retention_storage_heaters", "is_innovation": False, "innovation_uplift": 0} ] funding5.check_funding( measures=measures5, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="Manual charge control", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding5.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED in funding5.eco4_eligibility_caveats # Test with innovation solar, an eligible heating system but a package that excludes the required # fabric upgrades funding6 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures6 = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, ] funding6.check_funding( measures=measures6, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="controls for high heat retention storage heaters", is_cavity=True, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=False, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding6.eco4_eligible assert EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET in funding6.eco4_eligibility_caveats # Test with innovation solar, an eligible heating system but a package that includes the required # fabric upgrades funding7 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures7 = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0} ] funding7.check_funding( measures=measures7, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Air source heat pump, radiators", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding7.eco4_eligible assert not funding7.eco4_eligibility_caveats def test_eco4_sh_solar_pv_requires_heating( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """Solar PV as innovation measure requires ASHP or HHRSH.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}] funding.check_funding( measures=measures, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats def test_eco4_sh_solar_pv_with_heating_is_ok( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """Solar PV innovation with ASHP should pass EPC D innovation rule.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social", ) measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0} ] funding.check_funding( measures=measures, starting_sap=60, # EPC D ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats def test_eco4_upgrade_requirement_e_to_c_pass( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC E upgraded to C should pass ECO4 upgrade rule.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] # E (SAP 50) → C (SAP 70) meets upgrade rule funding.check_funding( measures=measures, starting_sap=50, ending_sap=70, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible def test_eco4_upgrade_requirement_e_to_d_fail( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC E upgraded to D should FAIL ECO4 upgrade rule (needs to hit C).""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] # E (SAP 50) → D (SAP 65) does NOT meet ECO4 upgrade rule funding.check_funding( measures=measures, starting_sap=50, ending_sap=65, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible def test_eco4_upgrade_requirement_f_to_d_pass( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC F upgraded to D should pass ECO4 upgrade rule.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] # F (SAP 35) → D (SAP 60) is OK for ECO4 funding.check_funding( measures=measures, starting_sap=35, ending_sap=60, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible def test_eco4_upgrade_requirement_f_to_e_fail( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC F upgraded only to E should FAIL ECO4 upgrade rule (needs to hit at least D).""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}] # F (SAP 35) → E (SAP 50) does NOT meet ECO4 rule funding.check_funding( measures=measures, starting_sap=35, ending_sap=50, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, council_tax_band="B", current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible ### ------------------------- ### INNOVATION PRODUCTS ### ------------------------- def test_epc_d_social_no_innovation_no_heating( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45} ] funding.check_funding( measures=measures, starting_sap=61, ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="Manual charge control", is_cavity=True, has_wall_insulation_recommendation=False, has_roof_insulation_recommendation=False, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats def test_epc_d_social_with_heating_and_insulation( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) # Should NOT be eligible as the ASHP is not an innovation measure measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0} ] funding.check_funding( measures=measures, starting_sap=61, ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats def test_epc_d_social_solar_with_only_minimum_insulation_should_fail( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) # Solar PV innovation with insulation, but no heating system upgrade => not eligible measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0} ] funding.check_funding( measures=measures, starting_sap=61, ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="Manual charge control", is_cavity=True, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats def test_epc_d_social_solar_with_ashp_and_no_insulation_should_fail( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) # Solar PV innovation with heating, but no insulation when insulation is recommended => not eligible measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0} ] funding.check_funding( measures=measures, starting_sap=61, ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="Manual charge control", is_cavity=True, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.MINIMUM_INSULATION_PRECONDITIONS_NOT_MET in funding.eco4_eligibility_caveats def test_epc_d_social_solar_with_heating_and_minimum_insulation_should_pass( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) # Innovation solar + insulation measures + eligible heating upgrade = not valid because the heat pump isn;t # an innovation measure measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0} ] funding.check_funding( measures=measures, starting_sap=61, ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="Manual charge control", is_cavity=True, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert not funding.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats funding2 = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) # Innovation solar + insulation measures + eligible heating upgrade = should be valid because the # heat pump is an innovation measure measures2 = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "air_source_heat_pump", "is_innovation": True, "innovation_uplift": 0.25} ] funding2.check_funding( measures=measures2, starting_sap=61, ending_sap=69, floor_area=80, mainheat_description="Electric storage heaters", heating_control_description="Manual charge control", is_cavity=True, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding2.eco4_eligible assert not funding2.eco4_eligibility_caveats @pytest.mark.parametrize("scenario", innovation_scenarios) def test_custom_eco4_scenarios( scenario, mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) funding.check_funding( measures=scenario["measures"], starting_sap=scenario["starting_sap"], ending_sap=69, floor_area=80, mainheat_description=scenario["mainheat_description"], heating_control_description=scenario["heating_control_description"], is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, has_wall_insulation_recommendation=scenario.get("has_wall_insulation_recommendation", False), has_roof_insulation_recommendation=scenario.get("has_roof_insulation_recommendation", False), mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff ) assert funding.eco4_eligible == scenario["expected_eligibility"], f"Failed: {scenario['description']}" for caveat in scenario.get("expected_caveats", []): assert caveat in funding.eco4_eligibility_caveats, f"Missing caveat in: {scenario['description']}" for caveat in funding.eco4_eligibility_caveats: assert caveat in scenario.get("expected_caveats", []), f"Unexpected caveat in: {scenario['description']}" ### ------------------------- ### Innovation uplift scenarios ### ------------------------- def test_uplift( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Social" ) # # TODO: Add a scenario with multiple measures, where some are innovation, some are not and we have # TODO: Make sure private works too measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "internal_wall_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}, {"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0.25}, ] funding.check_funding( measures=measures, starting_sap=33, ending_sap=69, floor_area=71, mainheat_description="Electic storage heaters", heating_control_description="Manual charge control", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff, ) assert funding.eco4_funding == 5302.3949999999995 assert funding.full_project_abs == 280 # Doesn't include the eco4 uplift assert funding.eco4_uplift == 112.77 def _dummy_funding(): # Matrices/whlg are unused by _map_to_pre_main_heating; pass harmless placeholders return Funding( tenure="Social", eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, project_scores_matrix=None, partial_project_scores_matrix=None, whlg_eligible_postcodes=set(), ) @pytest.mark.parametrize("scenario", pre_main_heating_scenarios) def test_map_to_pre_main_heating(scenario): funding = _dummy_funding() # Build normalized mainheating / main_fuel using your attribute processors h = MainHeatAttributes(description=scenario["MAINHEAT_DESCRIPTION"]).process() f = MainFuelAttributes(description=scenario["MAIN_FUEL"]).process() result = funding._map_to_pre_main_heating( mainheating=h, main_fuel=f, mainheat_energy_eff=scenario["MAINHEAT_ENERGY_EFF"], ) assert result == scenario[ "expected"], f"Failed: {scenario['description']} -> {result} (expected {scenario['expected']})" # Large scale testing for various measures # measures = [ # {"type": "solar_pv", "is_innovation": True, "uplift": 0.45}, # {"type": "internal_wall_insulation", "is_innovation": False, "uplift": 0}, # {"type": "cavity_wall_insulation", "is_innovation": False, "uplift": 0}, # {"type": "external_wall_insulation", "is_innovation": False, "uplift": 0}, # {"type": "loft_insulation", "is_innovation": False, "uplift": 0}, # {"type": "air_source_heat_pump", "is_innovation": False, "uplift": 0}, # {"type": "double_glazing", "is_innovation": False, "uplift": 0}, # {"type": "cavity_wall_insulation", "is_innovation": True, "uplift": 0.25}, # {"type": "high_heat_retention_storage_heaters", "is_innovation": False, "uplift": 0}, # ] ### ------------------------- ### PRIVATE (PRS/Owner) — Innovation uplift behaviour ### ------------------------- def test_private_epc_e_solar_needs_heating( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC D private: Solar PV as innovation requires eligible low-carbon heating.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [{"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}] funding.check_funding( measures=measures, starting_sap=54, # EPC E - eligible for private on EPC ending_sap=69, floor_area=80, mainheat_description="Boiler and radiators, mains gas", # not eligible for solar innovation heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, has_wall_insulation_recommendation=False, has_roof_insulation_recommendation=False, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff, council_tax_band="B", ) assert not funding.eco4_eligible assert EligibilityCaveats.SOLAR_NEEDS_HEATING in funding.eco4_eligibility_caveats def test_private_epc_e_solar_with_heating_and_minimum_insulation_produces_uplift( mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes, mock_mainheating, mock_main_fuel, mock_mainheat_energy_eff ): """EPC E private: Solar PV innovation + eligible heating + required insulation -> eligible and uplift > 0.""" funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) measures = [ {"type": "solar_pv", "is_innovation": True, "innovation_uplift": 0.45}, {"type": "air_source_heat_pump", "is_innovation": False, "innovation_uplift": 0}, {"type": "cavity_wall_insulation", "is_innovation": False, "innovation_uplift": 0}, {"type": "loft_insulation", "is_innovation": False, "innovation_uplift": 0}, ] funding.check_funding( measures=measures, starting_sap=54, # EPC E ending_sap=69, floor_area=80, mainheat_description="Air source heat pump, radiators", # eligible low-carbon heating present heating_control_description="Programmer, room thermostat and TRVs", is_cavity=True, current_wall_uvalue=2, is_partial=False, existing_li_thickness=0, has_wall_insulation_recommendation=True, has_roof_insulation_recommendation=True, mainheating=mock_mainheating, main_fuel=mock_main_fuel, mainheat_energy_eff=mock_mainheat_energy_eff, council_tax_band="B", ) assert funding.eco4_eligible assert EligibilityCaveats.INNOVATION_REQUIRED not in funding.eco4_eligibility_caveats assert EligibilityCaveats.SOLAR_NEEDS_HEATING not in funding.eco4_eligibility_caveats # We don't pin an exact numeric value (depends on score matrices), # but innovation uplift should be positive when solar PV has an uplift. assert funding.eco4_uplift and funding.eco4_uplift > 0 # And total funding should include that uplift assert funding.eco4_funding and funding.eco4_funding > 0 def test_existing_gshp_to_ashp(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes): r = {'phase': 3, 'parts': [], 'type': 'heating', 'measure_type': 'air_source_heat_pump', 'description': 'Install a 5KW air source heat pump, and upgrade heating controls to Smart Thermostats, ' 'room sensors and smart radiator valves (time & temperature zone control). Ensure you have a ' 'single tariff', 'starting_u_value': None, 'new_u_value': None, 'sap_points': 7.7, 'already_installed': False, 'simulation_config': {'mainheat_energy_eff_ending': 'Good', 'hot_water_energy_eff_ending': 'Average', 'has_air_source_heat_pump_ending': True, 'has_ground_source_heat_pump_ending': False, 'extra_features_ending': None, 'thermostatic_control_ending': 'time and temperature zone control', 'switch_system_ending': None, 'multiple_room_thermostats_ending': False, 'mainheatc_energy_eff_ending': 'Very Good'}, 'description_simulation': {'mainheat-description': 'Air source heat pump, radiators, electric', 'mainheat-energy-eff': 'Good', 'hot-water-energy-eff': 'Average', 'hotwater-description': 'From main system', 'mainheatcont-description': 'Time and temperature zone control', 'mainheatc-energy-eff': 'Very Good'}, 'total': 13188.996000000001, 'contingency': 3145.8150000000005, 'contingency_rate': 0.35, 'vat': 2080.666, 'labour_hours': 44.7, 'labour_days': 6.0, 'innovation_rate': 0, 'recommendation_id': '6_phase=3', 'efficiency': 13188.996000000001, 'co2_equivalent_savings': 0.4999999999999998, 'heat_demand': 53.20000000000002, 'kwh_savings': 801.5000000000005, 'energy_cost_savings': 327.31316785714296 } funding = Funding( project_scores_matrix=mock_project_scores_matrix, partial_project_scores_matrix=mock_partial_scores_matrix, whlg_eligible_postcodes=mock_whlg_postcodes, eco4_social_cavity_abs_rate=13.5, eco4_social_solid_abs_rate=17, eco4_private_cavity_abs_rate=13.5, eco4_private_solid_abs_rate=17, gbis_social_cavity_abs_rate=21, gbis_social_solid_abs_rate=25, gbis_private_cavity_abs_rate=22, gbis_private_solid_abs_rate=28, tenure="Private", ) ( pps, ppf, iu, ups ) = funding.get_innovation_uplift( measure=r, starting_sap=62, floor_area=69, is_cavity=True, current_wall_uvalue=0.7, is_partial=False, existing_li_thickness=200, mainheating={ 'original_description': 'Ground source heat pump, radiators, electric', 'clean_description': 'Ground source heat pump, radiators, electric', 'has_radiators': True, 'has_fan_coil_units': False, 'has_pipes_in_screed_above_insulation': False, 'has_pipes_in_insulated_timber_floor': False, 'has_pipes_in_concrete_slab': False, 'has_boiler': False, 'has_air_source_heat_pump': False, 'has_room_heaters': False, 'has_electric_storage_heaters': False, 'has_warm_air': False, 'has_electric_underfloor_heating': False, 'has_electric_ceiling_heating': False, 'has_community_scheme': False, 'has_ground_source_heat_pump': True, 'has_no_system_present': False, 'has_portable_electric_heaters': False, 'has_water_source_heat_pump': False, 'has_electric_heat_pump': False, 'has_micro-cogeneration': False, 'has_solar_assisted_heat_pump': False, 'has_exhaust_source_heat_pump': False, 'has_community_heat_pump': False, 'has_hot-water-only': False, 'has_electric': True, 'has_mains_gas': False, 'has_wood_logs': False, 'has_coal': False, 'has_oil': False, 'has_wood_pellets': False, 'has_anthracite': False, 'has_dual_fuel_mineral_and_wood': False, 'has_smokeless_fuel': False, 'has_lpg': False, 'has_b30k': False, 'has_mineral_and_wood': False, 'has_dual_fuel_appliance': False, 'has_assumed': False, 'has_electricaire': False, 'has_assumed_for_most_rooms': False, 'has_underfloor_heating': False }, main_fuel={ 'original_description': 'electricity (not community)', 'clean_description': 'Electricity not community', 'fuel_type': 'electricity', 'tariff_type': None, 'is_community': False, 'no_individual_heating_or_community_network': False, 'complex_fuel_type': None }, mainheat_energy_eff="Poor", ) # All should be zero assert pps == 0 assert ppf == 0 assert iu == 0 assert ups == 0