From 361639a20bbe5d21bb42fc0256ff7ec94236664e Mon Sep 17 00:00:00 2001 From: julianajlk Date: Fri, 10 May 2024 14:52:14 -0400 Subject: [PATCH 1/3] fix: Apply voucher to submitted basket so the discount is reflected on webhooks processing --- ecommerce/extensions/payment/processors/webhooks.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ecommerce/extensions/payment/processors/webhooks.py b/ecommerce/extensions/payment/processors/webhooks.py index 05086466a57..e05676004a8 100644 --- a/ecommerce/extensions/payment/processors/webhooks.py +++ b/ecommerce/extensions/payment/processors/webhooks.py @@ -15,6 +15,7 @@ logger = logging.getLogger(__name__) +Applicator = get_class('offer.applicator', 'Applicator') Basket = get_model('basket', 'Basket') OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator') @@ -82,6 +83,11 @@ def handle_webhooks_payment(self, request, payment_intent, payment_method_type): try: basket = Basket.objects.get(id=basket_id) basket.strategy = strategy.Default() + + # Even though at this stage the basket has been submitted, because we never save the voucher + # application in other parts of the code where the voucher is applied to the basket, + # we need to apply it to the basket here. + Applicator().apply(basket, request.user, request) except Basket.DoesNotExist: logger.exception( '[Dynamic Payment Methods] Basket with ID %d does not exist.' From fc8dee51210c73f32ebd1592c3cef4845d78fa1f Mon Sep 17 00:00:00 2001 From: julianajlk Date: Fri, 10 May 2024 14:52:33 -0400 Subject: [PATCH 2/3] test: Add webhooks processing test with voucher --- .../payment/tests/processors/test_webhooks.py | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/ecommerce/extensions/payment/tests/processors/test_webhooks.py b/ecommerce/extensions/payment/tests/processors/test_webhooks.py index 3203bc82350..cca1a2f369d 100644 --- a/ecommerce/extensions/payment/tests/processors/test_webhooks.py +++ b/ecommerce/extensions/payment/tests/processors/test_webhooks.py @@ -5,15 +5,19 @@ import mock from django.test import RequestFactory from oscar.core.loading import get_class, get_model +from oscar.test.factories import RangeFactory +from ecommerce.core.constants import SEAT_PRODUCT_CLASS_NAME from ecommerce.extensions.basket.utils import basket_add_payment_intent_id_attribute from ecommerce.extensions.payment.processors.webhooks import StripeWebhooksProcessor from ecommerce.extensions.payment.tests.processors.mixins import PaymentProcessorTestCaseMixin +from ecommerce.extensions.test.factories import create_basket, prepare_voucher from ecommerce.tests.factories import UserFactory from ecommerce.tests.testcases import TestCase log = logging.getLogger(__name__) +Applicator = get_class('offer.applicator', 'Applicator') BillingAddress = get_model('order', 'BillingAddress') Country = get_model('address', 'Country') OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator') @@ -38,17 +42,20 @@ def _get_order_number_from_basket(self, basket): return OrderNumberGenerator().order_number(basket) def _build_payment_intent_data(self, basket, payment_intent_status=None): + # Apply coupon to basket so that the mocked return from Stripe has the discounted amount + Applicator().apply(basket, self.request.user, self.request) + return { "id": "pi_3OzUOMH4caH7G0X114tkIL0X", "object": "payment_intent", "status": "succeeded", - "amount": 14900, + "amount": basket.total_incl_tax, "charges": { "object": "list", "data": [{ "id": "py_3OzUOMH4caH7G0X11OOKbfIk", "object": "charge", - "amount": 14900, + "amount": basket.total_incl_tax, "billing_details": { "address": { "city": "Beverly Hills", @@ -175,3 +182,55 @@ def test_handle_webhooks_payment(self, mock_track, mock_retrieve, mock_handle_po self.basket.site, self.basket.owner, 'Payment Processor Response', properties ) mock_handle_post_order.assert_called_once() + + @mock.patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.create_order') + @mock.patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.handle_post_order') + @mock.patch('stripe.PaymentIntent.retrieve') + @mock.patch('ecommerce.extensions.payment.processors.webhooks.track_segment_event') + def test_handle_webhooks_payment_with_voucher(self, mock_track, mock_retrieve, mock_handle_post_order, mock_create_order): + """ + Verify a payment received via Stripe webhooks is processed, an order is created and fulfilled with a coupon. + """ + # Create a basket, add a voucher without applying + basket = create_basket(product_class=SEAT_PRODUCT_CLASS_NAME) + voucher, product = prepare_voucher( + _range=RangeFactory(includes_all_products=True), + benefit_value=10, + code='test101' + ) + basket.vouchers.add(voucher) + basket.add_product(product) + + succeeded_payment_intent = self._build_payment_intent_data(basket, payment_intent_status='succeeded') + + # Need to associate the Payment Intent to the Basket + basket_add_payment_intent_id_attribute(basket, succeeded_payment_intent['id']) + + mock_retrieve.return_value = { + 'id': succeeded_payment_intent['id'], + 'client_secret': succeeded_payment_intent['client_secret'], + 'payment_method': { + 'id': succeeded_payment_intent['payment_method'], + 'object': 'payment_method', + 'billing_details': succeeded_payment_intent['charges']['data'][0]['billing_details'], + 'type': succeeded_payment_intent['charges']['data'][0]['payment_method_details']['type'] + }, + 'amount': succeeded_payment_intent['amount'] + } + self.processor_class(self.site).handle_webhooks_payment( + self.request, succeeded_payment_intent, 'affirm' + ) + properties = { + 'basket_id': basket.id, + 'processor_name': 'stripe', + 'stripe_enabled': True, + 'total': basket.total_incl_tax, + 'success': True, + 'payment_method': succeeded_payment_intent['charges']['data'][0]['payment_method_details']['type'], + } + mock_track.assert_called_once_with( + basket.site, basket.owner, 'Payment Processor Response', properties + ) + mock_create_order.assert_called_once() + mock_handle_post_order.assert_called_once() + assert basket.total_incl_tax != basket.total_excl_tax_excl_discounts From 05ca82b3e766fa59c169f42c86ee33960aef3db6 Mon Sep 17 00:00:00 2001 From: julianajlk Date: Fri, 10 May 2024 16:37:35 -0400 Subject: [PATCH 3/3] test: Update webhooks processing test with voucher to check for Order --- .../payment/tests/processors/test_webhooks.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/ecommerce/extensions/payment/tests/processors/test_webhooks.py b/ecommerce/extensions/payment/tests/processors/test_webhooks.py index cca1a2f369d..c2b95882c0e 100644 --- a/ecommerce/extensions/payment/tests/processors/test_webhooks.py +++ b/ecommerce/extensions/payment/tests/processors/test_webhooks.py @@ -20,6 +20,7 @@ Applicator = get_class('offer.applicator', 'Applicator') BillingAddress = get_model('order', 'BillingAddress') Country = get_model('address', 'Country') +Order = get_model('order', 'Order') OrderNumberGenerator = get_class('order.utils', 'OrderNumberGenerator') @@ -145,10 +146,16 @@ def test_issue_credit_error(self): """ self.skipTest('Webhooks payments processor does not yet support issuing credit.') + @mock.patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.create_order') @mock.patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.handle_post_order') @mock.patch('stripe.PaymentIntent.retrieve') @mock.patch('ecommerce.extensions.payment.processors.webhooks.track_segment_event') - def test_handle_webhooks_payment(self, mock_track, mock_retrieve, mock_handle_post_order): + def test_handle_webhooks_payment( + self, + mock_track, + mock_retrieve, + mock_handle_post_order, + mock_create_order): """ Verify a payment received via Stripe webhooks is processed, an order is created and fulfilled. """ @@ -181,13 +188,13 @@ def test_handle_webhooks_payment(self, mock_track, mock_retrieve, mock_handle_po mock_track.assert_called_once_with( self.basket.site, self.basket.owner, 'Payment Processor Response', properties ) + mock_create_order.assert_called_once() mock_handle_post_order.assert_called_once() - @mock.patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.create_order') @mock.patch('ecommerce.extensions.checkout.mixins.EdxOrderPlacementMixin.handle_post_order') @mock.patch('stripe.PaymentIntent.retrieve') @mock.patch('ecommerce.extensions.payment.processors.webhooks.track_segment_event') - def test_handle_webhooks_payment_with_voucher(self, mock_track, mock_retrieve, mock_handle_post_order, mock_create_order): + def test_handle_webhooks_payment_with_voucher(self, mock_track, mock_retrieve, mock_handle_post_order): """ Verify a payment received via Stripe webhooks is processed, an order is created and fulfilled with a coupon. """ @@ -231,6 +238,7 @@ def test_handle_webhooks_payment_with_voucher(self, mock_track, mock_retrieve, m mock_track.assert_called_once_with( basket.site, basket.owner, 'Payment Processor Response', properties ) - mock_create_order.assert_called_once() mock_handle_post_order.assert_called_once() - assert basket.total_incl_tax != basket.total_excl_tax_excl_discounts + order = Order.objects.get(number=basket.order_number) + assert order.total_incl_tax == basket.total_incl_tax + assert basket.total_incl_tax != basket.total_incl_tax_excl_discounts