Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/hubspot integration #624

Open
wants to merge 3 commits into
base: feature/brevo
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions authentication/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from django.db import models
from django.contrib.auth.models import AbstractUser, BaseUserManager
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -52,6 +53,16 @@ class User(AbstractUser):
USERNAME_FIELD = "email_or_cell"
REQUIRED_FIELDS = ["tcpa_consent"]

def anonomize(self, external_id: str):
random_id = str(uuid.uuid4()).replace("-", "")
self.external_id = external_id
self.email_or_cell = f"{external_id}+{random_id}@myfriendben.org"
self.first_name = None
self.last_name = None
self.cell = None
self.email = None
self.save()

def save(self, **kwargs):
self.cell = self.cell or None
super().save(**kwargs)
Expand Down
67 changes: 23 additions & 44 deletions authentication/views.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
from django.conf import settings
from authentication.models import User
from integrations.services.cms_integration import get_cms_integration
from integrations.services.communications import MessageUser
from integrations.services.brevo import BrevoService
from screener.models import Screen
from rest_framework import viewsets, permissions, mixins
from rest_framework.response import Response
from authentication.serializers import UserSerializer, UserOffersSerializer
from sentry_sdk import capture_exception
from integrations.services.hubspot.integration import update_send_offers_hubspot, upsert_user_hubspot
import uuid


class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
Expand All @@ -24,56 +19,40 @@ class UserViewSet(mixins.UpdateModelMixin, viewsets.GenericViewSet):
def update(self, request, pk=None):
if pk is None:
return Response("Must have an associated screen", status=400)
screen = Screen.objects.get(uuid=pk)

screen: Screen = Screen.objects.get(uuid=pk)
user = screen.user
serializer = UserOffersSerializer(user, data=request.data) if user else UserSerializer(data=request.data)

if serializer.is_valid():
screen.user = serializer.save()
screen.save()
user: User = screen.user

Integration = get_cms_integration()
integration = Integration(user, screen)
message = MessageUser(screen, screen.get_language_code())
try:
Integration = get_cms_integration()
integration = Integration(user, screen)
message = MessageUser(screen, screen.get_language_code())

if screen.user.email is not None:
message.email(screen.user.email)
if screen.user.cell is not None:
message.text(str(screen.user.cell))
if screen.user.email is not None:
message.email(screen.user.email)
if screen.user.cell is not None:
message.text(str(screen.user.cell))

Integration = get_cms_integration()
integration = Integration(user, screen)
Integration = get_cms_integration()
integration = Integration(user, screen)

if not integration.should_add():
return Response(status=204)
if not integration.should_add():
return Response(status=204)

if user and user.external_id:
integration.update()
else:
integration.add()
if user and user.external_id:
integration.update()
else:
external_id = integration.add()
user.anonomize(external_id)
except Exception as e:
user.delete()
raise e

return Response(status=204)
return Response(serializer.errors, status=400)


def upsert_user_to_hubspot(screen, user):
if settings.DEBUG:
return
if user is None or screen.is_test_data is None:
return
should_upsert_user = (user.send_offers or user.send_updates) and user.external_id is None and user.tcpa_consent
if not should_upsert_user or screen.is_test_data:
return

hubspot_id = upsert_user_hubspot(user, screen=screen)
if hubspot_id:
random_id = str(uuid.uuid4()).replace("-", "")
user.external_id = hubspot_id
user.email_or_cell = f"{hubspot_id}+{random_id}@myfriendben.org"
user.first_name = None
user.last_name = None
user.cell = None
user.email = None
user.save()
else:
raise Exception("Failed to upsert user")
26 changes: 8 additions & 18 deletions integrations/management/commands/hubspotsync.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from django.core.management.base import BaseCommand
from authentication.models import User
from integrations.services.cms_integration import HubSpotIntegration
from screener.models import Screen
from django.db.models import Q
from integrations.services.hubspot.integration import upsert_user_hubspot
import time
import uuid


class Command(BaseCommand):
Expand Down Expand Up @@ -54,25 +53,16 @@ def sync_mfb_hubspot_users(self, limit):
screen = user_screens.first()
else:
continue
hubspot_id = upsert_user_hubspot(user, screen)
if hubspot_id:
self.replace_pii_with_hubspot_id(hubspot_id, user)

