Model/backend/tests/test_funding.py
Khalim Conn-Kowlessar 9a558c5bb5 added base unit tests
2025-08-01 18:44:49 +01:00

278 lines
9.8 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
@pytest.fixture
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
cost = 50.0
for floor in floor_segments:
for start in starting_bands:
for finish in finishing_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():
return pd.DataFrame([{"dummy": "data"}]) # not used for eligibility tests yet
@pytest.fixture
def mock_whlg_postcodes():
return pd.DataFrame([{"Postcode": "ab12cd"}])
### -------------------------
### PRIVATE RENTED SECTOR (PRS)
### -------------------------
def test_eco4_prs_eligible_with_swi(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
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",
)
# 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}]
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"
)
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):
"""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,
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}]
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"
)
assert not funding.eco4_eligible
def test_gbis_prs_general_eligibility(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
"""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,
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}]
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"
)
assert funding.gbis_eligible
def test_gbis_prs_low_income_caveat(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
"""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,
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": "cavity_wall_insulation", "is_innovation": False}]
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"
)
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):
"""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,
social_cavity_abs_rate=13.5,
social_solid_abs_rate=17,
private_cavity_abs_rate=13.5,
private_solid_abs_rate=17,
tenure="Social",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
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
)
assert funding.eco4_eligible
def test_eco4_sh_epc_d_requires_innovation(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
"""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,
social_cavity_abs_rate=13.5,
social_solid_abs_rate=17,
private_cavity_abs_rate=13.5,
private_solid_abs_rate=17,
tenure="Social",
)
measures = [{"type": "internal_wall_insulation", "is_innovation": False}]
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
)
assert not funding.eco4_eligible
assert EligibilityCaveats.INNOVATION_REQUIRED in funding.eco4_eligibility_caveats
def test_eco4_sh_solar_pv_requires_heating(mock_project_scores_matrix, mock_partial_scores_matrix, mock_whlg_postcodes):
"""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,
social_cavity_abs_rate=13.5,
social_solid_abs_rate=17,
private_cavity_abs_rate=13.5,
private_solid_abs_rate=17,
tenure="Social",
)
measures = [{"type": "solar_pv", "is_innovation": True}]
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
)
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):
"""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,
social_cavity_abs_rate=13.5,
social_solid_abs_rate=17,
private_cavity_abs_rate=13.5,
private_solid_abs_rate=17,
tenure="Social",
)
measures = [
{"type": "solar_pv", "is_innovation": True},
{"type": "air_source_heat_pump", "is_innovation": False}
]
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
)
assert funding.eco4_eligible
assert EligibilityCaveats.SOLAR_NEEDS_HEATING not in funding.eco4_eligibility_caveats