Merge pull request #1715 from Hestia-Homes/feature/hubspot-etl-epc-lodged-deal

Sync the epc_lodged_deal Yes/No dropdown into the HubSpot deal ETL
This commit is contained in:
Jun-te Kim 2026-07-30 17:26:53 +01:00 committed by GitHub
commit 0b433ac378
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 137 additions and 2 deletions

View file

@ -50,6 +50,9 @@ class HubspotDealData(SQLModel, table=True):
block_reference: Optional[str] = Field(default=None)
nonfunded_measures: Optional[str] = Field(default=None)
epc_prn: Optional[str] = Field(default=None)
# HubSpot carries this as a "Yes"/"No" dropdown (property id
# epc_lodged_deal); stored as a bool, with None for "not answered".
epc_lodged_deal: Optional[bool] = Field(default=None)
potential_post_sap_score_dropdown: Optional[str] = Field(default=None)
ei_score: Optional[str] = Field(default=None)
ei_score__potential_: Optional[str] = Field(default=None)

View file

@ -303,6 +303,7 @@ class HubspotClient:
"block_reference",
"nonfunded_measures",
"epc_prn",
"epc_lodged_deal",
"potential_post_sap_score_dropdown",
"ei_score",
"ei_score__potential_",

View file

@ -12,7 +12,7 @@ from etl.hubspot.project_data import ProjectData
from etl.hubspot.s3_uploader import S3Uploader
from backend.app.db.connection import db_read_session
from backend.app.db.models.organisation import Organisation
from etl.hubspot.utils import parse_hs_bool, parse_hs_date
from etl.hubspot.utils import parse_hs_bool, parse_hs_date, parse_hs_yes_no
from utils.logger import setup_logger
logger = setup_logger()
@ -238,6 +238,7 @@ class HubspotDataToDb:
"block_reference": deal_data.get("block_reference"),
"nonfunded_measures": deal_data.get("nonfunded_measures"),
"epc_prn": deal_data.get("epc_prn"),
"epc_lodged_deal": parse_hs_yes_no(deal_data.get("epc_lodged_deal")),
"potential_post_sap_score_dropdown": deal_data.get(
"potential_post_sap_score_dropdown"
),
@ -363,6 +364,7 @@ class HubspotDataToDb:
block_reference=deal_data.get("block_reference"),
nonfunded_measures=deal_data.get("nonfunded_measures"),
epc_prn=deal_data.get("epc_prn"),
epc_lodged_deal=parse_hs_yes_no(deal_data.get("epc_lodged_deal")),
potential_post_sap_score_dropdown=deal_data.get(
"potential_post_sap_score_dropdown"
),

View file

@ -4,7 +4,12 @@ from typing import Any, Dict, List, Optional
from backend.app.db.models.hubspot_deal_data import HubspotDealData
from domain.abri.models import UNSUCCESSFUL_OUTCOMES
from etl.hubspot.project_data import ProjectData
from etl.hubspot.utils import parse_hs_bool, parse_hs_date, parse_hs_int
from etl.hubspot.utils import (
parse_hs_bool,
parse_hs_date,
parse_hs_int,
parse_hs_yes_no,
)
from utilities.logger import setup_logger
logger = setup_logger()
@ -167,6 +172,18 @@ class HubspotDealDiffer:
if old_value != new_value:
return True
# --- Yes/No dropdown fields ---
yes_no_fields = [
("epc_lodged_deal", "epc_lodged_deal"),
]
for hs_field, db_field in yes_no_fields:
old_value = getattr(old_deal, db_field)
new_value = parse_hs_yes_no(new_deal.get(hs_field))
if old_value != new_value:
return True
# --- Time field ---
if old_deal.confirmed_survey_time != new_deal.get("confirmed_survey_time"):
return True

View file

@ -195,3 +195,65 @@ def test_update_existing_deal__planning_fields_overwritten() -> None:
assert existing.planning_comments == "New Comments"
assert existing.planning_status == "New Status"
assert existing.planning_suggested_approach == "New Approach"
def test_build_new_deal__epc_lodged_yes__is_true() -> None:
new_deal = _make_instance()._build_new_deal(
deal_id="MOCK_DEAL_ID",
deal_data={"epc_lodged_deal": "Yes"},
listing=None,
company=None,
project=None,
)
assert new_deal.epc_lodged_deal is True
def test_build_new_deal__epc_lodged_no__is_false() -> None:
new_deal = _make_instance()._build_new_deal(
deal_id="MOCK_DEAL_ID",
deal_data={"epc_lodged_deal": "No"},
listing=None,
company=None,
project=None,
)
assert new_deal.epc_lodged_deal is False
def test_build_new_deal__epc_lodged_unset__is_none() -> None:
new_deal = _make_instance()._build_new_deal(
deal_id="MOCK_DEAL_ID",
deal_data={},
listing=None,
company=None,
project=None,
)
assert new_deal.epc_lodged_deal is None
def test_update_existing_deal__epc_lodged_no__overwrites_true() -> None:
existing = HubspotDealData(deal_id="MOCK_DEAL_ID", epc_lodged_deal=True)
_make_instance()._update_existing_deal(
existing=existing,
deal_data={"epc_lodged_deal": "No"},
listing=None,
company=None,
)
assert existing.epc_lodged_deal is False
def test_update_existing_deal__epc_lodged_cleared__nulls_the_flag() -> None:
existing = HubspotDealData(deal_id="MOCK_DEAL_ID", epc_lodged_deal=True)
_make_instance()._update_existing_deal(
existing=existing,
deal_data={"epc_lodged_deal": ""},
listing=None,
company=None,
)
assert existing.epc_lodged_deal is None

View file

@ -1181,6 +1181,38 @@ def test_db_update_trigger__no_changes__returns_false() -> None:
assert result is False
def test_db_update_trigger__epc_lodged_unchanged__returns_false() -> None:
deal_id = uuid.uuid4()
old_deal = make_old_deal(id=deal_id, epc_lodged_deal=True)
new_deal = make_new_deal(deal_id, hs_object_id="1", epc_lodged_deal="Yes")
result = HubspotDealDiffer.check_for_db_update_trigger(
new_deal=new_deal,
new_company=None,
new_listing=None,
old_deal=old_deal,
)
assert result is False
def test_db_update_trigger__epc_lodged_flipped_to_no__returns_true() -> None:
deal_id = uuid.uuid4()
old_deal = make_old_deal(id=deal_id, epc_lodged_deal=True)
new_deal = make_new_deal(deal_id, hs_object_id="1", epc_lodged_deal="No")
result = HubspotDealDiffer.check_for_db_update_trigger(
new_deal=new_deal,
new_company=None,
new_listing=None,
old_deal=old_deal,
)
assert result is True
def test_db_update_trigger__dealname_changed__returns_true() -> None:
deal_id = uuid.uuid4()

View file

@ -31,3 +31,21 @@ def parse_hs_bool(value: Optional[str]) -> Optional[bool]:
if isinstance(value, bool):
return value
return str(value).strip().lower() == "true"
def parse_hs_yes_no(value: Optional[str]) -> Optional[bool]:
"""Reads a HubSpot 'Yes'/'No' dropdown as a bool.
Distinct from parse_hs_bool, which reads a HubSpot checkbox
("true"/"false") and would call "Yes" false. Anything that is neither
yes nor no unset, blank, or an option we don't know — is None rather
than False, so "not answered" never reads as "answered No".
"""
if value is None:
return None
normalised = str(value).strip().lower()
if normalised == "yes":
return True
if normalised == "no":
return False
return None