|
1 | 1 | import logging |
| 2 | +import time |
| 3 | +from typing import Any |
2 | 4 |
|
3 | 5 | from django.conf import settings |
4 | 6 |
|
@@ -44,88 +46,147 @@ def should_track(user: FFAdminUser) -> bool: |
44 | 46 |
|
45 | 47 | return True |
46 | 48 |
|
47 | | - def create_lead(self, user: FFAdminUser, organisation: Organisation = None) -> None: # type: ignore[assignment] |
48 | | - contact_data = self.client.get_contact(user) |
| 49 | + def update_company_active_subscription( |
| 50 | + self, subscription: Subscription |
| 51 | + ) -> dict[str, Any] | None: |
| 52 | + if not subscription.plan: |
| 53 | + return None |
49 | 54 |
|
50 | | - if contact_data: |
51 | | - # The user is already present in the system as a lead |
52 | | - # for an existing organisation, so return early. |
53 | | - return |
| 55 | + organisation = subscription.organisation |
54 | 56 |
|
55 | | - hubspot_id = self.get_or_create_organisation_hubspot_id(user, organisation) |
| 57 | + # Check if we're missing the associated hubspot id. |
| 58 | + if not getattr(organisation, "hubspot_organisation", None): |
| 59 | + return None |
| 60 | + |
| 61 | + response: dict[str, Any] | None = self.client.update_company( |
| 62 | + active_subscription=subscription.plan, |
| 63 | + hubspot_company_id=organisation.hubspot_organisation.hubspot_id, |
| 64 | + ) |
56 | 65 |
|
57 | | - response = self.client.create_contact(user, hubspot_id) |
| 66 | + return response |
58 | 67 |
|
| 68 | + def create_user_hubspot_contact(self, user: FFAdminUser) -> str | None: |
| 69 | + tracker = HubspotTracker.objects.filter(user=user).first() |
| 70 | + tracker_cookie = tracker.hubspot_cookie if tracker else None |
| 71 | + self.client.create_lead_form(user=user, hubspot_cookie=tracker_cookie) |
| 72 | + |
| 73 | + # Create lead form creates a contact asynchronously in hubspot but does not return the contact id |
| 74 | + # We need to get the contact id separately and retry 3 times |
| 75 | + contact = self._get_new_contact_with_retry(user, max_retries=3) |
| 76 | + if not contact: |
| 77 | + # Hubspot creates contact asynchronously |
| 78 | + # If not available on the spot, following steps will sync database with Hubspot |
| 79 | + logger.error(f"Failed to create contact for user {user.email}") |
| 80 | + return None |
| 81 | + |
| 82 | + hubspot_contact_id = contact.get("id") |
59 | 83 | HubspotLead.objects.update_or_create( |
60 | | - user=user, defaults={"hubspot_id": response["id"]} |
| 84 | + user=user, defaults={"hubspot_id": hubspot_contact_id} |
61 | 85 | ) |
62 | 86 |
|
63 | | - if tracker := HubspotTracker.objects.filter(user=user).first(): |
64 | | - self.client.create_lead_form( |
65 | | - user=user, hubspot_cookie=tracker.hubspot_cookie |
66 | | - ) |
| 87 | + return hubspot_contact_id |
| 88 | + |
| 89 | + def create_lead(self, user: FFAdminUser, organisation: Organisation) -> None: |
| 90 | + hubspot_contact_id = self._get_or_create_user_hubspot_id(user) |
| 91 | + if not hubspot_contact_id: |
| 92 | + return |
| 93 | + hubspot_org_id = self._get_or_create_organisation_hubspot_id(user, organisation) |
| 94 | + if not hubspot_org_id: |
| 95 | + return |
| 96 | + |
| 97 | + self.client.associate_contact_to_company( |
| 98 | + contact_id=hubspot_contact_id, |
| 99 | + company_id=hubspot_org_id, |
| 100 | + ) |
67 | 101 |
|
68 | | - def get_or_create_organisation_hubspot_id( |
| 102 | + def _get_new_contact_with_retry( |
| 103 | + self, user: FFAdminUser, max_retries: int = 3 |
| 104 | + ) -> dict[str, Any] | None: |
| 105 | + for retry in range(max_retries + 1): |
| 106 | + contact = self.client.get_contact(user) |
| 107 | + if contact: |
| 108 | + return contact # type: ignore[no-any-return] |
| 109 | + time.sleep(0.5 * retry) # 3 retries: 0.5s, 1s, 1.5s |
| 110 | + return None |
| 111 | + |
| 112 | + def _get_or_create_user_hubspot_id(self, user: FFAdminUser) -> str | None: |
| 113 | + hubspot_lead = HubspotLead.objects.filter(user=user).first() |
| 114 | + if hubspot_lead: |
| 115 | + hubspot_contact_id: str | None = hubspot_lead.hubspot_id |
| 116 | + else: |
| 117 | + # Fallback to sync database with Hubspot if contact hubspot_id was not saved |
| 118 | + contact_data = self.client.get_contact(user) |
| 119 | + if contact_data: |
| 120 | + hubspot_contact_id = contact_data["id"] |
| 121 | + HubspotLead.objects.update_or_create( |
| 122 | + user=user, defaults={"hubspot_id": hubspot_contact_id} |
| 123 | + ) |
| 124 | + else: |
| 125 | + logger.error( |
| 126 | + f"Fallback creating contact for user {user.email} when associating with organisation" |
| 127 | + ) |
| 128 | + hubspot_contact_id = self.create_user_hubspot_contact(user) |
| 129 | + |
| 130 | + return hubspot_contact_id |
| 131 | + |
| 132 | + def _get_or_create_organisation_hubspot_id( |
69 | 133 | self, |
70 | 134 | user: FFAdminUser, |
71 | | - organisation: Organisation = None, # type: ignore[assignment] |
72 | | - ) -> str: |
| 135 | + organisation: Organisation, |
| 136 | + ) -> str | None: |
73 | 137 | """ |
74 | 138 | Return the Hubspot API's id for an organisation. |
75 | 139 | """ |
76 | | - if organisation and getattr(organisation, "hubspot_organisation", None): |
| 140 | + if getattr(organisation, "hubspot_organisation", None): |
77 | 141 | return organisation.hubspot_organisation.hubspot_id |
78 | 142 |
|
79 | 143 | if user.email_domain in settings.HUBSPOT_IGNORE_ORGANISATION_DOMAINS: |
80 | | - domain = None |
81 | | - else: |
82 | | - domain = user.email_domain |
| 144 | + return None |
83 | 145 |
|
84 | | - if organisation: |
85 | | - response = self.client.create_company( |
| 146 | + domain = user.email_domain |
| 147 | + company_kwargs = {"domain": domain} |
| 148 | + company_kwargs["name"] = organisation.name |
| 149 | + company_kwargs["organisation_id"] = organisation.id |
| 150 | + company_kwargs["active_subscription"] = organisation.subscription.plan |
| 151 | + |
| 152 | + # As Hubspot creates/associates companies based on contact domain |
| 153 | + # we need to get the hubspot id when this user creates the company for the first time |
| 154 | + # and update the company name |
| 155 | + company = self._get_or_create_hubspot_company(**company_kwargs) |
| 156 | + org_hubspot_id: str = company["id"] |
| 157 | + |
| 158 | + properties = company.get("properties", {}) |
| 159 | + existing_name = properties.get("name") |
| 160 | + if existing_name != organisation.name: |
| 161 | + self.client.update_company( |
86 | 162 | name=organisation.name, |
87 | | - active_subscription=organisation.subscription.plan, |
88 | | - organisation_id=organisation.id, |
89 | | - domain=domain, |
| 163 | + hubspot_company_id=org_hubspot_id, |
90 | 164 | ) |
91 | 165 |
|
92 | | - # Store the organisation data in the database since we are |
93 | | - # unable to look them up via a unique identifier. |
94 | | - HubspotOrganisation.objects.create( |
95 | | - organisation=organisation, |
96 | | - hubspot_id=response["id"], |
97 | | - ) |
98 | | - else: |
99 | | - response = self._get_or_create_company_by_domain(domain) # type: ignore[arg-type] |
100 | | - |
101 | | - return response["id"] # type: ignore[no-any-return] |
102 | | - |
103 | | - def update_company_active_subscription( |
104 | | - self, subscription: Subscription |
105 | | - ) -> dict | None: # type: ignore[type-arg] |
106 | | - if not subscription.plan: |
107 | | - return # type: ignore[return-value] |
108 | | - |
109 | | - organisation = subscription.organisation |
110 | | - |
111 | | - # Check if we're missing the associated hubspot id. |
112 | | - if not getattr(organisation, "hubspot_organisation", None): |
113 | | - return # type: ignore[return-value] |
114 | | - |
115 | | - response = self.client.update_company( |
116 | | - active_subscription=subscription.plan, |
117 | | - hubspot_company_id=organisation.hubspot_organisation.hubspot_id, |
| 166 | + # Store the organisation data in the database since we are |
| 167 | + # unable to look them up via a unique identifier. |
| 168 | + HubspotOrganisation.objects.create( |
| 169 | + organisation=organisation, |
| 170 | + hubspot_id=org_hubspot_id, |
118 | 171 | ) |
119 | 172 |
|
120 | | - return response # type: ignore[no-any-return] |
| 173 | + return org_hubspot_id |
121 | 174 |
|
122 | | - def _get_or_create_company_by_domain(self, domain: str) -> dict: # type: ignore[type-arg] |
| 175 | + def _get_or_create_hubspot_company( |
| 176 | + self, |
| 177 | + domain: str, |
| 178 | + organisation_id: int, |
| 179 | + name: str, |
| 180 | + active_subscription: str | None = None, |
| 181 | + ) -> dict[str, Any]: |
123 | 182 | company = self.client.get_company_by_domain(domain) |
124 | 183 | if not company: |
125 | | - # Since we don't know the company's name, we pass the domain as |
126 | | - # both the name and the domain. This can then be manually |
127 | | - # updated in Hubspot if needed. |
128 | | - company = self.client.create_company(name=domain, domain=domain) |
| 184 | + company = self.client.create_company( |
| 185 | + name=name, |
| 186 | + domain=domain, |
| 187 | + organisation_id=organisation_id, |
| 188 | + active_subscription=active_subscription, |
| 189 | + ) |
129 | 190 |
|
130 | 191 | return company # type: ignore[no-any-return] |
131 | 192 |
|
|
0 commit comments