Skip to content
This repository has been archived by the owner on Nov 4, 2024. It is now read-only.

Commit

Permalink
feat: Add Dynamic Payment Methods BNPL (#4115)
Browse files Browse the repository at this point in the history
REV-3821
  • Loading branch information
julianajlk authored Apr 17, 2024
1 parent 9533cd4 commit 5482fe2
Show file tree
Hide file tree
Showing 14 changed files with 670 additions and 52 deletions.
1 change: 1 addition & 0 deletions ecommerce/extensions/basket/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
EMAIL_OPT_IN_ATTRIBUTE = "email_opt_in"
PURCHASER_BEHALF_ATTRIBUTE = "purchased_for_organization"
PAYMENT_INTENT_ID_ATTRIBUTE = "payment_intent_id"
DYNAMIC_PAYMENT_METHODS_ENABLED = "dynamic_payment_methods_enabled"

# .. toggle_name: enable_stripe_payment_processor
# .. toggle_type: waffle_flag
Expand Down
42 changes: 41 additions & 1 deletion ecommerce/extensions/basket/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@
from ecommerce.extensions.basket.constants import EMAIL_OPT_IN_ATTRIBUTE, ENABLE_STRIPE_PAYMENT_PROCESSOR
from ecommerce.extensions.basket.tests.mixins import BasketMixin
from ecommerce.extensions.basket.tests.test_utils import TEST_BUNDLE_ID
from ecommerce.extensions.basket.utils import _set_basket_bundle_status, apply_voucher_on_basket_and_check_discount
from ecommerce.extensions.basket.utils import (
_set_basket_bundle_status,
apply_voucher_on_basket_and_check_discount,
basket_add_dynamic_payment_methods_enabled,
basket_add_payment_intent_id_attribute
)
from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin
from ecommerce.extensions.offer.constants import DYNAMIC_DISCOUNT_FLAG
from ecommerce.extensions.offer.utils import format_benefit_value
Expand Down Expand Up @@ -325,6 +330,13 @@ def create_basket_and_add_product(self, product):
basket.add_product(product, 1)
return basket

def create_basket_and_add_product_stripe(self, product, payment_intent_id, payment_intent):
basket = self.create_empty_basket()
basket.add_product(product, 1)
basket_add_dynamic_payment_methods_enabled(basket, payment_intent)
basket_add_payment_intent_id_attribute(basket, payment_intent_id)
return basket

def create_seat(self, course, seat_price=100, cert_type='verified'):
return course.create_or_update_seat(cert_type, True, seat_price)

Expand Down Expand Up @@ -404,6 +416,7 @@ def assert_expected_response(
self,
basket,
enable_stripe_payment_processor=False,
is_dynamic_payment_methods_enabled=None,
url=None,
response=None,
status_code=200,
Expand All @@ -421,6 +434,7 @@ def assert_expected_response(
subject=None,
messages=None,
summary_discounts=None,
payment_intent_id=None,
**kwargs
):
if response is None:
Expand Down Expand Up @@ -458,6 +472,7 @@ def assert_expected_response(
'basket_id': basket.id,
'currency': currency,
'enable_stripe_payment_processor': enable_stripe_payment_processor,
'is_dynamic_payment_methods_enabled': is_dynamic_payment_methods_enabled,
'offers': offers,
'coupons': coupons,
'messages': messages if messages else [],
Expand All @@ -466,6 +481,7 @@ def assert_expected_response(
'summary_discounts': summary_discounts,
'summary_price': summary_price,
'order_total': order_total,
'payment_intent_id': payment_intent_id,
'products': [
{
'product_type': product_type,
Expand Down Expand Up @@ -712,6 +728,30 @@ def test_enable_stripe_payment_processor_flag(self, enable_stripe_payment_proces
enable_stripe_payment_processor=enable_stripe_payment_processor,
)

def test_cart_with_stripe_data(self):
"""
For Dynamic Payment Methods, the basket will contain Payment Intent information.
Verify that the basket contains Payment Intent ID and if DPM is enabled.
"""
payment_intent_id = 'pi_3OqcQ5H4caH7G0X11y8NKNa4'
payment_intent = {
'payment_method_types': [
'card',
'klarna',
]
}
seat = self.create_seat(self.course)
basket = self.create_basket_and_add_product_stripe(
seat, payment_intent_id, payment_intent
)
response = self.client.get(self.path)
self.assert_expected_response(
basket,
response=response,
is_dynamic_payment_methods_enabled=len(payment_intent['payment_method_types']) > 1,
payment_intent_id=payment_intent_id
)

@responses.activate
def test_enterprise_free_basket_redirect(self):
"""
Expand Down
19 changes: 19 additions & 0 deletions ecommerce/extensions/basket/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from ecommerce.courses.utils import mode_for_product
from ecommerce.extensions.analytics.utils import track_segment_event
from ecommerce.extensions.basket.constants import (
DYNAMIC_PAYMENT_METHODS_ENABLED,
EMAIL_OPT_IN_ATTRIBUTE,
ENABLE_STRIPE_PAYMENT_PROCESSOR,
PAYMENT_INTENT_ID_ATTRIBUTE,
Expand Down Expand Up @@ -394,6 +395,24 @@ def basket_add_organization_attribute(basket, request_data):
)


@newrelic.agent.function_trace()
def basket_add_dynamic_payment_methods_enabled(basket, payment_intent):
"""
Adds a boolean value which is True if there is more than
'card' payment method type in the Stripe Payment Intent.
"""
dynamic_payment_methods_enabled_attribute, __ = BasketAttributeType.objects.get_or_create(
name=DYNAMIC_PAYMENT_METHODS_ENABLED)
# Do a get_or_create and update value_text after (instead of update_or_create)
# to prevent a particularly slow full table scan that uses a LIKE
basket_attribute, __ = BasketAttribute.objects.get_or_create(
attribute_type=dynamic_payment_methods_enabled_attribute,
basket=basket,
)
basket_attribute.value_text = len(payment_intent['payment_method_types']) > 1
basket_attribute.save()


@newrelic.agent.function_trace()
def basket_add_payment_intent_id_attribute(basket, payment_intent_id):
"""
Expand Down
32 changes: 31 additions & 1 deletion ecommerce/extensions/basket/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@
translate_basket_line_for_segment
)
from ecommerce.extensions.basket import message_utils
from ecommerce.extensions.basket.constants import ENABLE_STRIPE_PAYMENT_PROCESSOR
from ecommerce.extensions.basket.constants import (
DYNAMIC_PAYMENT_METHODS_ENABLED,
ENABLE_STRIPE_PAYMENT_PROCESSOR,
PAYMENT_INTENT_ID_ATTRIBUTE
)
from ecommerce.extensions.basket.exceptions import BadRequestException, RedirectException, VoucherException
from ecommerce.extensions.basket.utils import (
add_invalid_code_message_to_url,
Expand Down Expand Up @@ -677,6 +681,8 @@ def _serialize_context(self, context, lines_data):
self._add_offers(response)
self._add_coupons(response, context)
self._add_enable_stripe_payment_processor(response)
self._add_payment_intent_id(response, self.request.basket)
self._add_is_dynamic_payment_methods(response, self.request.basket)
return response

def _add_products(self, response, lines_data):
Expand Down Expand Up @@ -739,6 +745,30 @@ def _add_enable_stripe_payment_processor(self, response):
self.request, ENABLE_STRIPE_PAYMENT_PROCESSOR
)

def _add_payment_intent_id(self, response, basket):
try:
payment_intent_id_attribute = BasketAttributeType.objects.get(name=PAYMENT_INTENT_ID_ATTRIBUTE)
payment_intent_attr = BasketAttribute.objects.get(
basket=basket,
attribute_type=payment_intent_id_attribute
)
response['payment_intent_id'] = payment_intent_attr.value_text.strip()
except (BasketAttribute.DoesNotExist, BasketAttributeType.DoesNotExist):
response['payment_intent_id'] = None

def _add_is_dynamic_payment_methods(self, response, basket):
try:
dynamic_payment_methods_enabled_attribute = BasketAttributeType.objects.get(
name=DYNAMIC_PAYMENT_METHODS_ENABLED)
payment_intent_attr = BasketAttribute.objects.get(
basket=basket,
attribute_type=dynamic_payment_methods_enabled_attribute
)
is_dynamic_payment_methods_enabled = payment_intent_attr.value_text.strip()
response['is_dynamic_payment_methods_enabled'] = is_dynamic_payment_methods_enabled == 'True'
except (BasketAttribute.DoesNotExist, BasketAttributeType.DoesNotExist):
response['is_dynamic_payment_methods_enabled'] = None

def _get_response_status(self, response):
return message_utils.get_response_status(response['messages'])

Expand Down
20 changes: 16 additions & 4 deletions ecommerce/extensions/checkout/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def add_payment_event(self, event): # pylint: disable = arguments-differ
self._payment_events = []
self._payment_events.append(event)

def handle_payment(self, response, basket): # pylint: disable=arguments-differ
def handle_payment(self, response, basket): # pylint: disable=arguments-differ, inconsistent-return-statements
"""
Handle any payment processing and record payment sources and events.
Expand All @@ -111,14 +111,26 @@ def handle_payment(self, response, basket): # pylint: disable=arguments-differ
# If payment didn't go through, the handle_processor_response function will raise an error. We want to
# send the event regardless of if the payment didn't go through.
try:
handled_processor_response = self.payment_processor.handle_processor_response(response, basket=basket)
processor_response = self.payment_processor.handle_processor_response(response, basket=basket)
except Exception as ex:
properties.update({'success': False, 'payment_error': type(ex).__name__, })
raise
else:
# If the processor_response has a status, it's a InProgressProcessorResponse,
# which means the payment is part of dynamic payment methods
if 'status' in processor_response._fields:
logger.info(
'Dynamic Payment Method in progress for edX order %s and basket %s, '
'returning Payment Intent %s with status %s to the payment MFE.',
processor_response.order_number,
processor_response.basket_id,
processor_response.transaction_id,
processor_response.status,
)
return processor_response
# We only record successful payments in the database.
self.record_payment(basket, handled_processor_response)
properties.update({'total': handled_processor_response.total, 'success': True, })
self.record_payment(basket, processor_response)
properties.update({'total': processor_response.total, 'success': True, })
finally:
track_segment_event(basket.site, basket.owner, 'Payment Processor Response', properties)

Expand Down
4 changes: 4 additions & 0 deletions ecommerce/extensions/payment/processors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
HandledProcessorResponse = namedtuple('HandledProcessorResponse',
['transaction_id', 'total', 'currency', 'card_number', 'card_type'])

InProgressProcessorResponse = namedtuple('InProgressProcessorResponse',
['basket_id', 'order_number', 'transaction_id', 'confirmation_client_secret',
'status', 'payment_method', 'total'])

logger = logging.getLogger(__name__)


Expand Down
Loading

0 comments on commit 5482fe2

Please sign in to comment.