diff --git a/infrastructure/postgres/product_table.py b/infrastructure/postgres/product_table.py index b353b3006..fe2639d17 100644 --- a/infrastructure/postgres/product_table.py +++ b/infrastructure/postgres/product_table.py @@ -22,3 +22,6 @@ class MaterialRow(SQLModel, table=True): cost_unit: Optional[str] = Field(default=None) description: Optional[str] = Field(default=None) is_active: bool = Field(default=True) + # Whether a solar_pv Option's product bundle includes a battery; the Scenario + # Export reads it to pivot solar-with-battery to its own column (ADR-0065). + includes_battery: bool = Field(default=False) diff --git a/tests/repositories/scenario_export/test_scenario_export_postgres_repository.py b/tests/repositories/scenario_export/test_scenario_export_postgres_repository.py index 68e3dfa8f..a6199f32f 100644 --- a/tests/repositories/scenario_export/test_scenario_export_postgres_repository.py +++ b/tests/repositories/scenario_export/test_scenario_export_postgres_repository.py @@ -8,7 +8,11 @@ from sqlalchemy import Engine from sqlmodel import Session from datatypes.epc.domain.epc import Epc -from infrastructure.postgres.modelling import PlanModel # registers `plan` +from infrastructure.postgres.modelling import ( # registers `plan` + `recommendation` + PlanModel, + RecommendationModel, +) +from infrastructure.postgres.product_table import MaterialRow # registers `material` from infrastructure.postgres.property_table import PropertyRow # registers `property` from repositories.scenario_export.scenario_export_postgres_repository import ( ScenarioExportPostgresRepository, @@ -91,3 +95,91 @@ def test_excludes_requested_properties_without_a_default_plan_under_the_scenario # assert — only #1 (a default Plan under scenario 5) survives (model A). assert [r.property_id for r in rows] == [1] + + +def test_maps_the_default_plans_selected_measures_with_battery_and_exclusions( + db_engine: Engine, +) -> None: + # arrange — a default Plan with two selected measures (one solar_pv whose + # material carries a battery), plus a non-default and an already-installed + # recommendation that must be excluded. + with Session(db_engine) as session: + session.add(PropertyRow(id=1, portfolio_id=100, landlord_property_id="LP1")) + session.add( + PlanModel( + id=10, portfolio_id=100, property_id=1, scenario_id=5, is_default=True + ) + ) + session.add(MaterialRow(id=1, type="loft_insulation", includes_battery=False)) + session.add(MaterialRow(id=2, type="solar_pv", includes_battery=True)) + session.add( + RecommendationModel( + plan_id=10, + property_id=1, + type="loft_insulation", + measure_type="loft_insulation", + description="Loft insulation", + estimated_cost=1200.0, + sap_points=3.0, + co2_equivalent_savings=0.4, + kwh_savings=900.0, + energy_cost_savings=120.0, + material_id=1, + default=True, + already_installed=False, + ) + ) + session.add( + RecommendationModel( + plan_id=10, + property_id=1, + type="solar_pv", + measure_type="solar_pv", + description="Solar PV with battery", + estimated_cost=6000.0, + sap_points=5.0, + material_id=2, + default=True, + already_installed=False, + ) + ) + session.add( + RecommendationModel( + plan_id=10, + property_id=1, + type="double_glazing", + measure_type="double_glazing", + description="Double glazing (not selected)", + estimated_cost=4000.0, + default=False, + already_installed=False, + ) + ) + session.add( + RecommendationModel( + plan_id=10, + property_id=1, + type="cavity_wall_insulation", + measure_type="cavity_wall_insulation", + description="CWI (already installed)", + estimated_cost=800.0, + default=True, + already_installed=True, + ) + ) + session.commit() + + # act + with Session(db_engine) as session: + rows = ScenarioExportPostgresRepository(session).rows_for( + portfolio_id=100, scenario_id=5, property_ids=[1] + ) + + # assert — only the two selected measures map through, the battery flag is + # resolved from the material, and savings are carried per measure. + by_type = {m.measure_type: m for m in rows[0].measures} + assert set(by_type) == {"loft_insulation", "solar_pv"} + assert by_type["loft_insulation"].estimated_cost == 1200.0 + assert by_type["loft_insulation"].kwh_savings == 900.0 + assert by_type["loft_insulation"].includes_battery is False + assert by_type["solar_pv"].includes_battery is True