diff --git a/backend/app/db/functions/recommendations_functions.py b/backend/app/db/functions/recommendations_functions.py new file mode 100644 index 00000000..b544b43b --- /dev/null +++ b/backend/app/db/functions/recommendations_functions.py @@ -0,0 +1,71 @@ +from sqlalchemy.orm import sessionmaker +from backend.app.db.connection import db_engine +from backend.app.db.models.recommendations import Plan, Recommendation, RecommendationMaterials + + +def create_plan(plan): + """ + This function will create a record for the plan in the database if it does not exist. + :param plan: dictionary of data representing a plan to be created + """ + + Session = sessionmaker(bind=db_engine) + with Session() as session: + new_plan = Plan(**plan) + session.add(new_plan) + session.commit() + + return new_plan.id + + +def create_recommendation(recommendation): + """ + This function will create a record for the recommendation in the database if it does not exist. + :param recommendation: dictionary of data representing a recommendation to be created + """ + + Session = sessionmaker(bind=db_engine) + with Session() as session: + new_recommendation = Recommendation(**recommendation) + session.add(new_recommendation) + session.commit() + + return new_recommendation.id + + +def create_recommendation_material(recommendation_id, material_id, depth): + """ + This function will create a record for the recommendation_material in the database if it does not exist. + :param recommendation_id: ID of the recommendation + :param material_id: ID of the material + :param depth: depth of the material, may be null if a material where depth is not applicable + """ + + Session = sessionmaker(bind=db_engine) + with Session() as session: + new_recommendation_material = RecommendationMaterials( + recommendation_id=recommendation_id, + material_id=material_id, + depth=depth + ) + session.add(new_recommendation_material) + session.commit() + + return new_recommendation_material.id + + +def create_plan_recommendations(plan_id, recommendation_ids): + """ + This function will create a record for the plan_recommendation in the database if it does not exist. + :param plan_id: ID of the plan + :param recommendation_ids: list of recommendation IDs + """ + + Session = sessionmaker(bind=db_engine) + with Session() as session: + for recommendation_id in recommendation_ids: + session.execute( + 'INSERT INTO plan_recommendations (plan_id, recommendation_id) VALUES (:plan_id, :recommendation_id)', + {'plan_id': plan_id, 'recommendation_id': recommendation_id} + ) + session.commit() diff --git a/backend/app/db/models/recommendations.py b/backend/app/db/models/recommendations.py new file mode 100644 index 00000000..66d1a063 --- /dev/null +++ b/backend/app/db/models/recommendations.py @@ -0,0 +1,55 @@ +from sqlalchemy import Column, BigInteger, String, Float, Boolean, TIMESTAMP, ForeignKey +from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy.sql import func +from backend.app.db.models.portfolio import Portfolio, PropertyModel +from backend.app.db.models.materials import Material + +Base = declarative_base() + + +class Recommendation(Base): + __tablename__ = 'recommendation' + + id = Column(BigInteger, primary_key=True, autoincrement=True) + property_id = Column(BigInteger, ForeignKey(PropertyModel.id), nullable=False) + created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + type = Column(String, nullable=False) + description = Column(String, nullable=False) + estimated_cost = Column(Float) + default = Column(Boolean, nullable=False) + starting_u_value = Column(Float) + new_u_value = Column(Float) + sap_points = Column(Float) + heat_demand = Column(Float) + co2_equivalent_savings = Column(Float) + energy_savings = Column(Float) + energy_cost_savings = Column(Float) + property_valuation_increase = Column(Float) + rental_yield_increase = Column(Float) + total_work_hours = Column(Float) + + +class RecommendationMaterials(Base): + __tablename__ = 'recommendation_materials' + + id = Column(BigInteger, primary_key=True, autoincrement=True) + recommendation_id = Column(BigInteger, ForeignKey('recommendation.id'), nullable=False) + material_id = Column(BigInteger, ForeignKey(Material.id), nullable=False) + created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + + +class Plan(Base): + __tablename__ = 'plan' + + id = Column(BigInteger, primary_key=True, autoincrement=True) + portfolio_id = Column(BigInteger, ForeignKey(Portfolio.id), nullable=False) + created_at = Column(TIMESTAMP, nullable=False, server_default=func.now()) + is_default = Column(Boolean, nullable=False) + + +class PlanRecommendations(Base): + __tablename__ = 'plan_recommendations' + + id = Column(BigInteger, primary_key=True, autoincrement=True) + plan_id = Column(BigInteger, ForeignKey('plan.id'), nullable=False) + recommendation_id = Column(BigInteger, ForeignKey('recommendation.id'), nullable=False) diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 5fbc5f1a..cb21009e 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -19,6 +19,9 @@ from backend.app.db.functions.property_functions import ( create_property, create_property_targets, update_property_data, create_property_details_epc ) from backend.app.db.functions.materials_functions import get_materials +from backend.app.db.functions.recommendations_functions import ( + create_plan, create_recommendation, create_recommendation_material, create_plan_recommendations +) # TODO: This is placeholder until data is stored in DB from backend.app.plan.uvalue_estimates_walls import uvalue_estimates_walls @@ -249,9 +252,52 @@ async def trigger_plan(body: PlanTriggerRequest): update_property_data(property_id=p.id, portfolio_id=body.portfolio_id, property_data=property_data) # Upload recommendations + + # TODO: We start off by optimising the recommendations + recommendations_to_upload = recommendations[p.id] if not recommendations: continue # Create a plan + new_plan_id = create_plan( + { + "portfolio_id": body.portfolio_id, + "is_default": True + } + ) + + # upload recommendations + uploaded_recommendation_ids = [] + for rec in recommendations_to_upload: + # TODO: implement costs (at least a placeholder) + estimated_cost = sum([x["cost"] if x["cost"] else 0 for x in rec["parts"]]) + + recommendation_id = create_recommendation( + { + "type": rec["type"], # TODO: Add this to output + "description": rec["description"], # TODO: Add this to output + "estimated_cost": estimated_cost, + "default": True, + "starting_u_value": rec.get("starting_u_value"), # TODO: Add this to output + "new_u_value": rec.get("new_u_value"), + "sap_points": rec["sap_points"] # TODO: Add this to output + # Remaining outputs yet to be handled + } + ) + uploaded_recommendation_ids.append(recommendation_id) + + # create the bridging between the recommendation and the materials + for part in rec["parts"]: + create_recommendation_material( + recommendation_id=recommendation_id, + material_id=part["id"], + depth=part["depths"][0] if part["depths"] else None, + ) + + # Finally, match the recommendation to the plan + create_plan_recommendations( + plan_id=new_plan_id, + recommendation_ids=uploaded_recommendation_ids + ) return Response(status_code=200)