try:
hubspot = HubSpotIntegration(user, screen)
hubspot_id = hubspot.add()
user.anonomize(hubspot_id)
status["completed"].append((user.id, hubspot_id))
else:
except Exception:
status["failed"].append((user.id, hubspot_id))

# Delay to prevent hitting rate limit of 100 req per 10 seconds
time.sleep(0.2)
processed += 1
return status

# stores an external id from hubspot and then clears all of the PII
def replace_pii_with_hubspot_id(self, hubspot_id, user):
random_id = str(uuid.uuid4()).replace("-", "")
user.external_id = hubspot_id
user.email_or_cell = f"{hubspot_id}+{random_id}@myfriendben.org"
user.first_name = None
user.last_name = None
user.cell = None
user.email = None
user.save()
return user
166 changes: 129 additions & 37 deletions integrations/services/cms_integration.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import re
from django.conf import settings
from integrations.services.brevo import BrevoService
from hubspot import HubSpot
from decouple import config
from hubspot.crm.contacts import BatchInputSimplePublicObjectBatchInput, SimplePublicObjectInput
import sib_api_v3_sdk
from sib_api_v3_sdk.rest import ApiException
from hubspot.crm.contacts.exceptions import ApiException as HubSpotApiException
from django.conf import settings
from django.utils import timezone
from screener.models import Message, Screen
from translations.models import Translation
import uuid
import json
from pprint import pprint
from sentry_sdk import capture_message

from authentication.models import User
from screener.models import Screen


class CmsIntegration:
def __init__(self, user, screen):
def __init__(self, user: User, screen: Screen):
self.user = user
self.screen = screen

def add(self):
def add(self) -> str:
raise NotImplementedError("")

def update(self):
Expand All @@ -30,13 +31,123 @@ def should_add(self):


class HubSpotIntegration(CmsIntegration):
def add(self):
# Implement the logic for adding a user to HubSpot
pass
MAX_HOUSEHOLD_SIZE = 8
api_client = HubSpot(access_token=config("HUBSPOT"))

def add(self) -> str:
data = self._hubspot_contact_data()

try:
api_response = self._create_contact(data)
contact_id = api_response.id
except HubSpotApiException as e:
http_body = json.loads(e.body)
if http_body["category"] == "CONFLICT":
contact_id = self._get_conflict_contact_id(e)
self._update_contact(contact_id, data)
else:
raise e

return contact_id

def update(self):
# Implement the logic for updating a user in HubSpot
pass
data = self._hubspot_send_offers_data()

self._update_contact(self.user.external_id, data)

def should_add(self):
if settings.DEBUG:
return False
if self.user is None or self.screen.is_test_data is None:
return False
should_upsert_user = (
(self.user.send_offers or self.user.send_updates)
and self.user.external_id is None
and self.user.tcpa_consent
)
if not should_upsert_user or self.screen.is_test_data:
return False
return True

def _hubspot_contact_data(self):
contact = {
"email": self.user.email,
"firstname": self.user.first_name,
"lastname": self.user.last_name,
"phone": str(self.user.cell),
"benefits_screener_id": self.user.id,
"ab01___send_offers": self.user.send_offers,
"ab01___send_updates": self.user.send_updates,
"ab01___tcpa_consent_to_contact": self.user.tcpa_consent,
"hs_language": self.user.language_code,
"ab01___1st_mfb_completion_date": self.user.date_joined.date().isoformat(),
"full_name": f"{self.user.first_name} {self.user.last_name}",
}

if self.screen:
contact["ab01___screener_id"] = self.screen.id
contact["ab01___uuid"] = str(self.screen.uuid)
contact["ab01___county"] = self.screen.county
contact["ab01___number_of_household_members"] = self.screen.household_size
contact["ab01___mfb_annual_income"] = int(self.screen.calc_gross_income("yearly", ["all"]))

members = self.screen.household_members.all()
if len(members) > self.MAX_HOUSEHOLD_SIZE:
capture_message(f"screen has more than {self.MAX_HOUSEHOLD_SIZE} household members", level="error")

