diff --git a/backend/app/db/models/hubspot_user.py b/backend/app/db/models/hubspot_user.py new file mode 100644 index 00000000..424a0c17 --- /dev/null +++ b/backend/app/db/models/hubspot_user.py @@ -0,0 +1,13 @@ +from sqlmodel import SQLModel, Field +from datetime import datetime +from typing import Optional + + +class HubspotUser(SQLModel, table=True): + __tablename__ = "hubspot_users" + + hubspot_owner_id: str = Field(primary_key=True) + first_name: Optional[str] = Field(default=None) + last_name: Optional[str] = Field(default=None) + email: Optional[str] = Field(default=None) + updated_at: datetime diff --git a/etl/hubspot/hubspotClient.py b/etl/hubspot/hubspotClient.py index b24b1db4..92a6c7e1 100644 --- a/etl/hubspot/hubspotClient.py +++ b/etl/hubspot/hubspotClient.py @@ -255,13 +255,13 @@ class HubspotClient: "dampmould_growth", "damp_mould_and_repairs_comments", "pre_sap_score_dropdown", - "assigned_coordinator", + "coordinator_user", "mtp_completion_date", "mtp_re_model_completion_date", "ioe_v3_completion_date", "proposed_measures_dropdown", "approved_package", - "assigned_designer", + "designer_user", "design_completion_date", "actual_measures_installed", "installer", @@ -300,6 +300,20 @@ class HubspotClient: deal_info: dict[str, str] = cast(dict[str, str], deal.properties) # type: ignore[reportUnknownMemberType] return deal_info + def get_owner_info(self, owner_id: str) -> Optional[dict[str, Optional[str]]]: + try: + owner = self._call_with_retry( + lambda: self.client.crm.owners.owners_api.get_by_id(owner_id) # type: ignore[reportUnknownMemberType] + ) + return { + "first_name": owner.first_name, # type: ignore[reportUnknownMemberType] + "last_name": owner.last_name, # type: ignore[reportUnknownMemberType] + "email": getattr(owner, "email", None), + } + except Exception: + self.logger.warning(f"Failed to fetch HubSpot owner {owner_id}") + return None + def get_deal_and_company_and_listing( self, deal_id: str ) -> tuple[dict[str, str], Optional[str], Optional[dict[str, str]]]: diff --git a/etl/hubspot/hubspotDataTodB.py b/etl/hubspot/hubspotDataTodB.py index e6cb5250..b63f6d28 100644 --- a/etl/hubspot/hubspotDataTodB.py +++ b/etl/hubspot/hubspotDataTodB.py @@ -1,9 +1,10 @@ import os -from sqlmodel import select +from sqlmodel import select, Session from datetime import datetime, timezone from typing import Dict, Optional from backend.app.db.models.hubspot_deal_data import HubspotDealData +from backend.app.db.models.hubspot_user import HubspotUser from etl.hubspot.company_data import CompanyData from etl.hubspot.hubspotClient import HubspotClient from etl.hubspot.s3_uploader import S3Uploader @@ -12,7 +13,6 @@ from backend.app.db.models.organisation import Organisation from etl.hubspot.utils import parse_hs_bool, parse_hs_date from utils.logger import setup_logger - logger = setup_logger() @@ -95,6 +95,16 @@ class HubspotDataToDb: with db_read_session() as session: deal_id = deal_data.get("hs_object_id") + resolved_deal_data = { + **deal_data, + "coordinator_user": self._resolve_owner_name( + deal_data.get("coordinator_user"), hubspot_client, session + ), + "designer_user": self._resolve_owner_name( + deal_data.get("designer_user"), hubspot_client, session + ), + } + statement = select(HubspotDealData).where( HubspotDealData.deal_id == deal_id ) @@ -104,7 +114,7 @@ class HubspotDataToDb: self._handle_existing_photo_upload(existing, hubspot_client) print(f"🔄 Updating existing deal (deal_id={deal_id})") - self._update_existing_deal(existing, deal_data, listing, company) + self._update_existing_deal(existing, resolved_deal_data, listing, company) session.add(existing) session.commit() @@ -114,7 +124,7 @@ class HubspotDataToDb: else: print(f"🆕 Inserting new deal (deal_id={deal_id})") new_record: HubspotDealData = self._build_new_deal( - deal_id, deal_data, listing, company + deal_id, resolved_deal_data, listing, company ) # Handle upload at insert time @@ -125,6 +135,45 @@ class HubspotDataToDb: session.refresh(new_record) return new_record + def _resolve_owner_name( + self, + owner_id: Optional[str], + hubspot_client: HubspotClient, + session: Session, + ) -> Optional[str]: + if not owner_id: + return None + + existing: Optional[HubspotUser] = session.get(HubspotUser, owner_id) + owner_info = hubspot_client.get_owner_info(owner_id) + + if owner_info is None: + if existing: + return f"{existing.first_name or ''} {existing.last_name or ''}".strip() or None + return None + + now = datetime.now(timezone.utc) + if existing: + existing.first_name = owner_info["first_name"] + existing.last_name = owner_info["last_name"] + existing.email = owner_info["email"] + existing.updated_at = now + session.add(existing) + else: + session.add( + HubspotUser( + hubspot_owner_id=owner_id, + first_name=owner_info["first_name"], + last_name=owner_info["last_name"], + email=owner_info["email"], + updated_at=now, + ) + ) + + first = owner_info["first_name"] or "" + last = owner_info["last_name"] or "" + return f"{first} {last}".strip() or None + def _update_existing_deal( self, existing: HubspotDealData, @@ -170,7 +219,7 @@ class HubspotDataToDb: "ei_score__potential_": deal_data.get("ei_score__potential_"), "epc_sap_score": deal_data.get("epc_sap_score"), "epc_sap_score__potential_": deal_data.get("epc_sap_score__potential_"), - "coordinator": deal_data.get("assigned_coordinator"), + "coordinator": deal_data.get("coordinator_user"), "mtp_completion_date": parse_hs_date(deal_data.get("mtp_completion_date")), "mtp_re_model_completion_date": parse_hs_date( deal_data.get("mtp_re_model_completion_date") @@ -180,7 +229,7 @@ class HubspotDataToDb: ), "proposed_measures": deal_data.get("proposed_measures_dropdown"), "approved_package": deal_data.get("approved_package"), - "designer": deal_data.get("assigned_designer"), + "designer": deal_data.get("designer_user"), "design_completion_date": parse_hs_date( deal_data.get("design_completion_date") ), @@ -267,7 +316,7 @@ class HubspotDataToDb: ei_score__potential_=deal_data.get("ei_score__potential_"), epc_sap_score=deal_data.get("epc_sap_score"), epc_sap_score__potential_=deal_data.get("epc_sap_score__potential_"), - coordinator=deal_data.get("assigned_coordinator"), + coordinator=deal_data.get("coordinator_user"), mtp_completion_date=parse_hs_date(deal_data.get("mtp_completion_date")), mtp_re_model_completion_date=parse_hs_date( deal_data.get("mtp_re_model_completion_date") @@ -277,7 +326,7 @@ class HubspotDataToDb: ), proposed_measures=deal_data.get("proposed_measures_dropdown"), approved_package=deal_data.get("approved_package"), - designer=deal_data.get("assigned_designer"), + designer=deal_data.get("designer_user"), design_completion_date=parse_hs_date( deal_data.get("design_completion_date") ), diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 6a9f9971..9e7069fc 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -71,10 +71,10 @@ class HubspotDealDiffer: "ei_score__potential_": "ei_score__potential_", "epc_sap_score": "epc_sap_score", "epc_sap_score__potential_": "epc_sap_score__potential_", - "assigned_coordinator": "coordinator", + "coordinator_user": "coordinator", "proposed_measures_dropdown": "proposed_measures", "approved_package": "approved_package", - "assigned_designer": "designer", + "designer_user": "designer", "actual_measures_installed": "actual_measures_installed", "installer": "installer", "installer_handover": "installer_handover",