From 925fcd082526dba4da5006eb4aa01162dae7b153 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Tue, 26 Mar 2024 14:42:56 +0000 Subject: [PATCH 1/8] Test client call on subscription save with and without settings set --- .../test_unit_hubspot_lead_tracking.py | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py b/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py index 109f06c80d97..be11dae846e2 100644 --- a/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py +++ b/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py @@ -212,3 +212,61 @@ def test_hubspot_with_existing_contact_and_new_organisation( # further hubspot resources. mock_create_company.assert_not_called() mock_create_contact.assert_not_called() + + +def test_update_company_active_subscription( + organisation: Organisation, + settings: SettingsWrapper, + mocker: MockerFixture, +) -> None: + settings.ENABLE_HUBSPOT_LEAD_TRACKING = True + mock_update_company = mocker.patch( + "integrations.lead_tracking.hubspot.client.HubspotClient.update_company" + ) + hubspot_id = "12345" + # Create an existing hubspot organisation to mimic a previous + # successful API call. + HubspotOrganisation.objects.create( + organisation=organisation, + hubspot_id=hubspot_id, + ) + + assert organisation.subscription.plan == "free" + + # When + organisation.subscription.plan = "scale-up-v2" + organisation.subscription.save() + + # Then + mock_update_company.assert_called_once_with( + active_subscription=organisation.subscription.plan, + hubspot_company_id=hubspot_id, + ) + + +def test_update_company_active_subscription_not_called( + organisation: Organisation, + settings: SettingsWrapper, + mocker: MockerFixture, +) -> None: + # Set to False to ensure update doesn't happen. + settings.ENABLE_HUBSPOT_LEAD_TRACKING = False + mock_update_company = mocker.patch( + "integrations.lead_tracking.hubspot.client.HubspotClient.update_company" + ) + hubspot_id = "12345" + # Create an existing hubspot organisation to mimic a previous + # successful API call. + HubspotOrganisation.objects.create( + organisation=organisation, + hubspot_id=hubspot_id, + ) + + assert organisation.subscription.plan == "free" + + # When + organisation.subscription.plan = "scale-up-v2" + organisation.subscription.save() + + # Then + mock_update_company.assert_not_called() From 28b091ff1c38a552b8c99cc8407aa2acd8799485 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Tue, 26 Mar 2024 14:43:56 +0000 Subject: [PATCH 2/8] Update hubspot when subscription plan changes --- api/organisations/models.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api/organisations/models.py b/api/organisations/models.py index 5b9ff1368d38..e9967161d83c 100644 --- a/api/organisations/models.py +++ b/api/organisations/models.py @@ -17,7 +17,10 @@ from simple_history.models import HistoricalRecords from app.utils import is_enterprise, is_saas -from integrations.lead_tracking.hubspot.tasks import track_hubspot_lead +from integrations.lead_tracking.hubspot.tasks import ( + track_hubspot_lead, + update_hubspot_active_subscription, +) from organisations.chargebee import ( get_customer_id_from_subscription_id, get_max_api_calls_for_plan, @@ -251,6 +254,13 @@ def can_auto_upgrade_seats(self) -> bool: def is_free_plan(self) -> bool: return self.plan == FREE_PLAN_ID + @hook(AFTER_SAVE, when="plan", has_changed=True) + def update_hubspot_active_subscription(self): + if not settings.ENABLE_HUBSPOT_LEAD_TRACKING: + return + + update_hubspot_active_subscription.delay(args=(self.id,)) + @hook(AFTER_SAVE, when="cancellation_date", has_changed=True) @hook(AFTER_SAVE, when="subscription_id", has_changed=True) def update_mailer_lite_subscribers(self): From 14730a2b66c6fd29a950cd5b8ff5098560587105 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Tue, 26 Mar 2024 14:44:54 +0000 Subject: [PATCH 3/8] Add client code for updating a company --- .../lead_tracking/hubspot/client.py | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/api/integrations/lead_tracking/hubspot/client.py b/api/integrations/lead_tracking/hubspot/client.py index 6e96528154ed..2e9db9cb5fd8 100644 --- a/api/integrations/lead_tracking/hubspot/client.py +++ b/api/integrations/lead_tracking/hubspot/client.py @@ -2,7 +2,10 @@ import hubspot from django.conf import settings -from hubspot.crm.companies import SimplePublicObjectInputForCreate +from hubspot.crm.companies import ( + SimplePublicObjectInput, + SimplePublicObjectInputForCreate, +) from hubspot.crm.contacts import BatchReadInputSimplePublicObjectId from users.models import FFAdminUser @@ -64,8 +67,11 @@ def create_contact(self, user: FFAdminUser, hubspot_company_id: str) -> dict: ) return response.to_dict() - def create_company(self, name: str) -> dict: - properties = {"name": name} + def create_company(self, name: str, active_subscription: str) -> dict: + properties = { + "name": name, + "active_subscription": active_subscription, + } simple_public_object_input_for_create = SimplePublicObjectInputForCreate( properties=properties, ) @@ -75,3 +81,16 @@ def create_company(self, name: str) -> dict: ) return response.to_dict() + + def update_company(self, active_subscription: str, hubspot_company_id: str) -> dict: + properties = { + "active_subscription": active_subscription, + } + simple_public_object_input = SimplePublicObjectInput(properties=properties) + + response = self.client.crm.companies.basic_api.update( + company_id=hubspot_company_id, + simple_public_object_input=simple_public_object_input, + ) + + return response.to_dict() From e145a7f47807b40b929a56dbfe37caa66a1ed336 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Tue, 26 Mar 2024 14:45:29 +0000 Subject: [PATCH 4/8] Add subscription tracking to lead tracker --- .../lead_tracking/hubspot/lead_tracker.py | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/api/integrations/lead_tracking/hubspot/lead_tracker.py b/api/integrations/lead_tracking/hubspot/lead_tracker.py index f6122ecc4678..32863f31d35d 100644 --- a/api/integrations/lead_tracking/hubspot/lead_tracker.py +++ b/api/integrations/lead_tracking/hubspot/lead_tracker.py @@ -3,7 +3,11 @@ from django.conf import settings from integrations.lead_tracking.lead_tracking import LeadTracker -from organisations.models import HubspotOrganisation, Organisation +from organisations.models import ( + HubspotOrganisation, + Organisation, + Subscription, +) from users.models import FFAdminUser from .client import HubspotClient @@ -65,14 +69,36 @@ def get_or_create_organisation_hubspot_id(self, organisation: Organisation) -> s if getattr(organisation, "hubspot_organisation", None): return organisation.hubspot_organisation.hubspot_id - response = self.client.create_company(name=organisation.name) + response = self.client.create_company( + name=organisation.name, + active_subscription=organisation.subscription.plan, + ) + # Store the organisation data in the database since we are # unable to look them up via a unique identifier. HubspotOrganisation.objects.create( organisation=organisation, hubspot_id=response["id"], ) + return response["id"] + def update_company_active_subscription(self, subscription: Subscription) -> dict: + if not subscription.plan: + return + + organisation = subscription.organisation + + # Check if we're missing the associated hubspot id. + if not getattr(organisation, "hubspot_organisation", None): + return + + response = self.client.update_company( + active_subscription=subscription.plan, + hubspot_company_id=organisation.hubspot_organisation.hubspot_id, + ) + + return response + def _get_client(self) -> HubspotClient: return HubspotClient() From b66c2a4660510404cef447ebdea6b8f8e1362c61 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Tue, 26 Mar 2024 14:46:11 +0000 Subject: [PATCH 5/8] Add update_hubspot_active_subscription task --- api/integrations/lead_tracking/hubspot/tasks.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api/integrations/lead_tracking/hubspot/tasks.py b/api/integrations/lead_tracking/hubspot/tasks.py index 2b737e82e256..e946f0f949da 100644 --- a/api/integrations/lead_tracking/hubspot/tasks.py +++ b/api/integrations/lead_tracking/hubspot/tasks.py @@ -22,3 +22,16 @@ def track_hubspot_lead(user_id: int, organisation_id: int) -> None: hubspot_lead_tracker = HubspotLeadTracker() hubspot_lead_tracker.create_lead(user=user, organisation=organisation) + + +@register_task_handler() +def update_hubspot_active_subscription(subscription_id: int) -> None: + assert settings.ENABLE_HUBSPOT_LEAD_TRACKING + + from organisations.models import Subscription + + from .lead_tracker import HubspotLeadTracker + + subscription = Subscription.objects.get(id=subscription_id) + hubspot_lead_tracker = HubspotLeadTracker() + hubspot_lead_tracker.update_company_active_subscription(subscription) From 86b7ed6638506b48c772e8c41800e3a28be6db3a Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Tue, 26 Mar 2024 15:23:14 +0000 Subject: [PATCH 6/8] Update call signature check --- .../lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py b/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py index be11dae846e2..63ba011fb31f 100644 --- a/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py +++ b/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py @@ -88,7 +88,9 @@ def test_hubspot_with_new_contact_and_new_organisation( # Then organisation.refresh_from_db() assert organisation.hubspot_organisation.hubspot_id == future_hubspot_id - mock_create_company.assert_called_once_with(name=organisation.name) + mock_create_company.assert_called_once_with( + name=organisation.name, active_subscription="free" + ) mock_create_contact.assert_called_once_with(user, future_hubspot_id) mock_get_contact.assert_called_once_with(user) From a0546225e4cebbac754ae906c04b94a2eeb553cb Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Wed, 27 Mar 2024 15:09:30 +0000 Subject: [PATCH 7/8] Explicitly rely on synchronous tasks --- .../lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py b/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py index 63ba011fb31f..eb76e4ecf9f0 100644 --- a/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py +++ b/api/tests/unit/integrations/lead_tracking/hubspot/test_unit_hubspot_lead_tracking.py @@ -8,6 +8,7 @@ Organisation, OrganisationRole, ) +from task_processor.task_run_method import TaskRunMethod from users.models import FFAdminUser @@ -222,6 +223,8 @@ def test_update_company_active_subscription( mocker: MockerFixture, ) -> None: settings.ENABLE_HUBSPOT_LEAD_TRACKING = True + settings.TASK_RUN_METHOD = TaskRunMethod.SYNCHRONOUSLY + mock_update_company = mocker.patch( "integrations.lead_tracking.hubspot.client.HubspotClient.update_company" ) @@ -253,6 +256,8 @@ def test_update_company_active_subscription_not_called( ) -> None: # Set to False to ensure update doesn't happen. settings.ENABLE_HUBSPOT_LEAD_TRACKING = False + settings.TASK_RUN_METHOD = TaskRunMethod.SYNCHRONOUSLY + mock_update_company = mocker.patch( "integrations.lead_tracking.hubspot.client.HubspotClient.update_company" ) From cc02f6dac43d9c841adb6fa62af1b48ab5927a3d Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Wed, 27 Mar 2024 15:10:10 +0000 Subject: [PATCH 8/8] Update to union type --- api/integrations/lead_tracking/hubspot/lead_tracker.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/integrations/lead_tracking/hubspot/lead_tracker.py b/api/integrations/lead_tracking/hubspot/lead_tracker.py index 32863f31d35d..a7201785fe46 100644 --- a/api/integrations/lead_tracking/hubspot/lead_tracker.py +++ b/api/integrations/lead_tracking/hubspot/lead_tracker.py @@ -83,7 +83,9 @@ def get_or_create_organisation_hubspot_id(self, organisation: Organisation) -> s return response["id"] - def update_company_active_subscription(self, subscription: Subscription) -> dict: + def update_company_active_subscription( + self, subscription: Subscription + ) -> dict | None: if not subscription.plan: return