for i, member in enumerate(members):
if i >= self.MAX_HOUSEHOLD_SIZE:
break

contact[f"ab01___hhm{i + 1}_age"] = member.age

return contact

def _hubspot_send_offers_data(self):
return {
"ab01___send_offers": self.user.send_offers,
"ab01___send_updates": self.user.send_updates,
}

def _get_conflict_contact_id(self, e):
http_body = json.loads(e.body)
# strip everything out of the error message except the contact id
# https://community.hubspot.com/t5/APIs-Integrations/Contacts-v3-contact-exists-error/m-p/364629
contact_id = re.sub("[^0-9]", "", http_body["message"])
return contact_id

def _create_contact(self, data):
simple_public_object_input = SimplePublicObjectInput(properties=data)
api_response = self.api_client.crm.contacts.basic_api.create(
simple_public_object_input_for_create=simple_public_object_input
)
return api_response

def _update_contact(self, contact_id, data):
simple_public_object_input = SimplePublicObjectInput(properties=data)
api_response = self.api_client.crm.contacts.basic_api.update(
contact_id, simple_public_object_input=simple_public_object_input
)
return api_response

@classmethod
def format_email_new_benefit(cls, external_id: str, num_benefits: int, value_benefits: int):
contact = {
"id": external_id,
"properties": {
"ab01___number_of_new_benefits": num_benefits,
"ab01___new_benefit_total_value": value_benefits,
},
}

return contact

@classmethod
def bulk_update(cls, data):
batch_input_simple_public_object_batch_input = BatchInputSimplePublicObjectBatchInput(data)
cls.api_client.crm.contacts.batch_api.update(
batch_input_simple_public_object_batch_input=batch_input_simple_public_object_batch_input
)


class BrevoIntegration(CmsIntegration):
Expand All @@ -51,7 +162,7 @@ def __init__(self, user, screen):
self.email_instance = sib_api_v3_sdk.TransactionalEmailsApi(sib_api_v3_sdk.ApiClient(configuration))
self.front_end_domain = settings.FRONTEND_DOMAIN

def add(self):
def add(self) -> str:
contact = {
"first_name": self.user.first_name,
"last_name": self.user.last_name,
Expand Down Expand Up @@ -83,36 +194,17 @@ def add(self):
contact[f"hhm{i + 1}_age"] = member.age

create_contact = sib_api_v3_sdk.CreateContact(email=self.user.email, attributes=contact, list_ids=[6])
try:
brevo_id = self.api_instance.create_contact(create_contact)
pprint(brevo_id)

if brevo_id:
random_id = str(uuid.uuid4()).replace("-", "")
self.user.external_id = brevo_id
self.user.email_or_cell = f"{brevo_id}+{random_id}@myfriendben.org"
self.user.first_name = None
self.user.last_name = None
self.user.cell = None
self.user.email = None
self.user.save()
print("saved user")
except ApiException as e:
print("Exception when calling ContactsApi->create_contact: %s\n" % e)
return self.api_instance.create_contact(create_contact)

def update(self):
ext_id_dict = json.loads(self.user.external_id.replace("'", '"'))
data = {"send_offers": self.user.send_offers, "send_updates": self.user.send_updates}
id_value = ext_id_dict["id"]
try:
update_attributes = sib_api_v3_sdk.UpdateContact(attributes=data)
self.api_instance.update_contact(id_value, update_attributes)
except ApiException as e:
print(f"Exception when calling ContactsApi->update_contact: {e}")
update_attributes = sib_api_v3_sdk.UpdateContact(attributes=data)
self.api_instance.update_contact(id_value, update_attributes)

def should_add(self):
if settings.DEBUG:
print("DEBUG set to True")
return False
if self.user is None or self.screen.is_test_data is None:
return False
Expand Down
2 changes: 1 addition & 1 deletion integrations/services/communications/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class MessageUser:
cell_auth_token = config("TWILIO_TOKEN")
cell_from_phone_number = config("TWILIO_PHONE_NUMBER")

email_from = settings.EMAIL_FROM
email_from = config("EMAIL_FROM")
email_api_key = config("SENDGRID")

def __init__(self, screen: Screen, lang: str) -> None:
Expand Down
Empty file.
Loading