Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added hubspot ecommerce bridge (#276)
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
Showing
8 changed files
with
653 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, {}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
|
||
|
||
@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, | ||
} | ||
) |
Oops, something went wrong.