mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
[Cadded upgrade logic to funding engine
This commit is contained in:
parent
9a558c5bb5
commit
30e1eca74b
2 changed files with 165 additions and 19 deletions
|
|
@ -83,7 +83,8 @@ class Funding:
|
|||
return "98-199"
|
||||
return "200"
|
||||
|
||||
def _split_measures(self, measures: List[dict]):
|
||||
@staticmethod
|
||||
def _split_measures(measures: List[dict]):
|
||||
"""
|
||||
Extracts measure types and flags innovation.
|
||||
measures: list of dicts like {"type": "solar_pv", "is_innovation": True}
|
||||
|
|
@ -93,11 +94,24 @@ class Funding:
|
|||
innovation_measures = [m["type"] for m in measures if m.get("is_innovation", False)]
|
||||
return measure_types, has_innovation, innovation_measures
|
||||
|
||||
@staticmethod
|
||||
def _meets_upgrade_target(starting_sap: int, ending_sap: int) -> bool:
|
||||
"""
|
||||
ECO4 Upgrade Requirement:
|
||||
- EPC E/D (SAP ≥ 39): must upgrade to EPC C (SAP ≥ 69)
|
||||
- EPC F/G (SAP < 39): must upgrade to EPC D (SAP ≥ 55)
|
||||
"""
|
||||
if starting_sap >= 39 and ending_sap >= 69:
|
||||
return True
|
||||
if starting_sap < 39 and ending_sap >= 55:
|
||||
return True
|
||||
return False
|
||||
|
||||
# -----------------------
|
||||
# Private Rented Sector
|
||||
# -----------------------
|
||||
|
||||
def eco4_prs_eligibility(self, starting_sap: int, measure_types: List, mainheat_description: str,
|
||||
def eco4_prs_eligibility(self, starting_sap: int, ending_sap: int, measure_types: List, mainheat_description: str,
|
||||
heating_control_description: str):
|
||||
"""
|
||||
ECO4 PRS eligibility:
|
||||
|
|
@ -106,24 +120,27 @@ class Funding:
|
|||
- Tenant must be on benefits (flagged)
|
||||
"""
|
||||
meets_epc = starting_sap <= 54 # EPC E–G
|
||||
meets_upgrade_target = self._meets_upgrade_target(starting_sap, ending_sap)
|
||||
|
||||
has_swi = "internal_wall_insulation" in measure_types or "external_wall_insulation" in measure_types
|
||||
has_renewable = "air_source_heat_pump" in measure_types or "ground_source_heat_pump" in measure_types
|
||||
has_ftch = "first_time_central_heating" in measure_types
|
||||
has_dhc = "district_heating_connection" in measure_types
|
||||
|
||||
has_eligible_electric_heating = any(x in mainheat_description for x in [
|
||||
"air source heat pump", "ground source heat pump", "boiler and radiators, electric"
|
||||
]) or (
|
||||
("electric storage heaters" in mainheat_description)
|
||||
and (
|
||||
heating_control_description.lower() == "controls for high heat "
|
||||
"retention storage heaters")
|
||||
)
|
||||
has_eligible_electric_heating = (
|
||||
any(x in mainheat_description for x in [
|
||||
"air source heat pump", "ground source heat pump", "boiler and radiators, electric"
|
||||
]) or (
|
||||
("electric storage heaters" in mainheat_description)
|
||||
and (heating_control_description.lower() == "controls for high heat retention storage heaters")
|
||||
)
|
||||
)
|
||||
|
||||
solar_counts_as_renewable = has_eligible_electric_heating and "solar_pv" in measure_types
|
||||
|
||||
if meets_epc and (has_swi or has_renewable or has_ftch or has_dhc or solar_counts_as_renewable):
|
||||
if meets_upgrade_target and meets_epc and (
|
||||
has_swi or has_renewable or has_ftch or has_dhc or solar_counts_as_renewable
|
||||
):
|
||||
self.eco4_eligible = True
|
||||
self.eco4_eligibility_caveats.append(EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME)
|
||||
|
||||
|
|
@ -163,7 +180,7 @@ class Funding:
|
|||
# Social Housing
|
||||
# -----------------------
|
||||
|
||||
def eco4_sh_eligibility(self, starting_sap: int, measure_types: List, has_innovation: bool,
|
||||
def eco4_sh_eligibility(self, starting_sap: int, ending_sap: int, measure_types: List, has_innovation: bool,
|
||||
innovation_measures: List):
|
||||
"""
|
||||
ECO4 Social Housing eligibility.
|
||||
|
|
@ -172,7 +189,9 @@ class Funding:
|
|||
If solar PV is the innovation measure, must also install ASHP or HHRSH.
|
||||
"""
|
||||
meets_epc = starting_sap <= 69
|
||||
if not meets_epc:
|
||||
meets_upgrade_target = self._meets_upgrade_target(starting_sap, ending_sap)
|
||||
|
||||
if not meets_epc or not meets_upgrade_target:
|
||||
return
|
||||
|
||||
# EPC D innovation rule
|
||||
|
|
@ -262,7 +281,8 @@ class Funding:
|
|||
|
||||
if self.tenure == "Private":
|
||||
# ECO4 PRS
|
||||
self.eco4_prs_eligibility(starting_sap, measure_types, mainheat_description, heating_control_description)
|
||||
self.eco4_prs_eligibility(starting_sap, ending_sap, measure_types, mainheat_description,
|
||||
heating_control_description)
|
||||
# GBIS PRS
|
||||
self.gbis_prs_eligibility(starting_sap, council_tax_band or "", measure_types)
|
||||
|
||||
|
|
@ -273,7 +293,7 @@ class Funding:
|
|||
|
||||
elif self.tenure == "Social":
|
||||
# ECO4 Social
|
||||
self.eco4_sh_eligibility(starting_sap, measure_types, has_innovation, innovation_measures)
|
||||
self.eco4_sh_eligibility(starting_sap, ending_sap, measure_types, has_innovation, innovation_measures)
|
||||
# GBIS Social
|
||||
self.gbis_sh_eligibility(starting_sap, measure_types, has_innovation, innovation_measures)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,15 @@ from backend.Funding import Funding, EligibilityCaveats
|
|||
def mock_project_scores_matrix():
|
||||
data = []
|
||||
floor_segments = ["0-72", "73-97", "98-199", "200"]
|
||||
starting_bands = ["Low_G", "High_G", "Low_F", "High_F", "Low_E", "High_E", "Low_D", "High_D", "Low_C", "High_C"]
|
||||
finishing_bands = ["Low_C", "High_C", "Low_B"] # covers likely improvement targets
|
||||
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 starting_bands:
|
||||
for finish in finishing_bands:
|
||||
for start in bands:
|
||||
for finish in bands:
|
||||
if start != finish: # skip identical start/finish (no SAP movement)
|
||||
data.append({
|
||||
"Floor Area Segment": floor,
|
||||
|
|
@ -276,3 +278,127 @@ def test_eco4_sh_solar_pv_with_heating_is_ok(mock_project_scores_matrix, mock_pa
|
|||
|
||||
assert funding.eco4_eligible
|
||||
assert EligibilityCaveats.SOLAR_NEEDS_HEATING not 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):
|
||||
"""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,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Private",
|
||||
)
|
||||
|
||||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
||||
|
||||
def test_eco4_upgrade_requirement_e_to_d_fail(mock_project_scores_matrix, mock_partial_scores_matrix,
|
||||
mock_whlg_postcodes):
|
||||
"""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,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Private",
|
||||
)
|
||||
|
||||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
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):
|
||||
"""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,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Private",
|
||||
)
|
||||
|
||||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
assert funding.eco4_eligible
|
||||
|
||||
|
||||
def test_eco4_upgrade_requirement_f_to_e_fail(mock_project_scores_matrix, mock_partial_scores_matrix,
|
||||
mock_whlg_postcodes):
|
||||
"""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,
|
||||
social_cavity_abs_rate=13.5,
|
||||
social_solid_abs_rate=17,
|
||||
private_cavity_abs_rate=13.5,
|
||||
private_solid_abs_rate=17,
|
||||
tenure="Private",
|
||||
)
|
||||
|
||||
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
assert not funding.eco4_eligible
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue