Skip to content

Commit 651ce59

Browse files
authored
feat(onboarding): Add API to support onboarding support request (#5331)
1 parent 65d835b commit 651ce59

20 files changed

Lines changed: 584 additions & 4 deletions

File tree

api/api/urls/v1.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
feature_health_webhook,
5757
name="feature-health-webhook",
5858
),
59+
re_path(r"^onboarding/", include("onboarding.urls", namespace="onboarding")),
5960
# Client SDK urls
6061
re_path(r"^flags/$", SDKFeatureStates.as_view(), name="flags"),
6162
re_path(r"^identities/$", SDKIdentities.as_view(), name="sdk-identities"),

api/app/settings/common.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"projects.tags",
124124
"api_keys",
125125
"webhooks",
126+
"onboarding",
126127
# 2FA
127128
"custom_auth.mfa.trench",
128129
# health check plugins
@@ -732,6 +733,18 @@
732733
"USER_THROTTLE_CACHE_OPTIONS", default={}
733734
)
734735

736+
ONBOARDING_REQUEST_THROTTLE_CACHE_NAME = "onboarding-request-throttle"
737+
ONBOARDING_REQUEST_THROTTLE_CACHE_BACKEND = env.str(
738+
"ONBOARDING_REQUEST_THROTTLE_CACHE_BACKEND",
739+
"django.core.cache.backends.db.DatabaseCache",
740+
)
741+
ONBOARDING_REQUEST_THROTTLE_CACHE_LOCATION = env.str(
742+
"ONBOARDING_REQUEST_THROTTLE_CACHE_LOCATION", "onboarding-request-throttle"
743+
)
744+
ONBOARDING_REQUEST_THROTTLE_CACHE_OPTIONS: dict[str, str] = env.dict(
745+
"ONBOARDING_REQUEST_THROTTLE_CACHE_OPTIONS", default={}
746+
)
747+
735748
# Using Redis for cache
736749
# To use Redis for caching, set the cache backend to `django_redis.cache.RedisCache`.
737750
# and set the cache location to the redis url
@@ -807,6 +820,14 @@
807820
"LOCATION": USER_THROTTLE_CACHE_LOCATION,
808821
"OPTIONS": USER_THROTTLE_CACHE_OPTIONS,
809822
},
823+
ONBOARDING_REQUEST_THROTTLE_CACHE_NAME: {
824+
"BACKEND": ONBOARDING_REQUEST_THROTTLE_CACHE_BACKEND,
825+
"LOCATION": ONBOARDING_REQUEST_THROTTLE_CACHE_LOCATION,
826+
"OPTIONS": ONBOARDING_REQUEST_THROTTLE_CACHE_OPTIONS,
827+
"TIMEOUT": None,
828+
"MAX_ENTRIES": 10000,
829+
"KEY_PREFIX": "onboarding-req",
830+
},
810831
}
811832

812833
TRENCH_AUTH = {

api/core/helpers.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import re
2+
from typing import Any
23

34
from django.conf import settings
45
from django.contrib.sites import models as sites_models
@@ -34,7 +35,7 @@ def get_current_site_url(request: HttpRequest | Request | None = None) -> str:
3435
return f"{scheme}://{domain}"
3536

3637

37-
def get_ip_address_from_request(request): # type: ignore[no-untyped-def]
38+
def get_ip_address_from_request(request: Request) -> Any | None:
3839
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
3940
return (
4041
x_forwarded_for.split(",")[0]

api/core/middleware/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def __init__(self, get_response): # type: ignore[no-untyped-def]
1414

1515
def __call__(self, request): # type: ignore[no-untyped-def]
1616
if request.path.startswith("/admin"):
17-
ip = get_ip_address_from_request(request) # type: ignore[no-untyped-call]
17+
ip = get_ip_address_from_request(request)
1818
if (
1919
settings.ALLOWED_ADMIN_IP_ADDRESSES
2020
and ip not in settings.ALLOWED_ADMIN_IP_ADDRESSES

api/integrations/lead_tracking/hubspot/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616

1717
from integrations.lead_tracking.hubspot.constants import (
18+
HUBSPOT_API_LEAD_SOURCE_SELF_HOSTED,
1819
HUBSPOT_FORM_ID,
1920
HUBSPOT_PORTAL_ID,
2021
HUBSPOT_ROOT_FORM_URL,
@@ -111,7 +112,11 @@ def create_contact(
111112
"lastname": user.last_name,
112113
"hs_marketable_status": user.marketing_consent_given,
113114
}
115+
return self._create_contact(properties, hubspot_company_id)
114116

117+
def _create_contact(
118+
self, properties: dict[str, Any], hubspot_company_id: str
119+
) -> dict[str, str]:
115120
response = self.client.crm.contacts.basic_api.create(
116121
simple_public_object_input_for_create=SimplePublicObjectInputForCreate(
117122
properties=properties,
@@ -130,6 +135,17 @@ def create_contact(
130135
)
131136
return response.to_dict() # type: ignore[no-any-return]
132137

138+
def create_self_hosted_contact(
139+
self, email: str, first_name: str, last_name: str, hubspot_company_id: str
140+
) -> None:
141+
properties = {
142+
"email": email,
143+
"firstname": first_name,
144+
"lastname": last_name,
145+
"api_lead_source": HUBSPOT_API_LEAD_SOURCE_SELF_HOSTED,
146+
}
147+
self._create_contact(properties, hubspot_company_id)
148+
133149
def get_company_by_domain(self, domain: str) -> dict[str, Any] | None:
134150
"""
135151
Domain should be unique in Hubspot by design, so we should only ever have

api/integrations/lead_tracking/hubspot/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@
22
HUBSPOT_PORTAL_ID = "143451822"
33
HUBSPOT_FORM_ID = "562ee023-fb3f-4645-a217-4d8c9b4e45be"
44
HUBSPOT_ROOT_FORM_URL = "https://api.hsforms.com/submissions/v3/integration/submit"
5+
HUBSPOT_API_LEAD_SOURCE_SELF_HOSTED = "self-hosted"
6+
HUBSPOT_ACTIVE_SUBSCRIPTION_SELF_HOSTED = "free-opensource"

api/integrations/lead_tracking/hubspot/services.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
from rest_framework.request import Request
44

5-
from integrations.lead_tracking.hubspot.constants import HUBSPOT_COOKIE_NAME
5+
from integrations.lead_tracking.hubspot.client import HubspotClient
6+
from integrations.lead_tracking.hubspot.constants import (
7+
HUBSPOT_ACTIVE_SUBSCRIPTION_SELF_HOSTED,
8+
HUBSPOT_COOKIE_NAME,
9+
)
610
from users.models import HubspotTracker
711

812
logger = logging.getLogger(__name__)
@@ -35,3 +39,21 @@ def register_hubspot_tracker(request: Request) -> None:
3539
logger.info(
3640
f"Created HubspotTracker instance for user {request.user.id} with cookie {hubspot_cookie}"
3741
)
42+
43+
44+
def create_self_hosted_onboarding_lead(
45+
email: str, first_name: str, last_name: str, organisation_name: str
46+
) -> None:
47+
email_domain = email.split("@")[1]
48+
hubspot_client = HubspotClient()
49+
company = hubspot_client.get_company_by_domain(email_domain)
50+
if not company:
51+
company = hubspot_client.create_company(
52+
name=organisation_name,
53+
domain=email_domain,
54+
active_subscription=HUBSPOT_ACTIVE_SUBSCRIPTION_SELF_HOSTED,
55+
)
56+
57+
company_id = company["id"]
58+
59+
hubspot_client.create_self_hosted_contact(email, first_name, last_name, company_id)

api/integrations/lead_tracking/hubspot/tasks.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,20 @@ def track_hubspot_lead_without_organisation(user_id: int) -> None:
6262
return
6363

6464
track_hubspot_lead(user.id)
65+
66+
67+
@register_task_handler()
68+
def create_self_hosted_onboarding_lead_task(
69+
email: str, first_name: str, last_name: str, organisation_name: str
70+
) -> None:
71+
# Avoid circular imports.
72+
from integrations.lead_tracking.hubspot.services import (
73+
create_self_hosted_onboarding_lead,
74+
)
75+
76+
create_self_hosted_onboarding_lead(
77+
first_name=first_name,
78+
last_name=last_name,
79+
email=email,
80+
organisation_name=organisation_name,
81+
)

api/onboarding/__init__.py

Whitespace-only changes.

api/onboarding/apps.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class OnboardingConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "onboarding"

0 commit comments

Comments
 (0)