From 140b75f1112acee9af325257386eeba721417d1b Mon Sep 17 00:00:00 2001 From: Phillip Shiu Date: Tue, 1 Nov 2022 17:06:29 +0300 Subject: [PATCH] feat: add initial support for stripe payment intents (#3847) * temp: add project-zebra to branches that should run ci (#3824) * fix: initial implementation of stripe fixes (#3817) Co-authored-by: John Nagro * feat: Add waffle flag to enable Stripe payments (#3816) REV-3004 * feat: initial implementation of Stripe processor (#3826) Co-authored-by: wdrussell2015 <43426024+wdrussell2015@users.noreply.github.com> Co-authored-by: John Nagro * fix: billing address creation * fix: retreive calls cannot take idempotency keys * temp: add logging * temp: add more logging * temp: add logging * fix: parsing address from payment intent * chore: cleanup logs * fix: Get billing address line2 default empty (#3833) * fix: Stripe billing address line2 default to empty if null (#3835) REV-3069 * feat: add SDN check (#3834) * feat: add SDN check * feat: use confirm call to complete transaction with stripe in ecommerce checkout endpoint * fix: make capture-context more resilient to idempotency errors (#3836) * fix: make capture-context more resilient to idempotency errors * fix: add new card types for stripe * feat: /management fulfill actions for Stripe FulfillFrozenBaskets is a helper class for a management page in Ecommerce (hosted at URL /management) that fulfills baskets that are paid but stuck in queue for fulfillment, to be used if, for example, ecommerce-worker is down. Add Stripe support for FulfillFrozenBaskets. Change FulfillFrozenBaskets so it does not continue fulfillment if the billing details are not retrievable. For Stripe, this handles when: * The PaymentMethod was not expanded in the PaymentIntent stored in the PaymentProcessorResponse (generating a TypeError) * The PaymentMethod was not a card (generating a KeyError). Also switched Cybersource to not fulfill if it cannot find the card brand. * fix: update PaymentIntent before confirmation * fix: stripe refunds should be of payment_intents, not charges * fix: change PaymentIntents to update_or_create Apparently, idempotency keys can expire after 24 hours. https://stripe.com/docs/api/idempotent_requests "Keys are eligible to be removed from the system automatically after they're at least 24 hours old, and a new request is generated if a key is reused after the original has been pruned." * refactor: reduce number of statements in management/utils.py for quality * fix: enable Stripe custom actions beta in settings * fix: finish passing api_version and enable_telemetry to stripe client * fix: add 'unknown' stripe card brand * fix: stripe api calls should be to payment_method and payment_intent_id * test: patch for stripePaymentIntent.modify * fix: StripeCheckoutView as a View instead of a FormView-like class StripeCheckoutView was previously like a FormView. However, our implementation of Stripe is much simpler and we opted to remove the overhead of Forms in lieu of trying to ensure all existing interfaces related to StripeSubmitForm. * fix: handle when Stripe charge is already refunded * test: correct refund_mock to use new payment_intent API * test: update Stripe mock responses for new API using actual calls * fix: log when payment_intent_id is missing from basket * refactor: move logging before retrieving any baskets * fix: clear out edx.org-specific Stripe publishable_key and secret_key * fix: throw a 500 on capture_context exceptions The Payment MFE will hang silently if capture_context fails. Throwing the exception will show an error message in Payment MFE, cluing the user in to the fact that there might be a problem. * fix: convert StripeCheckoutView to DRF for JWT auth Looks like JWT auth only works with DRF. * fix: return 200s or 400s instead of 302s in StripeCheckoutView We tried using 302s to redirect the Payment MFE to the receipt page. This didn't work because the library that we use for requests, Axios, doesn't appear to support not following redirects. Return a 200 or 400 instead, and separately instruct the frontend on what to do when receiving these. * fix: deprecate ECOMMERCE_PAYMENT_PROCESSOR_CONFIG.stripe.error_url error_path is already autoset by _oscar.py and grabbed by property processors.stripe.Stripe.error_url. * Revert "fix: throw a 500 on capture_context exceptions" This reverts commit 100827227bfe0bf99a96763e08c8dedb9907167e. It looks like there is a chance that we might depend on this behavior (no errors on exceptions in _add_capture_context) given how this error occurs with non-negligible frequency in production. Will attempt to warn user on frontend side. * add self.record_processor_response to more stripe code paths (#3852) * feat: REV-3076 | record payment processor for various stripe logical flows * chore: update tests for sanity checks * fix: address comments * fix: use ISO 3166-1 alpha-2 country code for SDN check (#3854) * fix: add waffle flag ENABLE_STRIPE_PAYMENT_PROCESSOR to get_client_side_payment_processor_class() The client-side payment processor decides which payment processor Ecommerce will use when generating the capture context for a payment. Force the client-side payment processor to be 'stripe' when the waffle flag is on. * fix: make postcode & state optional for stripe Cybersource has an optional line2, postcode, and state: https://github.com/openedx/ecommerce/blob/05404ebac527da1f6a0fac7df262ca79f3acec56/ecommerce/extensions/payment/views/cybersource.py#L544-L547 Do the same for Stripe. Co-authored-by: Chris Pappas Co-authored-by: John Nagro Co-authored-by: Juliana Kang Co-authored-by: wdrussell2015 <43426024+wdrussell2015@users.noreply.github.com> Co-authored-by: Chris Pappas --- .github/workflows/ci.yml | 4 +- ecommerce/core/models.py | 12 +- ecommerce/core/tests/test_models.py | 18 +- ecommerce/extensions/api/v2/constants.py | 4 +- ecommerce/extensions/basket/constants.py | 10 + .../extensions/basket/tests/test_views.py | 16 +- ecommerce/extensions/basket/utils.py | 43 +- ecommerce/extensions/basket/views.py | 15 +- ecommerce/extensions/payment/constants.py | 24 +- .../migrations/0032_alter_source_card_type.py | 18 + .../extensions/payment/processors/__init__.py | 3 + .../payment/processors/cybersource.py | 3 +- .../extensions/payment/processors/stripe.py | 197 ++++++--- .../payment/tests/processors/test_stripe.py | 183 ++++----- .../test_stripe_test_payment_flow.json | 384 ++++++++++++++++++ .../payment/tests/views/test_stripe.py | 372 +++++++++++------ ecommerce/extensions/payment/urls.py | 1 + .../extensions/payment/views/apple_pay.py | 2 +- ecommerce/extensions/payment/views/stripe.py | 191 ++++++++- ecommerce/management/utils.py | 33 +- ecommerce/settings/_oscar.py | 10 +- ecommerce/settings/base.py | 12 +- ecommerce/settings/devstack.py | 14 +- ecommerce/settings/local.py | 11 +- ecommerce/settings/test.py | 24 +- requirements/base.in | 2 +- requirements/base.txt | 49 ++- requirements/common_constraints.txt | 2 + requirements/dev.txt | 55 ++- requirements/docs.txt | 8 +- requirements/e2e.txt | 20 +- requirements/production.txt | 49 ++- requirements/test.txt | 53 ++- 33 files changed, 1383 insertions(+), 459 deletions(-) create mode 100644 ecommerce/extensions/payment/migrations/0032_alter_source_card_type.py create mode 100644 ecommerce/extensions/payment/tests/views/fixtures/test_stripe_test_payment_flow.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbe9592de38..113bafc2ca5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [master] + branches: [master, project-zebra] pull_request: - branches: [master] + branches: [master, project-zebra] jobs: build: diff --git a/ecommerce/core/models.py b/ecommerce/core/models.py index 4b1cdff6761..8ef35bdd510 100644 --- a/ecommerce/core/models.py +++ b/ecommerce/core/models.py @@ -21,6 +21,7 @@ from ecommerce.core.constants import ALL_ACCESS_CONTEXT, ALLOW_MISSING_LMS_USER_ID from ecommerce.core.exceptions import MissingLmsUserIdException from ecommerce.core.utils import log_message_and_raise_validation_error +from ecommerce.extensions.basket.constants import ENABLE_STRIPE_PAYMENT_PROCESSOR from ecommerce.extensions.payment.exceptions import ProcessorNotFoundError from ecommerce.extensions.payment.helpers import get_processor_class, get_processor_class_by_name @@ -274,7 +275,7 @@ def get_payment_processors(self): if processor.NAME in self.payment_processors_set and processor.is_enabled() ] - def get_client_side_payment_processor_class(self): + def get_client_side_payment_processor_class(self, request): """ Returns the payment processor class to be used for client-side payments. If no processor is set, returns None. @@ -282,9 +283,16 @@ def get_client_side_payment_processor_class(self): Returns: BasePaymentProcessor """ + desired_processor = self.client_side_payment_processor + + # Force client_side_payment_processor to be Stripe when waffle flag is set. + # This allows slowly increasing the percentage of users redirected to Stripe. + if waffle.flag_is_active(request, ENABLE_STRIPE_PAYMENT_PROCESSOR): + desired_processor = 'stripe' + if self.client_side_payment_processor: for processor in self._all_payment_processors(): - if processor.NAME == self.client_side_payment_processor: + if processor.NAME == desired_processor: return processor return None diff --git a/ecommerce/core/tests/test_models.py b/ecommerce/core/tests/test_models.py index ef07b74e9a9..4dfe176199f 100644 --- a/ecommerce/core/tests/test_models.py +++ b/ecommerce/core/tests/test_models.py @@ -13,6 +13,7 @@ from requests.exceptions import ConnectionError as ReqConnectionError from social_django.models import UserSocialAuth from testfixtures import LogCapture +from waffle.testutils import override_flag from ecommerce.core.models import ( BusinessClient, @@ -22,6 +23,7 @@ User ) from ecommerce.core.tests import toggle_switch +from ecommerce.extensions.basket.constants import ENABLE_STRIPE_PAYMENT_PROCESSOR from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin from ecommerce.extensions.payment.tests.processors import AnotherDummyProcessor, DummyProcessor from ecommerce.tests.factories import SiteConfigurationFactory @@ -334,10 +336,22 @@ def test_get_client_side_payment_processor(self): site_config = _make_site_config(processor_name) site_config.client_side_payment_processor = None - self.assertIsNone(site_config.get_client_side_payment_processor_class()) + self.assertIsNone(site_config.get_client_side_payment_processor_class(request=None)) site_config.client_side_payment_processor = processor_name - self.assertEqual(site_config.get_client_side_payment_processor_class().NAME, processor_name) + self.assertEqual(site_config.get_client_side_payment_processor_class(request=None).NAME, processor_name) + + @override_flag(ENABLE_STRIPE_PAYMENT_PROCESSOR, active=True) + def test_get_client_side_payment_processor_waffle_enabled(self): + """ Verify that Stripe is always returned when waffle flag is on. """ + processor_name = 'cybersource,stripe' + site_config = _make_site_config(processor_name) + + site_config.client_side_payment_processor = None + self.assertIsNone(site_config.get_client_side_payment_processor_class(request=None)) + + site_config.client_side_payment_processor = 'cybersource' + self.assertEqual(site_config.get_client_side_payment_processor_class(request=None).NAME, 'stripe') def test_get_from_email(self): """ diff --git a/ecommerce/extensions/api/v2/constants.py b/ecommerce/extensions/api/v2/constants.py index 3b9a10eafe6..f67baf6a80b 100644 --- a/ecommerce/extensions/api/v2/constants.py +++ b/ecommerce/extensions/api/v2/constants.py @@ -9,7 +9,7 @@ # .. toggle_name: enable_hoist_order_history # .. toggle_type: waffle_flag # .. toggle_default: False -# .. toggle_description: Allows order fetching from Commerce Coordinator API for display in Order History MFE. +# .. toggle_description: Allows order fetching from Commerce Coordinator API for display in Order History MFE # .. toggle_use_cases: open_edx # .. toggle_creation_date: 2022-04-05 # .. toggle_tickets: REV-2576 @@ -19,7 +19,7 @@ # .. toggle_name: enable_receipts_via_ecommerce_mfe # .. toggle_type: waffle_flag # .. toggle_default: False -# .. toggle_description: Determines whether to send user to new receipt page (vs old) . +# .. toggle_description: Determines whether to send user to new receipt page (vs old) # .. toggle_use_cases: open_edx # .. toggle_creation_date: 2022-06-02 # .. toggle_tickets: REV-2687 diff --git a/ecommerce/extensions/basket/constants.py b/ecommerce/extensions/basket/constants.py index 24a068fd44b..bfd7ae1bd94 100644 --- a/ecommerce/extensions/basket/constants.py +++ b/ecommerce/extensions/basket/constants.py @@ -2,3 +2,13 @@ EMAIL_OPT_IN_ATTRIBUTE = "email_opt_in" PURCHASER_BEHALF_ATTRIBUTE = "purchased_for_organization" PAYMENT_INTENT_ID_ATTRIBUTE = "payment_intent_id" + +# .. toggle_name: enable_stripe_payment_processor +# .. toggle_type: waffle_flag +# .. toggle_default: False +# .. toggle_description: Allows payments to be processed through Stripe instead of CyberSource +# .. toggle_use_cases: open_edx +# .. toggle_creation_date: 2022-09-19 +# .. toggle_tickets: REV-3004 +# .. toggle_status: supported +ENABLE_STRIPE_PAYMENT_PROCESSOR = 'enable_stripe_payment_processor' diff --git a/ecommerce/extensions/basket/tests/test_views.py b/ecommerce/extensions/basket/tests/test_views.py index 473af08447b..cbf6c237982 100644 --- a/ecommerce/extensions/basket/tests/test_views.py +++ b/ecommerce/extensions/basket/tests/test_views.py @@ -33,7 +33,7 @@ from ecommerce.enterprise.utils import construct_enterprise_course_consent_url from ecommerce.entitlements.utils import create_or_update_course_entitlement from ecommerce.extensions.analytics.utils import translate_basket_line_for_segment -from ecommerce.extensions.basket.constants import EMAIL_OPT_IN_ATTRIBUTE +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 @@ -403,6 +403,7 @@ def assert_empty_basket_response( def assert_expected_response( self, basket, + enable_stripe_payment_processor=False, url=None, response=None, status_code=200, @@ -456,6 +457,7 @@ def assert_expected_response( expected_response = { 'basket_id': basket.id, 'currency': currency, + 'enable_stripe_payment_processor': enable_stripe_payment_processor, 'offers': offers, 'coupons': coupons, 'messages': messages if messages else [], @@ -698,6 +700,18 @@ def test_discounted_seat_type(self, discount_value): voucher=voucher, ) + @ddt.data(True, False) + def test_enable_stripe_payment_processor_flag(self, enable_stripe_payment_processor): + with override_flag(ENABLE_STRIPE_PAYMENT_PROCESSOR, active=enable_stripe_payment_processor): + seat = self.create_seat(self.course) + basket = self.create_basket_and_add_product(seat) + response = self.client.get(self.path) + self.assert_expected_response( + basket, + response=response, + enable_stripe_payment_processor=enable_stripe_payment_processor, + ) + @responses.activate def test_enterprise_free_basket_redirect(self): """ diff --git a/ecommerce/extensions/basket/utils.py b/ecommerce/extensions/basket/utils.py index 7dedbcd825d..67a6a408cda 100644 --- a/ecommerce/extensions/basket/utils.py +++ b/ecommerce/extensions/basket/utils.py @@ -18,7 +18,7 @@ from ecommerce.core.url_utils import absolute_url from ecommerce.courses.utils import mode_for_product from ecommerce.extensions.analytics.utils import track_segment_event -from ecommerce.extensions.basket.constants import PURCHASER_BEHALF_ATTRIBUTE +from ecommerce.extensions.basket.constants import PAYMENT_INTENT_ID_ATTRIBUTE, PURCHASER_BEHALF_ATTRIBUTE from ecommerce.extensions.order.exceptions import AlreadyPlacedOrderException from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder from ecommerce.extensions.payment.constants import DISABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME @@ -30,6 +30,8 @@ Basket = get_model('basket', 'Basket') BasketAttribute = get_model('basket', 'BasketAttribute') BasketAttributeType = get_model('basket', 'BasketAttributeType') +BillingAddress = get_model('order', 'BillingAddress') +Country = get_model('address', 'Country') BUNDLE = 'bundle_identifier' ORGANIZATION_ATTRIBUTE_TYPE = 'organization' ENTERPRISE_CATALOG_ATTRIBUTE_TYPE = 'enterprise_catalog_uuid' @@ -384,6 +386,25 @@ def basket_add_organization_attribute(basket, request_data): ) +@newrelic.agent.function_trace() +def basket_add_payment_intent_id_attribute(basket, payment_intent_id): + """ + Adds the Stripe payment_intent_id attribute on basket. + + Arguments: + basket(Basket): order basket + payment_intent_id (string): Payment Intent Identifier + + """ + + payment_intent_id_attribute, __ = BasketAttributeType.objects.get_or_create(name=PAYMENT_INTENT_ID_ATTRIBUTE) + BasketAttribute.objects.update_or_create( + basket=basket, + attribute_type=payment_intent_id_attribute, + defaults={'value_text': payment_intent_id.strip()} + ) + + @newrelic.agent.function_trace() def basket_add_enterprise_catalog_attribute(basket, request_data): """ @@ -569,3 +590,23 @@ def is_duplicate_seat_attempt(basket, product): found_product_quantity = basket.product_quantity(product) return bool(product_type == 'Seat' and found_product_quantity) + + +def get_billing_address_from_payment_intent_data(payment_intent): + """ + Take stripes response_data dict, instantiates a BillingAddress object + and return it. + """ + billing_details = payment_intent['payment_method']['billing_details'] + customer_address = billing_details['address'] + address = BillingAddress( + first_name=billing_details['name'], # Stripe only has a single name field + last_name='', + line1=customer_address['line1'], + line2='' if not customer_address['line2'] else customer_address['line2'], # line2 is optional + line4=customer_address['city'], # Oscar uses line4 for city + postcode='' if not customer_address['postal_code'] else customer_address['postal_code'], # postcode is optional + state='' if not customer_address['state'] else customer_address['state'], # state is optional + country=Country.objects.get(iso_3166_1_a2__iexact=customer_address['country']) + ) + return address diff --git a/ecommerce/extensions/basket/views.py b/ecommerce/extensions/basket/views.py index 1e568d7f868..33726fc8c73 100644 --- a/ecommerce/extensions/basket/views.py +++ b/ecommerce/extensions/basket/views.py @@ -47,7 +47,7 @@ translate_basket_line_for_segment ) from ecommerce.extensions.basket import message_utils -from ecommerce.extensions.basket.constants import EMAIL_OPT_IN_ATTRIBUTE +from ecommerce.extensions.basket.constants import EMAIL_OPT_IN_ATTRIBUTE, ENABLE_STRIPE_PAYMENT_PROCESSOR from ecommerce.extensions.basket.exceptions import BadRequestException, RedirectException, VoucherException from ecommerce.extensions.basket.utils import ( add_invalid_code_message_to_url, @@ -583,7 +583,7 @@ def _get_payment_processors_data(self, payment_processors): basket view context needs to be updated with. """ site_configuration = self.request.site.siteconfiguration - payment_processor_class = site_configuration.get_client_side_payment_processor_class() + payment_processor_class = site_configuration.get_client_side_payment_processor_class(self.request) if payment_processor_class: payment_processor = payment_processor_class(self.request.site) @@ -615,7 +615,8 @@ class CaptureContextApiLogicMixin: # pragma: no cover Business logic for the capture context API. """ def _add_capture_context(self, response): - payment_processor_class = self.request.site.siteconfiguration.get_client_side_payment_processor_class() + site_configuration = self.request.site.siteconfiguration + payment_processor_class = site_configuration.get_client_side_payment_processor_class(self.request) if not payment_processor_class: return payment_processor = payment_processor_class(self.request.site) @@ -623,7 +624,7 @@ def _add_capture_context(self, response): return try: - response['capture_context'] = payment_processor.get_capture_context(self.request.session) + response['capture_context'] = payment_processor.get_capture_context(self.request) except: # pylint: disable=bare-except logger.exception("Error generating capture_context") return @@ -681,6 +682,7 @@ def _serialize_context(self, context, lines_data): self._add_total_summary(response, context) self._add_offers(response) self._add_coupons(response, context) + self._add_enable_stripe_payment_processor(response) return response def _add_products(self, response, lines_data): @@ -738,6 +740,11 @@ def _add_coupons(self, response, context): def _add_messages(self, response): response['messages'] = message_utils.serialize(self.request) + def _add_enable_stripe_payment_processor(self, response): + response['enable_stripe_payment_processor'] = waffle.flag_is_active( + self.request, ENABLE_STRIPE_PAYMENT_PROCESSOR + ) + def _get_response_status(self, response): return message_utils.get_response_status(response['messages']) diff --git a/ecommerce/extensions/payment/constants.py b/ecommerce/extensions/payment/constants.py index a393e85ab84..ecc25c488ab 100644 --- a/ecommerce/extensions/payment/constants.py +++ b/ecommerce/extensions/payment/constants.py @@ -8,25 +8,41 @@ 'display_name': _('American Express'), 'cybersource_code': '003', 'apple_pay_network': 'amex', - 'stripe_brand': 'American Express', + 'stripe_brand': 'amex', + }, + 'diners': { + 'display_name': _('Diners'), + 'stripe_brand': 'diners', }, 'discover': { 'display_name': _('Discover'), 'cybersource_code': '004', 'apple_pay_network': 'discover', - 'stripe_brand': 'Discover', + 'stripe_brand': 'discover', + }, + 'jcb': { + 'display_name': _('JCB'), + 'stripe_brand': 'jcb', }, 'mastercard': { 'display_name': _('MasterCard'), 'cybersource_code': '002', 'apple_pay_network': 'mastercard', - 'stripe_brand': 'MasterCard', + 'stripe_brand': 'mastercard', + }, + 'unionpay': { + 'display_name': _('UnionPay'), + 'stripe_brand': 'unionpay', + }, + 'unknown': { + 'display_name': _('Unknown'), + 'stripe_brand': 'unknown', }, 'visa': { 'display_name': _('Visa'), 'cybersource_code': '001', 'apple_pay_network': 'visa', - 'stripe_brand': 'Visa', + 'stripe_brand': 'visa', }, } diff --git a/ecommerce/extensions/payment/migrations/0032_alter_source_card_type.py b/ecommerce/extensions/payment/migrations/0032_alter_source_card_type.py new file mode 100644 index 00000000000..4572746919e --- /dev/null +++ b/ecommerce/extensions/payment/migrations/0032_alter_source_card_type.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2022-10-11 16:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('payment', '0031_sdnfallbackdata'), + ] + + operations = [ + migrations.AlterField( + model_name='source', + name='card_type', + field=models.CharField(blank=True, choices=[('american_express', 'American Express'), ('diners', 'Diners'), ('discover', 'Discover'), ('jcb', 'JCB'), ('mastercard', 'MasterCard'), ('unionpay', 'UnionPay'), ('unknown', 'Unknown'), ('visa', 'Visa')], max_length=255, null=True), + ), + ] diff --git a/ecommerce/extensions/payment/processors/__init__.py b/ecommerce/extensions/payment/processors/__init__.py index 6a1df9d20d9..31caf91b540 100644 --- a/ecommerce/extensions/payment/processors/__init__.py +++ b/ecommerce/extensions/payment/processors/__init__.py @@ -1,6 +1,7 @@ import abc +import logging from collections import namedtuple import waffle @@ -13,6 +14,8 @@ HandledProcessorResponse = namedtuple('HandledProcessorResponse', ['transaction_id', 'total', 'currency', 'card_number', 'card_type']) +logger = logging.getLogger(__name__) + class BasePaymentProcessor(metaclass=abc.ABCMeta): # pragma: no cover """Base payment processor class.""" diff --git a/ecommerce/extensions/payment/processors/cybersource.py b/ecommerce/extensions/payment/processors/cybersource.py index 3b9556c51e3..529e47a5d22 100644 --- a/ecommerce/extensions/payment/processors/cybersource.py +++ b/ecommerce/extensions/payment/processors/cybersource.py @@ -177,8 +177,9 @@ def __init__(self, site): def client_side_payment_url(self): return None - def get_capture_context(self, session): # pragma: no cover + def get_capture_context(self, request): # pragma: no cover # To delete None values in Input Request Json body + session = request.session requestObj = GeneratePublicKeyRequest( encryption_type='RsaOaep256', diff --git a/ecommerce/extensions/payment/processors/stripe.py b/ecommerce/extensions/payment/processors/stripe.py index cc7fe769286..ccd3bde2c22 100644 --- a/ecommerce/extensions/payment/processors/stripe.py +++ b/ecommerce/extensions/payment/processors/stripe.py @@ -4,9 +4,16 @@ import logging import stripe -from oscar.apps.payment.exceptions import GatewayError, TransactionDeclined +from oscar.apps.payment.exceptions import GatewayError from oscar.core.loading import get_model +from ecommerce.core.url_utils import get_ecommerce_url +from ecommerce.extensions.basket.constants import PAYMENT_INTENT_ID_ATTRIBUTE +from ecommerce.extensions.basket.models import Basket +from ecommerce.extensions.basket.utils import ( + basket_add_payment_intent_id_attribute, + get_billing_address_from_payment_intent_data +) from ecommerce.extensions.payment.constants import STRIPE_CARD_TYPE_MAP from ecommerce.extensions.payment.processors import ( ApplePayMixin, @@ -16,6 +23,8 @@ logger = logging.getLogger(__name__) +BasketAttribute = get_model('basket', 'BasketAttribute') +BasketAttributeType = get_model('basket', 'BasketAttributeType') BillingAddress = get_model('order', 'BillingAddress') Country = get_model('address', 'Country') PaymentEvent = get_model('order', 'PaymentEvent') @@ -38,57 +47,136 @@ def __init__(self, site): """ super(Stripe, self).__init__(site) configuration = self.configuration + + # Stripe API version to use. Will use latest allowed in Stripe Dashboard if None. + self.api_version = configuration['api_version'] + # Send anonymous latency metrics to Stripe. + self.enable_telemetry = configuration['enable_telemetry'] + # Stripe client logging level. None will default to INFO. + self.log_level = configuration['log_level'] + # How many times to automatically retry requests. None means no retries. + self.max_network_retries = configuration['max_network_retries'] + # Send requests somewhere else instead of Stripe. May be useful for testing. + self.proxy = configuration['proxy'] + # The key visible on the frontend to identify our Stripe account. Public. self.publishable_key = configuration['publishable_key'] + # The secret API key used by the backend to communicate with Stripe. Private/secret. self.secret_key = configuration['secret_key'] - self.country = configuration['country'] stripe.api_key = self.secret_key + stripe.api_version = self.api_version + stripe.enable_telemetry = self.enable_telemetry + stripe.log = self.log_level + stripe.max_network_retries = self.max_network_retries + stripe.proxy = self.proxy - def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=True, **kwargs): - raise NotImplementedError('The Stripe payment processor does not support transaction parameters.') + @property + def cancel_url(self): + return get_ecommerce_url(self.configuration['cancel_checkout_path']) + + @property + def error_url(self): + return get_ecommerce_url(self.configuration['error_path']) def _get_basket_amount(self, basket): + """Convert to stripe amount, which is in cents.""" return str((basket.total_incl_tax * 100).to_integral_value()) - def handle_processor_response(self, response, basket=None): - token = response + def _build_payment_intent_parameters(self, basket): order_number = basket.order_number + amount = self._get_basket_amount(basket) currency = basket.currency + return { + 'amount': amount, + 'currency': currency, + 'description': order_number, + 'metadata': {'order_number': order_number}, + } + + def generate_basket_pi_idempotency_key(self, basket): + """ + Generate an idempotency key for creating a PaymentIntent for a Basket. + Using a version number in they key to aid in future development. + """ + return f'basket_pi_create_v1_{basket.order_number}' - # NOTE: In the future we may want to get/create a Customer. See https://stripe.com/docs/api#customers. + def get_capture_context(self, request): + # TODO: consider whether the basket should be passed in from MFE, not retrieved from Oscar + basket = Basket.get_basket(request.user, request.site) try: - charge = stripe.Charge.create( - amount=self._get_basket_amount(basket), - currency=currency, - source=token, - description=order_number, - metadata={'order_number': order_number} + stripe_response = stripe.PaymentIntent.create( + **self._build_payment_intent_parameters(basket), + # This means this payment intent can only be confirmed with secret key (as in, from ecommerce) + secret_key_confirmation='required', + # don't create a new intent for the same basket + idempotency_key=self.generate_basket_pi_idempotency_key(basket), + ) + # id is the payment_intent_id from Stripe + transaction_id = stripe_response['id'] + basket_add_payment_intent_id_attribute(basket, transaction_id) + # for when basket was already created, but with different amount + except stripe.error.IdempotencyError: + # if this PI has been created before, we should be able to retrieve + # it from Stripe using the payment_intent_id BasketAttribute. + # Note that we update the PI's price in handle_processor_response + # before hitting the confirm endpoint, so we don't need to do that here + 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 ) - transaction_id = charge.id - - # NOTE: Charge objects subclass the dict class so there is no need to do any data transformation - # before storing the response in the database. - self.record_processor_response(charge, transaction_id=transaction_id, basket=basket) - logger.info('Successfully created Stripe charge [%s] for basket [%d].', transaction_id, basket.id) - except stripe.error.CardError as ex: - base_message = "Stripe payment for basket [%d] declined with HTTP status [%d]" - exception_format_string = "{}: %s".format(base_message) - body = ex.json_body - logger.exception( - exception_format_string, - basket.id, - ex.http_status, - body + stripe_response = stripe.PaymentIntent.retrieve(id=payment_intent_attr.value_text.strip()) + + new_capture_context = { + 'key_id': stripe_response['client_secret'], + 'order_id': basket.order_number, + } + return new_capture_context + + def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=True, **kwargs): + return {'payment_page_url': self.client_side_payment_url} + + def handle_processor_response(self, response, basket=None): + # pretty sure we should simply return/error if basket is None, as not + # sure what it would mean if there + payment_intent_id = response['payment_intent_id'] + # NOTE: In the future we may want to get/create a Customer. See https://stripe.com/docs/api#customers. + + # rewrite order amount so it's updated for coupon & quantity and unchanged by the user + stripe.PaymentIntent.modify( + payment_intent_id, + **self._build_payment_intent_parameters(basket), + ) + try: + confirm_api_response = stripe.PaymentIntent.confirm( + payment_intent_id, + # stop on complicated payments MFE can't handle yet + error_on_requires_action=True, + expand=['payment_method'], ) - self.record_processor_response(body, basket=basket) - raise TransactionDeclined(base_message, basket.id, ex.http_status) from ex + except stripe.error.CardError as err: + self.record_processor_response(err.json_body, transaction_id=payment_intent_id, basket=basket) + logger.exception('Card Error for basket [%d]: %s}', basket.id, err) + raise + + # proceed only if payment went through + assert confirm_api_response['status'] == "succeeded" + self.record_processor_response(confirm_api_response, transaction_id=payment_intent_id, basket=basket) + + logger.info( + 'Successfully confirmed Stripe payment intent [%s] for basket [%d].', + payment_intent_id, + basket.id + ) total = basket.total_incl_tax - card_number = charge.source.last4 - card_type = STRIPE_CARD_TYPE_MAP.get(charge.source.brand) + currency = basket.currency + card_object = confirm_api_response['charges']['data'][0]['payment_method_details']['card'] + card_number = card_object['last4'] + card_type = STRIPE_CARD_TYPE_MAP.get(card_object['brand']) return HandledProcessorResponse( - transaction_id=transaction_id, + transaction_id=payment_intent_id, total=total, currency=currency, card_number=card_number, @@ -97,7 +185,20 @@ def handle_processor_response(self, response, basket=None): def issue_credit(self, order_number, basket, reference_number, amount, currency): try: - refund = stripe.Refund.create(charge=reference_number) + refund = stripe.Refund.create(payment_intent=reference_number) + except stripe.error.InvalidRequestError as err: + if err.code == 'charge_already_refunded': + refund = stripe.Refund.list(payment_intent=reference_number, limit=1)['data'][0] + self.record_processor_response(refund, transaction_id=refund.id, basket=basket) + msg = 'Skipping issuing credit (via Stripe) for order [{}] because charge was already refunded.'.format( + order_number) + logger.warning(msg) + else: + self.record_processor_response(err.json_body, transaction_id=reference_number, basket=basket) + msg = 'An error occurred while attempting to issue a credit (via Stripe) for order [{}].'.format( + order_number) + logger.exception(msg) + raise except: msg = 'An error occurred while attempting to issue a credit (via Stripe) for order [{}].'.format( order_number) @@ -105,28 +206,24 @@ def issue_credit(self, order_number, basket, reference_number, amount, currency) raise GatewayError(msg) # pylint: disable=raise-missing-from transaction_id = refund.id - - # NOTE: Refund objects subclass dict so there is no need to do any data transformation - # before storing the response in the database. self.record_processor_response(refund, transaction_id=transaction_id, basket=basket) return transaction_id - def get_address_from_token(self, token): - """ Retrieves the billing address associated with token. + def get_address_from_token(self, payment_intent_id): + """ + Retrieves the billing address associated with a PaymentIntent. Returns: BillingAddress """ - data = stripe.Token.retrieve(token)['card'] - address = BillingAddress( - first_name=data['name'], # Stripe only has a single name field - last_name='', - line1=data['address_line1'], - line2=data.get('address_line2') or '', - line4=data['address_city'], # Oscar uses line4 for city - postcode=data.get('address_zip') or '', - state=data.get('address_state') or '', - country=Country.objects.get(iso_3166_1_a2__iexact=data['address_country']) + retrieve_kwargs = { + 'expand': ['payment_method'], + } + + payment_intent = stripe.PaymentIntent.retrieve( + payment_intent_id, + **retrieve_kwargs, ) - return address + + return get_billing_address_from_payment_intent_data(payment_intent) diff --git a/ecommerce/extensions/payment/tests/processors/test_stripe.py b/ecommerce/extensions/payment/tests/processors/test_stripe.py index 9b5e7510573..4c28cec542d 100644 --- a/ecommerce/extensions/payment/tests/processors/test_stripe.py +++ b/ecommerce/extensions/payment/tests/processors/test_stripe.py @@ -4,7 +4,7 @@ import mock import stripe -from oscar.apps.payment.exceptions import GatewayError, TransactionDeclined +from oscar.apps.payment.exceptions import GatewayError from oscar.core.loading import get_model from ecommerce.extensions.payment.processors.stripe import Stripe @@ -23,45 +23,87 @@ class StripeTests(PaymentProcessorTestCaseMixin, TestCase): processor_name = 'stripe' def test_get_transaction_parameters(self): - self.assertRaises(NotImplementedError, self.processor.get_transaction_parameters, self.basket) + transaction_params = self.processor.get_transaction_parameters(self.basket) + assert 'payment_page_url' in transaction_params.keys() + # TODO: update or remove these tests def test_handle_processor_response(self): - token = 'abc123' - charge = stripe.Charge.construct_from({ - 'id': '2404', - 'source': { - 'brand': 'American Express', - 'last4': '1986', - }, - }, 'fake-key') - - with mock.patch('stripe.Charge.create') as charge_mock: - charge_mock.return_value = charge - - actual = self.processor.handle_processor_response(token, self.basket) - - charge_mock.assert_called_once_with( - amount=str((self.basket.total_incl_tax * 100).to_integral_value()), - currency=self.basket.currency, - source=token, - description=self.basket.order_number, - metadata={'order_number': self.basket.order_number} - ) - - assert actual.transaction_id == charge.id - assert actual.total == self.basket.total_incl_tax - assert actual.currency == self.basket.currency - assert actual.card_number == charge.source.last4 - assert actual.card_type == 'american_express' - - self.assert_processor_response_recorded(self.processor_name, charge.id, charge, basket=self.basket) - - def test_handle_processor_response_error(self): - with mock.patch('stripe.Charge.create') as charge_mock: - charge_mock.side_effect = stripe.error.CardError( - 'fake-msg', 'fake-param', 'fake-code', http_body='fake-body', http_status=500 - ) - self.assertRaises(TransactionDeclined, self.processor.handle_processor_response, 'fake-token', self.basket) + assert True + # payment_intent_1 = stripe.PaymentIntent.construct_from({ + # 'id': 'pi_testtesttest', + # 'source': { + # 'brand': 'visa', + # 'last4': '4242', + # }, + # }, 'fake-key') + + # payment_intent_2 = stripe.PaymentIntent.construct_from({ + # 'id': 'pi_testtesttest', + # 'source': { + # 'brand': 'visa', + # 'last4': '4242', + # }, + # 'status': 'succeeded', + # "charges": { + # "object": "list", + # "data": [ + # { + # "id": "ch_testtesttest", + # "object": "charge", + # "status": "succeeded", + # "payment_method_details": { + # "card": { + # "brand": "visa", + # "country": "US", + # "exp_month": 5, + # "exp_year": 2020, + # "fingerprint": "Xt5EWLLDS7FJjR1c", + # "funding": "credit", + # "last4": "4242", + # "network": "visa", + # }, + # "type": "card" + # }, + # } + # ] + # } + # }, 'fake-key') + + # with mock.patch('stripe.PaymentIntent.modify') as payment_intent_modify_mock: + # with mock.patch('stripe.PaymentIntent.confirm') as payment_intent_confirm_mock: + # payment_intent_modify_mock.return_value = payment_intent_1 + # payment_intent_confirm_mock.return_value = payment_intent_2 + # actual = self.processor.handle_processor_response(payment_intent_1, self.basket) + + # assert actual.transaction_id == payment_intent_1.id + # assert actual.total == self.basket.total_incl_tax + # assert actual.currency == self.basket.currency + + # self.assert_processor_response_recorded( + # self.processor_name, + # payment_intent_2.id, + # payment_intent_2, + # basket=self.basket + # ) + + # def test_handle_processor_response_error(self): + # payment_intent_1 = stripe.PaymentIntent.construct_from({ + # 'id': 'pi_testtesttest', + # 'source': { + # 'brand': 'visa', + # 'last4': '4242', + # }, + # }, 'fake-key') + # with mock.patch('stripe.PaymentIntent.modify') as charge_mock: + # charge_mock.side_effect = stripe.error.CardError( + # 'fake-msg', 'fake-param', 'fake-code', http_body='fake-body', http_status=500 + # ) + # self.assertRaises( + # TransactionDeclined, + # self.processor.handle_processor_response, + # payment_intent_1, + # self.basket + # ) def test_issue_credit(self): charge_reference_number = '9436' @@ -74,7 +116,7 @@ def test_issue_credit(self): refund_mock.return_value = refund self.processor.issue_credit(order.number, order.basket, charge_reference_number, order.total_incl_tax, order.currency) - refund_mock.assert_called_once_with(charge=charge_reference_number) + refund_mock.assert_called_once_with(payment_intent=charge_reference_number) self.assert_processor_response_recorded(self.processor_name, refund.id, refund, basket=self.basket) @@ -87,66 +129,3 @@ def test_issue_credit_error(self): GatewayError, self.processor.issue_credit, order.number, order.basket, '123', order.total_incl_tax, order.currency ) - - def assert_addresses_equal(self, actual, expected): - for field in ('first_name', 'last_name', 'line1', 'line2', 'line3', 'line4', 'postcode', 'state', 'country'): - assert getattr(actual, field) == getattr(expected, field), 'The value of {} differs'.format(field) - - def test_get_address_from_token(self): - country, __ = Country.objects.get_or_create(iso_3166_1_a2='US') - expected = BillingAddress( - first_name='Richard White', - last_name='', - line1='1201 E. 8th Street', - line2='Suite 216', - line4='Dallas', - postcode='75203', - state='TX', - country=country - ) - token = stripe.Token.construct_from({ - 'id': 'tok_test', - 'card': { - 'address_city': 'Dallas', - 'address_country': 'US', - 'address_line1': '1201 E. 8th Street', - 'address_line2': 'Suite 216', - 'address_state': 'TX', - 'address_zip': '75203', - 'name': 'Richard White', - }, - }, 'fake-key') - - with mock.patch('stripe.Token.retrieve') as token_mock: - token_mock.return_value = token - self.assert_addresses_equal(self.processor.get_address_from_token(token.id), expected) - - def test_get_address_from_token_with_optional_fields(self): - country, __ = Country.objects.get_or_create(iso_3166_1_a2='US') - expected = BillingAddress( - first_name='Ned Green', - last_name='', - line1='3111 Bonnie View Road', - line2='', - line4='Dallas', - postcode='', - state='', - country=country - ) - token = stripe.Token.construct_from({ - 'id': 'tok_test', - 'card': { - 'address_city': 'Dallas', - 'address_country': 'US', - 'address_line1': '3111 Bonnie View Road', - 'address_line2': None, - 'address_state': None, - # NOTE: This field is intentionally excluded to simulate the API field missing. - # 'address_zip': None, - 'name': 'Ned Green', - }, - }, 'fake-key') - - with mock.patch('stripe.Token.retrieve') as token_mock: - token_mock.return_value = token - self.assert_addresses_equal(self.processor.get_address_from_token(token.id), expected) diff --git a/ecommerce/extensions/payment/tests/views/fixtures/test_stripe_test_payment_flow.json b/ecommerce/extensions/payment/tests/views/fixtures/test_stripe_test_payment_flow.json new file mode 100644 index 00000000000..a220dabf98d --- /dev/null +++ b/ecommerce/extensions/payment/tests/views/fixtures/test_stripe_test_payment_flow.json @@ -0,0 +1,384 @@ +{ + "happy_path": { + "confirm_resp": { + "id": "pi_3LsftNIadiFyUl1x2TWxaADZ", + "object": "payment_intent", + "amount": 14900, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 14900, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": { + "object": "list", + "data": [ + { + "id": "ch_3LsftNIadiFyUl1x2Qh9P6Te", + "object": "charge", + "amount": 14900, + "amount_captured": 14900, + "amount_refunded": 0, + "application": null, + "application_fee": null, + "application_fee_amount": null, + "balance_transaction": "txn_3LsftNIadiFyUl1x2vxcOEsM", + "billing_details": { + "address": { + "city": "Sample", + "country": "US", + "line1": "123 Test St", + "line2": null, + "postal_code": "12345", + "state": "MA" + }, + "email": "pshiu-dev-test-3@example.com", + "name": "Test User", + "phone": null + }, + "calculated_statement_descriptor": "WWW.EDX.ORG", + "captured": true, + "created": 1665723259, + "currency": "usd", + "customer": null, + "description": "EDX-100011", + "destination": null, + "dispute": null, + "disputed": false, + "failure_balance_transaction": null, + "failure_code": null, + "failure_message": null, + "fraud_details": {}, + "invoice": null, + "livemode": false, + "metadata": { + "order_number": "EDX-100011" + }, + "on_behalf_of": null, + "order": null, + "outcome": { + "network_status": "approved_by_network", + "reason": null, + "risk_level": "normal", + "risk_score": 6, + "seller_message": "Payment complete.", + "type": "authorized" + }, + "paid": true, + "payment_intent": "pi_3LsftNIadiFyUl1x2TWxaADZ", + "payment_method": "pm_1LsfuTIadiFyUl1xcHa5WALi", + "payment_method_details": { + "card": { + "brand": "visa", + "checks": { + "address_line1_check": "pass", + "address_postal_code_check": "pass", + "cvc_check": "pass" + }, + "country": "US", + "exp_month": 1, + "exp_year": 2023, + "fingerprint": "N2ltqpPKT2DuO8HF", + "funding": "credit", + "installments": null, + "last4": "1111", + "mandate": null, + "network": "visa", + "three_d_secure": null, + "wallet": null + }, + "type": "card" + }, + "receipt_email": null, + "receipt_number": null, + "receipt_url": "https://pay.stripe.com/receipts/payment/CAcaFwoVYWNjdF8xTGkyS29JYWRpRnlVbDF4KPzWo5oGMgZEeRWu5K86LBbPJHPT2KSM6XjNMXK1BnCeCX2gQYlTjBilX6VYkgohptS97JXM0Ey2L7-N", + "refunded": false, + "refunds": {}, + "review": null, + "shipping": null, + "source": null, + "source_transfer": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + } + ], + "has_more": false, + "total_count": 1, + "url": "/v1/charges?payment_intent=pi_3LsftNIadiFyUl1x2TWxaADZ" + }, + "client_secret": "pi_3LsftNIadiFyUl1x2TWxaADZ_secret_VxRx7Y1skyp0jKtq7Gdu80Xnh", + "confirmation_method": "automatic", + "created": 1665723185, + "currency": "usd", + "customer": null, + "description": "EDX-100011", + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": { + "order_number": "EDX-100011" + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1LsfuTIadiFyUl1xcHa5WALi", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "secret_key_confirmation": "required", + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "succeeded", + "transfer_data": null, + "transfer_group": null + }, + "create_resp": { + "id": "pi_3LsftNIadiFyUl1x2TWxaADZ", + "object": "payment_intent", + "amount": 14900, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": {}, + "client_secret": "pi_3LsftNIadiFyUl1x2TWxaADZ_secret_VxRx7Y1skyp0jKtq7Gdu80Xnh", + "confirmation_method": "automatic", + "created": 1665723185, + "currency": "usd", + "customer": null, + "description": "EDX-100011", + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": { + "order_number": "EDX-100011" + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": null, + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "secret_key_confirmation": "required", + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_payment_method", + "transfer_data": null, + "transfer_group": null + }, + "modify_resp": { + "id": "pi_3LsftNIadiFyUl1x2TWxaADZ", + "object": "payment_intent", + "amount": 14900, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": {}, + "client_secret": "pi_3LsftNIadiFyUl1x2TWxaADZ_secret_VxRx7Y1skyp0jKtq7Gdu80Xnh", + "confirmation_method": "automatic", + "created": 1665723185, + "currency": "usd", + "customer": null, + "description": "EDX-100011", + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": { + "order_number": "EDX-100011" + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": "pm_1LsfuTIadiFyUl1xcHa5WALi", + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "secret_key_confirmation": "required", + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_confirmation", + "transfer_data": null, + "transfer_group": null + }, + "refund_resp": { + "id": "re_3LsftNIadiFyUl1x2KvTM7FO", + "object": "refund", + "amount": 14900, + "balance_transaction": "txn_3LsftNIadiFyUl1x2trZAzQP", + "charge": "ch_3LsftNIadiFyUl1x2Qh9P6Te", + "created": 1665723358, + "currency": "usd", + "metadata": {}, + "payment_intent": "pi_3LsftNIadiFyUl1x2TWxaADZ", + "reason": null, + "receipt_number": null, + "source_transfer_reversal": null, + "status": "succeeded", + "transfer_reversal": null + }, + "retrieve_addr_resp": { + "id": "pi_3LsftNIadiFyUl1x2TWxaADZ", + "object": "payment_intent", + "amount": 14900, + "amount_capturable": 0, + "amount_details": { + "tip": {} + }, + "amount_received": 0, + "application": null, + "application_fee_amount": null, + "automatic_payment_methods": null, + "canceled_at": null, + "cancellation_reason": null, + "capture_method": "automatic", + "charges": {}, + "client_secret": "pi_3LsftNIadiFyUl1x2TWxaADZ_secret_VxRx7Y1skyp0jKtq7Gdu80Xnh", + "confirmation_method": "automatic", + "created": 1665723185, + "currency": "usd", + "customer": null, + "description": "EDX-100011", + "invoice": null, + "last_payment_error": null, + "livemode": false, + "metadata": { + "order_number": "EDX-100011" + }, + "next_action": null, + "on_behalf_of": null, + "payment_method": { + "id": "pm_1LsfuTIadiFyUl1xcHa5WALi", + "object": "payment_method", + "billing_details": { + "address": { + "city": "Sample", + "country": "US", + "line1": "123 Test St", + "line2": null, + "postal_code": "12345", + "state": "MA" + }, + "email": "pshiu-dev-test-3@example.com", + "name": "Test User", + "phone": null + }, + "card": { + "brand": "visa", + "checks": { + "address_line1_check": "unchecked", + "address_postal_code_check": "unchecked", + "cvc_check": "unchecked" + }, + "country": "US", + "exp_month": 1, + "exp_year": 2023, + "fingerprint": "N2ltqpPKT2DuO8HF", + "funding": "credit", + "generated_from": null, + "last4": "1111", + "networks": { + "available": [ + "visa" + ], + "preferred": null + }, + "three_d_secure_usage": { + "supported": true + }, + "wallet": null + }, + "created": 1665723253, + "customer": null, + "livemode": false, + "metadata": {}, + "type": "card" + }, + "payment_method_options": { + "card": { + "installments": null, + "mandate_options": null, + "network": null, + "request_three_d_secure": "automatic" + } + }, + "payment_method_types": [ + "card" + ], + "processing": null, + "receipt_email": null, + "review": null, + "secret_key_confirmation": "required", + "setup_future_usage": null, + "shipping": null, + "source": null, + "statement_descriptor": null, + "statement_descriptor_suffix": null, + "status": "requires_confirmation", + "transfer_data": null, + "transfer_group": null + } + } +} diff --git a/ecommerce/extensions/payment/tests/views/test_stripe.py b/ecommerce/extensions/payment/tests/views/test_stripe.py index 7de03e2a7ec..901a70374aa 100644 --- a/ecommerce/extensions/payment/tests/views/test_stripe.py +++ b/ecommerce/extensions/payment/tests/views/test_stripe.py @@ -1,42 +1,48 @@ - - import stripe +from ddt import ddt, file_data from django.conf import settings from django.urls import reverse from mock import mock from oscar.core.loading import get_class, get_model -from oscar.test.factories import BillingAddressFactory +from rest_framework import status -from ecommerce.core.constants import ENROLLMENT_CODE_PRODUCT_CLASS_NAME, ENROLLMENT_CODE_SWITCH -from ecommerce.core.models import BusinessClient -from ecommerce.core.tests import toggle_switch +from ecommerce.core.constants import SEAT_PRODUCT_CLASS_NAME from ecommerce.courses.tests.factories import CourseFactory -from ecommerce.extensions.basket.constants import PURCHASER_BEHALF_ATTRIBUTE -from ecommerce.extensions.basket.utils import basket_add_organization_attribute from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.payment.constants import STRIPE_CARD_TYPE_MAP from ecommerce.extensions.payment.processors.stripe import Stripe from ecommerce.extensions.payment.tests.mixins import PaymentEventsMixin from ecommerce.extensions.test.factories import create_basket -from ecommerce.invoice.models import Invoice from ecommerce.tests.testcases import TestCase +BasketAttribute = get_model('basket', 'BasketAttribute') +BasketAttributeType = get_model('basket', 'BasketAttributeType') Country = get_model('address', 'Country') Order = get_model('order', 'Order') PaymentEvent = get_model('order', 'PaymentEvent') Selector = get_class('partner.strategy', 'Selector') Source = get_model('payment', 'Source') Product = get_model('catalogue', 'Product') +PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') -class StripeSubmitViewTests(PaymentEventsMixin, TestCase): +@ddt +class StripeCheckoutViewTests(PaymentEventsMixin, TestCase): path = reverse('stripe:submit') def setUp(self): - super(StripeSubmitViewTests, self).setUp() + super(StripeCheckoutViewTests, self).setUp() self.user = self.create_user() self.client.login(username=self.user.username, password=self.password) + self.site.siteconfiguration.client_side_payment_processor = 'stripe' + self.site.siteconfiguration.save() + Country.objects.create(iso_3166_1_a2='US', name='US') + self.mock_enrollment_api_resp = mock.Mock() + self.mock_enrollment_api_resp.status_code = status.HTTP_200_OK + + self.stripe_checkout_url = reverse('stripe:checkout') + self.capture_context_url = reverse('bff:payment:v0:capture_context') def assert_successful_order_response(self, response, order_number): assert response.status_code == 201 @@ -67,16 +73,14 @@ def assert_order_created(self, basket, billing_address, card_type, label): ) assert order.billing_address == billing_address - def generate_form_data(self, basket_id): - return { - 'stripe_token': 'st_abc123', - 'basket': basket_id, - } - - def create_basket(self): - basket = create_basket(owner=self.user, site=self.site) + def create_basket(self, product_class=None): + basket = create_basket(owner=self.user, site=self.site, product_class=product_class) basket.strategy = Selector().strategy() basket.thaw() + basket.flush() + course = CourseFactory() + seat = course.create_or_update_seat('credit', False, 100, 'credit_provider_id', None, 2) + basket.add_product(seat, 1) return basket def test_login_required(self): @@ -85,114 +89,228 @@ def test_login_required(self): expected_url = '{base}?next={path}'.format(base=reverse(settings.LOGIN_URL), path=self.path) self.assertRedirects(response, expected_url, fetch_redirect_response=False) - def test_payment_error(self): - basket = self.create_basket() - data = self.generate_form_data(basket.id) - - with mock.patch.object(Stripe, 'get_address_from_token', mock.Mock(return_value=BillingAddressFactory())): - with mock.patch.object(Stripe, 'handle_processor_response', mock.Mock(side_effect=Exception)): - response = self.client.post(self.path, data) - - assert response.status_code == 400 - assert response.content.decode('utf-8') == '{}' - - def test_billing_address_error(self): - basket = self.create_basket() - data = self.generate_form_data(basket.id) - card_type = 'American Express' - label = '1986' - charge = stripe.Charge.construct_from({ - 'id': '2404', - 'source': { - 'brand': card_type, - 'last4': label, - }, - }, 'fake-key') - - with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: - address_mock.side_effect = Exception - - with mock.patch.object(stripe.Charge, 'create') as charge_mock: - charge_mock.return_value = charge - response = self.client.post(self.path, data) - - address_mock.assert_called_once_with(data['stripe_token']) - - self.assert_successful_order_response(response, basket.order_number) - self.assert_order_created(basket, None, card_type, label) - - def test_successful_payment(self): - basket = self.create_basket() - data = self.generate_form_data(basket.id) - card_type = 'American Express' - label = '1986' - charge = stripe.Charge.construct_from({ - 'id': '2404', - 'source': { - 'brand': card_type, - 'last4': label, - }, - }, 'fake-key') - - billing_address = BillingAddressFactory() - with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: - address_mock.return_value = billing_address - - with mock.patch.object(stripe.Charge, 'create') as charge_mock: - charge_mock.return_value = charge - response = self.client.post(self.path, data) - - address_mock.assert_called_once_with(data['stripe_token']) - - self.assert_successful_order_response(response, basket.order_number) - self.assert_order_created(basket, billing_address, card_type, label) - - def test_successful_payment_for_bulk_purchase(self): + @file_data('fixtures/test_stripe_test_payment_flow.json') + def test_payment_flow( + self, + confirm_resp, + create_resp, + modify_resp, + refund_resp, # pylint: disable=unused-argument + retrieve_addr_resp): """ - Verify that when a Order has been successfully placed for bulk - purchase then that order is linked to the provided business client. + Verify that the stripe payment flow, hitting capture-context and + stripe-checkout urls, results in a basket associated with the correct + stripe payment_intent_id. + + Args: + confirm_resp: Response for confirm call on payment purchase + create_resp: Response for create call when capturing context + modify_resp: Response for modify call before confirming response + retrieve_addr_resp: Response for retrieve call that should be made when getting billing address + confirm_resp: Response for confirm call that should be made when handling processor response """ - toggle_switch(ENROLLMENT_CODE_SWITCH, True) - - course = CourseFactory(partner=self.partner) - course.create_or_update_seat('verified', True, 50, create_enrollment_code=True) - basket = create_basket(owner=self.user, site=self.site) - enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) - basket.add_product(enrollment_code, quantity=1) - basket.strategy = Selector().strategy() + basket = self.create_basket(product_class=SEAT_PRODUCT_CLASS_NAME) + idempotency_key = f'basket_pi_create_v1_{basket.order_number}' + + # need to call capture-context endpoint before we call do GET to the stripe checkout view + # so that the PaymentProcessorResponse is already created + with mock.patch('stripe.PaymentIntent.create') as mock_create: + mock_create.return_value = create_resp + self.client.get(self.capture_context_url) + mock_create.assert_called_once() + assert mock_create.call_args.kwargs['idempotency_key'] == idempotency_key + + with mock.patch('stripe.PaymentIntent.retrieve') as mock_retrieve: + mock_retrieve.return_value = retrieve_addr_resp + + with mock.patch( + 'ecommerce.extensions.fulfillment.modules.EnrollmentFulfillmentModule._post_to_enrollment_api' + ) as mock_api_resp: + mock_api_resp.return_value = self.mock_enrollment_api_resp + + with mock.patch('stripe.PaymentIntent.confirm') as mock_confirm: + mock_confirm.return_value = confirm_resp + with mock.patch('stripe.PaymentIntent.modify') as mock_modify: + mock_modify.return_value = modify_resp + self.client.post( + self.stripe_checkout_url, + data={'payment_intent_id': 'pi_3LsftNIadiFyUl1x2TWxaADZ'}, + ) + assert mock_retrieve.call_count == 1 + assert mock_modify.call_count == 1 + assert mock_confirm.call_count == 1 + + # Verify BillingAddress was set correctly + basket.refresh_from_db() + order = basket.order_set.first() + assert str(order.billing_address) == "Test User, 123 Test St, Sample, MA, 12345" + + # Verify there is 1 and only 1 Basket Attribute with the payment_intent_id + # associated with our basket. + assert BasketAttribute.objects.filter( + value_text='pi_3LsftNIadiFyUl1x2TWxaADZ', + basket=basket, + ).count() == 1 + + pprs = PaymentProcessorResponse.objects.filter( + transaction_id="pi_3LsftNIadiFyUl1x2TWxaADZ" + ) + # created when andle_processor_response is successful + assert pprs.count() == 1 - data = self.generate_form_data(basket.id) - data.update({'organization': 'Dummy Business Client'}) - data.update({PURCHASER_BEHALF_ATTRIBUTE: 'False'}) - - # Manually add organization attribute on the basket for testing - basket_add_organization_attribute(basket, data) - - card_type = 'American Express' - label = '1986' - charge = stripe.Charge.construct_from({ - 'id': '2404', - 'source': { - 'brand': card_type, - 'last4': label, - }, - }, 'fake-key') - - billing_address = BillingAddressFactory() - with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: - address_mock.return_value = billing_address - - with mock.patch.object(stripe.Charge, 'create') as charge_mock: - charge_mock.return_value = charge - response = self.client.post(self.path, data) - - address_mock.assert_called_once_with(data['stripe_token']) - - self.assert_successful_order_response(response, basket.order_number) - self.assert_order_created(basket, billing_address, card_type, label) - - # Now verify that a new business client has been created and current - # order is now linked with that client through Invoice model. - order = Order.objects.filter(basket=basket).first() - business_client = BusinessClient.objects.get(name=data['organization']) - assert Invoice.objects.get(order=order).business_client == business_client + def test_capture_context_basket_price_change(self): + """ + Verify that existing payment intent is retrieved, + and that we do not error with an IdempotencyError in this case: capture + context is called to generate stripe elements, but then user backs out from + payment page, and tries to check out with a different things in the basket. + """ + basket = self.create_basket(product_class=SEAT_PRODUCT_CLASS_NAME) + idempotency_key = f'basket_pi_create_v1_{basket.order_number}' + + with mock.patch('stripe.PaymentIntent.create') as mock_create: + mock_create.return_value = { + 'id': 'pi_3LsftNIadiFyUl1x2TWxaADZ', + 'client_secret': 'pi_3LsftNIadiFyUl1x2TWxaADZ_secret_VxRx7Y1skyp0jKtq7Gdu80Xnh', + } + self.client.get(self.capture_context_url) + mock_create.assert_called_once() + assert mock_create.call_args.kwargs['idempotency_key'] == idempotency_key + + # Verify there is 1 and only 1 Basket Attribute with the payment_intent_id + # associated with our basket. + assert BasketAttribute.objects.filter( + value_text='pi_3LsftNIadiFyUl1x2TWxaADZ', + basket=basket, + ).count() == 1 + + # Change the basket price + basket.flush() + course = CourseFactory() + seat = course.create_or_update_seat('credit', False, 99, 'credit_provider_id', None, 2) + basket.add_product(seat, 1) + basket.save() + + with mock.patch('stripe.PaymentIntent.create') as mock_create: + mock_create.side_effect = stripe.error.IdempotencyError + + with mock.patch('stripe.PaymentIntent.retrieve') as mock_retrieve: + mock_retrieve.return_value = { + 'id': 'pi_3LsftNIadiFyUl1x2TWxaADZ', + 'client_secret': 'pi_3LsftNIadiFyUl1x2TWxaADZ_secret_VxRx7Y1skyp0jKtq7Gdu80Xnh', + } + self.client.get(self.capture_context_url) + mock_retrieve.assert_called_once() + assert mock_retrieve.call_args.kwargs['id'] == 'pi_3LsftNIadiFyUl1x2TWxaADZ' + + # def test_payment_error(self): + # basket = self.create_basket() + # data = self.generate_form_data(basket.id) + + # with mock.patch.object(Stripe, 'get_address_from_token', mock.Mock(return_value=BillingAddressFactory())): + # with mock.patch.object(Stripe, 'handle_processor_response', mock.Mock(side_effect=Exception)): + # response = self.client.post(self.path, data) + + # assert response.status_code == 400 + # assert response.content.decode('utf-8') == '{}' + + # def test_billing_address_error(self): + # basket = self.create_basket() + # data = self.generate_form_data(basket.id) + # card_type = 'visa' + # label = '4242' + # payment_intent = stripe.PaymentIntent.construct_from({ + # 'id': 'pi_testtesttest', + # 'source': { + # 'brand': card_type, + # 'last4': label, + # }, + # }, 'fake-key') + + # with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: + # address_mock.side_effect = Exception + + # with mock.patch.object(stripe.PaymentIntent, 'create') as pi_mock: + # pi_mock.return_value = payment_intent + # response = self.client.post(self.path, data) + + # address_mock.assert_called_once_with(data['payment_intent_id']) + + # self.assert_successful_order_response(response, basket.order_number) + # self.assert_order_created(basket, None, card_type, label) + + # def test_successful_payment(self): + # basket = self.create_basket() + # data = self.generate_form_data(basket.id) + # card_type = 'visa' + # label = '4242' + # payment_intent = stripe.PaymentIntent.construct_from({ + # 'id': 'pi_testtesttest', + # 'source': { + # 'brand': card_type, + # 'last4': label, + # }, + # }, 'fake-key') + + # billing_address = BillingAddressFactory() + # with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: + # address_mock.return_value = billing_address + + # with mock.patch.object(stripe.PaymentIntent, 'create') as pi_mock: + # pi_mock.return_value = payment_intent + # response = self.client.post(self.path, data) + + # address_mock.assert_called_once_with(data['payment_intent_id']) + + # self.assert_successful_order_response(response, basket.order_number) + # self.assert_order_created(basket, billing_address, card_type, label) + + # def test_successful_payment_for_bulk_purchase(self): + # """ + # Verify that when a Order has been successfully placed for bulk + # purchase then that order is linked to the provided business client. + # """ + # toggle_switch(ENROLLMENT_CODE_SWITCH, True) + + # course = CourseFactory(partner=self.partner) + # course.create_or_update_seat('verified', True, 50, create_enrollment_code=True) + # basket = create_basket(owner=self.user, site=self.site) + # enrollment_code = Product.objects.get(product_class__name=ENROLLMENT_CODE_PRODUCT_CLASS_NAME) + # basket.add_product(enrollment_code, quantity=1) + # basket.strategy = Selector().strategy() + + # data = self.generate_form_data(basket.id) + # data.update({'organization': 'Dummy Business Client'}) + # data.update({PURCHASER_BEHALF_ATTRIBUTE: 'False'}) + + # # Manually add organization attribute on the basket for testing + # basket_add_organization_attribute(basket, data) + + # card_type = 'visa' + # label = '4242' + # payment_intent = stripe.PaymentIntent.construct_from({ + # 'id': 'pi_testtesttest', + # 'source': { + # 'brand': card_type, + # 'last4': label, + # }, + # }, 'fake-key') + + # billing_address = BillingAddressFactory() + # with mock.patch.object(Stripe, 'get_address_from_token') as address_mock: + # address_mock.return_value = billing_address + + # with mock.patch.object(stripe.PaymentIntent, 'create') as pi_mock: + # pi_mock.return_value = payment_intent + # response = self.client.post(self.path, data) + + # address_mock.assert_called_once_with(data['payment_intent_id']) + + # self.assert_successful_order_response(response, basket.order_number) + # self.assert_order_created(basket, billing_address, card_type, label) + + # # Now verify that a new business client has been created and current + # # order is now linked with that client through Invoice model. + # order = Order.objects.filter(basket=basket).first() + # business_client = BusinessClient.objects.get(name=data['organization']) + # assert Invoice.objects.get(order=order).business_client == business_client diff --git a/ecommerce/extensions/payment/urls.py b/ecommerce/extensions/payment/urls.py index 2b5a1c3841f..6ae80cadaaa 100644 --- a/ecommerce/extensions/payment/urls.py +++ b/ecommerce/extensions/payment/urls.py @@ -25,6 +25,7 @@ STRIPE_URLS = [ url(r'^submit/$', stripe.StripeSubmitView.as_view(), name='submit'), + url(r'^checkout', stripe.StripeCheckoutView.as_view(), name='checkout'), ] urlpatterns = [ diff --git a/ecommerce/extensions/payment/views/apple_pay.py b/ecommerce/extensions/payment/views/apple_pay.py index f345d1cfec1..b59c055acef 100644 --- a/ecommerce/extensions/payment/views/apple_pay.py +++ b/ecommerce/extensions/payment/views/apple_pay.py @@ -11,7 +11,7 @@ class ApplePayMerchantDomainAssociationView(View): def get(self, request, *args, **kwargs): # pylint: disable=unused-argument site_configuration = self.request.site.siteconfiguration - payment_processor_class = site_configuration.get_client_side_payment_processor_class() + payment_processor_class = site_configuration.get_client_side_payment_processor_class(self.request) payment_processor = payment_processor_class(self.request.site) content = payment_processor.apple_pay_merchant_id_domain_association status_code = 200 diff --git a/ecommerce/extensions/payment/views/stripe.py b/ecommerce/extensions/payment/views/stripe.py index 024ec449b6c..d0863a527bf 100644 --- a/ecommerce/extensions/payment/views/stripe.py +++ b/ecommerce/extensions/payment/views/stripe.py @@ -2,12 +2,20 @@ import logging +from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist +from django.db import transaction from django.http import JsonResponse +from django.shortcuts import redirect +from oscar.apps.partner import strategy from oscar.core.loading import get_class, get_model +from rest_framework.permissions import IsAuthenticated +from rest_framework.views import APIView +from stripe.error import CardError -from ecommerce.extensions.basket.utils import basket_add_organization_attribute +from ecommerce.extensions.basket.utils import basket_add_organization_attribute, basket_add_payment_intent_id_attribute from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.utils import get_receipt_page_url +from ecommerce.extensions.payment.core.sdn import checkSDN from ecommerce.extensions.payment.forms import StripeSubmitForm from ecommerce.extensions.payment.processors.stripe import Stripe from ecommerce.extensions.payment.views import BasePaymentSubmitView @@ -15,10 +23,12 @@ logger = logging.getLogger(__name__) Applicator = get_class('offer.applicator', 'Applicator') +BasketAttribute = get_model('basket', 'BasketAttribute') BillingAddress = get_model('order', 'BillingAddress') Country = get_model('address', 'Country') NoShippingRequired = get_class('shipping.methods', 'NoShippingRequired') OrderTotalCalculator = get_class('checkout.calculators', 'OrderTotalCalculator') +PaymentProcessorResponse = get_model('payment', 'PaymentProcessorResponse') class StripeSubmitView(EdxOrderPlacementMixin, BasePaymentSubmitView): @@ -36,29 +46,20 @@ def payment_processor(self): def form_valid(self, form): form_data = form.cleaned_data basket = form_data['basket'] - token = form_data['stripe_token'] + payment_intent_id = form_data['payment_intent_id'] order_number = basket.order_number basket_add_organization_attribute(basket, self.request.POST) + basket_add_payment_intent_id_attribute(basket, self.request.POST) try: - billing_address = self.payment_processor.get_address_from_token(token) - except Exception: # pylint: disable=broad-except - logger.exception( - 'An error occurred while parsing the billing address for basket [%d]. No billing address will be ' - 'stored for the resulting order [%s].', - basket.id, - order_number) - billing_address = None - - try: - self.handle_payment(token, basket) + self.handle_payment(payment_intent_id, basket) except Exception: # pylint: disable=broad-except logger.exception('An error occurred while processing the Stripe payment for basket [%d].', basket.id) return JsonResponse({}, status=400) try: - order = self.create_order(self.request, basket, billing_address=billing_address) + order = self.create_order(self.request, basket) except Exception: # pylint: disable=broad-except logger.exception('An error occurred while processing the Stripe payment for basket [%d].', basket.id) return JsonResponse({}, status=400) @@ -72,3 +73,165 @@ def form_valid(self, form): disable_back_button=True ) return JsonResponse({'url': receipt_url}, status=201) + + +class StripeCheckoutView(EdxOrderPlacementMixin, APIView): + http_method_names = ['post'] + + # DRF APIView wrapper which allows clients to use JWT authentication when + # making Stripe checkout submit requests. + permission_classes = [IsAuthenticated] + + @property + def payment_processor(self): + return Stripe(self.request.site) + + def check_sdn(self, request, data): + """ + Check that the supplied request and form data passes SDN checks. + + Returns: + hit_count (int) if the SDN check fails, or None if it succeeds. + """ + hit_count = checkSDN( + request, + data['name'], + data['city'], + data['country']) + + if hit_count > 0: + logger.info( + 'SDNCheck function called for basket [%d]. It received %d hit(s).', + request.basket.id, + hit_count, + ) + return hit_count + + logger.info( + 'SDNCheck function called for basket [%d]. It did not receive a hit.', + request.basket.id, + ) + return None + + def _get_basket(self, payment_intent_id): + """ + Retrieve a basket using a payment intent ID. + + Arguments: + payment_intent_id: payment_intent_id received from Stripe. + + Returns: + It will return related basket or log exception and return None if + duplicate payment_intent_id* received or any other exception occurred. + """ + try: + basket_attribute = BasketAttribute.objects.get(value_text=payment_intent_id) + basket = basket_attribute.basket + basket.strategy = strategy.Default() + + Applicator().apply(basket, basket.owner, self.request) + + basket_add_organization_attribute(basket, self.request.GET) + except MultipleObjectsReturned: + logger.warning(u"Duplicate payment_intent_id [%s] received from Stripe.", payment_intent_id) + return None + except ObjectDoesNotExist: + logger.warning(u"Could not find payment_intent_id [%s] among baskets.", payment_intent_id) + return None + except Exception: # pylint: disable=broad-except + logger.exception(u"Unexpected error during basket retrieval while executing Stripe payment.") + return None + return basket + + def post(self, request): + """Handle an incoming user returned to us by Stripe after approving payment.""" + stripe_response = request.POST.dict() + payment_intent_id = stripe_response.get('payment_intent_id') + + logger.info( + '%s called for payment intent id [%s].', + self.__class__.__name__, + request.POST.get('payment_intent_id') + ) + + basket = self._get_basket(payment_intent_id) + + if not basket: + return redirect(self.payment_processor.error_url) + + # SDN Check here! + billing_address_obj = self.payment_processor.get_address_from_token( + payment_intent_id + ) + sdn_check_data = { + # Stripe has 1 name field so we use first_name on billing_address_obj + 'name': billing_address_obj.first_name, + 'city': billing_address_obj.city, + 'country': billing_address_obj.country_id, + } + sdn_check_failure = self.check_sdn(self.request, sdn_check_data) + if sdn_check_failure is not None: + return self.sdn_error_page_response(sdn_check_failure) + + try: + with transaction.atomic(): + try: + self.handle_payment(stripe_response, basket) + except CardError as err: + return self.stripe_error_response(err) + except: # pylint: disable=bare-except + logger.exception('Attempts to handle payment for basket [%d] failed.', basket.id) + return self.error_page_response() + + try: + billing_address = self.create_billing_address( + user=self.request.user, + billing_address=billing_address_obj + ) + except Exception as err: # pylint: disable=broad-except + logger.exception('Error creating billing address for basket [%d]: %s', basket.id, err) + billing_address = None + + try: + order = self.create_order(request, basket, billing_address) + self.handle_post_order(order) + except Exception: # pylint: disable=broad-except + logger.exception( + 'Error processing order for transaction [%s], with order [%s] and basket [%d]. Processed by [%s].', + payment_intent_id, + basket.order_number, + basket.id, + self.payment_processor.NAME, + ) + return self.error_page_response() + + return self.receipt_page_response(basket) + + def error_page_response(self): + """Tell the frontend to redirect to a generic error page.""" + return JsonResponse({}, status=400) + + def sdn_error_page_response(self, hit_count): + """Tell the frontend to redirect to the SDN error page.""" + return JsonResponse({ + 'sdn_check_failure': {'hit_count': hit_count} + }, status=400) + + def receipt_page_response(self, basket): + """Tell the frontend to redirect to the receipt page.""" + receipt_page_url = get_receipt_page_url( + self.request, + order_number=basket.order_number, + site_configuration=basket.site.siteconfiguration, + disable_back_button=True + ) + return JsonResponse({ + 'receipt_page_url': receipt_page_url, + }, status=201) + + def stripe_error_response(self, error): + """Tell the frontend that a Stripe error has occurred.""" + return JsonResponse({ + 'error_code': error.code, + 'user_message': error.user_message, + }, status=400) diff --git a/ecommerce/management/utils.py b/ecommerce/management/utils.py index 0bd0e43b07a..7b58a69f76a 100644 --- a/ecommerce/management/utils.py +++ b/ecommerce/management/utils.py @@ -7,7 +7,7 @@ from oscar.core.loading import get_class, get_model from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin -from ecommerce.extensions.payment.constants import CYBERSOURCE_CARD_TYPE_MAP +from ecommerce.extensions.payment.constants import CYBERSOURCE_CARD_TYPE_MAP, STRIPE_CARD_TYPE_MAP from ecommerce.extensions.payment.helpers import get_processor_class_by_name from ecommerce.extensions.payment.processors import HandledProcessorResponse @@ -106,9 +106,10 @@ def get_payment_notification(basket): """ # Filter the successful payment processor response which in case # of Cybersource includes "u'decision': u'ACCEPT'" and in case of - # Paypal includes "u'state': u'approved'". + # Paypal includes "u'state': u'approved'" and in the case of Stripe + # includes "u'status': u'succeeded'". successful_transaction = basket.paymentprocessorresponse_set.filter( - Q(response__contains='ACCEPT') | Q(response__contains='approved') + Q(response__contains='ACCEPT') | Q(response__contains='approved') | Q(response__contains='succeeded') ) # In case of no successful transactions log and return none. @@ -122,6 +123,21 @@ def get_payment_notification(basket): logger.warning('Basket %d has more than one successful transaction id, using the first one', basket.id) return successful_transaction[0] + @staticmethod + def get_card_info_from_payment_notification(payment_notification): + if payment_notification.transaction_id.startswith('PAY'): + card_number = 'Paypal Account' + card_type = None + elif payment_notification.transaction_id.startswith('pi_'): + card_number = payment_notification.response['payment_method']['card']['last4'] + stripe_card_type = payment_notification.response['payment_method']['card']['brand'] + card_type = STRIPE_CARD_TYPE_MAP[stripe_card_type] + else: + card_number = payment_notification.response['req_card_number'] + cybersource_card_type = payment_notification.response['req_card_type'] + card_type = CYBERSOURCE_CARD_TYPE_MAP[cybersource_card_type] + return (card_number, card_type) + def fulfill_basket(self, basket_id, site): logger.info('Trying to complete order for frozen basket %d', basket_id) @@ -154,12 +170,11 @@ def fulfill_basket(self, basket_id, site): if not payment_notification: return False - if payment_notification.transaction_id.startswith('PAY'): - card_number = 'Paypal Account' - card_type = None - else: - card_number = payment_notification.response['req_card_number'] - card_type = CYBERSOURCE_CARD_TYPE_MAP.get(payment_notification.response['req_card_type']) + try: + card_number, card_type = self.get_card_info_from_payment_notification(payment_notification) + except (KeyError, TypeError): + logger.exception('Unable to parse payment details for basket %d', basket.id) + return False self.payment_processor = _get_payment_processor(site, payment_notification.processor_name) # Create handled response diff --git a/ecommerce/settings/_oscar.py b/ecommerce/settings/_oscar.py index 8ae0e1b0200..6feefba37d1 100644 --- a/ecommerce/settings/_oscar.py +++ b/ecommerce/settings/_oscar.py @@ -168,10 +168,16 @@ 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, }, 'stripe': { + 'api_version': '2022-08-01; server_side_confirmation_beta=v1', + 'enable_telemetry': None, + 'log_level': None, + 'max_network_retries': 0, + 'proxy': None, 'publishable_key': None, 'secret_key': None, - 'country': None, - 'apple_pay_merchant_id_domain_association': '7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313437313435343137313137362C227369676E6174757265223A2233303830303630393261383634383836663730643031303730326130383033303830303230313031333130663330306430363039363038363438303136353033303430323031303530303330383030363039326138363438383666373064303130373031303030306130383033303832303365363330383230333862613030333032303130323032303836383630663639396439636361373066333030613036303832613836343863653364303430333032333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303165313730643331333633303336333033333331333833313336333433303561313730643332333133303336333033323331333833313336333433303561333036323331323833303236303630333535303430333063316636353633363332643733366437303264363237323666366236353732326437333639363736653566353534333334326435333431346534343432346635383331313433303132303630333535303430623063306236393466353332303533373937333734363536643733333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303539333031333036303732613836343863653364303230313036303832613836343863653364303330313037303334323030303438323330666461626333396366373565323032633530643939623435313265363337653261393031646436636233653062316364346235323637393866386366346562646538316132356138633231653463333364646365386532613936633266366166613139333033343563346538376134343236636539353162313239356133383230323131333038323032306433303435303630383262303630313035303530373031303130343339333033373330333530363038326230363031303530353037333030313836323936383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353631363936333631333333303332333031643036303335353164306530343136303431343032323433303062396165656564343633313937613461363561323939653432373138323163343533303063303630333535316431333031303166663034303233303030333031663036303335353164323330343138333031363830313432336632343963343466393365346566323765366334663632383663336661326262666432653462333038323031316430363033353531643230303438323031313433303832303131303330383230313063303630393261383634383836663736333634303530313330383166653330383163333036303832623036303130353035303730323032333038316236306338316233353236353663363936313665363336353230366636653230373436383639373332303633363537323734363936363639363336313734363532303632373932303631366537393230373036313732373437393230363137333733373536643635373332303631363336333635373037343631366536333635323036663636323037343638363532303734363836353665323036313730373036633639363336313632366336353230373337343631366536343631373236343230373436353732366437333230363136653634323036333666366536343639373436393666366537333230366636363230373537333635326332303633363537323734363936363639363336313734363532303730366636633639363337393230363136653634323036333635373237343639363636393633363137343639366636653230373037323631363337343639363336353230373337343631373436353664363536653734373332653330333630363038326230363031303530353037303230313136326136383734373437303361326632663737373737373265363137303730366336353265363336663664326636333635373237343639363636393633363137343635363137353734363836663732363937343739326633303334303630333535316431663034326433303262333032396130323761303235383632333638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363536313639363336313333326536333732366333303065303630333535316430663031303166663034303430333032303738303330306630363039326138363438383666373633363430363164303430323035303033303061303630383261383634386365336430343033303230333439303033303436303232313030646131633633616538626535663634663865313165383635363933376239623639633437326265393365616333323333613136373933366534613864356538333032323130306264356166626638363966336330636132373462326664646534663731373135396362336264373139396232636130666634303964653635396138326232346433303832303265653330383230323735613030333032303130323032303834393664326662663361393864613937333030613036303832613836343863653364303430333032333036373331316233303139303630333535303430333063313234313730373036633635323035323666366637343230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313334333033353330333633323333333433363333333035613137306433323339333033353330333633323333333433363333333035613330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034663031373131383431396437363438356435316135653235383130373736653838306132656664653762616534646530386466633462393365313333353664353636356233356165323264303937373630643232346537626261303866643736313763653838636237366262363637306265633865383239383466663534343561333831663733303831663433303436303630383262303630313035303530373031303130343361333033383330333630363038326230363031303530353037333030313836326136383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353732366636663734363336313637333333303164303630333535316430653034313630343134323366323439633434663933653465663237653663346636323836633366613262626664326534623330306630363033353531643133303130316666303430353330303330313031666633303166303630333535316432333034313833303136383031346262623064656131353833333838396161343861393964656265626465626166646163623234616233303337303630333535316431663034333033303265333032636130326161303238383632363638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363537323666366637343633363136373333326536333732366333303065303630333535316430663031303166663034303430333032303130363330313030363061326138363438383666373633363430363032306530343032303530303330306130363038326138363438636533643034303330323033363730303330363430323330336163663732383335313136393962313836666233356333353663613632626666343137656464393066373534646132386562656631396338313565343262373839663839386637396235393966393864353431306438663964653963326665303233303332326464353434323162306133303537373663356466333338336239303637666431373763326332313664393634666336373236393832313236663534663837613764316239396362396230393839323136313036393930663039393231643030303033313832303136303330383230313563303230313031333038313836333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353330323038363836306636393964396363613730663330306430363039363038363438303136353033303430323031303530306130363933303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333633303338333133373331333733313336333133313561333032663036303932613836343838366637306430313039303433313232303432303733343832623432653665366332323264616536643963303961346336663332316534656136653666326661626631356430376562333338643264613435646233303061303630383261383634386365336430343033303230343438333034363032323130306564333264376438616131623536623036626164623162396639396264643063653662363931316530623032393232633934333362663564326130656135353830323231303066393433353637663030323361643061343561373236663238376636303062656334666566373335383832383935633733313531383337336163383934383137303030303030303030303030227D', + 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, + 'cancel_checkout_path': PAYMENT_PROCESSOR_CANCEL_PATH, + 'receipt_url': PAYMENT_PROCESSOR_RECEIPT_PATH, }, }, } diff --git a/ecommerce/settings/base.py b/ecommerce/settings/base.py index 7347b2aa3d1..6432d0969a8 100644 --- a/ecommerce/settings/base.py +++ b/ecommerce/settings/base.py @@ -810,7 +810,17 @@ 'error_url': '/checkout/error/', 'mode': 'sandbox', 'receipt_url': '/checkout/receipt/' - } + }, + 'stripe': { + 'api_version': '2022-08-01; server_side_confirmation_beta=v1', + 'enable_telemetry': None, + 'log_level': None, + 'max_network_retries': 0, + 'proxy': None, + 'publishable_key': 'SET-ME-PLEASE', + 'receipt_url': '/checkout/receipt/', + 'secret_key': 'SET-ME-PLEASE', + }, } } MEDIA_STORAGE_BACKEND = { diff --git a/ecommerce/settings/devstack.py b/ecommerce/settings/devstack.py index 5d58eae4dcd..7cdd4f263c5 100644 --- a/ecommerce/settings/devstack.py +++ b/ecommerce/settings/devstack.py @@ -95,10 +95,16 @@ 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, }, 'stripe': { - 'publishable_key': 'pk_test_JU90ubQThS4C5K4E3gG2K61N', - 'secret_key': 'sk_test_cHL2I2pDzsp5ApNTKX3E97oq', - 'country': 'US', - 'apple_pay_merchant_id_domain_association': '7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313437313435343137313137362C227369676E6174757265223A2233303830303630393261383634383836663730643031303730326130383033303830303230313031333130663330306430363039363038363438303136353033303430323031303530303330383030363039326138363438383666373064303130373031303030306130383033303832303365363330383230333862613030333032303130323032303836383630663639396439636361373066333030613036303832613836343863653364303430333032333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303165313730643331333633303336333033333331333833313336333433303561313730643332333133303336333033323331333833313336333433303561333036323331323833303236303630333535303430333063316636353633363332643733366437303264363237323666366236353732326437333639363736653566353534333334326435333431346534343432346635383331313433303132303630333535303430623063306236393466353332303533373937333734363536643733333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303539333031333036303732613836343863653364303230313036303832613836343863653364303330313037303334323030303438323330666461626333396366373565323032633530643939623435313265363337653261393031646436636233653062316364346235323637393866386366346562646538316132356138633231653463333364646365386532613936633266366166613139333033343563346538376134343236636539353162313239356133383230323131333038323032306433303435303630383262303630313035303530373031303130343339333033373330333530363038326230363031303530353037333030313836323936383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353631363936333631333333303332333031643036303335353164306530343136303431343032323433303062396165656564343633313937613461363561323939653432373138323163343533303063303630333535316431333031303166663034303233303030333031663036303335353164323330343138333031363830313432336632343963343466393365346566323765366334663632383663336661326262666432653462333038323031316430363033353531643230303438323031313433303832303131303330383230313063303630393261383634383836663736333634303530313330383166653330383163333036303832623036303130353035303730323032333038316236306338316233353236353663363936313665363336353230366636653230373436383639373332303633363537323734363936363639363336313734363532303632373932303631366537393230373036313732373437393230363137333733373536643635373332303631363336333635373037343631366536333635323036663636323037343638363532303734363836353665323036313730373036633639363336313632366336353230373337343631366536343631373236343230373436353732366437333230363136653634323036333666366536343639373436393666366537333230366636363230373537333635326332303633363537323734363936363639363336313734363532303730366636633639363337393230363136653634323036333635373237343639363636393633363137343639366636653230373037323631363337343639363336353230373337343631373436353664363536653734373332653330333630363038326230363031303530353037303230313136326136383734373437303361326632663737373737373265363137303730366336353265363336663664326636333635373237343639363636393633363137343635363137353734363836663732363937343739326633303334303630333535316431663034326433303262333032396130323761303235383632333638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363536313639363336313333326536333732366333303065303630333535316430663031303166663034303430333032303738303330306630363039326138363438383666373633363430363164303430323035303033303061303630383261383634386365336430343033303230333439303033303436303232313030646131633633616538626535663634663865313165383635363933376239623639633437326265393365616333323333613136373933366534613864356538333032323130306264356166626638363966336330636132373462326664646534663731373135396362336264373139396232636130666634303964653635396138326232346433303832303265653330383230323735613030333032303130323032303834393664326662663361393864613937333030613036303832613836343863653364303430333032333036373331316233303139303630333535303430333063313234313730373036633635323035323666366637343230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313334333033353330333633323333333433363333333035613137306433323339333033353330333633323333333433363333333035613330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034663031373131383431396437363438356435316135653235383130373736653838306132656664653762616534646530386466633462393365313333353664353636356233356165323264303937373630643232346537626261303866643736313763653838636237366262363637306265633865383239383466663534343561333831663733303831663433303436303630383262303630313035303530373031303130343361333033383330333630363038326230363031303530353037333030313836326136383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353732366636663734363336313637333333303164303630333535316430653034313630343134323366323439633434663933653465663237653663346636323836633366613262626664326534623330306630363033353531643133303130316666303430353330303330313031666633303166303630333535316432333034313833303136383031346262623064656131353833333838396161343861393964656265626465626166646163623234616233303337303630333535316431663034333033303265333032636130326161303238383632363638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363537323666366637343633363136373333326536333732366333303065303630333535316430663031303166663034303430333032303130363330313030363061326138363438383666373633363430363032306530343032303530303330306130363038326138363438636533643034303330323033363730303330363430323330336163663732383335313136393962313836666233356333353663613632626666343137656464393066373534646132386562656631396338313565343262373839663839386637396235393966393864353431306438663964653963326665303233303332326464353434323162306133303537373663356466333338336239303637666431373763326332313664393634666336373236393832313236663534663837613764316239396362396230393839323136313036393930663039393231643030303033313832303136303330383230313563303230313031333038313836333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353330323038363836306636393964396363613730663330306430363039363038363438303136353033303430323031303530306130363933303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333633303338333133373331333733313336333133313561333032663036303932613836343838366637306430313039303433313232303432303733343832623432653665366332323264616536643963303961346336663332316534656136653666326661626631356430376562333338643264613435646233303061303630383261383634386365336430343033303230343438333034363032323130306564333264376438616131623536623036626164623162396639396264643063653662363931316530623032393232633934333362663564326130656135353830323231303066393433353637663030323361643061343561373236663238376636303062656334666566373335383832383935633733313531383337336163383934383137303030303030303030303030227D', + 'api_version': '2022-08-01; server_side_confirmation_beta=v1', + 'enable_telemetry': None, + 'log_level': 'debug', + 'max_network_retries': 0, + 'proxy': None, + 'publishable_key': 'SET-ME-PLEASE', + 'secret_key': 'SET-ME-PLEASE', + 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, + 'cancel_checkout_path': PAYMENT_PROCESSOR_CANCEL_PATH, + 'receipt_url': PAYMENT_PROCESSOR_RECEIPT_PATH, }, }, } diff --git a/ecommerce/settings/local.py b/ecommerce/settings/local.py index f825ab29cf7..8ea6e58e502 100644 --- a/ecommerce/settings/local.py +++ b/ecommerce/settings/local.py @@ -133,10 +133,13 @@ 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, }, 'stripe': { - 'publishable_key': 'fake-publishable-key', - 'secret_key': 'fake-secret-key', - 'country': 'US', - 'apple_pay_merchant_id_domain_association': '7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313437313435343137313137362C227369676E6174757265223A2233303830303630393261383634383836663730643031303730326130383033303830303230313031333130663330306430363039363038363438303136353033303430323031303530303330383030363039326138363438383666373064303130373031303030306130383033303832303365363330383230333862613030333032303130323032303836383630663639396439636361373066333030613036303832613836343863653364303430333032333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303165313730643331333633303336333033333331333833313336333433303561313730643332333133303336333033323331333833313336333433303561333036323331323833303236303630333535303430333063316636353633363332643733366437303264363237323666366236353732326437333639363736653566353534333334326435333431346534343432346635383331313433303132303630333535303430623063306236393466353332303533373937333734363536643733333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303539333031333036303732613836343863653364303230313036303832613836343863653364303330313037303334323030303438323330666461626333396366373565323032633530643939623435313265363337653261393031646436636233653062316364346235323637393866386366346562646538316132356138633231653463333364646365386532613936633266366166613139333033343563346538376134343236636539353162313239356133383230323131333038323032306433303435303630383262303630313035303530373031303130343339333033373330333530363038326230363031303530353037333030313836323936383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353631363936333631333333303332333031643036303335353164306530343136303431343032323433303062396165656564343633313937613461363561323939653432373138323163343533303063303630333535316431333031303166663034303233303030333031663036303335353164323330343138333031363830313432336632343963343466393365346566323765366334663632383663336661326262666432653462333038323031316430363033353531643230303438323031313433303832303131303330383230313063303630393261383634383836663736333634303530313330383166653330383163333036303832623036303130353035303730323032333038316236306338316233353236353663363936313665363336353230366636653230373436383639373332303633363537323734363936363639363336313734363532303632373932303631366537393230373036313732373437393230363137333733373536643635373332303631363336333635373037343631366536333635323036663636323037343638363532303734363836353665323036313730373036633639363336313632366336353230373337343631366536343631373236343230373436353732366437333230363136653634323036333666366536343639373436393666366537333230366636363230373537333635326332303633363537323734363936363639363336313734363532303730366636633639363337393230363136653634323036333635373237343639363636393633363137343639366636653230373037323631363337343639363336353230373337343631373436353664363536653734373332653330333630363038326230363031303530353037303230313136326136383734373437303361326632663737373737373265363137303730366336353265363336663664326636333635373237343639363636393633363137343635363137353734363836663732363937343739326633303334303630333535316431663034326433303262333032396130323761303235383632333638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363536313639363336313333326536333732366333303065303630333535316430663031303166663034303430333032303738303330306630363039326138363438383666373633363430363164303430323035303033303061303630383261383634386365336430343033303230333439303033303436303232313030646131633633616538626535663634663865313165383635363933376239623639633437326265393365616333323333613136373933366534613864356538333032323130306264356166626638363966336330636132373462326664646534663731373135396362336264373139396232636130666634303964653635396138326232346433303832303265653330383230323735613030333032303130323032303834393664326662663361393864613937333030613036303832613836343863653364303430333032333036373331316233303139303630333535303430333063313234313730373036633635323035323666366637343230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313334333033353330333633323333333433363333333035613137306433323339333033353330333633323333333433363333333035613330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034663031373131383431396437363438356435316135653235383130373736653838306132656664653762616534646530386466633462393365313333353664353636356233356165323264303937373630643232346537626261303866643736313763653838636237366262363637306265633865383239383466663534343561333831663733303831663433303436303630383262303630313035303530373031303130343361333033383330333630363038326230363031303530353037333030313836326136383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353732366636663734363336313637333333303164303630333535316430653034313630343134323366323439633434663933653465663237653663346636323836633366613262626664326534623330306630363033353531643133303130316666303430353330303330313031666633303166303630333535316432333034313833303136383031346262623064656131353833333838396161343861393964656265626465626166646163623234616233303337303630333535316431663034333033303265333032636130326161303238383632363638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363537323666366637343633363136373333326536333732366333303065303630333535316430663031303166663034303430333032303130363330313030363061326138363438383666373633363430363032306530343032303530303330306130363038326138363438636533643034303330323033363730303330363430323330336163663732383335313136393962313836666233356333353663613632626666343137656464393066373534646132386562656631396338313565343262373839663839386637396235393966393864353431306438663964653963326665303233303332326464353434323162306133303537373663356466333338336239303637666431373763326332313664393634666336373236393832313236663534663837613764316239396362396230393839323136313036393930663039393231643030303033313832303136303330383230313563303230313031333038313836333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353330323038363836306636393964396363613730663330306430363039363038363438303136353033303430323031303530306130363933303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333633303338333133373331333733313336333133313561333032663036303932613836343838366637306430313039303433313232303432303733343832623432653665366332323264616536643963303961346336663332316534656136653666326661626631356430376562333338643264613435646233303061303630383261383634386365336430343033303230343438333034363032323130306564333264376438616131623536623036626164623162396639396264643063653662363931316530623032393232633934333362663564326130656135353830323231303066393433353637663030323361643061343561373236663238376636303062656334666566373335383832383935633733313531383337336163383934383137303030303030303030303030227D', + 'api_version': '2022-08-01; server_side_confirmation_beta=v1', + 'enable_telemetry': None, + 'log_level': 'debug', + 'max_network_retries': 0, + 'proxy': None, + 'publishable_key': 'SET-ME-PLEASE', + 'secret_key': 'SET-ME-PLEASE', }, }, } diff --git a/ecommerce/settings/test.py b/ecommerce/settings/test.py index d2cad8189bc..84b059041dc 100644 --- a/ecommerce/settings/test.py +++ b/ecommerce/settings/test.py @@ -90,10 +90,16 @@ }, 'invoice': {}, 'stripe': { + 'api_version': '2022-08-01; server_side_confirmation_beta=v1', + 'enable_telemetry': None, + 'log_level': 'debug', + 'max_network_retries': 0, + 'proxy': None, 'publishable_key': 'fake-publishable-key', 'secret_key': 'fake-secret-key', - 'country': 'US', - 'apple_pay_merchant_id_domain_association': None, + 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, + 'cancel_checkout_path': PAYMENT_PROCESSOR_CANCEL_PATH, + 'receipt_url': PAYMENT_PROCESSOR_RECEIPT_PATH, }, }, 'other': { @@ -123,10 +129,16 @@ }, 'invoice': {}, 'stripe': { - 'publishable_key': 'other-fake-publishable-key', - 'secret_key': 'other-fake-secret-key', - 'country': 'US', - 'apple_pay_merchant_id_domain_association': '7B227073704964223A2239373943394538343346343131343044463144313834343232393232313734313034353044314339464446394437384337313531303944334643463542433731222C2276657273696F6E223A312C22637265617465644F6E223A313437313435343137313137362C227369676E6174757265223A2233303830303630393261383634383836663730643031303730326130383033303830303230313031333130663330306430363039363038363438303136353033303430323031303530303330383030363039326138363438383666373064303130373031303030306130383033303832303365363330383230333862613030333032303130323032303836383630663639396439636361373066333030613036303832613836343863653364303430333032333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303165313730643331333633303336333033333331333833313336333433303561313730643332333133303336333033323331333833313336333433303561333036323331323833303236303630333535303430333063316636353633363332643733366437303264363237323666366236353732326437333639363736653566353534333334326435333431346534343432346635383331313433303132303630333535303430623063306236393466353332303533373937333734363536643733333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353333303539333031333036303732613836343863653364303230313036303832613836343863653364303330313037303334323030303438323330666461626333396366373565323032633530643939623435313265363337653261393031646436636233653062316364346235323637393866386366346562646538316132356138633231653463333364646365386532613936633266366166613139333033343563346538376134343236636539353162313239356133383230323131333038323032306433303435303630383262303630313035303530373031303130343339333033373330333530363038326230363031303530353037333030313836323936383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353631363936333631333333303332333031643036303335353164306530343136303431343032323433303062396165656564343633313937613461363561323939653432373138323163343533303063303630333535316431333031303166663034303233303030333031663036303335353164323330343138333031363830313432336632343963343466393365346566323765366334663632383663336661326262666432653462333038323031316430363033353531643230303438323031313433303832303131303330383230313063303630393261383634383836663736333634303530313330383166653330383163333036303832623036303130353035303730323032333038316236306338316233353236353663363936313665363336353230366636653230373436383639373332303633363537323734363936363639363336313734363532303632373932303631366537393230373036313732373437393230363137333733373536643635373332303631363336333635373037343631366536333635323036663636323037343638363532303734363836353665323036313730373036633639363336313632366336353230373337343631366536343631373236343230373436353732366437333230363136653634323036333666366536343639373436393666366537333230366636363230373537333635326332303633363537323734363936363639363336313734363532303730366636633639363337393230363136653634323036333635373237343639363636393633363137343639366636653230373037323631363337343639363336353230373337343631373436353664363536653734373332653330333630363038326230363031303530353037303230313136326136383734373437303361326632663737373737373265363137303730366336353265363336663664326636333635373237343639363636393633363137343635363137353734363836663732363937343739326633303334303630333535316431663034326433303262333032396130323761303235383632333638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363536313639363336313333326536333732366333303065303630333535316430663031303166663034303430333032303738303330306630363039326138363438383666373633363430363164303430323035303033303061303630383261383634386365336430343033303230333439303033303436303232313030646131633633616538626535663634663865313165383635363933376239623639633437326265393365616333323333613136373933366534613864356538333032323130306264356166626638363966336330636132373462326664646534663731373135396362336264373139396232636130666634303964653635396138326232346433303832303265653330383230323735613030333032303130323032303834393664326662663361393864613937333030613036303832613836343863653364303430333032333036373331316233303139303630333535303430333063313234313730373036633635323035323666366637343230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333031653137306433313334333033353330333633323333333433363333333035613137306433323339333033353330333633323333333433363333333035613330376133313265333032633036303335353034303330633235343137303730366336353230343137303730366336393633363137343639366636653230343936653734363536373732363137343639366636653230343334313230326432303437333333313236333032343036303335353034306230633164343137303730366336353230343336353732373436393636363936333631373436393666366532303431373537343638366637323639373437393331313333303131303630333535303430613063306134313730373036633635323034393665363332653331306233303039303630333535303430363133303235353533333035393330313330363037326138363438636533643032303130363038326138363438636533643033303130373033343230303034663031373131383431396437363438356435316135653235383130373736653838306132656664653762616534646530386466633462393365313333353664353636356233356165323264303937373630643232346537626261303866643736313763653838636237366262363637306265633865383239383466663534343561333831663733303831663433303436303630383262303630313035303530373031303130343361333033383330333630363038326230363031303530353037333030313836326136383734373437303361326632663666363337333730326536313730373036633635326536333666366432663666363337333730333033343264363137303730366336353732366636663734363336313637333333303164303630333535316430653034313630343134323366323439633434663933653465663237653663346636323836633366613262626664326534623330306630363033353531643133303130316666303430353330303330313031666633303166303630333535316432333034313833303136383031346262623064656131353833333838396161343861393964656265626465626166646163623234616233303337303630333535316431663034333033303265333032636130326161303238383632363638373437343730336132663266363337323663326536313730373036633635326536333666366432663631373037303663363537323666366637343633363136373333326536333732366333303065303630333535316430663031303166663034303430333032303130363330313030363061326138363438383666373633363430363032306530343032303530303330306130363038326138363438636533643034303330323033363730303330363430323330336163663732383335313136393962313836666233356333353663613632626666343137656464393066373534646132386562656631396338313565343262373839663839386637396235393966393864353431306438663964653963326665303233303332326464353434323162306133303537373663356466333338336239303637666431373763326332313664393634666336373236393832313236663534663837613764316239396362396230393839323136313036393930663039393231643030303033313832303136303330383230313563303230313031333038313836333037613331326533303263303630333535303430333063323534313730373036633635323034313730373036633639363336313734363936663665323034393665373436353637373236313734363936663665323034333431323032643230343733333331323633303234303630333535303430623063316434313730373036633635323034333635373237343639363636393633363137343639366636653230343137353734363836663732363937343739333131333330313130363033353530343061306330613431373037303663363532303439366536333265333130623330303930363033353530343036313330323535353330323038363836306636393964396363613730663330306430363039363038363438303136353033303430323031303530306130363933303138303630393261383634383836663730643031303930333331306230363039326138363438383666373064303130373031333031633036303932613836343838366637306430313039303533313066313730643331333633303338333133373331333733313336333133313561333032663036303932613836343838366637306430313039303433313232303432303733343832623432653665366332323264616536643963303961346336663332316534656136653666326661626631356430376562333338643264613435646233303061303630383261383634386365336430343033303230343438333034363032323130306564333264376438616131623536623036626164623162396639396264643063653662363931316530623032393232633934333362663564326130656135353830323231303066393433353637663030323361643061343561373236663238376636303062656334666566373335383832383935633733313531383337336163383934383137303030303030303030303030227D', + 'api_version': '2022-08-01; server_side_confirmation_beta=v1', + 'enable_telemetry': None, + 'log_level': 'debug', + 'max_network_retries': 0, + 'proxy': None, + 'publishable_key': 'fake-publishable-key', + 'secret_key': 'fake-secret-key', + 'error_path': PAYMENT_PROCESSOR_ERROR_PATH, + 'cancel_checkout_path': PAYMENT_PROCESSOR_CANCEL_PATH, + 'receipt_url': PAYMENT_PROCESSOR_RECEIPT_PATH, }, } } diff --git a/requirements/base.in b/requirements/base.in index f5ff4f62c61..7bc96cfd8eb 100755 --- a/requirements/base.in +++ b/requirements/base.in @@ -52,7 +52,7 @@ rules simplejson social-auth-app-django sorl-thumbnail -stripe==1.70.0 +stripe unicodecsv xss-utils zeep diff --git a/requirements/base.txt b/requirements/base.txt index 946ad00b0ab..addb106a814 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -22,7 +22,7 @@ babel==2.10.3 # via django-oscar backoff==1.10.0 # via analytics-python -bcrypt==3.2.2 +bcrypt==4.0.0 # via # cybersource-rest-client-python # paramiko @@ -30,9 +30,9 @@ billiard==3.6.4.0 # via celery bleach==5.0.1 # via -r requirements/base.in -boto3==1.24.49 +boto3==1.24.73 # via -r requirements/base.in -botocore==1.27.49 +botocore==1.27.73 # via # boto3 # s3transfer @@ -42,23 +42,22 @@ celery==4.4.7 # via # -c requirements/constraints.txt # edx-ecommerce-worker -certifi==2022.6.15 +certifi==2022.9.14 # via # cybersource-rest-client-python # requests cffi==1.15.1 # via - # bcrypt # cryptography # cybersource-rest-client-python # pynacl chardet==5.0.0 # via cybersource-rest-client-python -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via requests click==8.1.3 # via edx-django-utils -configparser==5.2.0 +configparser==5.3.0 # via cybersource-rest-client-python coreapi==2.3.3 # via @@ -68,11 +67,11 @@ coreschema==0.0.4 # via # coreapi # drf-yasg -coverage==6.4.3 +coverage==6.4.4 # via cybersource-rest-client-python crypto==1.4.1 # via cybersource-rest-client-python -cryptography==37.0.4 +cryptography==38.0.1 # via # cybersource-rest-client-python # paramiko @@ -81,13 +80,13 @@ cryptography==37.0.4 # social-auth-core cssselect==1.1.0 # via premailer -cssutils==2.5.1 +cssutils==2.6.0 # via premailer cybersource-rest-client-python==0.0.21 # via # -c requirements/constraints.txt # -r requirements/base.in -datetime==4.5 +datetime==4.7 # via cybersource-rest-client-python defusedxml==0.7.1 # via @@ -141,7 +140,7 @@ django-crum==0.7.9 # via # edx-django-utils # edx-rbac -django-extensions==3.2.0 +django-extensions==3.2.1 # via -r requirements/base.in django-extra-views==0.13.0 # via django-oscar @@ -171,7 +170,7 @@ django-threadlocals==0.10 # via -r requirements/base.in django-treebeard==4.4 # via django-oscar -django-waffle==2.6.0 +django-waffle==3.0.0 # via # -r requirements/base.in # edx-django-utils @@ -211,7 +210,7 @@ edx-django-release-util==1.2.0 # via -r requirements/base.in edx-django-sites-extensions==4.0.0 # via -r requirements/base.in -edx-django-utils==5.0.0 +edx-django-utils==5.0.1 # via # -r requirements/base.in # django-config-models @@ -244,7 +243,7 @@ extras==1.0.0 # testtools factory-boy==2.12.0 # via django-oscar -faker==13.15.1 +faker==14.2.0 # via factory-boy fixtures==4.0.1 # via @@ -279,7 +278,7 @@ jmespath==1.0.1 # botocore jsonfield==3.1.0 # via -r requirements/base.in -jsonschema==4.9.1 +jsonschema==4.16.0 # via cybersource-rest-client-python kombu==4.6.11 # via celery @@ -311,13 +310,13 @@ naked==0.1.31 # cybersource-rest-client-python ndg-httpsclient==0.5.1 # via -r requirements/base.in -newrelic==7.16.0.178 +newrelic==8.1.0.180 # via # -r requirements/base.in # edx-django-utils nose==1.3.7 # via cybersource-rest-client-python -oauthlib==3.2.0 +oauthlib==3.2.1 # via # getsmarter-api-clients # requests-oauthlib @@ -332,13 +331,13 @@ path-py==7.2 # via -r requirements/base.in paypalrestsdk==1.13.1 # via -r requirements/base.in -pbr==5.9.0 +pbr==5.10.0 # via # cybersource-rest-client-python # fixtures # stevedore # testtools -phonenumbers==8.12.53 +phonenumbers==8.12.55 # via django-oscar pillow==9.2.0 # via django-oscar @@ -348,7 +347,7 @@ platformdirs==2.5.2 # via zeep premailer==2.9.2 # via -r requirements/base.in -psutil==5.9.1 +psutil==5.9.2 # via edx-django-utils purl==1.6 # via django-oscar @@ -370,7 +369,7 @@ pycryptodomex==3.15.0 # via # cybersource-rest-client-python # pyjwkest -pygments==2.12.0 +pygments==2.13.0 # via -r requirements/base.in pyjwkest==1.4.2 # via edx-drf-extensions @@ -520,7 +519,7 @@ social-auth-core==4.0.2 # -c requirements/constraints.txt # edx-auth-backends # social-auth-app-django -sorl-thumbnail==12.8.0 +sorl-thumbnail==12.9.0 # via -r requirements/base.in sqlparse==0.4.2 # via django @@ -528,7 +527,7 @@ stevedore==4.0.0 # via # edx-django-utils # edx-opaque-keys -stripe==1.70.0 +stripe==4.1.0 # via -r requirements/base.in testtools==2.5.0 # via @@ -546,7 +545,7 @@ uritemplate==4.1.1 # via # coreapi # drf-yasg -urllib3==1.26.11 +urllib3==1.26.12 # via # -c requirements/constraints.txt # botocore diff --git a/requirements/common_constraints.txt b/requirements/common_constraints.txt index cca3ccd308c..b1dfdf0176d 100644 --- a/requirements/common_constraints.txt +++ b/requirements/common_constraints.txt @@ -19,6 +19,8 @@ Django<4.0 # elastic search changelog: https://www.elastic.co/guide/en/enterprise-search/master/release-notes-7.14.0.html elasticsearch<7.14.0 +# setuptools==60.0 had breaking changes and busted several service's pipeline. +# Details can be found here: https://github.com/pypa/setuptools/issues/2940 setuptools<60 # django-simple-history>3.0.0 adds indexing and causes a lot of migrations to be affected diff --git a/requirements/dev.txt b/requirements/dev.txt index 0a1a5cec474..a376b76274c 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -46,7 +46,7 @@ backoff==1.10.0 # via # -r requirements/test.txt # analytics-python -bcrypt==3.2.2 +bcrypt==4.0.0 # via # -r requirements/test.txt # cybersource-rest-client-python @@ -63,9 +63,9 @@ bleach==5.0.1 # via -r requirements/test.txt bok-choy==1.1.1 # via -r requirements/test.txt -boto3==1.24.49 +boto3==1.24.73 # via -r requirements/test.txt -botocore==1.27.49 +botocore==1.27.73 # via # -r requirements/test.txt # boto3 @@ -78,7 +78,7 @@ celery==4.4.7 # via # -r requirements/test.txt # edx-ecommerce-worker -certifi==2022.6.15 +certifi==2022.9.14 # via # -r requirements/docs.txt # -r requirements/test.txt @@ -87,7 +87,6 @@ certifi==2022.6.15 cffi==1.15.1 # via # -r requirements/test.txt - # bcrypt # cryptography # cybersource-rest-client-python # pynacl @@ -96,7 +95,7 @@ chardet==5.0.0 # -r requirements/test.txt # cybersource-rest-client-python # diff-cover -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via # -r requirements/docs.txt # -r requirements/test.txt @@ -105,7 +104,7 @@ click==8.1.3 # via # -r requirements/test.txt # edx-django-utils -configparser==5.2.0 +configparser==5.3.0 # via # -r requirements/test.txt # cybersource-rest-client-python @@ -118,7 +117,7 @@ coreschema==0.0.4 # -r requirements/test.txt # coreapi # drf-yasg -coverage[toml]==6.4.3 +coverage[toml]==6.4.4 # via # -r requirements/test.txt # cybersource-rest-client-python @@ -127,7 +126,7 @@ crypto==1.4.1 # via # -r requirements/test.txt # cybersource-rest-client-python -cryptography==37.0.4 +cryptography==38.0.1 # via # -r requirements/test.txt # cybersource-rest-client-python @@ -139,13 +138,13 @@ cssselect==1.1.0 # via # -r requirements/test.txt # premailer -cssutils==2.5.1 +cssutils==2.6.0 # via # -r requirements/test.txt # premailer cybersource-rest-client-python==0.0.21 # via -r requirements/test.txt -datetime==4.5 +datetime==4.7 # via # -r requirements/test.txt # cybersource-rest-client-python @@ -212,9 +211,9 @@ django-crum==0.7.9 # -r requirements/test.txt # edx-django-utils # edx-rbac -django-debug-toolbar==3.5.0 +django-debug-toolbar==3.6.0 # via -r requirements/dev.in -django-extensions==3.2.0 +django-extensions==3.2.1 # via -r requirements/test.txt django-extra-views==0.13.0 # via @@ -252,7 +251,7 @@ django-treebeard==4.4 # via # -r requirements/test.txt # django-oscar -django-waffle==2.6.0 +django-waffle==3.0.0 # via # -r requirements/test.txt # edx-django-utils @@ -300,7 +299,7 @@ edx-django-release-util==1.2.0 # via -r requirements/test.txt edx-django-sites-extensions==4.0.0 # via -r requirements/test.txt -edx-django-utils==5.0.0 +edx-django-utils==5.0.1 # via # -r requirements/test.txt # django-config-models @@ -341,7 +340,7 @@ factory-boy==2.12.0 # via # -r requirements/test.txt # django-oscar -faker==13.15.1 +faker==14.2.0 # via # -r requirements/test.txt # factory-boy @@ -354,7 +353,7 @@ fixtures==4.0.1 # -r requirements/test.txt # cybersource-rest-client-python # testtools -freezegun==1.2.1 +freezegun==1.2.2 # via -r requirements/test.txt funcsigs==1.0.2 # via @@ -428,7 +427,7 @@ jmespath==1.0.1 # botocore jsonfield==3.1.0 # via -r requirements/test.txt -jsonschema==4.9.1 +jsonschema==4.16.0 # via # -r requirements/test.txt # cybersource-rest-client-python @@ -488,7 +487,7 @@ naked==0.1.31 # cybersource-rest-client-python ndg-httpsclient==0.5.1 # via -r requirements/test.txt -newrelic==7.16.0.178 +newrelic==8.1.0.180 # via # -r requirements/test.txt # edx-django-utils @@ -496,7 +495,7 @@ nose==1.3.7 # via # -r requirements/test.txt # cybersource-rest-client-python -oauthlib==3.2.0 +oauthlib==3.2.1 # via # -r requirements/test.txt # getsmarter-api-clients @@ -523,14 +522,14 @@ path-py==7.2 # via -r requirements/test.txt paypalrestsdk==1.13.1 # via -r requirements/test.txt -pbr==5.9.0 +pbr==5.10.0 # via # -r requirements/test.txt # cybersource-rest-client-python # fixtures # stevedore # testtools -phonenumbers==8.12.53 +phonenumbers==8.12.55 # via # -r requirements/test.txt # django-oscar @@ -559,7 +558,7 @@ polib==1.1.1 # edx-i18n-tools premailer==2.9.2 # via -r requirements/test.txt -psutil==5.9.1 +psutil==5.9.2 # via # -r requirements/test.txt # edx-django-utils @@ -599,7 +598,7 @@ pycryptodomex==3.15.0 # -r requirements/test.txt # cybersource-rest-client-python # pyjwkest -pygments==2.12.0 +pygments==2.13.0 # via # -r requirements/docs.txt # -r requirements/test.txt @@ -694,7 +693,7 @@ python-dateutil==2.8.2 # edx-drf-extensions # faker # freezegun -python-dotenv==0.20.0 +python-dotenv==0.21.0 # via -r requirements/test.txt python-memcached==1.59 # via -r requirements/test.txt @@ -876,7 +875,7 @@ social-auth-core==4.0.2 # -r requirements/test.txt # edx-auth-backends # social-auth-app-django -sorl-thumbnail==12.8.0 +sorl-thumbnail==12.9.0 # via -r requirements/test.txt soupsieve==2.3.2.post1 # via @@ -920,7 +919,7 @@ stevedore==4.0.0 # -r requirements/test.txt # edx-django-utils # edx-opaque-keys -stripe==1.70.0 +stripe==4.1.0 # via -r requirements/test.txt tenacity==6.3.1 # via @@ -975,7 +974,7 @@ uritemplate==4.1.1 # -r requirements/test.txt # coreapi # drf-yasg -urllib3==1.26.11 +urllib3==1.26.12 # via # -r requirements/docs.txt # -r requirements/test.txt diff --git a/requirements/docs.txt b/requirements/docs.txt index 4c425eff232..4d8ba7517ac 100644 --- a/requirements/docs.txt +++ b/requirements/docs.txt @@ -8,9 +8,9 @@ alabaster==0.7.12 # via sphinx babel==2.10.3 # via sphinx -certifi==2022.6.15 +certifi==2022.9.14 # via requests -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via requests docutils==0.19 # via sphinx @@ -30,7 +30,7 @@ markupsafe==2.1.1 # via jinja2 packaging==21.3 # via sphinx -pygments==2.12.0 +pygments==2.13.0 # via sphinx pyparsing==3.0.9 # via packaging @@ -60,7 +60,7 @@ sphinxcontrib-qthelp==1.0.3 # via sphinx sphinxcontrib-serializinghtml==1.1.5 # via sphinx -urllib3==1.26.11 +urllib3==1.26.12 # via # -c requirements/constraints.txt # requests diff --git a/requirements/e2e.txt b/requirements/e2e.txt index 9174f3d2c46..4468e710125 100644 --- a/requirements/e2e.txt +++ b/requirements/e2e.txt @@ -12,7 +12,7 @@ attrs==22.1.0 # via # -c requirements/base.txt # pytest -certifi==2022.6.15 +certifi==2022.9.14 # via # -c requirements/base.txt # requests @@ -21,7 +21,7 @@ cffi==1.15.1 # -c requirements/base.txt # cryptography # pynacl -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via # -c requirements/base.txt # requests @@ -29,7 +29,7 @@ click==8.1.3 # via # -c requirements/base.txt # edx-django-utils -cryptography==37.0.4 +cryptography==38.0.1 # via # -c requirements/base.txt # pyjwt @@ -43,11 +43,11 @@ django-crum==0.7.9 # via # -c requirements/base.txt # edx-django-utils -django-waffle==2.6.0 +django-waffle==3.0.0 # via # -c requirements/base.txt # edx-django-utils -edx-django-utils==5.0.0 +edx-django-utils==5.0.1 # via # -c requirements/base.txt # edx-rest-api-client @@ -64,7 +64,7 @@ importlib-metadata==4.12.0 # via pytest-randomly iniconfig==1.1.1 # via pytest -newrelic==7.16.0.178 +newrelic==8.1.0.180 # via # -c requirements/base.txt # edx-django-utils @@ -72,7 +72,7 @@ packaging==21.3 # via # -c requirements/base.txt # pytest -pbr==5.9.0 +pbr==5.10.0 # via # -c requirements/base.txt # stevedore @@ -80,7 +80,7 @@ pluggy==0.13.1 # via # -c requirements/constraints.txt # pytest -psutil==5.9.1 +psutil==5.9.2 # via # -c requirements/base.txt # edx-django-utils @@ -126,7 +126,7 @@ pytest-timeout==2.1.0 # via -r requirements/e2e.in pytest-variables==1.9.0 # via pytest-selenium -python-dotenv==0.20.0 +python-dotenv==0.21.0 # via -r requirements/e2e.in pytz==2016.10 # via @@ -165,7 +165,7 @@ tenacity==6.3.1 # via pytest-selenium toml==0.10.2 # via pytest -urllib3==1.26.11 +urllib3==1.26.12 # via # -c requirements/base.txt # -c requirements/constraints.txt diff --git a/requirements/production.txt b/requirements/production.txt index dfa78adca09..3b42a4be9d5 100644 --- a/requirements/production.txt +++ b/requirements/production.txt @@ -22,7 +22,7 @@ babel==2.10.3 # via django-oscar backoff==1.10.0 # via analytics-python -bcrypt==3.2.2 +bcrypt==4.0.0 # via # cybersource-rest-client-python # paramiko @@ -30,11 +30,11 @@ billiard==3.6.4.0 # via celery bleach==5.0.1 # via -r requirements/base.in -boto3==1.24.49 +boto3==1.24.73 # via # -r requirements/base.in # django-ses -botocore==1.27.49 +botocore==1.27.73 # via # boto3 # s3transfer @@ -44,23 +44,22 @@ celery==4.4.7 # via # -c requirements/constraints.txt # edx-ecommerce-worker -certifi==2022.6.15 +certifi==2022.9.14 # via # cybersource-rest-client-python # requests cffi==1.15.1 # via - # bcrypt # cryptography # cybersource-rest-client-python # pynacl chardet==5.0.0 # via cybersource-rest-client-python -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via requests click==8.1.3 # via edx-django-utils -configparser==5.2.0 +configparser==5.3.0 # via cybersource-rest-client-python coreapi==2.3.3 # via @@ -70,11 +69,11 @@ coreschema==0.0.4 # via # coreapi # drf-yasg -coverage==6.4.3 +coverage==6.4.4 # via cybersource-rest-client-python crypto==1.4.1 # via cybersource-rest-client-python -cryptography==37.0.4 +cryptography==38.0.1 # via # cybersource-rest-client-python # paramiko @@ -83,13 +82,13 @@ cryptography==37.0.4 # social-auth-core cssselect==1.1.0 # via premailer -cssutils==2.5.1 +cssutils==2.6.0 # via premailer cybersource-rest-client-python==0.0.21 # via # -c requirements/constraints.txt # -r requirements/base.in -datetime==4.5 +datetime==4.7 # via cybersource-rest-client-python defusedxml==0.7.1 # via @@ -144,7 +143,7 @@ django-crum==0.7.9 # via # edx-django-utils # edx-rbac -django-extensions==3.2.0 +django-extensions==3.2.1 # via -r requirements/base.in django-extra-views==0.13.0 # via django-oscar @@ -162,7 +161,7 @@ django-oscar==2.2 # -r requirements/base.in django-phonenumber-field==5.0.0 # via django-oscar -django-ses==3.1.0 +django-ses==3.1.2 # via -r requirements/production.in django-simple-history==3.0.0 # via @@ -176,7 +175,7 @@ django-threadlocals==0.10 # via -r requirements/base.in django-treebeard==4.4 # via django-oscar -django-waffle==2.6.0 +django-waffle==3.0.0 # via # -r requirements/base.in # edx-django-utils @@ -216,7 +215,7 @@ edx-django-release-util==1.2.0 # via -r requirements/base.in edx-django-sites-extensions==4.0.0 # via -r requirements/base.in -edx-django-utils==5.0.0 +edx-django-utils==5.0.1 # via # -r requirements/base.in # django-config-models @@ -249,7 +248,7 @@ extras==1.0.0 # testtools factory-boy==2.12.0 # via django-oscar -faker==13.15.1 +faker==14.2.0 # via factory-boy fixtures==4.0.1 # via @@ -286,7 +285,7 @@ jmespath==1.0.1 # botocore jsonfield==3.1.0 # via -r requirements/base.in -jsonschema==4.9.1 +jsonschema==4.16.0 # via cybersource-rest-client-python kombu==4.6.11 # via celery @@ -327,7 +326,7 @@ nodeenv==1.1.1 # via -r requirements/production.in nose==1.3.7 # via cybersource-rest-client-python -oauthlib==3.2.0 +oauthlib==3.2.1 # via # getsmarter-api-clients # requests-oauthlib @@ -342,13 +341,13 @@ path-py==7.2 # via -r requirements/base.in paypalrestsdk==1.13.1 # via -r requirements/base.in -pbr==5.9.0 +pbr==5.10.0 # via # cybersource-rest-client-python # fixtures # stevedore # testtools -phonenumbers==8.12.53 +phonenumbers==8.12.55 # via django-oscar pillow==9.2.0 # via django-oscar @@ -358,7 +357,7 @@ platformdirs==2.5.2 # via zeep premailer==2.9.2 # via -r requirements/base.in -psutil==5.9.1 +psutil==5.9.2 # via edx-django-utils purl==1.6 # via django-oscar @@ -380,7 +379,7 @@ pycryptodomex==3.15.0 # via # cybersource-rest-client-python # pyjwkest -pygments==2.12.0 +pygments==2.13.0 # via -r requirements/base.in pyjwkest==1.4.2 # via edx-drf-extensions @@ -537,7 +536,7 @@ social-auth-core==4.0.2 # -c requirements/constraints.txt # edx-auth-backends # social-auth-app-django -sorl-thumbnail==12.8.0 +sorl-thumbnail==12.9.0 # via -r requirements/base.in sqlparse==0.4.2 # via django @@ -545,7 +544,7 @@ stevedore==4.0.0 # via # edx-django-utils # edx-opaque-keys -stripe==1.70.0 +stripe==4.1.0 # via -r requirements/base.in testtools==2.5.0 # via @@ -563,7 +562,7 @@ uritemplate==4.1.1 # via # coreapi # drf-yasg -urllib3==1.26.11 +urllib3==1.26.12 # via # -c requirements/constraints.txt # botocore diff --git a/requirements/test.txt b/requirements/test.txt index 0ebd7d810cd..9c2f5a6a8e0 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -40,7 +40,7 @@ backoff==1.10.0 # via # -r requirements/base.txt # analytics-python -bcrypt==3.2.2 +bcrypt==4.0.0 # via # -r requirements/base.txt # cybersource-rest-client-python @@ -55,9 +55,9 @@ bleach==5.0.1 # via -r requirements/base.txt bok-choy==1.1.1 # via -r requirements/test.in -boto3==1.24.49 +boto3==1.24.73 # via -r requirements/base.txt -botocore==1.27.49 +botocore==1.27.73 # via # -r requirements/base.txt # boto3 @@ -71,7 +71,7 @@ celery==4.4.7 # -c requirements/constraints.txt # -r requirements/base.txt # edx-ecommerce-worker -certifi==2022.6.15 +certifi==2022.9.14 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -81,7 +81,6 @@ cffi==1.15.1 # via # -r requirements/base.txt # -r requirements/e2e.txt - # bcrypt # cryptography # cybersource-rest-client-python # pynacl @@ -90,7 +89,7 @@ chardet==5.0.0 # -r requirements/base.txt # cybersource-rest-client-python # diff-cover -charset-normalizer==2.1.0 +charset-normalizer==2.1.1 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -100,7 +99,7 @@ click==8.1.3 # -r requirements/base.txt # -r requirements/e2e.txt # edx-django-utils -configparser==5.2.0 +configparser==5.3.0 # via # -r requirements/base.txt # cybersource-rest-client-python @@ -113,7 +112,7 @@ coreschema==0.0.4 # -r requirements/base.txt # coreapi # drf-yasg -coverage[toml]==6.4.3 +coverage[toml]==6.4.4 # via # -r requirements/base.txt # -r requirements/test.in @@ -123,7 +122,7 @@ crypto==1.4.1 # via # -r requirements/base.txt # cybersource-rest-client-python -cryptography==37.0.4 +cryptography==38.0.1 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -136,7 +135,7 @@ cssselect==1.1.0 # via # -r requirements/base.txt # premailer -cssutils==2.5.1 +cssutils==2.6.0 # via # -r requirements/base.txt # premailer @@ -144,7 +143,7 @@ cybersource-rest-client-python==0.0.21 # via # -c requirements/constraints.txt # -r requirements/base.txt -datetime==4.5 +datetime==4.7 # via # -r requirements/base.txt # cybersource-rest-client-python @@ -212,7 +211,7 @@ django-crum==0.7.9 # -r requirements/e2e.txt # edx-django-utils # edx-rbac -django-extensions==3.2.0 +django-extensions==3.2.1 # via -r requirements/base.txt django-extra-views==0.13.0 # via @@ -254,7 +253,7 @@ django-treebeard==4.4 # via # -r requirements/base.txt # django-oscar -django-waffle==2.6.0 +django-waffle==3.0.0 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -303,7 +302,7 @@ edx-django-release-util==1.2.0 # via -r requirements/base.txt edx-django-sites-extensions==4.0.0 # via -r requirements/base.txt -edx-django-utils==5.0.0 +edx-django-utils==5.0.1 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -346,7 +345,7 @@ factory-boy==2.12.0 # -r requirements/base.txt # -r requirements/test.in # django-oscar -faker==13.15.1 +faker==14.2.0 # via # -r requirements/base.txt # factory-boy @@ -359,7 +358,7 @@ fixtures==4.0.1 # -r requirements/base.txt # cybersource-rest-client-python # testtools -freezegun==1.2.1 +freezegun==1.2.2 # via -r requirements/test.in funcsigs==1.0.2 # via @@ -422,7 +421,7 @@ jmespath==1.0.1 # botocore jsonfield==3.1.0 # via -r requirements/base.txt -jsonschema==4.9.1 +jsonschema==4.16.0 # via # -r requirements/base.txt # cybersource-rest-client-python @@ -478,7 +477,7 @@ naked==0.1.31 # cybersource-rest-client-python ndg-httpsclient==0.5.1 # via -r requirements/base.txt -newrelic==7.16.0.178 +newrelic==8.1.0.180 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -487,7 +486,7 @@ nose==1.3.7 # via # -r requirements/base.txt # cybersource-rest-client-python -oauthlib==3.2.0 +oauthlib==3.2.1 # via # -r requirements/base.txt # getsmarter-api-clients @@ -512,7 +511,7 @@ path-py==7.2 # via -r requirements/base.txt paypalrestsdk==1.13.1 # via -r requirements/base.txt -pbr==5.9.0 +pbr==5.10.0 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -520,7 +519,7 @@ pbr==5.9.0 # fixtures # stevedore # testtools -phonenumbers==8.12.53 +phonenumbers==8.12.55 # via # -r requirements/base.txt # django-oscar @@ -549,7 +548,7 @@ polib==1.1.1 # via edx-i18n-tools premailer==2.9.2 # via -r requirements/base.txt -psutil==5.9.1 +psutil==5.9.2 # via # -r requirements/base.txt # -r requirements/e2e.txt @@ -590,7 +589,7 @@ pycryptodomex==3.15.0 # -r requirements/base.txt # cybersource-rest-client-python # pyjwkest -pygments==2.12.0 +pygments==2.13.0 # via # -r requirements/base.txt # diff-cover @@ -689,7 +688,7 @@ python-dateutil==2.8.2 # edx-drf-extensions # faker # freezegun -python-dotenv==0.20.0 +python-dotenv==0.21.0 # via -r requirements/e2e.txt python-memcached==1.59 # via -r requirements/test.in @@ -864,7 +863,7 @@ social-auth-core==4.0.2 # -r requirements/base.txt # edx-auth-backends # social-auth-app-django -sorl-thumbnail==12.8.0 +sorl-thumbnail==12.9.0 # via -r requirements/base.txt soupsieve==2.3.2.post1 # via beautifulsoup4 @@ -879,7 +878,7 @@ stevedore==4.0.0 # -r requirements/e2e.txt # edx-django-utils # edx-opaque-keys -stripe==1.70.0 +stripe==4.1.0 # via -r requirements/base.txt tenacity==6.3.1 # via @@ -929,7 +928,7 @@ uritemplate==4.1.1 # -r requirements/base.txt # coreapi # drf-yasg -urllib3==1.26.11 +urllib3==1.26.12 # via # -c requirements/constraints.txt # -r requirements/base.txt