diff --git a/backend/Funding.py b/backend/Funding.py index d17074cb..016db276 100644 --- a/backend/Funding.py +++ b/backend/Funding.py @@ -6,431 +6,26 @@ from typing import List from backend.app.plan.schemas import HousingType -class FundingOld: - """ - Given a property, this class identifies if the home is possibly eligible for funding under - the various funding schemes. It will also calculate the expected amount of funding available - and flag any tenant specific requirements that need to be considered to the funding to be attained - """ - - SCHEMES = ["eco4", "gbis", "whlg"] - - ECO_SAP_SCORE_THREHOLDS = [ - {'Band': 'High_A', 'From': 96.0, 'Up to': 100.0, 'Mid-point': 98.0}, - {'Band': 'Low_A', 'From': 92.0, 'Up to': 96.0, 'Mid-point': 94.0}, - {'Band': 'High_B', 'From': 86.0, 'Up to': 91.0, 'Mid-point': 88.5}, - {'Band': 'Low_B', 'From': 81.0, 'Up to': 86.0, 'Mid-point': 83.5}, - {'Band': 'High_C', 'From': 74.5, 'Up to': 80.0, 'Mid-point': 77.25}, - {'Band': 'Low_C', 'From': 69.0, 'Up to': 74.5, 'Mid-point': 71.75}, - {'Band': 'High_D', 'From': 61.5, 'Up to': 68.0, 'Mid-point': 64.75}, - {'Band': 'Low_D', 'From': 55.0, 'Up to': 61.5, 'Mid-point': 58.25}, - {'Band': 'High_E', 'From': 46.5, 'Up to': 54.0, 'Mid-point': 50.25}, - {'Band': 'Low_E', 'From': 39.0, 'Up to': 46.5, 'Mid-point': 42.75}, - {'Band': 'High_F', 'From': 29.5, 'Up to': 38.0, 'Mid-point': 33.75}, - {'Band': 'Low_F', 'From': 21.0, 'Up to': 29.5, 'Mid-point': 25.25}, - {'Band': 'High_G', 'From': 10.5, 'Up to': 20.0, 'Mid-point': 15.25}, - {'Band': 'Low_G', 'From': 1.0, 'Up to': 10.5, 'Mid-point': 5.75} - ] - - def __init__( - self, - tenure: HousingType, - starting_epc, - starting_sap, - postcode, - floor_area, - council_tax_band, - property_recommendations, - project_scores_matrix, - whlg_eligible_postcodes, - gbis_abs_rate: int, - eco4_abs_rate: int, - ): - """ - Use Pydantic to validate the parameter types - :param tenure: Indicates if the property is a social or private home - :param starting_epc: The current EPC rating of the property - :param starting_sap: The current SAP score for the property - :param floor_area: The total floor area of the property - :param council_tax_band: The council tax band of the property - :param property_recommendations: The recommendations for the property - :param project_scores_matrix: The matrix of project scores for ECO4 - :param whlg_eligible_postcodes: The postcodes eligible for WHLG - :param gbis_abs_rate: The assumed £/abs achieved by the installer for GBIS - :param eco4_abs_rate: The assumed £/abs achieved by the installer for ECO4 - """ - - # TODO: Things we need to include: - # 1) Amount of funding - # 2) Fundable measures, as a subset of measures may be fundable, not all - - self.tenure = tenure - self.starting_epc = starting_epc - self.starting_sap = starting_sap - self.postcode = postcode - self.starting_eco_band = self.sap_to_eco_band(self.starting_sap) - self.floor_area_segment = self.classify_floor_area(floor_area) - self.gbis_abs_rate = gbis_abs_rate - self.eco4_abs_rate = eco4_abs_rate - self.council_tax_band = council_tax_band - - self.recommendations = property_recommendations - - self.measure_types = list({r["measure_type"] for r in property_recommendations if r["default"]}) - - # Load in the eco4 project scores matrix - # Filter the matrix on scores relevant to this property - self.project_scores_matrix = project_scores_matrix[ - (project_scores_matrix["Floor Area Segment"] == self.floor_area_segment) & - (project_scores_matrix["Starting Band"] == self.starting_eco_band) - ] - - # The postcode column is already lower case - self.whlg_eligible_postcodes = whlg_eligible_postcodes[ - whlg_eligible_postcodes["Postcode"] == self.postcode.lower() - ] - - # Store the final outputs - self.gbis_eligibiltiy = {} - self.eco4_eligibility = {} - self.whlg_eligibility = {} - - def output( - self, - scheme: str, - eligible: bool, - types: List[str], - measure_types: List[str], - project_score: float, - estimated_funding: float, - notify_tenant_benefits_requirements: bool, - notify_council_tax_band_requirements: bool, - notify_tenant_low_income_requirements: bool, - innovation_required: bool, - ): - """" - """ - - if scheme not in self.SCHEMES: - raise ValueError("Scheme not recognised") - - return { - "scheme": scheme, - "eligible": eligible, - "type": types, - "measure_types": measure_types, - "project_score": project_score, - "estimated_funding": estimated_funding, - "requires_benefits": notify_tenant_benefits_requirements, - "requires_council_tax_band": notify_council_tax_band_requirements, - "requires_low_income": notify_tenant_low_income_requirements, - "innovation_required": innovation_required, - } - - @staticmethod - def classify_floor_area(floor_area): - if floor_area <= 72: - return "0-72" - - if floor_area <= 97: - return "73-97" - - if floor_area <= 199: - return "98-199" - - return "200" - - def eco4(self): - """ - Checks if a property is eligible for ECO4 - :return: - """ - pass - - def find_gbis_measures(self, measures): - """ - The best measure is one that: - 1) Creates some SAP movement, therefore enables eligiblity - 2) Generates the most funding - 3) Has a reasonable ROI - :return: - """ - measure_table = pd.DataFrame([ - m for m in self.recommendations if - (m["type"] in measures) or (m["measure_type"] in measures) and m["default"] - ]) - - measure_table["post_install_sap"] = measure_table["sap_points"] + self.starting_sap - # We classify the movement - measure_table["Finishing Band"] = np.floor(measure_table["post_install_sap"]).apply( - lambda points: self.sap_to_eco_band(points) - ) - # Remove any measures that generate zero SAP movement - measure_table = measure_table[measure_table["Finishing Band"] != self.starting_eco_band] - - if measure_table.empty: - raise NotImplementedError("No measures available, handle me!") - - # We merge on the project matrix, on post install band - measure_table = measure_table.merge( - self.project_scores_matrix, how="left", on="Finishing Band" - ) - # Cost Savings is the abs - measure_table["estimated_funding"] = measure_table["Cost Savings"] * self.gbis_abs_rate - # We cap any estimated funding at the install cost - measure_table["estimated_funding"] = np.where( - measure_table["estimated_funding"] >= measure_table["total"], - measure_table["total"], - measure_table["estimated_funding"] - ) - - # Sort by the measure that will cost the client the least, per sap point - measure_table["cost_minus_funding"] = measure_table["total"] - measure_table["estimated_funding"] - measure_table["cost_minus_funding_per_sap"] = measure_table["cost_minus_funding"] / measure_table["sap_points"] - measure_table = measure_table.sort_values(["cost_minus_funding_per_sap", "total"], ascending=[True, False]) - - return measure_table[ - ["type", "measure_type", "Cost Savings", "estimated_funding"] - ].rename(columns={"Cost Savings": "project_score"}).to_dict("records") - - def sap_to_eco_band(self, sap_points): - """ - Giuven a sap point score, this function will classify the points into the SAP half-band - :param sap_points: - :return: - """ - - if sap_points > 100: - return "High_A" - - classification = [ - x for x in self.ECO_SAP_SCORE_THREHOLDS if (x["From"] <= sap_points) and (sap_points <= x["Up to"]) - ] - - if len(classification) != 1: - raise Exception("We should have a single classifcation for SAP points to half band") - - return classification[0]['Band'] - - def gbis_prs(self): - """ - Checks if a private rental is eligible for GBIS. There are the following possible options - 1) General Eligibilty, contigent on EPC D-G and council tax band A-D. Excludes CWI, LI and heating - controls - 2) Low income group - contigent on EPC D-G and tenant must receive benefits. Excludes heating controls - 3) GBIS Flex route 1, 3 - Great British Insulation Scheme Routes 1 and 3 are for pre-installation - SAP bands D-G for owner-occupied households, D-E for private rented sector households - (Including F & G if exempt from MEES). If houseold is low income. Excludes heating controls - 4) GBIS Flex route 2 - EPC E - G and low income household. Excludes heating controls - - Eligible measures: - • Solid wall - • pitched roof - • flat roof - • under floor - • solid floor park home and - • room in-roof insulation - - :return: - """ - - valid_measures = [ - "internal_wall_insulation", - "external_wall_insulation", - "flat_roof_insulation", - "suspended_floor_insulation", - "room_roof_insulation", - # Not available for every eligiblity type - "cavity_wall_insulation", - "loft_insulation", - ] - - # General Eligibility - if ( - (self.starting_epc in ["G", "D", "E", "F"]) and - any( - [measure in valid_measures for measure in self.measure_types - if measure not in ["cavity_wall_insulation", "loft_insulation"]] - ) and - (self.council_tax_band in [None, "A", "B", "C", "D"]) - ): - # This function pulls out the various measures that can provide funding under GBIS - recommended_measures = self.find_gbis_measures( - measures=[m for m in valid_measures if m not in ["cavity_wall_insulation", "loft_insulation"]] - ) - # If the council tax band is missing, we nofify the customer that this is a requirement that - # should be checked - return [ - self.output( - scheme="gbis", - eligible=True, - types=[m["type"]], # This is single measure so we only have one type - measure_types=[m["measure_type"]], - project_score=m["project_score"], - estimated_funding=m["estimated_funding"], - notify_tenant_benefits_requirements=False, - notify_council_tax_band_requirements=self.council_tax_band is None, - notify_tenant_low_income_requirements=False, - innovation_required=False - ) for m in recommended_measures - ] - - # Low income/flex - if ( - (self.starting_sap in ["G", "D", "E", "F"]) and - any([measure in valid_measures for measure in self.measure_types]) - ): - # Find the best measure, and can also include CWI/LI but requires the tenant to be - # low inome or on benefits - # We find the best measure for GBIS - recommended_measures = self.find_gbis_measures(measures=valid_measures) - return [ - self.output( - scheme="gbis", - eligible=True, - types=[m["type"]], # This is single measure so we only have one type - measure_types=[m["measure_type"]], - project_score=m["project_score"], - estimated_funding=m["estimated_funding"], - notify_tenant_benefits_requirements=True, - notify_council_tax_band_requirements=False, - notify_tenant_low_income_requirements=True, - innovation_required=False - ) for m in recommended_measures - ] - - # Otherwise, no funding availability - return [] - - def gbis_social(self): - """ - Because this is social housing, we have two typical means for eligibility - 1) EPC D, where an innovation measure is required - 2) EPC G-E, where an innovation measure isn't required - :return: - """ - valid_measures = [ - "internal_wall_insulation", - "external_wall_insulation", - "flat_roof_insulation", - "suspended_floor_insulation", - "room_roof_insulation", - # Not available for every eligiblity type - "cavity_wall_insulation", - "loft_insulation", - "heating_control" - ] - - recommended_measures = self.find_gbis_measures( - measures=valid_measures - ) - - # All measures are available - if self.starting_sap == "D": - return [ - self.output( - scheme="gbis", - eligible=True, - types=[m["type"]], # This is single measure so we only have one type - measure_types=[m["measure_type"]], - project_score=m["project_score"], - estimated_funding=m["estimated_funding"], - notify_tenant_benefits_requirements=False, - notify_council_tax_band_requirements=False, - notify_tenant_low_income_requirements=False, - innovation_required=True - ) for m in recommended_measures - ] - - if self.starting_sap in ["G", "F", "E"]: - return [ - self.output( - scheme="gbis", - eligible=True, - types=[m["type"]], # This is single measure so we only have one type - measure_types=[m["measure_type"]], - project_score=m["project_score"], - estimated_funding=m["estimated_funding"], - notify_tenant_benefits_requirements=False, - notify_council_tax_band_requirements=False, - notify_tenant_low_income_requirements=False, - innovation_required=False - ) for m in recommended_measures - ] - - return [] - - def gbis(self): - """ - Check if a property is eligible for GBIS - :return: - """ - - if self.tenure == "Private": - self.gbis_eligibiltiy = self.gbis_prs() - return - - if self.tenure == "Social": - self.gbis_eligibiltiy = self.gbis_social() - - raise NotImplementedError("Implement social/oo") - - def whlg(self): - if self.tenure == "Social": - # We can't do anything for social housing - self.whlg_eligibility = [] - return - - if not self.whlg_eligible_postcodes.empty: - raise Exception("Implement me") - # self.whlg_eligibility = [ - # self.output( - # scheme, - # eligible, - # types, - # measure_types, - # project_score: float, - # estimated_funding: float, - # notify_tenant_benefits_requirements: bool, - # notify_council_tax_band_requirements: bool, - # notify_tenant_low_income_requirements: bool, - # innovation_required: bool, - # ) - # ] - - def eco4(self): - if self.tenure == "Private": - self.eco4_eligibiltiy = self.eco4_prs() - return - - def check_eligibiltiy(self): - """ - This function instigates the checking process - :return: - """ - - self.gbis() - # self.eco4() - self.whlg() - - class EligibilityCaveats(Enum): TENANT_ON_BENEFITS_OR_LOW_INCOME = "tenant_on_benefits_or_low_income" + INNOVATION_REQUIRED = "innovation_required" + SOLAR_NEEDS_HEATING = "solar_needs_heating" class Funding: """ - New class to handle funding calculation + Handles eligibility and funding calculations for ECO4 & GBIS (PRS + Social Housing). """ def __init__( self, - tenure: HousingType, + tenure: str, # 'Private' or 'Social' social_cavity_abs_rate: float, social_solid_abs_rate: float, private_cavity_abs_rate: float, private_solid_abs_rate: float, project_scores_matrix, + partial_project_scores_matrix, whlg_eligible_postcodes ): self.tenure = tenure @@ -443,10 +38,17 @@ class Funding: self.ending_sap_band = None self.floor_area_band = None self.project_scores_matrix = project_scores_matrix + self.partial_project_scores_matrix = partial_project_scores_matrix self.whlg_eligible_postcodes = whlg_eligible_postcodes self.eco4_eligible = False - self.eligbility_caveat = None + self.eco4_eligibility_caveats = [] + self.gbis_eligible = False + self.gbis_eligibility_caveats = [] + + # ----------------------- + # Utility Helpers + # ----------------------- @staticmethod def get_sap_band(sap_score_number): @@ -466,151 +68,219 @@ class Funding: ("High_G", 10.5, 21), ("Low_G", 1, 10.5), ] - for band, lower, upper in bands: if lower <= sap_score_number < upper: return band - return None @staticmethod def get_floor_area_band(floor_area): if floor_area <= 72: return "0-72" - if floor_area <= 97: return "73-97" - if floor_area <= 199: return "98-199" - return "200" - def eco4_prs_eligibility( - self, starting_sap: int, measures: List, mainheat_description: str, heating_control_description: str - ): + def _split_measures(self, measures: List[dict]): """ - Handles the eligibility criteria for private rental properties under eco - :return: + Extracts measure types and flags innovation. + measures: list of dicts like {"type": "solar_pv", "is_innovation": True} """ + measure_types = [m["type"] for m in measures] + has_innovation = any(m.get("is_innovation", False) for m in measures) + innovation_measures = [m["type"] for m in measures if m.get("is_innovation", False)] + return measure_types, has_innovation, innovation_measures - # Help to heat group - # 1) EPC E - G - # 2) Must receive one of SWI, FTCH, renewable heating or DHC - # 3) Tenant must be on benefits + # ----------------------- + # Private Rented Sector + # ----------------------- - # We don't consider the tenant being on benefits - we just notify the end user that this is a requirement + def eco4_prs_eligibility(self, starting_sap: int, measure_types: List, mainheat_description: str, + heating_control_description: str): + """ + ECO4 PRS eligibility: + - EPC E–G + - Must include SWI, FTCH, renewable heating, or DHC + - Tenant must be on benefits (flagged) + """ + meets_epc = starting_sap <= 54 # EPC E–G - meets_epc = starting_sap <= 54 - has_solid_wall = "internal_wall_insulation" in measures or "external_wall_insulation" in measures - # We check if the property has a heating system that means solar pv counts as a renewable heating system + 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" - ]) | (("electric storage heaters" in mainheat_description) and - (heating_control_description.lower() == "controls for high heat retention storage heaters") - ) + ]) or ( + ("electric storage heaters" in mainheat_description) + and ( + heating_control_description.lower() == "controls for high heat " + "retention storage heaters") + ) - # Counts as renewable heating - solar_renweable_heating = has_eligible_electric_heating & ("solar_pv" in measures) - # Is a renewable heating - ashp = "air_source_heat_pump" in measures + solar_counts_as_renewable = has_eligible_electric_heating and "solar_pv" in measure_types - # Meets the EPC criteria, has the measure requirement and tenant must be on benefits - if meets_epc & (solar_renweable_heating or ashp or has_solid_wall): + if meets_epc and (has_swi or has_renewable or has_ftch or has_dhc or solar_counts_as_renewable): self.eco4_eligible = True - self.eligbility_caveat = EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME + self.eco4_eligibility_caveats.append(EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME) + + def gbis_prs_eligibility(self, starting_sap: int, council_tax_band: str, measure_types: List): + """ + GBIS PRS eligibility: + - General route: Council Tax Band & EPC D–G + - Low-income route: tenant on benefits (flagged) + """ + gbis_measures = { + "general": [ + "internal_wall_insulation", "external_wall_insulation", + "flat_roof_insulation", "suspended_floor_insulation", + "room_roof_insulation", "solid_floor_insulation", "park_home_insulation" + ], + "low_income": [ + "internal_wall_insulation", "external_wall_insulation", + "flat_roof_insulation", "suspended_floor_insulation", + "room_roof_insulation", "solid_floor_insulation", + "cavity_wall_insulation", "loft_insulation", "park_home_insulation" + ] + } + + meets_epc = starting_sap <= 69 # EPC D–G + + # General route + if meets_epc and council_tax_band in ["A", "B", "C", "D", "E"]: + if any(m in gbis_measures["general"] for m in measure_types): + self.gbis_eligible = True + + # Low-income route + if meets_epc and any(m in gbis_measures["low_income"] for m in measure_types): + self.gbis_eligible = True + self.gbis_eligibility_caveats.append(EligibilityCaveats.TENANT_ON_BENEFITS_OR_LOW_INCOME) + + # ----------------------- + # Social Housing + # ----------------------- + + def eco4_sh_eligibility(self, starting_sap: int, measure_types: List, has_innovation: bool, + innovation_measures: List): + """ + ECO4 Social Housing eligibility. + - EPC E–G: eligible, no income check. + - EPC D: innovation measure required. + If solar PV is the innovation measure, must also install ASHP or HHRSH. + """ + meets_epc = starting_sap <= 69 + if not meets_epc: return - return False + # EPC D innovation rule + if 55 <= starting_sap <= 68: # EPC D + if not has_innovation: + self.eco4_eligible = False + self.eco4_eligibility_caveats.append(EligibilityCaveats.INNOVATION_REQUIRED) + return - def gbis_prs_eligibiltiy(self): + if "solar_pv" in innovation_measures and not any( + m in measure_types for m in ["air_source_heat_pump", "high_heat_retention_storage_heater"] + ): + self.eco4_eligible = False + self.eco4_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING) + return + + self.eco4_eligible = True + + def gbis_sh_eligibility(self, starting_sap: int, measure_types: List, has_innovation: bool, + innovation_measures: List): """ - Determines if a project is eligible for GBIS funding for private rental properties + GBIS Social Housing eligibility. + - EPC E–G: insulation measures OK. + - EPC D: innovation measure required (same solar PV + heating rule). """ + meets_epc = starting_sap <= 69 + if not meets_epc: + return + + if 55 <= starting_sap <= 68: # EPC D + if not has_innovation: + self.gbis_eligible = False + self.gbis_eligibility_caveats.append(EligibilityCaveats.INNOVATION_REQUIRED) + return + + if "solar_pv" in innovation_measures and not any( + m in measure_types for m in ["air_source_heat_pump", "high_heat_retention_storage_heater"] + ): + self.gbis_eligible = False + self.gbis_eligibility_caveats.append(EligibilityCaveats.SOLAR_NEEDS_HEATING) + return + + self.gbis_eligible = True + + # ----------------------- + # Score Lookup + # ----------------------- def calculate_full_project_abs(self): - - # Filter the project scores matrix + """Look up ABS score for full projects.""" data = self.project_scores_matrix[ (self.project_scores_matrix["Floor Area Segment"] == self.floor_area_band) & (self.project_scores_matrix["Starting Band"] == self.starting_sap_band) & (self.project_scores_matrix["Finishing Band"] == self.ending_sap_band) ] - if data.emtpy: - raise ValueError("Missing abs rate, check the project scores matrix") + if data.empty: + raise ValueError("Missing ABS rate, check the project scores matrix") return data["Cost Savings"].values[0] + # ----------------------- + # Main Entry Point + # ----------------------- + def check_funding( - self, measures: List, + self, measures: List[dict], starting_sap: int, ending_sap: int, floor_area: float, mainheat_description: str, heating_control_description: str, - is_cavity: bool + is_cavity: bool, + council_tax_band: str = None ): """ - Given a list of measures, this function will check if the package of measures is fundable - :param measures: - :param starting_sap: - :param ending_sap: - :param floor_area: - :param mainheat_description: - :param heating_control_description: - :param is_cavity: Indicates if the property has cavity wall insulation - :return: + Given a list of measures, check ECO4/GBIS eligibility. """ - # If it's an E or D, should get to an EPC C - if starting_sap >= 55 and ending_sap < 69: - raise NotImplementedError("This property doesn't have sufficient SAP movement") - - if starting_sap <= 38 & ending_sap <= 55: - # F or G should get to D - raise NotImplementedError("Implement F or G to D eligibility") + # Normalize measures + measure_types, has_innovation, innovation_measures = self._split_measures(measures) + # Track EPC bands and floor area self.starting_sap_band = self.get_sap_band(starting_sap) self.ending_sap_band = self.get_sap_band(ending_sap) self.floor_area_band = self.get_floor_area_band(floor_area) - ######################## - # Private - ######################## - # 1) ECO4 - # 2) GBIS - if self.tenure == "Private": - self.eco4_prs_eligibility( - starting_sap=starting_sap, - measures=measures, - mainheat_description=mainheat_description, - heating_control_description=heating_control_description - ) + # ECO4 PRS + self.eco4_prs_eligibility(starting_sap, measure_types, mainheat_description, heating_control_description) + # GBIS PRS + self.gbis_prs_eligibility(starting_sap, council_tax_band or "", measure_types) - # Need to implement - # 1) Package has to include an insulation measure - # 2) We should use the funding for the measure that has the largest partial project score - # TODO: check the rules around GBIS eligibility and heating controls - self.gbis_prs_eligibiltiy() + if self.eco4_eligible: + eco4_abs = self.calculate_full_project_abs() + eco4_funding = eco4_abs * (self.private_cavity_abs_rate if is_cavity else self.private_solid_abs_rate) + return {"eco4_funding": eco4_funding} - if not is_eco4_eligible: - return - eco4_abs = self.calculate_full_project_abs() - # We estimate rates now - eco4_funding = ( - eco4_abs * self.private_cavity_abs_rate if is_cavity else eco4_abs & self.private_solid_abs_rate - ) + elif self.tenure == "Social": + # ECO4 Social + self.eco4_sh_eligibility(starting_sap, measure_types, has_innovation, innovation_measures) + # GBIS Social + self.gbis_sh_eligibility(starting_sap, measure_types, has_innovation, innovation_measures) - ######################## - # Social - ######################## - # 1) ECO4 - # 2) GBIS + if self.eco4_eligible: + eco4_abs = self.calculate_full_project_abs() + eco4_funding = eco4_abs * (self.social_cavity_abs_rate if is_cavity else self.social_solid_abs_rate) + return {"eco4_funding": eco4_funding} - if self.tenure == "Social": - pass - - raise NotImplementedError("Only implemented for Private or Social housing") + else: + raise NotImplementedError("Only 'Private' and 'Social' tenures are supported.") diff --git a/backend/tests/test_funding.py b/backend/tests/test_funding.py index 3a830eae..da5a71e1 100644 --- a/backend/tests/test_funding.py +++ b/backend/tests/test_funding.py @@ -17,35 +17,48 @@ def get_funding_data(): project_scores_matrix.columns = ['Floor Area Segment', 'Starting Band', 'Finishing Band', 'Cost Savings'] project_scores_matrix["Cost Savings"] = project_scores_matrix["Cost Savings"].astype(float) + partial_project_scores_matrix = read_csv_from_s3( + bucket_name="retrofit-data-dev", + filepath="funding/ECO4_Partial_Project_Scores_Matrix_v6.csv", + ) + partial_project_scores_matrix = pd.DataFrame(partial_project_scores_matrix) + partial_project_scores_matrix["Cost Savings"] = partial_project_scores_matrix["Cost Savings"].astype(float) + whlg_eligible_postcodes = read_csv_from_s3( bucket_name="retrofit-data-dev", filepath="funding/whlg eligible postcodes.csv", ) whlg_eligible_postcodes = pd.DataFrame(whlg_eligible_postcodes) - return project_scores_matrix, whlg_eligible_postcodes + return project_scores_matrix, partial_project_scores_matrix, whlg_eligible_postcodes -# class TestFunding: -# -# def test_prs(self): -# eco_project_scores_matrix, whlg_eligible_postcodes = get_funding_data() -# funding = Funding( -# project_scores_matrix=eco_project_scores_matrix, -# whlg_eligible_postcodes=whlg_eligible_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_1 = ["internal_wall_insulation", "solar_pv"] -# funding.check_funding( -# measures=measures_1, -# starting_sap=54, -# ending_sap=69, -# floor_area=73, -# mainheat_description="Boiler and radiators, mains gas", -# heating_control_description="Programmer, room thermostat and TRVs", -# is_cavity=True -# ) + +class TestFunding: + + def test_prs(self): + fps_matrix, pps_matrix, whlg_eligible_postcodes = get_funding_data() + funding = Funding( + project_scores_matrix=fps_matrix, + partial_project_scores_matrix=pps_matrix, + whlg_eligible_postcodes=whlg_eligible_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_1 = [ + {"type": "internal_wall_insulation", "is_innovation": False}, + {"type": "solar_pv", "is_innovation": True}, + ] + + funding.check_funding( + measures=measures_1, + starting_sap=54, + ending_sap=69, + floor_area=73, + mainheat_description="Boiler and radiators, mains gas", + heating_control_description="Programmer, room thermostat and TRVs", + is_cavity=True + )