mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
1477 lines
56 KiB
Python
1477 lines
56 KiB
Python
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
|