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

Jk/hubspot ecommerce sync #319

Merged
merged 1 commit into from
May 21, 2019
Merged
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
135 changes: 135 additions & 0 deletions ecommerce/hubspot_api.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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()

jklingen92 marked this conversation as resolved.
Show resolved Hide resolved

@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,
}
)
Loading