From 44ed1bfabebace7929d0b83f9a6d1508896d08e2 Mon Sep 17 00:00:00 2001 From: Zach Aysan Date: Wed, 27 Mar 2024 12:53:03 -0400 Subject: [PATCH] feat: Add subscription to Hubspot tracker (#3676) --- .../lead_tracking/hubspot/client.py | 29 +++++++- .../lead_tracking/hubspot/lead_tracker.py | 28 +++++++- .../lead_tracking/hubspot/tasks.py | 13 ++++ api/organisations/models.py | 12 +++- .../test_unit_hubspot_lead_tracking.py | 67 ++++++++++++++++++- 5 files changed, 143 insertions(+), 6 deletions(-) diff --git a/api/integrations/lead_tracking/hubspot/client.py b/api/integrations/lead_tracking/hubspot/client.py index d6eb9aa2bbb1..8d0f9544fe8f 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,15 @@ def create_contact(self, user: FFAdminUser, hubspot_company_id: str) -> dict: ) return response.to_dict() - def create_company(self, name: str, organisation_id: int) -> dict: - properties = {"name": name, "orgid": str(organisation_id)} + def create_company( + self, name: str, active_subscription: str, organisation_id: int + ) -> dict: + properties = { + "name": name, + "active_subscription": active_subscription, + "orgid": str(organisation_id), + } + simple_public_object_input_for_create = SimplePublicObjectInputForCreate( properties=properties, ) @@ -75,3 +85,16 @@ def create_company(self, name: str, organisation_id: int) -> 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() diff --git a/api/integrations/lead_tracking/hubspot/lead_tracker.py b/api/integrations/lead_tracking/hubspot/lead_tracker.py index cc9ddd2c18f3..15722d01ea54 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 @@ -67,15 +71,37 @@ def get_or_create_organisation_hubspot_id(self, organisation: Organisation) -> s response = self.client.create_company( name=organisation.name, + active_subscription=organisation.subscription.plan, organisation_id=organisation.id, ) + # 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 | None: + 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() 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) 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): 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 692ed1353b73..8f16082d12d0 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 @@ -89,7 +90,9 @@ def test_hubspot_with_new_contact_and_new_organisation( organisation.refresh_from_db() assert organisation.hubspot_organisation.hubspot_id == future_hubspot_id mock_create_company.assert_called_once_with( - name=organisation.name, organisation_id=organisation.id + name=organisation.name, + active_subscription="free", + organisation_id=organisation.id, ) mock_create_contact.assert_called_once_with(user, future_hubspot_id) mock_get_contact.assert_called_once_with(user) @@ -214,3 +217,65 @@ 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 + settings.TASK_RUN_METHOD = TaskRunMethod.SYNCHRONOUSLY + + 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 + settings.TASK_RUN_METHOD = TaskRunMethod.SYNCHRONOUSLY + + 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()