Skip to content

Commit

Permalink
Added hubspot ecommerce bridge (#276)
Browse files Browse the repository at this point in the history
Initial task files

Add ecommerce bridge sync

Add product mapping, sync in user serializer

Add management command to configure settings, add stubs for other models

Run black, fix UserSerializer / ProfileSerializer interaction

Fix parameter error and simplify tests

Fix fake data, add urllib, stub model id params

Fix function calls

Add unit tests for task functions

Remove created_on from sync, fix urls

Url bug

Fix test parametrization
  • Loading branch information
Jake Klingensmith committed May 21, 2019
1 parent 117423b commit 602fb80
Show file tree
Hide file tree
Showing 8 changed files with 653 additions and 25 deletions.
135 changes: 135 additions & 0 deletions ecommerce/hubspot_api.py
@@ -0,0 +1,135 @@
"""
Hubspot Ecommerce Bridge API sync utilities
https://developers.hubspot.com/docs/methods/ecomm-bridge/ecomm-bridge-overview
"""
import time
from urllib.parse import urljoin, urlencode
import requests
from django.conf import settings


HUBSPOT_API_BASE_URL = "https://api.hubapi.com"


def send_hubspot_request(
endpoint, api_url, method, body=None, query_params=None, **kwargs
):
"""
Send a request to Hubspot using the given params, body and api key specified in settings
Args:
endpoint (String): Specific endpoint to hit. Can be the empty string
api_url (String): The url path to append endpoint to
method (String): GET, POST, or PUT
body (serializable data): Data to be JSON serialized and sent with a PUT or POST request
query_params (Dict): Params to be added to the query string
kwargs: keyword arguments to add to the request method
Returns:
HTML response to the constructed url
"""

base_url = urljoin(f"{HUBSPOT_API_BASE_URL}/", api_url)
if endpoint:
base_url = urljoin(f"{base_url}/", endpoint)
if query_params is None:
query_params = {}
if "hapikey" not in query_params:
query_params["hapikey"] = settings.HUBSPOT_API_KEY
params = urlencode(query_params)
url = f"{base_url}?{params}"
if method == "GET":
return requests.get(url=url, **kwargs)
if method == "PUT":
return requests.put(url=url, json=body, **kwargs)
if method == "POST":
return requests.post(url=url, json=body, **kwargs)


def make_sync_message(object_id, properties):
"""
Create data for sync message
Args:
object_id (ObjectID): Internal ID to match with Hubspot object
properties (dict): dict of properties to be synced
Returns:
dict to be serialized as body in sync-message
"""
for key in properties.keys():
if properties[key] is None:
properties[key] = ""
return {
"integratorObjectId": str(object_id),
"action": "UPSERT",
"changeOccurredTimestamp": int(time.time() * 1000),
"propertyNameToValues": dict(properties),
}


def get_sync_errors(limit=200, offset=0):
"""
Get errors that have occurred during sync
Args:
limit (Int): The number of errors to be returned
offset (Int): The index of the first error to be returned
Returns:
HTML response including error data
"""
response = send_hubspot_request(
"sync-errors",
"/extensions/ecomm/v1",
"GET",
query_params={"limit": limit, "offset": offset},
)
response.raise_for_status()
return response


def make_contact_sync_message(user_id):
"""
Create the body of a sync message for a contact. This will flatten the contained LegalAddress and Profile
serialized data into one larger serializable dict
Args:
user_id (ObjectID): ID of user to sync contact with
Returns:
dict containing serializable sync-message data
"""
from users.models import User
from users.serializers import UserSerializer

user = User.objects.get(id=user_id)
properties = UserSerializer(user).data
properties.update(properties.pop("legal_address") or {})
properties.update(properties.pop("profile") or {})
properties["street_address"] = "\n".join(properties.pop("street_address"))
return make_sync_message(user.id, properties)


def make_deal_sync_message(order_id):
"""
Create the body of a sync message for a deal.
Args:
Returns:
dict containing serializable sync-message data
"""
return make_sync_message(order_id, {})


def make_line_item_sync_message(line_id):
"""
Create the body of a sync message for a line item.
Args:
Returns:
dict containing serializable sync-message data
"""
return make_sync_message(line_id, {})


def make_product_sync_message(product_id):
"""
Create the body of a sync message for a product.
Args:
Returns:
dict containing serializable sync-message data
"""
return make_sync_message(product_id, {})
102 changes: 102 additions & 0 deletions ecommerce/hubspot_api_test.py
@@ -0,0 +1,102 @@
"""
Hubspot API tests
"""
# pylint: disable=redefined-outer-name
from urllib.parse import urlencode

import pytest
from faker import Faker
from django.conf import settings

from ecommerce.hubspot_api import (
send_hubspot_request,
HUBSPOT_API_BASE_URL,
make_sync_message,
make_contact_sync_message,
)
from users.serializers import UserSerializer

fake = Faker()


@pytest.mark.parametrize("request_method", ["GET", "PUT", "POST"])
@pytest.mark.parametrize(
"endpoint,api_url,expected_url",
[
[
"sync-errors",
"/extensions/ecomm/v1",
f"{HUBSPOT_API_BASE_URL}/extensions/ecomm/v1/sync-errors",
],
[
"",
"/extensions/ecomm/v1/installs",
f"{HUBSPOT_API_BASE_URL}/extensions/ecomm/v1/installs",
],
[
"CONTACT",
"/extensions/ecomm/v1/sync-messages",
f"{HUBSPOT_API_BASE_URL}/extensions/ecomm/v1/sync-messages/CONTACT",
],
],
)
def test_send_hubspot_request(mocker, request_method, endpoint, api_url, expected_url):
"""Test sending hubspot request with method = GET"""
value = fake.pyint()
query_params = {"param": value}

# Include hapikey when generating url to match request call against
full_query_params = {"param": value, "hapikey": settings.HUBSPOT_API_KEY}
mock_request = mocker.patch(
f"ecommerce.hubspot_api.requests.{request_method.lower()}"
)
url_params = urlencode(full_query_params)
url = f"{expected_url}?{url_params}"
if request_method == "GET":
send_hubspot_request(
endpoint, api_url, request_method, query_params=query_params
)
mock_request.assert_called_once_with(url=url)
else:
body = fake.pydict()
send_hubspot_request(
endpoint, api_url, request_method, query_params=query_params, body=body
)
mock_request.assert_called_once_with(url=url, json=body)


def test_make_sync_message():
"""Test make_sync_message produces a properly formatted sync-message"""
object_id = fake.pyint()
value = fake.word()
properties = {"prop": value, "blank": None}
sync_message = make_sync_message(object_id, properties)
time = sync_message["changeOccurredTimestamp"]
assert sync_message == (
{
"integratorObjectId": str(object_id),
"action": "UPSERT",
"changeOccurredTimestamp": time,
"propertyNameToValues": {"prop": value, "blank": ""},
}
)


def test_make_contact_sync_message(user):
"""Test make_contact_sync_message serializes a user and returns a properly formatted sync message"""
contact_sync_message = make_contact_sync_message(user.id)

serialized_user = UserSerializer(user).data
serialized_user.update(serialized_user.pop("legal_address") or {})
serialized_user.update(serialized_user.pop("profile") or {})
serialized_user["street_address"] = "\n".join(serialized_user.pop("street_address"))

time = contact_sync_message["changeOccurredTimestamp"]
assert contact_sync_message == (
{
"integratorObjectId": str(user.id),
"action": "UPSERT",
"changeOccurredTimestamp": time,
"propertyNameToValues": serialized_user,
}
)

0 comments on commit 602fb80

Please sign in to comment.