diff --git a/etl/hubspot/hubspot_deal_differ.py b/etl/hubspot/hubspot_deal_differ.py index 2d47d5778..2842b735c 100644 --- a/etl/hubspot/hubspot_deal_differ.py +++ b/etl/hubspot/hubspot_deal_differ.py @@ -5,6 +5,9 @@ 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 utilities.logger import setup_logger + +logger = setup_logger() class HubspotDealDiffer: @@ -200,31 +203,59 @@ class HubspotDealDiffer: new_listing: Optional[Dict[str, str]], old_deal: HubspotDealData, ) -> Optional[Dict[str, Any]]: + logger.info( + "Evaluating Abri triggers for HubSpot deal %s " + "(project_code=%r, associated_project_id=%s)", + hubspot_deal_id, + new_deal.get("project_code"), + new_project["project_id"] if new_project is not None else None, + ) + amend_job = HubspotDealDiffer.check_for_abri_job_amendment( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ) + log_job = HubspotDealDiffer.check_for_abri_job_logging( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ) + sync_tenant_data = HubspotDealDiffer.check_for_abri_tenant_data_fetch( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ) + abandon_job = HubspotDealDiffer.check_for_abri_deal_abandonment( + new_deal=new_deal, new_project=new_project, old_deal=old_deal + ) + logger.info( + "Abri trigger results for HubSpot deal %s: " + "amend_job=%s log_job=%s sync_tenant_data=%s abandon_job=%s", + hubspot_deal_id, + amend_job, + log_job, + sync_tenant_data, + abandon_job, + ) + flows: List[str] = [] - if HubspotDealDiffer.check_for_abri_job_amendment( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): + if amend_job: flows.append("amend_job") - if HubspotDealDiffer.check_for_abri_job_logging( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): + if log_job: flows.append("log_job") - if HubspotDealDiffer.check_for_abri_tenant_data_fetch( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): + if sync_tenant_data: flows.append("sync_tenant_data") - if HubspotDealDiffer.check_for_abri_deal_abandonment( - new_deal=new_deal, new_project=new_project, old_deal=old_deal - ): + if abandon_job: flows.append("abandon_job") if not flows: + logger.info( + "No Abri flows triggered for HubSpot deal %s", hubspot_deal_id + ) return None + logger.info( + "Abri flows triggered for HubSpot deal %s: %s", hubspot_deal_id, flows + ) + confirmed_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) last_submission_date = parse_hs_date(new_deal.get("last_submission_date")) listing = new_listing or {} - return { + message: Dict[str, Any] = { "hubspot_deal_id": hubspot_deal_id, "flows": flows, "place_ref": listing.get("owner_property_id"), @@ -246,6 +277,12 @@ class HubspotDealDiffer: # the other flows, which ignore it. "outcome": new_deal.get("outcome"), } + logger.info( + "Constructed Abri message for HubSpot deal %s: %s", + hubspot_deal_id, + message, + ) + return message @staticmethod def check_for_abri_job_logging( @@ -256,10 +293,19 @@ class HubspotDealDiffer: if not HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project): return False + new_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) + logger.info( + "Abri job-logging check: old_confirmed_survey_date=%s " + "new_confirmed_survey_date=%s", + old_deal.confirmed_survey_date, + new_survey_date, + ) + + # Only a first-time survey date (previously empty) logs a new job. if old_deal.confirmed_survey_date is not None: return False - return parse_hs_date(new_deal.get("confirmed_survey_date")) is not None + return new_survey_date is not None @staticmethod def check_for_abri_job_amendment( @@ -270,6 +316,18 @@ class HubspotDealDiffer: if not HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project): return False + new_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) + new_survey_time = new_deal.get("confirmed_survey_time") + logger.info( + "Abri job-amendment check: " + "old_confirmed_survey_date=%s new_confirmed_survey_date=%s " + "old_confirmed_survey_time=%s new_confirmed_survey_time=%s", + old_deal.confirmed_survey_date, + new_survey_date, + old_deal.confirmed_survey_time, + new_survey_time, + ) + # A first-time survey date logs a new job, not an amendment. if old_deal.confirmed_survey_date is None: return False @@ -278,11 +336,10 @@ class HubspotDealDiffer: # check, but the amendoptiappt route cannot express a cancellation. # Cancellation semantics (the "delete" action / canceljob route) need # a deliberate decision before this trigger is wired to a consumer. - new_survey_date = parse_hs_date(new_deal.get("confirmed_survey_date")) if old_deal.confirmed_survey_date != new_survey_date: return True - return old_deal.confirmed_survey_time != new_deal.get("confirmed_survey_time") + return old_deal.confirmed_survey_time != new_survey_time @staticmethod def check_for_abri_tenant_data_fetch( @@ -293,10 +350,21 @@ class HubspotDealDiffer: if not HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project): return False + new_commencement_date = parse_hs_date( + new_deal.get("expected_commencement_date") + ) + logger.info( + "Abri tenant-data-fetch check: old_commencement_date=%s " + "new_commencement_date=%s", + old_deal.expected_commencement_date, + new_commencement_date, + ) + + # Only a first-time commencement date (previously empty) fires the fetch. if old_deal.expected_commencement_date is not None: return False - return parse_hs_date(new_deal.get("expected_commencement_date")) is not None + return new_commencement_date is not None @staticmethod def check_for_abri_deal_abandonment( @@ -307,6 +375,19 @@ class HubspotDealDiffer: if not HubspotDealDiffer._is_abri_condition_deal(new_deal, new_project): return False + logger.info( + "Abri deal-abandonment check: " + "old_number_of_attempts=%s old_outcome=%r " + "new_number_of_attempts=%s new_outcome=%r " + "attempts_threshold=%s", + old_deal.number_of_attempts, + old_deal.outcome, + new_deal.get("number_of_attempts"), + new_deal.get("outcome"), + HubspotDealDiffer.ABANDONMENT_ATTEMPTS_THRESHOLD, + ) + + # Already abandoned before this change — nothing new to fire. if HubspotDealDiffer._is_abandoned( old_deal.number_of_attempts, old_deal.outcome ): @@ -333,19 +414,43 @@ class HubspotDealDiffer: attempts is None or attempts < HubspotDealDiffer.ABANDONMENT_ATTEMPTS_THRESHOLD ): + logger.info( + "Abri _is_abandoned=False (attempts gate): " + "raw_attempts=%r parsed_attempts=%s threshold=%s", + number_of_attempts, + attempts, + HubspotDealDiffer.ABANDONMENT_ATTEMPTS_THRESHOLD, + ) return False - return (outcome or "").lower() in UNSUCCESSFUL_OUTCOMES + outcome_is_unsuccessful = (outcome or "").lower() in UNSUCCESSFUL_OUTCOMES + logger.info( + "Abri _is_abandoned=%s (outcome gate): " + "parsed_attempts=%s outcome=%r outcome_is_unsuccessful=%s", + outcome_is_unsuccessful, + attempts, + outcome, + outcome_is_unsuccessful, + ) + return outcome_is_unsuccessful @staticmethod def _is_abri_condition_deal( new_deal: Dict[str, str], new_project: Optional[ProjectData] ) -> bool: if new_project is not None: - return ( + is_match = ( new_project["project_id"] == HubspotDealDiffer.ABRI_CONDITION_ASSOCIATED_PROJECT_ID ) + logger.info( + "Abri condition-deal check via associated project: " + "project_id=%s expected=%s match=%s", + new_project["project_id"], + HubspotDealDiffer.ABRI_CONDITION_ASSOCIATED_PROJECT_ID, + is_match, + ) + return is_match # TEST_ABRI_PROJECT_CODE_OVERRIDE lets a non-production deploy fire on a # sandbox deal's own project code; unset (the production default) falls @@ -355,7 +460,15 @@ class HubspotDealDiffer: or HubspotDealDiffer.ABRI_CONDITION_PROJECT_CODE ) new_project_code = (new_deal.get("project_code") or "").lower() - return new_project_code == expected_code.lower() + is_match = new_project_code == expected_code.lower() + logger.info( + "Abri condition-deal check via project_code: " + "new_project_code=%r expected=%r match=%s", + new_project_code, + expected_code.lower(), + is_match, + ) + return is_match @staticmethod def _has_valid_pashub_link(new_pashub_link: str) -> bool: