-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adds events system, and adds support to call webhooks when certain transaction events happen. Fixes some bugs in the cart and checkout processes.
- Loading branch information
Showing
24 changed files
with
711 additions
and
26 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
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
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
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
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,13 @@ | ||
""" | ||
Constants for the payments app. | ||
""" | ||
|
||
PAYMENT_HOOK_ACTION_PRE_SALE = "presale" | ||
PAYMENT_HOOK_ACTION_POST_SALE = "postsale" | ||
PAYMENT_HOOK_ACTION_POST_REFUND = "postrefund" | ||
|
||
PAYMENT_HOOK_ACTIONS = [ | ||
PAYMENT_HOOK_ACTION_PRE_SALE, | ||
PAYMENT_HOOK_ACTION_POST_SALE, | ||
PAYMENT_HOOK_ACTION_POST_REFUND, | ||
] |
Empty file.
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,90 @@ | ||
"""Post-sale hook implementations for payments.""" | ||
|
||
import logging | ||
|
||
import pluggy | ||
import requests | ||
|
||
from payments.constants import PAYMENT_HOOK_ACTION_POST_SALE | ||
from payments.models import Order | ||
from payments.serializers.v0 import WebhookBase, WebhookBaseSerializer, WebhookOrder | ||
|
||
hookimpl = pluggy.HookimplMarker("unified_ecommerce") | ||
|
||
|
||
class PostSaleSendEmails: | ||
"""Send email when the order is fulfilled.""" | ||
|
||
@hookimpl | ||
def post_sale(self, order_id, source): | ||
"""Send email when the order is fulfilled.""" | ||
log = logging.getLogger(__name__) | ||
|
||
msg = "Sending email for order %s with source %s" | ||
log.info(msg, order_id, source) | ||
|
||
|
||
class IntegratedSystemWebhooks: | ||
"""Figures out what webhook endpoints to call, and calls them.""" | ||
|
||
def post_sale_impl(self, order_id, source): | ||
"""Call the webhook endpoints for the order.""" | ||
|
||
log = logging.getLogger(__name__) | ||
|
||
log.info( | ||
"Calling webhook endpoints for order %s with source %s", order_id, source | ||
) | ||
|
||
order = Order.objects.prefetch_related("lines", "lines__product_version").get( | ||
pk=order_id | ||
) | ||
|
||
systems = [ | ||
product.system | ||
for product in [ | ||
line.product_version._object_version.object # noqa: SLF001 | ||
for line in order.lines.all() | ||
] | ||
] | ||
|
||
for system in systems: | ||
system_webhook_url = system.webhook_url | ||
system_slug = system.slug | ||
if system_webhook_url: | ||
log.info( | ||
("Calling webhook endpoint %s for order %s with source %s"), | ||
system_webhook_url, | ||
order_id, | ||
source, | ||
) | ||
|
||
order_info = WebhookOrder( | ||
order=order, | ||
lines=[ | ||
line | ||
for line in order.lines.all() | ||
if line.product.system.slug == system_slug | ||
], | ||
) | ||
|
||
webhook_data = WebhookBase( | ||
type=PAYMENT_HOOK_ACTION_POST_SALE, | ||
system_key=system.api_key, | ||
user=order.purchaser, | ||
data=order_info, | ||
) | ||
|
||
serializer = WebhookBaseSerializer(webhook_data) | ||
|
||
requests.post( | ||
system_webhook_url, | ||
json=serializer.data, | ||
timeout=30, | ||
) | ||
|
||
@hookimpl | ||
def post_sale(self, order_id, source): | ||
"""Call the implementation of this, so we can test it more easily.""" | ||
|
||
self.post_sale_impl(order_id, source) |
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,111 @@ | ||
"""Tests for post-sale hooks.""" | ||
|
||
import pytest | ||
import reversion | ||
from reversion.models import Version | ||
|
||
from payments.constants import PAYMENT_HOOK_ACTION_POST_SALE | ||
from payments.factories import LineFactory, OrderFactory | ||
from payments.hooks.post_sale import IntegratedSystemWebhooks | ||
from payments.models import Order | ||
from payments.serializers.v0 import WebhookBase, WebhookBaseSerializer, WebhookOrder | ||
from system_meta.factories import ProductFactory | ||
from system_meta.models import IntegratedSystem | ||
from unified_ecommerce.constants import ( | ||
POST_SALE_SOURCE_BACKOFFICE, | ||
POST_SALE_SOURCE_REDIRECT, | ||
) | ||
|
||
pytestmark = [pytest.mark.django_db] | ||
|
||
|
||
@pytest.fixture() | ||
def fulfilled_order(): | ||
"""Create a fulfilled order.""" | ||
|
||
order = OrderFactory.create(state=Order.STATE.FULFILLED) | ||
|
||
with reversion.create_revision(): | ||
product = ProductFactory.create() | ||
|
||
product_version = Version.objects.get_for_object(product).first() | ||
LineFactory.create(order=order, product_version=product_version) | ||
|
||
return order | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"source", [POST_SALE_SOURCE_BACKOFFICE, POST_SALE_SOURCE_REDIRECT] | ||
) | ||
def test_integrated_system_webhook(mocker, fulfilled_order, source): | ||
"""Test fire the webhook.""" | ||
|
||
mocked_request = mocker.patch("requests.post") | ||
webhook = IntegratedSystemWebhooks() | ||
system_id = fulfilled_order.lines.first().product_version.field_dict["system_id"] | ||
system = IntegratedSystem.objects.get(pk=system_id) | ||
|
||
order_info = WebhookOrder( | ||
order=fulfilled_order, | ||
lines=fulfilled_order.lines.all(), | ||
) | ||
|
||
webhook_data = WebhookBase( | ||
type=PAYMENT_HOOK_ACTION_POST_SALE, | ||
system_key=system.api_key, | ||
user=fulfilled_order.purchaser, | ||
data=order_info, | ||
) | ||
|
||
serialized_webhook_data = WebhookBaseSerializer(webhook_data) | ||
|
||
webhook.post_sale_impl(fulfilled_order.id, source) | ||
|
||
mocked_request.assert_called_with( | ||
system.webhook_url, json=serialized_webhook_data.data, timeout=30 | ||
) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"source", [POST_SALE_SOURCE_BACKOFFICE, POST_SALE_SOURCE_REDIRECT] | ||
) | ||
def test_integrated_system_webhook_multisystem(mocker, fulfilled_order, source): | ||
"""Test fire the webhook with an order with lines from >1 system.""" | ||
|
||
with reversion.create_revision(): | ||
product = ProductFactory.create() | ||
|
||
product_version = Version.objects.get_for_object(product).first() | ||
LineFactory.create(order=fulfilled_order, product_version=product_version) | ||
|
||
mocked_request = mocker.patch("requests.post") | ||
webhook = IntegratedSystemWebhooks() | ||
|
||
serialized_calls = [] | ||
|
||
for system in IntegratedSystem.objects.all(): | ||
order_info = WebhookOrder( | ||
order=fulfilled_order, | ||
lines=[ | ||
line | ||
for line in fulfilled_order.lines.all() | ||
if line.product.system.slug == system.slug | ||
], | ||
) | ||
|
||
webhook_data = WebhookBase( | ||
type=PAYMENT_HOOK_ACTION_POST_SALE, | ||
system_key=system.api_key, | ||
user=fulfilled_order.purchaser, | ||
data=order_info, | ||
) | ||
|
||
serialized_order = WebhookBaseSerializer(webhook_data).data | ||
serialized_calls.append( | ||
mocker.call(system.webhook_url, json=serialized_order, timeout=30) | ||
) | ||
|
||
webhook.post_sale_impl(fulfilled_order.id, source) | ||
|
||
assert mocked_request.call_count == 2 | ||
mocked_request.assert_has_calls(serialized_calls, any_order=True) |
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,36 @@ | ||
"""Hookspecs for the payments app.""" | ||
# ruff: noqa: ARG001 | ||
|
||
import pluggy | ||
|
||
hookspec = pluggy.HookspecMarker("unified_ecommerce") | ||
|
||
|
||
@hookspec | ||
def post_sale(order_id, source): | ||
""" | ||
Trigger post-sale events. | ||
This happens when the order has been completed, either via the browser | ||
redirect or via the back-office webhook. The caller should specify the | ||
source from the POST_SALE_SOURCES list (in unified_ecommerce.constants). | ||
Args: | ||
order_id (int): ID of the order that has been completed. | ||
source (str): Source of the order that has been completed; one of POST_SALE_SOURCES. | ||
""" | ||
|
||
|
||
@hookspec | ||
def post_refund(order_id, source): | ||
""" | ||
Trigger post-refund events. | ||
This happens when the order has been refunded. These generally should just | ||
come back from the back-office webhook but the source option is specified | ||
in case that changes. | ||
Args: | ||
order_id (int): ID of the order that has been completed. | ||
source (str): Source of the order that has been completed; one of POST_SALE_SOURCES. | ||
""" |
Oops, something went wrong.