feat(modelling): cut plan→recommendation readers onto plan_id

Rewrite the three structurally-identical m2m-join readers
(portfolio_functions.aggregate_portfolio_recommendations,
Outputs.get_recommendations_from_db, export get_recommendations) to join
PlanModel directly via recommendation.plan_id, dropping the plan_recommendations
join and its now-unused import. The writers set plan_id (prior slice), so the
rows resolve. test_export pins the export reader through the cut (its fixtures
now set recommendation.plan_id). A portfolio_functions DB characterization test
lands with the scenario consolidation (which provides the full-parity scenario
table the aggregation writes to).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 21:09:43 +00:00
parent 27fcc5b184
commit af5dbe325d
4 changed files with 14 additions and 24 deletions

View file

@ -11,7 +11,6 @@ from backend.app.db.models.portfolio import PropertyModel, PropertyDetailsEpcMod
from backend.app.db.models.recommendations import ( from backend.app.db.models.recommendations import (
Recommendation, Recommendation,
PlanModel, PlanModel,
PlanRecommendations,
) )
@ -124,20 +123,15 @@ class Outputs:
return plans_data return plans_data
def get_recommendations_from_db(self, plan_ids): def get_recommendations_from_db(self, plan_ids):
# Get recommendations through PlanRecommendations for those plans and that are default # Get default recommendations for those plans, linked by recommendation.plan_id
recommendations_query = ( recommendations_query = (
self.session.query(Recommendation, PlanModel.scenario_id) self.session.query(Recommendation, PlanModel.scenario_id)
.join(
PlanRecommendations,
Recommendation.id == PlanRecommendations.recommendation_id,
)
.join( .join(
PlanModel, PlanModel,
PlanModel.id PlanModel.id == Recommendation.plan_id, # access scenario_id
== PlanRecommendations.plan_id, # Join with Plan to access scenario_id
) )
.filter( .filter(
PlanRecommendations.plan_id.in_(plan_ids), Recommendation.plan_id.in_(plan_ids),
Recommendation.default == True, # Filtering for default recommendations Recommendation.default == True, # Filtering for default recommendations
) )
.all() .all()

View file

@ -1,7 +1,6 @@
from sqlalchemy import func from sqlalchemy import func
from backend.app.db.models.recommendations import ( from backend.app.db.models.recommendations import (
PlanModel, PlanModel,
PlanRecommendations,
Recommendation, Recommendation,
ScenarioModel, ScenarioModel,
) )
@ -26,11 +25,7 @@ def aggregate_portfolio_recommendations(
), ),
func.sum(Recommendation.energy_cost_savings).label("energy_cost_savings"), func.sum(Recommendation.energy_cost_savings).label("energy_cost_savings"),
) )
.join( .join(PlanModel, PlanModel.id == Recommendation.plan_id)
PlanRecommendations,
PlanRecommendations.recommendation_id == Recommendation.id,
)
.join(PlanModel, PlanModel.id == PlanRecommendations.plan_id)
.filter( .filter(
PlanModel.portfolio_id == portfolio_id, PlanModel.portfolio_id == portfolio_id,
PlanModel.scenario_id == scenario_id, PlanModel.scenario_id == scenario_id,

View file

@ -8,7 +8,6 @@ from collections import defaultdict
from backend.app.db.models.recommendations import ( from backend.app.db.models.recommendations import (
Recommendation, Recommendation,
PlanModel, PlanModel,
PlanRecommendations,
RecommendationMaterials, RecommendationMaterials,
) )
from backend.app.db.models.portfolio import ( from backend.app.db.models.portfolio import (
@ -157,13 +156,9 @@ class DbMethods:
stmt = ( stmt = (
select(Recommendation, PlanModel.scenario_id, PlanModel.name) select(Recommendation, PlanModel.scenario_id, PlanModel.name)
.join( .join(PlanModel, PlanModel.id == Recommendation.plan_id)
PlanRecommendations,
Recommendation.id == PlanRecommendations.recommendation_id,
)
.join(PlanModel, PlanModel.id == PlanRecommendations.plan_id)
.where( .where(
PlanRecommendations.plan_id.in_(plan_ids), Recommendation.plan_id.in_(plan_ids),
Recommendation.default.is_(True), Recommendation.default.is_(True),
Recommendation.already_installed.is_(False), Recommendation.already_installed.is_(False),
) )

View file

@ -171,13 +171,17 @@ def test_default_export_integration(db_session):
# 5) Insert recommendation # 5) Insert recommendation
# ---------------------------------------- # ----------------------------------------
rec_to_plan = dict(
zip(plan_recs_df["recommendation_id"], plan_recs_df["plan_id"])
)
recs = [ recs = [
Recommendation( Recommendation(
plan_id=rec_to_plan.get(row["id"]),
**{ **{
col: row[col] col: row[col]
for col in Recommendation.__table__.columns.keys() for col in Recommendation.__table__.columns.keys()
if col in row if col in row and col != "plan_id"
} },
) )
for _, row in recommendations_df.iterrows() for _, row in recommendations_df.iterrows()
] ]
@ -607,9 +611,11 @@ def test_solar_with_battery_example(db_session):
# ------------------------------------------------- # -------------------------------------------------
recommendations_df.loc[0, "measure_type"] = "solar_pv" recommendations_df.loc[0, "measure_type"] = "solar_pv"
rec_to_plan = dict(zip(plan_recs_df.recommendation_id, plan_recs_df.plan_id))
for row in recommendations_df.itertuples(index=False): for row in recommendations_df.itertuples(index=False):
rec = Recommendation( rec = Recommendation(
id=row.id, id=row.id,
plan_id=rec_to_plan.get(row.id),
property_id=row.property_id, property_id=row.property_id,
measure_type=row.measure_type, measure_type=row.measure_type,
estimated_cost=row.estimated_cost, estimated_cost=row.estimated_cost,