Model/backend/tests/test_funding.py
2026-01-20 23:48:22 +00:00

1477 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 DG & council tax band AD 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 DG 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