diff --git a/ecommerce/core/management/commands/create_or_update_site.py b/ecommerce/core/management/commands/create_or_update_site.py index 36d9717c882..0d21910755b 100644 --- a/ecommerce/core/management/commands/create_or_update_site.py +++ b/ecommerce/core/management/commands/create_or_update_site.py @@ -144,20 +144,12 @@ def add_arguments(self, parser): type=str, required=True, help='URL for Discovery service API calls.') - parser.add_argument('--enable-microfrontend-for-basket-page', - action='store', - dest='enable_microfrontend_for_basket_page', - type=bool, - required=False, - help='Use the microfrontend implementation of the ' - 'basket page instead of the server-side template') parser.add_argument('--payment-microfrontend-url', action='store', dest='payment_microfrontend_url', type=str, - required=False, - help='URL for the Payment Microfrontend ' - '(used if Enable Microfrontend for Basket Page is set)') + required=True, + help='URL for the Payment Microfrontend.') def handle(self, *args, **options): # pylint: disable=too-many-statements site_id = options.get('site_id') @@ -179,10 +171,7 @@ def handle(self, *args, **options): # pylint: disable=too-many-statements base_cookie_domain = options.get('base_cookie_domain', '') discovery_api_url = options.get('discovery_api_url') - enable_microfrontend_for_basket_page = bool(options.get('enable_microfrontend_for_basket_page', False)) - payment_microfrontend_url = options.get( - 'payment_microfrontend_url' - ) if enable_microfrontend_for_basket_page else None + payment_microfrontend_url = options.get('payment_microfrontend_url') try: site = Site.objects.get(id=site_id) @@ -240,7 +229,6 @@ def handle(self, *args, **options): # pylint: disable=too-many-statements 'oauth_settings': oauth_settings, 'base_cookie_domain': base_cookie_domain, 'discovery_api_url': discovery_api_url, - 'enable_microfrontend_for_basket_page': enable_microfrontend_for_basket_page, 'payment_microfrontend_url': payment_microfrontend_url, } if payment_support_email: diff --git a/ecommerce/core/management/commands/tests/test_create_or_update_site.py b/ecommerce/core/management/commands/tests/test_create_or_update_site.py index 4c50aa2b914..0d19d3be640 100644 --- a/ecommerce/core/management/commands/tests/test_create_or_update_site.py +++ b/ecommerce/core/management/commands/tests/test_create_or_update_site.py @@ -33,6 +33,7 @@ def setUp(self): self.payment_support_url = 'http://fake.server/support' self.base_cookie_domain = '.fake.server' self.discovery_api_url = 'https://fake.discovery.server/api/v1/' + self.payment_microfrontend_url = 'http://payment_mfe_fake.server' def _check_site_configuration(self, site, partner): site_configuration = site.siteconfiguration @@ -92,6 +93,7 @@ def _call_command(self, backend_service_client_id, backend_service_client_secret, from_email, + payment_microfrontend_url, lms_public_url_root=None, site_id=None, site_name=None, @@ -118,7 +120,8 @@ def _call_command(self, '--sso-client-secret={}'.format(sso_client_secret), '--backend-service-client-id={}'.format(backend_service_client_id), '--backend-service-client-secret={}'.format(backend_service_client_secret), - '--from-email={from_email}'.format(from_email=from_email) + '--from-email={from_email}'.format(from_email=from_email), + '--payment-microfrontend-url={}'.format(payment_microfrontend_url), ] # Optional arguments @@ -186,6 +189,7 @@ def test_create_site(self): backend_service_client_secret=self.backend_service_client_secret, segment_key=self.segment_key, from_email=self.from_email, + payment_microfrontend_url=self.payment_microfrontend_url, discovery_api_url=self.discovery_api_url, ) @@ -218,6 +222,7 @@ def test_update_site(self): backend_service_client_secret=self.backend_service_client_secret, segment_key=self.segment_key, from_email=self.from_email, + payment_microfrontend_url=self.payment_microfrontend_url, enable_enrollment_codes=True, payment_support_email=self.payment_support_email, payment_support_url=self.payment_support_url, @@ -239,7 +244,8 @@ def test_update_site(self): self.assertEqual(site_configuration.payment_support_email, self.payment_support_email) self.assertEqual(site_configuration.payment_support_url, self.payment_support_url) self.assertTrue(site_configuration.send_refund_notifications) - self.assertEqual(site.siteconfiguration.base_cookie_domain, self.base_cookie_domain) + self.assertEqual(site_configuration.base_cookie_domain, self.base_cookie_domain) + self.assertEqual(site_configuration.payment_microfrontend_url, self.payment_microfrontend_url) def test_update_site_with_updated_oauth(self): """ @@ -265,6 +271,7 @@ def test_update_site_with_updated_oauth(self): backend_service_client_secret=self.backend_service_client_secret, segment_key=self.segment_key, from_email=self.from_email, + payment_microfrontend_url=self.payment_microfrontend_url, enable_enrollment_codes=True, payment_support_email=self.payment_support_email, payment_support_url=self.payment_support_url, @@ -286,7 +293,8 @@ def test_update_site_with_updated_oauth(self): self.assertEqual(site_configuration.payment_support_email, self.payment_support_email) self.assertEqual(site_configuration.payment_support_url, self.payment_support_url) self.assertTrue(site_configuration.send_refund_notifications) - self.assertEqual(site.siteconfiguration.base_cookie_domain, self.base_cookie_domain) + self.assertEqual(site_configuration.base_cookie_domain, self.base_cookie_domain) + self.assertEqual(site_configuration.payment_microfrontend_url, self.payment_microfrontend_url) @data( ['--site-id=1'], diff --git a/ecommerce/core/models.py b/ecommerce/core/models.py index 4b1cdff6761..bb095424d51 100644 --- a/ecommerce/core/models.py +++ b/ecommerce/core/models.py @@ -188,15 +188,9 @@ class SiteConfiguration(models.Model): max_length=255, blank=True ) - enable_microfrontend_for_basket_page = models.BooleanField( - verbose_name=_('Enable Microfrontend for Basket Page'), - help_text=_('Use the microfrontend implementation of the basket page instead of the server-side template'), - blank=True, - default=False - ) payment_microfrontend_url = models.URLField( verbose_name=_('Payment Microfrontend URL'), - help_text=_('URL for the Payment Microfrontend (used if Enable Microfrontend for Basket Page is set)'), + help_text=_('URL for the Payment Microfrontend'), null=True, blank=True ) @@ -373,9 +367,7 @@ def enterprise_grant_data_sharing_url(self): @property def payment_domain_name(self): - if self.enable_microfrontend_for_basket_page: - return urlsplit(self.payment_microfrontend_url).netloc - return self.site.domain + return urlsplit(self.payment_microfrontend_url).netloc @property def oauth_api_client(self): diff --git a/ecommerce/coupons/tests/test_views.py b/ecommerce/coupons/tests/test_views.py index c3d1151df03..18142f95d75 100644 --- a/ecommerce/coupons/tests/test_views.py +++ b/ecommerce/coupons/tests/test_views.py @@ -851,7 +851,7 @@ def test_inactive_user_email_domain_restricted_coupon_redemption(self): self.assert_redirected_to_email_confirmation(response) def get_coupon_redeem_success_expected_redirect_url(self): - return self.get_full_url(path=reverse('basket:summary')) + '?coupon_redeem_redirect=1' + return self.site_configuration.payment_microfrontend_url + '?coupon_redeem_redirect=1' @ddt.ddt diff --git a/ecommerce/coupons/views.py b/ecommerce/coupons/views.py index 9aaf073fcc4..20fb8c9b898 100644 --- a/ecommerce/coupons/views.py +++ b/ecommerce/coupons/views.py @@ -33,7 +33,7 @@ parse_consent_params ) from ecommerce.extensions.api import exceptions -from ecommerce.extensions.basket.utils import get_payment_microfrontend_or_basket_url, prepare_basket +from ecommerce.extensions.basket.utils import get_payment_microfrontend, prepare_basket from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.offer.utils import get_redirect_to_email_confirmation_if_required @@ -298,7 +298,7 @@ def get(self, request): # pylint: disable=too-many-statements # The coupon_redeem_redirect query param is used to communicate to the Payment MFE that it may redirect # and should not display the payment form before making that determination. # TODO: It would be cleaner if the user could be redirected to their final destination up front. - redirect_url = get_payment_microfrontend_or_basket_url(self.request) + "?coupon_redeem_redirect=1" + redirect_url = get_payment_microfrontend(self.request) + "?coupon_redeem_redirect=1" return HttpResponseRedirect(redirect_url) diff --git a/ecommerce/extensions/basket/apps.py b/ecommerce/extensions/basket/apps.py index 02c1d188465..aa290928c59 100644 --- a/ecommerce/extensions/basket/apps.py +++ b/ecommerce/extensions/basket/apps.py @@ -1,27 +1,7 @@ -from django.conf.urls import url -from django.contrib.auth.decorators import login_required from oscar.apps.basket import apps -from oscar.core.loading import get_class class BasketConfig(apps.BasketConfig): name = 'ecommerce.extensions.basket' - - # pylint: disable=attribute-defined-outside-init - def ready(self): - super().ready() - self.basket_add_items_view = get_class('basket.views', 'BasketAddItemsView') - self.summary_view = get_class('basket.views', 'BasketSummaryView') - - def get_urls(self): - urls = [ - url(r'^$', login_required(self.summary_view.as_view()), name='summary'), - url(r'^add/(?P\d+)/$', self.add_view.as_view(), name='add'), - url(r'^vouchers/add/$', self.add_voucher_view.as_view(), name='vouchers-add'), - url(r'^vouchers/(?P\d+)/remove/$', self.remove_voucher_view.as_view(), name='vouchers-remove'), - url(r'^saved/$', login_required(self.saved_view.as_view()), name='saved'), - url(r'^add/$', self.basket_add_items_view.as_view(), name='basket-add'), - ] - return self.post_process_urls(urls) diff --git a/ecommerce/extensions/basket/tests/test_utils.py b/ecommerce/extensions/basket/tests/test_utils.py index 6c4a21abf1c..ca4df0110bd 100644 --- a/ecommerce/extensions/basket/tests/test_utils.py +++ b/ecommerce/extensions/basket/tests/test_utils.py @@ -26,7 +26,6 @@ apply_voucher_on_basket_and_check_discount, attribute_cookie_data, get_basket_switch_data, - get_payment_microfrontend_url_if_configured, is_duplicate_seat_attempt, prepare_basket ) @@ -35,7 +34,6 @@ from ecommerce.extensions.order.exceptions import AlreadyPlacedOrderException from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder from ecommerce.extensions.partner.models import StockRecord -from ecommerce.extensions.payment.constants import DISABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME from ecommerce.extensions.test.factories import create_order, prepare_voucher from ecommerce.referrals.models import Referral from ecommerce.tests.testcases import TestCase, TransactionTestCase @@ -684,26 +682,6 @@ def test_apply_voucher_on_basket_and_check_discount_with_multiple_vouchers(self) self.assertEqual(applied, False) self.assertEqual(msg, 'Basket does not qualify for coupon code {code}.'.format(code=invalid_voucher.code)) - @ddt.data( - (True, '/payment', False, '/payment'), # Microfrontend not disabled - (True, '/payment', True, None), # Microfrontend disabled - ) - @ddt.unpack - def test_disable_microfrontend_for_basket_page_flag( - self, - microfrontend_enabled, - payment_microfrontend_url, - disable_microfrontend_flag_active, - expected_result - ): - """ - Verify that the `disable_microfrontend_for_basket_page_flag` correctly disables the microfrontend url retrieval - """ - with override_flag(DISABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME, active=disable_microfrontend_flag_active): - self.site_configuration.enable_microfrontend_for_basket_page = microfrontend_enabled - self.site_configuration.payment_microfrontend_url = payment_microfrontend_url - self.assertEqual(get_payment_microfrontend_url_if_configured(self.request), expected_result) - def test_prepare_basket_with_duplicate_seat(self): """ Verify a basket fixes the case where flush doesn't work and we attempt adding duplicate seat. """ with mock.patch('ecommerce.extensions.basket.utils.Basket.flush'): diff --git a/ecommerce/extensions/basket/tests/test_views.py b/ecommerce/extensions/basket/tests/test_views.py index 473af08447b..8ef33248e8f 100644 --- a/ecommerce/extensions/basket/tests/test_views.py +++ b/ecommerce/extensions/basket/tests/test_views.py @@ -1,5 +1,4 @@ import datetime -import itertools import urllib.error import urllib.parse from contextlib import contextmanager @@ -7,313 +6,45 @@ import ddt import mock -import pytz import responses from django.conf import settings -from django.contrib.messages import get_messages -from django.http import HttpResponseRedirect from django.test import override_settings from django.urls import reverse -from edx_django_utils.cache import RequestCache, TieredCache -from oscar.apps.basket.forms import BasketVoucherForm +from edx_django_utils.cache import RequestCache from oscar.core.loading import get_class, get_model from oscar.test import factories from requests.exceptions import ConnectionError as ReqConnectionError -from requests.exceptions import RequestException, Timeout from testfixtures import LogCapture -from waffle.testutils import override_flag -from ecommerce.core.exceptions import SiteConfigurationError -from ecommerce.core.tests import toggle_switch -from ecommerce.core.url_utils import absolute_url, get_lms_url -from ecommerce.core.utils import get_cache_key -from ecommerce.coupons.tests.mixins import CouponMixin, DiscoveryMockMixin +from ecommerce.core.url_utils import absolute_url +from ecommerce.coupons.tests.mixins import DiscoveryMockMixin from ecommerce.courses.tests.factories import CourseFactory from ecommerce.enterprise.tests.mixins import EnterpriseServiceMockMixin 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.tests.mixins import BasketMixin from ecommerce.extensions.basket.tests.test_utils import TEST_BUNDLE_ID from ecommerce.extensions.basket.utils import _set_basket_bundle_status, apply_voucher_on_basket_and_check_discount -from ecommerce.extensions.catalogue.tests.mixins import DiscoveryTestMixin -from ecommerce.extensions.offer.constants import DYNAMIC_DISCOUNT_FLAG -from ecommerce.extensions.offer.utils import format_benefit_value -from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder -from ecommerce.extensions.payment.constants import CLIENT_SIDE_CHECKOUT_FLAG_NAME -from ecommerce.extensions.payment.forms import PaymentForm -from ecommerce.extensions.payment.tests.processors import DummyProcessor -from ecommerce.extensions.test.factories import create_order, prepare_voucher -from ecommerce.tests.factories import ProductFactory, SiteConfigurationFactory, StockRecordFactory -from ecommerce.tests.mixins import ApiMockMixin, LmsApiMockMixin +from ecommerce.extensions.test.factories import prepare_voucher +from ecommerce.tests.factories import ProductFactory +from ecommerce.tests.mixins import LmsApiMockMixin from ecommerce.tests.testcases import TestCase Applicator = get_class('offer.applicator', 'Applicator') -Basket = get_model('basket', 'Basket') BasketAttribute = get_model('basket', 'BasketAttribute') BasketAttributeType = get_model('basket', 'BasketAttributeType') Benefit = get_model('offer', 'Benefit') -Catalog = get_model('catalogue', 'Catalog') Condition = get_model('offer', 'Condition') ConditionalOffer = get_model('offer', 'ConditionalOffer') -OrderLine = get_model('order', 'Line') -Product = get_model('catalogue', 'Product') -ProductAttribute = get_model('catalogue', 'ProductAttribute') Selector = get_class('partner.strategy', 'Selector') -StockRecord = get_model('partner', 'StockRecord') Voucher = get_model('voucher', 'Voucher') -VoucherAddView = get_class('basket.views', 'VoucherAddView') VoucherApplication = get_model('voucher', 'VoucherApplication') COUPON_CODE = 'COUPONTEST' BUNDLE = 'bundle_identifier' -@ddt.ddt -class BasketAddItemsViewTests(CouponMixin, DiscoveryTestMixin, DiscoveryMockMixin, LmsApiMockMixin, BasketMixin, - EnterpriseServiceMockMixin, TestCase): - """ BasketAddItemsView view tests. """ - path = reverse('basket:basket-add') - - def setUp(self): - super(BasketAddItemsViewTests, self).setUp() - self.user = self.create_user() - self.client.login(username=self.user.username, password=self.password) - - self.course = CourseFactory(partner=self.partner) - product = self.course.create_or_update_seat('verified', False, 50) - self.stock_record = StockRecordFactory(product=product, partner=self.partner) - self.catalog = Catalog.objects.create(partner=self.partner) - self.catalog.stock_records.add(self.stock_record) - - def _get_response(self, product_skus, **url_params): - qs = urllib.parse.urlencode({'sku': product_skus}, True) - url = '{root}?{qs}'.format(root=self.path, qs=qs) - for name, value in url_params.items(): - url += '&{}={}'.format(name, value) - return self.client.get(url) - - def test_add_multiple_products_to_basket(self): - """ Verify the basket accepts multiple products. """ - products = ProductFactory.create_batch(3, stockrecords__partner=self.partner) - response = self._get_response([product.stockrecords.first().partner_sku for product in products]) - self.assertEqual(response.status_code, 303) - - basket = response.wsgi_request.basket - self.assertEqual(basket.status, Basket.OPEN) - self.assertEqual(basket.lines.count(), len(products)) - - def test_basket_with_utm_params(self): - """ Verify the basket includes utm params after redirect. """ - products = ProductFactory.create_batch(3, stockrecords__partner=self.partner) - response = self._get_response( - [product.stockrecords.first().partner_sku for product in products], - utm_source='test', - ) - expected_url = self.get_full_url(reverse('basket:summary')) + '?utm_source=test' - self.assertEqual(response.url, expected_url) - - @responses.activate - def test_redirect_to_basket_summary(self): - """ - Verify the view redirects to the basket summary page, and that the user's basket is prepared for checkout. - """ - self.create_coupon(catalog=self.catalog, code=COUPON_CODE, benefit_value=5) - - self.mock_course_runs_endpoint(self.site_configuration.discovery_api_url, course_run=self.course) - response = self._get_response(self.stock_record.partner_sku, code=COUPON_CODE) - expected_url = self.get_full_url(reverse('basket:summary')) - self.assertRedirects(response, expected_url, status_code=303) - - basket = Basket.objects.get(owner=self.user, site=self.site) - self.assertEqual(basket.status, Basket.OPEN) - self.assertEqual(basket.lines.count(), 1) - self.assertTrue(basket.contains_a_voucher) - self.assertEqual(basket.lines.first().product, self.stock_record.product) - - @ddt.data(*itertools.product((True, False), (True, False))) - @ddt.unpack - def test_microfrontend_for_single_course_purchase_if_configured(self, enable_redirect, set_url): - microfrontend_url = self.configure_redirect_to_microfrontend(enable_redirect, set_url) - response = self._get_response(self.stock_record.partner_sku, utm_source='test') - - expect_microfrontend = enable_redirect and set_url - expected_url = microfrontend_url if expect_microfrontend else self.get_full_url(reverse('basket:summary')) - expected_url += '?utm_source=test' - self.assertRedirects(response, expected_url, status_code=303, fetch_redirect_response=False) - - def test_add_invalid_code_to_basket(self): - """ - When the BasketAddItemsView receives an invalid code as a parameter, add a message to the url. - This message will be displayed on the payment page. - """ - microfrontend_url = self.configure_redirect_to_microfrontend(True, True) - response = self._get_response(self.stock_record.partner_sku, code='invalidcode') - expected_url = microfrontend_url + '?error_message=Code%20invalidcode%20is%20invalid.' - self.assertRedirects(response, expected_url, status_code=303, fetch_redirect_response=False) - - @responses.activate - def test_microfrontend_for_enrollment_code_seat(self): - microfrontend_url = self.configure_redirect_to_microfrontend() - - course, __, enrollment_code = self.prepare_course_seat_and_enrollment_code() - basket = factories.BasketFactory(owner=self.user, site=self.site) - basket.add_product(enrollment_code, 1) - self.mock_course_runs_endpoint(self.site_configuration.discovery_api_url, course_run=course) - - response = self._get_response(enrollment_code.stockrecords.first().partner_sku) - self.assertRedirects(response, microfrontend_url, status_code=303, fetch_redirect_response=False) - - def test_add_multiple_products_no_skus_provided(self): - """ Verify the Bad request exception is thrown when no skus are provided. """ - response = self.client.get(self.path) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.content.decode('utf-8'), 'No SKUs provided.') - - def test_add_multiple_products_no_available_products(self): - """ Verify the Bad request exception is thrown when no skus are provided. """ - response = self.client.get(self.path, data=[('sku', 1), ('sku', 2)]) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.content.decode('utf-8'), 'Products with SKU(s) [1, 2] do not exist.') - - @ddt.data(Voucher.SINGLE_USE, Voucher.MULTI_USE) - def test_add_multiple_products_and_use_voucher(self, usage): - """ Verify the basket accepts multiple products and a single use voucher. """ - products = ProductFactory.create_batch(3, stockrecords__partner=self.partner) - product_range = factories.RangeFactory(products=products) - voucher, __ = prepare_voucher(_range=product_range, usage=usage) - - response = self._get_response( - [product.stockrecords.first().partner_sku for product in products], - code=voucher.code, - ) - self.assertEqual(response.status_code, 303) - basket = response.wsgi_request.basket - self.assertEqual(basket.status, Basket.OPEN) - self.assertTrue(basket.contains_voucher(voucher.code)) - - def test_all_already_purchased_products(self): - """ - Test user can not purchase products again using the multiple item view - """ - course = CourseFactory(partner=self.partner) - product1 = course.create_or_update_seat("Verified", True, 0) - product2 = course.create_or_update_seat("Professional", True, 0) - stock_record = StockRecordFactory(product=product1, partner=self.partner) - catalog = Catalog.objects.create(partner=self.partner) - catalog.stock_records.add(stock_record) - stock_record = StockRecordFactory(product=product2, partner=self.partner) - catalog.stock_records.add(stock_record) - - with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=True): - response = self._get_response( - [product.stockrecords.first().partner_sku for product in [product1, product2]], - ) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['error'], 'You have already purchased these products') - - def test_not_already_purchased_products(self): - """ - Test user can purchase products which have not been already purchased - """ - products = ProductFactory.create_batch(3, stockrecords__partner=self.partner) - with mock.patch.object(UserAlreadyPlacedOrder, 'user_already_placed_order', return_value=False): - response = self._get_response([product.stockrecords.first().partner_sku for product in products]) - self.assertEqual(response.status_code, 303) - - def test_one_already_purchased_product(self): - """ - Test prepare_basket removes already purchased product and checkout for the rest of products - """ - order = create_order(site=self.site, user=self.user) - products = ProductFactory.create_batch(3, stockrecords__partner=self.partner) - products.append(OrderLine.objects.get(order=order).product) - response = self._get_response([product.stockrecords.first().partner_sku for product in products]) - basket = response.wsgi_request.basket - self.assertEqual(response.status_code, 303) - self.assertEqual(basket.lines.count(), len(products) - 1) - - def test_no_available_product(self): - """ The view should return HTTP 400 if the product is not available for purchase. """ - product = self.stock_record.product - product.expires = pytz.utc.localize(datetime.datetime.min) - product.save() - self.assertFalse(Selector().strategy().fetch_for_product(product).availability.is_available_to_buy) - - expected_content = 'No product is available to buy.' - response = self._get_response(self.stock_record.partner_sku) - self.assertEqual(response.status_code, 400) - self.assertEqual(response.content.decode('utf-8'), expected_content) - - def test_with_both_unavailable_and_available_products(self): - """ Verify the basket ignores unavailable products and continue with available products. """ - products = ProductFactory.create_batch(3, stockrecords__partner=self.partner) - - products[0].expires = pytz.utc.localize(datetime.datetime.min) - products[0].save() - self.assertFalse(Selector().strategy().fetch_for_product(products[0]).availability.is_available_to_buy) - - response = self._get_response([product.stockrecords.first().partner_sku for product in products]) - self.assertEqual(response.status_code, 303) - basket = response.wsgi_request.basket - self.assertEqual(basket.status, Basket.OPEN) - - @ddt.data( - ('false', 'False'), - ('true', 'True'), - ) - @ddt.unpack - def test_email_opt_in_when_explicitly_given(self, opt_in, expected_value): - """ - Verify the email_opt_in query string is saved into a BasketAttribute. - """ - response = self._get_response(self.stock_record.partner_sku, email_opt_in=opt_in) - basket = response.wsgi_request.basket - basket_attribute = BasketAttribute.objects.get( - basket=basket, - attribute_type=BasketAttributeType.objects.get(name=EMAIL_OPT_IN_ATTRIBUTE), - ) - self.assertEqual(basket_attribute.value_text, expected_value) - - def test_email_opt_in_when_not_given(self): - """ - Verify that email_opt_in defaults to false if not specified. - """ - response = self._get_response(self.stock_record.partner_sku) - basket = response.wsgi_request.basket - basket_attribute = BasketAttribute.objects.get( - basket=basket, - attribute_type=BasketAttributeType.objects.get(name=EMAIL_OPT_IN_ATTRIBUTE), - ) - self.assertEqual(basket_attribute.value_text, 'False') - - @responses.activate - def test_enterprise_free_basket_redirect(self): - """ - Verify redirect to FreeCheckoutView when basket is free - and an Enterprise-related offer is applied. - """ - enterprise_offer = self.prepare_enterprise_offer() - - self.mock_catalog_contains_course_runs( - [self.course.id], - enterprise_offer.condition.enterprise_customer_uuid, - enterprise_customer_catalog_uuid=enterprise_offer.condition.enterprise_customer_catalog_uuid, - ) - - opts = { - 'ec_uuid': str(enterprise_offer.condition.enterprise_customer_uuid), - 'course_id': self.course.id, - 'username': self.user.username, - } - self.mock_consent_get(**opts) - - response = self._get_response(self.stock_record.partner_sku) - self.assertEqual(response.status_code, 302) - self.assertEqual(response.url, absolute_url(self.request, 'checkout:free-checkout')) - - class BasketLogicTestMixin: """ Helper functions for Basket API and BasketSummaryView tests. """ def create_empty_basket(self): @@ -796,428 +527,6 @@ def test_enterprise_offer_free_basket_with_wrong_basket(self): self.assertEqual(response.data['redirect'], absolute_url(self.request, 'checkout:free-checkout')) -@ddt.ddt -class BasketSummaryViewTests(EnterpriseServiceMockMixin, DiscoveryTestMixin, DiscoveryMockMixin, LmsApiMockMixin, - ApiMockMixin, BasketMixin, BasketLogicTestMixin, TestCase): - """ BasketSummaryView basket view tests. """ - path = reverse('basket:summary') - - def setUp(self): - super(BasketSummaryViewTests, self).setUp() - self.user = self.create_user() - self.client.login(username=self.user.username, password=self.password) - self.course = CourseFactory(name='BasketSummaryTest', partner=self.partner) - site_configuration = self.site.siteconfiguration - - site_configuration.payment_processors = DummyProcessor.NAME - site_configuration.client_side_payment_processor = DummyProcessor.NAME - site_configuration.save() - - toggle_switch(settings.PAYMENT_PROCESSOR_SWITCH_PREFIX + DummyProcessor.NAME, True) - responses.start() - - def tearDown(self): - super().tearDown() - responses.reset() - - @ddt.data(ReqConnectionError, RequestException, Timeout) - def test_course_api_failure(self, error): - """ Verify a connection error and timeout are logged when they happen. """ - seat = self.create_seat(self.course) - basket = self.create_basket_and_add_product(seat) - self.assertEqual(basket.lines.count(), 1) - - logger_name = 'ecommerce.extensions.basket.views' - self.mock_api_error( - error=error, - url=get_lms_url('api/courses/v1/courses/{}/'.format(self.course.id)) - ) - - with LogCapture(logger_name) as logger: - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - logger.check( - ( - logger_name, 'ERROR', - u'Failed to retrieve data from Discovery Service for course [{}].'.format(self.course.id) - ) - ) - - def test_non_seat_product(self): - """Verify the basket accepts non-seat product types.""" - title = 'Test Product 123' - description = 'All hail the test product.' - product = factories.ProductFactory(title=title, description=description) - self.create_basket_and_add_product(product) - - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - line_data = response.context['formset_lines_data'][0][1] - self.assertEqual(line_data['product_title'], title) - self.assertEqual(line_data['product_description'], description) - - def test_enrollment_code_seat_type(self): - """Verify the correct seat type attribute is retrieved.""" - course, __, enrollment_code = self.prepare_course_seat_and_enrollment_code() - self.create_basket_and_add_product(enrollment_code) - self.mock_course_runs_endpoint(self.site_configuration.discovery_api_url, course_run=course) - - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - self.assertFalse(response.context['show_voucher_form']) - line_data = response.context['formset_lines_data'][0][1] - self.assertEqual(line_data['seat_type'], enrollment_code.attr.seat_type.capitalize()) - - def test_microfrontend_for_single_course_purchase(self): - microfrontend_url = self.configure_redirect_to_microfrontend() - - seat = self.create_seat(self.course) - self.create_basket_and_add_product(seat) - response = self.client.get(self.path) - self.assertRedirects(response, microfrontend_url, status_code=302, fetch_redirect_response=False) - - def test_microfrontend_with_consent_failed_param(self): - microfrontend_url = self.configure_redirect_to_microfrontend() - - params = 'consent_failed=THISISACOUPONCODE' - url = '{}?{}'.format(self.path, params) - response = self.client.get(url) - expected_redirect_url = '{}?{}'.format(microfrontend_url, params) - self.assertRedirects(response, expected_redirect_url, status_code=302, fetch_redirect_response=False) - - def test_microfrontend_for_enrollment_code_seat_type(self): - microfrontend_url = self.configure_redirect_to_microfrontend() - - course, __, enrollment_code = self.prepare_course_seat_and_enrollment_code() - self.create_basket_and_add_product(enrollment_code) - self.mock_course_runs_endpoint(self.site_configuration.discovery_api_url, course_run=course) - response = self.client.get(self.path) - self.assertRedirects(response, microfrontend_url, status_code=302, fetch_redirect_response=False) - - @ddt.data( - (Benefit.PERCENTAGE, 100), - (Benefit.PERCENTAGE, 50), - (Benefit.FIXED, 50) - ) - @ddt.unpack - @override_settings(PAYMENT_PROCESSORS=['ecommerce.extensions.payment.tests.processors.DummyProcessor']) - def test_response_success(self, benefit_type, benefit_value): - """ Verify a successful response is returned. """ - seat = self.create_seat(self.course, 500) - basket = self.create_basket_and_add_product(seat) - self.mock_access_token_response() - self.create_and_apply_benefit_to_basket(basket, seat, benefit_type, benefit_value) - - self.assertEqual(basket.lines.count(), 1) - self.mock_course_run_detail_endpoint( - self.course, discovery_api_url=self.site_configuration.discovery_api_url - ) - - benefit, __ = Benefit.objects.get_or_create(type=benefit_type, value=benefit_value) - with self.assert_events_fired_to_segment(basket): - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - - self.assertEqual(len(response.context['formset_lines_data']), 1) - - line_data = response.context['formset_lines_data'][0][1] - self.assertEqual(line_data['benefit_value'], format_benefit_value(benefit)) - self.assertEqual(line_data['seat_type'], seat.attr.certificate_type.capitalize()) - self.assertEqual(line_data['product_title'], self.course.name) - self.assertFalse(line_data['enrollment_code']) - self.assertEqual(response.context['payment_processors'][0].NAME, DummyProcessor.NAME) - - def test_track_segment_event_exception(self): - """ Verify error log when track_segment_event fails. """ - seat = self.create_seat(self.course) - basket = self.create_basket_and_add_product(seat) - self.mock_access_token_response() - self.mock_course_run_detail_endpoint( - self.course, discovery_api_url=self.site_configuration.discovery_api_url - ) - self.assertEqual(basket.lines.count(), 1) - - logger_name = 'ecommerce.extensions.basket.views' - with LogCapture(logger_name) as logger: - with mock.patch('ecommerce.extensions.basket.views.track_segment_event') as mock_track: - mock_track.side_effect = Exception() - - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - - logger.check(( - logger_name, 'ERROR', - u'Failed to fire Cart Viewed event for basket [{}]'.format(basket.id) - )) - - def assert_empty_basket(self): - """ Assert that the basket is empty on visiting the basket summary page. """ - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['formset_lines_data'], []) - self.assertEqual(response.context['total_benefit'], None) - - def test_no_basket_response(self): - """ Verify there are no form, line and benefit data in the context for a non-existing basket. """ - self.assert_empty_basket() - - def test_line_item_discount_data(self): - """ Verify that line item has correct discount data. """ - self.mock_course_runs_endpoint(self.site_configuration.discovery_api_url, course_run=self.course) - seat = self.create_seat(self.course) - basket = self.create_basket_and_add_product(seat) - self.create_and_apply_benefit_to_basket(basket, seat, Benefit.PERCENTAGE, 50) - - course_without_benefit = CourseFactory() - seat_without_benefit = self.create_seat(course_without_benefit) - basket.add_product(seat_without_benefit, 1) - - response = self.client.get(self.path) - lines = response.context['formset_lines_data'] - self.assertEqual(lines[0][1]['benefit_value'], '50%') - self.assertEqual(lines[1][1]['benefit_value'], None) - - @mock.patch('ecommerce.extensions.offer.dynamic_conditional_offer.get_decoded_jwt_discount_from_request') - @ddt.data( - {'discount_percent': 15, 'discount_applicable': True}, - {'discount_percent': 15, 'discount_applicable': False}, - None) - @override_flag(DYNAMIC_DISCOUNT_FLAG, active=True) - def test_line_item_discount_data_dynamic_discount(self, discount_json, mock_get_discount): - """ Verify that line item has correct discount data. """ - mock_get_discount.return_value = discount_json - - self.mock_course_runs_endpoint(self.site_configuration.discovery_api_url, course_run=self.course) - seat = self.create_seat(self.course) - basket = self.create_basket_and_add_product(seat) - Applicator().apply(basket) - - response = self.client.get(self.path) - lines = response.context['formset_lines_data'] - if discount_json and discount_json['discount_applicable']: - self.assertEqual( - lines[0][1]['line'].discount_value, - discount_json['discount_percent'] / Decimal('100') * lines[0][1]['line'].price_incl_tax) - else: - self.assertEqual( - lines[0][1]['line'].discount_value, - Decimal(0)) - - def test_cached_course(self): - """ Verify that the course info is cached. """ - seat = self.create_seat(self.course, 50) - basket = self.create_basket_and_add_product(seat) - self.mock_access_token_response() - self.assertEqual(basket.lines.count(), 1) - self.mock_course_run_detail_endpoint( - self.course, discovery_api_url=self.site_configuration.discovery_api_url - ) - - cache_key = get_cache_key( - site_domain=self.site, - resource="{}-{}".format('course_runs', self.course.id) - ) - course_before_cached_response = TieredCache.get_cached_response(cache_key) - self.assertFalse(course_before_cached_response.is_found) - - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - course_after_cached_response = TieredCache.get_cached_response(cache_key) - self.assertEqual(course_after_cached_response.value['title'], self.course.name) - - @ddt.data({ - 'course': 'edX+DemoX', - 'short_description': None, - 'title': 'Junk', - 'start': '2013-02-05T05:00:00Z', - }, { - 'course': 'edX+DemoX', - 'short_description': None, - }) - def test_empty_catalog_api_response(self, course_info): - """ Check to see if we can handle empty response from the catalog api """ - seat = self.create_seat(self.course) - self.create_basket_and_add_product(seat) - self.mock_access_token_response() - self.mock_course_run_detail_endpoint( - self.course, self.site_configuration.discovery_api_url, course_info - ) - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - line_data = response.context['formset_lines_data'][0][1] - self.assertEqual(line_data.get('image_url'), None) - self.assertEqual(line_data.get('course_short_description'), None) - - def assert_order_details_in_context(self, product): - """Assert order details message is in basket context for passed product.""" - self.create_basket_and_add_product(product) - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - self.assertIsNotNone(response.context['order_details_msg']) - - @ddt.data(True, False) - def test_order_details_msg(self, id_verification): - """Verify the order details message is displayed for seats and enrollment codes.""" - __, seat, enrollment_code = self.prepare_course_seat_and_enrollment_code( - seat_type='professional', id_verification=id_verification - ) - self.assert_order_details_in_context(seat) - self.assert_order_details_in_context(enrollment_code) - - @override_flag(CLIENT_SIDE_CHECKOUT_FLAG_NAME, active=True) - @override_settings(PAYMENT_PROCESSORS=['ecommerce.extensions.payment.tests.processors.DummyProcessor']) - def test_client_side_checkout(self): - """ Verify the view returns the data necessary to initiate client-side checkout. """ - seat = self.create_seat(self.course) - basket = self.create_basket_and_add_product(seat) - - response = self.client.get(self.get_full_url(self.path)) - self.assertEqual(response.status_code, 200) - self.assertTrue(response.context['enable_client_side_checkout']) - - actual_processor = response.context['client_side_payment_processor'] - self.assertIsInstance(actual_processor, DummyProcessor) - - payment_form = response.context['payment_form'] - self.assertIsInstance(payment_form, PaymentForm) - self.assertEqual(payment_form.initial['basket'], basket) - - @override_flag(CLIENT_SIDE_CHECKOUT_FLAG_NAME, active=True) - def test_client_side_checkout_with_invalid_configuration(self): - """ Verify an error is raised if a payment processor is defined as the client-side processor, - but is not active in the system.""" - self.site.siteconfiguration.client_side_payment_processor = 'blah' - self.site.siteconfiguration.save() - - seat = self.create_seat(self.course) - self.create_basket_and_add_product(seat) - - with self.assertRaises(SiteConfigurationError): - self.client.get(self.get_full_url(self.path)) - - def test_login_required_basket_summary(self): - """ The view should redirect to the login page if the user is not logged in. """ - self.client.logout() - response = self.client.get(self.path) - expected_url = '{path}?next={next}'.format(path=reverse(settings.LOGIN_URL), - next=urllib.parse.quote(self.path)) - self.assertRedirects(response, expected_url, target_status_code=302) - - @ddt.data( - (None, None), - ('invalid-date', None), - ('2017-02-01T00:00:00', datetime.datetime(2017, 2, 1)), - ) - @ddt.unpack - @override_settings(PAYMENT_PROCESSORS=['ecommerce.extensions.payment.tests.processors.DummyProcessor']) - def test_context_data_contains_course_dates(self, date_string, expected_result): - seat = self.create_seat(self.course) - self.mock_access_token_response() - self.create_basket_and_add_product(seat) - self.mock_course_run_detail_endpoint( - self.course, - self.site_configuration.discovery_api_url, - { - 'start': date_string, - 'end': date_string - } - ) - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - for _, line_data in response.context['formset_lines_data']: - self.assertEqual(line_data['course_start'], expected_result) - self.assertEqual(line_data['course_end'], expected_result) - - def test_course_about_url(self): - """ - Test that in case of bulk enrollment, We have the marketing url from course metadata - if present in response. - """ - course_run_info = { - "course": "edX+DemoX", - "title": 'course title here', - "short_description": 'Foo', - "start": "2013-02-05T05:00:00Z", - "image": { - "src": "/path/to/image.jpg", - }, - 'enrollment_end': None, - 'marketing_url': '/path/to/marketing/site' - } - self.mock_access_token_response() - course, __, enrollment_code = self.prepare_course_seat_and_enrollment_code() - self.create_basket_and_add_product(enrollment_code) - self.mock_course_run_detail_endpoint( - course, - self.site_configuration.discovery_api_url, - course_run_info - ) - - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - messages = list(response.context['messages']) - self.assertEqual(len(messages), 1) - self.assertContains(response, '/path/to/marketing/site', status_code=200) - - def test_failed_enterprise_consent_sends_message(self): - """ - Test that if we receive an indication via a query parameter that data sharing - consent was attempted, but failed, we send a message indicating such. - """ - seat = self.create_seat(self.course) - self.create_basket_and_add_product(seat) - - params = 'consent_failed=THISISACOUPONCODE' - - url = '{path}?{params}'.format( - path=self.get_full_url(self.path), - params=params - ) - response = self.client.get(url) - message = list(response.context['messages'])[0] - - self.assertEqual( - str(message), - 'Could not apply the code \'THISISACOUPONCODE\'; it requires data sharing consent.' - ) - - @responses.activate - def test_enterprise_free_basket_redirect(self): - self.course_run.create_or_update_seat('verified', True, Decimal(100)) - self.create_basket_and_add_product(self.course_run.seat_products[0]) - enterprise_offer = self.prepare_enterprise_offer(enterprise_customer_name='Foo Enterprise') - - opts = { - 'ec_uuid': str(enterprise_offer.condition.enterprise_customer_uuid), - 'course_id': self.course_run.seat_products[0].course_id, - 'username': self.user.username, - } - self.mock_consent_get(**opts) - - response = self.client.get(self.path) - self.assertRedirects( - response, - absolute_url(self.request, 'checkout:free-checkout'), - fetch_redirect_response=False, - ) - - @override_settings(PAYMENT_PROCESSORS=['ecommerce.extensions.payment.tests.processors.DummyProcessor']) - @ddt.data(100, 50) - def test_discounted_free_basket(self, percentage_benefit): - seat = self.create_seat(self.course, seat_price=100) - basket = self.create_basket_and_add_product(seat) - self.mock_access_token_response() - self.mock_course_run_detail_endpoint( - self.course, discovery_api_url=self.site_configuration.discovery_api_url - ) - - self.create_and_apply_benefit_to_basket(basket, seat, Benefit.PERCENTAGE, percentage_benefit) - - response = self.client.get(self.path) - self.assertEqual(response.status_code, 200) - self.assertEqual(response.context['free_basket'], percentage_benefit == 100) - - class VoucherAddMixin(LmsApiMockMixin, DiscoveryMockMixin): def setUp(self): super(VoucherAddMixin, self).setUp() @@ -1434,7 +743,7 @@ def test_activation_not_required_for_inactive_user_when_disabled_for_site(self): self.basket.add_product(product) messages = [{ 'message_type': 'info', - 'user_message': u"Coupon code '{code}' added to basket.".format(code=COUPON_CODE), + 'user_message': f"Coupon code '{COUPON_CODE}' added to basket.", }] self.assert_response( product=product, @@ -1500,107 +809,6 @@ def test_account_activation_rendered_with_error(self): self.assertIn(u'Not Found', response.content.decode('utf-8')) -class VoucherAddViewTests(VoucherAddMixin, TestCase): - """ Tests for VoucherAddView. """ - - def setUp(self): - super(VoucherAddViewTests, self).setUp() - self.view = VoucherAddView() - self.view.request = self.request - - self.form = BasketVoucherForm() - self.form.cleaned_data = {'code': COUPON_CODE} - - def get_error_message_from_request(self): - return list(get_messages(self.request))[-1].message - - def assert_response(self, **kwargs): - self.assert_form_valid_message(kwargs['messages'][0]['user_message']) - - def assert_response_redirect(self, expected_redirect_url): - resp = self.view.form_valid(self.form) - self.assertIsInstance(resp, HttpResponseRedirect) - self.assertEqual(resp.url, expected_redirect_url) - - def assert_form_valid_message(self, expected): - """ Asserts the expected message is logged via messages framework when the - view's form_valid method is called. """ - self.view.form_valid(self.form) - - actual = self.get_error_message_from_request() - self.assertEqual(str(actual), expected) - - def test_voucher_valid_without_site(self): - """ Verify coupon works when the sites on the coupon and request are the same. """ - self.mock_access_token_response() - self.mock_account_api(self.request, self.user.username, data={'is_active': True}) - __, product = prepare_voucher(code=COUPON_CODE, site=self.request.site) - self.basket.add_product(product) - self.assert_form_valid_message("Coupon code '{code}' added to basket.".format(code=COUPON_CODE)) - - def test_voucher_not_valid_for_other_site(self): - """ Verify correct error message is returned when coupon is applied against on the wrong site. """ - other_site = SiteConfigurationFactory().site - self.mock_access_token_response() - self.mock_account_api(self.request, self.user.username, data={'is_active': True}) - voucher, product = prepare_voucher(code=COUPON_CODE, site=other_site) - self.basket.add_product(product) - self.assert_form_valid_message("Coupon code '{code}' is not valid for this basket.".format(code=voucher.code)) - - def test_form_valid_without_basket_id(self): - """ Verify the view redirects to the basket summary view if the basket has no ID. """ - self.request.basket = Basket() - response = self.view.form_valid(self.form) - self.assertEqual(response.url, reverse('basket:summary')) - - def assert_basket_discounts(self, expected_offer_discounts=None, expected_voucher_discounts=None): - """Helper to determine if the expected offer is applied to a basket. - The basket is retrieved from the response because Oscar uses - SimpleLazyObjects to operate with baskets.""" - expected_offer_discounts = expected_offer_discounts or [] - expected_voucher_discounts = expected_voucher_discounts or [] - - response = self.client.get(reverse('basket:summary')) - basket = response.context['basket'] - - actual_offer_discounts = [discount['offer'] for discount in basket.offer_discounts] - actual_voucher_discounts = [discount['offer'] for discount in basket.voucher_discounts] - - self.assertEqual(actual_offer_discounts, expected_offer_discounts) - self.assertEqual(actual_voucher_discounts, expected_voucher_discounts) - - def test_coupon_applied_on_site_offer(self): - """Coupon offer supersedes site offer.""" - self.mock_access_token_response() - self.mock_account_api(self.request, self.user.username, data={'is_active': True}) - product_price = 100 - site_offer_discount = 20 - voucher_discount = 10 - - voucher, product = prepare_voucher(benefit_value=voucher_discount) - stockrecord = product.stockrecords.first() - stockrecord.price_excl_tax = product_price - stockrecord.save() - - _range = factories.RangeFactory(includes_all_products=True) - site_offer = factories.ConditionalOfferFactory( - offer_type=ConditionalOffer.SITE, - benefit=factories.BenefitFactory(range=_range, value=site_offer_discount), - condition=factories.ConditionFactory(type=Condition.COVERAGE, value=1, range=_range) - ) - self.basket.add_product(product) - # Only site offer is applied to the basket. - self.assert_basket_discounts([site_offer]) - - # Only the voucher offer is applied to the basket. - self.client.post(reverse('basket:vouchers-add'), data={'code': voucher.code}) - self.assert_basket_discounts(expected_voucher_discounts=[voucher.offers.first()]) - - # Site offer discount is still present after removing voucher. - self.client.post(reverse('basket:vouchers-remove', kwargs={'pk': voucher.id})) - self.assert_basket_discounts([site_offer]) - - class QuantityApiViewTests(PaymentApiResponseTestMixin, BasketMixin, DiscoveryMockMixin, TestCase): """ QuantityApiViewTests basket api tests. """ path = reverse('bff:payment:v0:quantity') diff --git a/ecommerce/extensions/basket/utils.py b/ecommerce/extensions/basket/utils.py index 7dedbcd825d..a4e722abb40 100644 --- a/ecommerce/extensions/basket/utils.py +++ b/ecommerce/extensions/basket/utils.py @@ -15,13 +15,11 @@ from oscar.apps.basket.signals import voucher_addition from oscar.core.loading import get_class, get_model -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.order.exceptions import AlreadyPlacedOrderException from ecommerce.extensions.order.utils import UserAlreadyPlacedOrder -from ecommerce.extensions.payment.constants import DISABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME from ecommerce.extensions.payment.utils import embargo_check from ecommerce.programs.utils import get_program from ecommerce.referrals.models import Referral @@ -34,8 +32,6 @@ ORGANIZATION_ATTRIBUTE_TYPE = 'organization' ENTERPRISE_CATALOG_ATTRIBUTE_TYPE = 'enterprise_catalog_uuid' StockRecord = get_model('partner', 'StockRecord') -OrderLine = get_model('order', 'Line') -Refund = get_model('refund', 'Refund') Voucher = get_model('voucher', 'Voucher') logger = logging.getLogger(__name__) @@ -64,29 +60,8 @@ def add_flex_microform_flag_to_url(url, request, force_flag=None): ) -def get_payment_microfrontend_or_basket_url(request): - url = get_payment_microfrontend_url_if_configured(request) - if not url: - url = absolute_url(request, 'basket:summary') - return url - - -def get_payment_microfrontend_url_if_configured(request): - if _use_payment_microfrontend(request): - return request.site.siteconfiguration.payment_microfrontend_url - - return None - - -def _use_payment_microfrontend(request): - """ - Return whether the current request should use the payment MFE. - """ - return ( - request.site.siteconfiguration.enable_microfrontend_for_basket_page and - request.site.siteconfiguration.payment_microfrontend_url and - not waffle.flag_is_active(request, DISABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME) - ) +def get_payment_microfrontend(request): + return request.site.siteconfiguration.payment_microfrontend_url @newrelic.agent.function_trace() @@ -102,14 +77,6 @@ def add_utm_params_to_url(url, params): return url -@newrelic.agent.function_trace() -def add_invalid_code_message_to_url(url, code): - if code: - message = 'error_message=Code {code} is invalid.'.format(code=str(code)) - url += '&' + message if '?' in url else '?' + message - return url - - @newrelic.agent.function_trace() def prepare_basket(request, products, voucher=None): """ diff --git a/ecommerce/extensions/basket/views.py b/ecommerce/extensions/basket/views.py index 1e568d7f868..ccad0c7b791 100644 --- a/ecommerce/extensions/basket/views.py +++ b/ecommerce/extensions/basket/views.py @@ -2,7 +2,6 @@ import logging -import time import urllib from collections import OrderedDict from datetime import datetime @@ -11,15 +10,11 @@ import dateutil.parser import newrelic.agent import waffle -from django.http import HttpResponseBadRequest, HttpResponseRedirect -from django.shortcuts import render +from django.http import HttpResponseRedirect from django.urls import reverse -from django.utils.html import escape from django.utils.translation import ugettext as _ -from edx_rest_framework_extensions.permissions import LoginRedirectIfUnauthenticated from opaque_keys.edx.keys import CourseKey from oscar.apps.basket.signals import voucher_removal -from oscar.apps.basket.views import VoucherAddView as BaseVoucherAddView from oscar.apps.basket.views import * # pylint: disable=wildcard-import, unused-wildcard-import from oscar.core.prices import Price from requests.exceptions import ConnectionError as ReqConnectionError @@ -41,23 +36,16 @@ parse_consent_params ) from ecommerce.extensions.analytics.utils import ( - prepare_analytics_data, track_braze_event, track_segment_event, 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.exceptions import BadRequestException, RedirectException, VoucherException +from ecommerce.extensions.basket.exceptions import RedirectException, VoucherException from ecommerce.extensions.basket.utils import ( - add_invalid_code_message_to_url, - add_utm_params_to_url, apply_offers_on_basket, apply_voucher_on_basket_and_check_discount, get_basket_switch_data, - get_payment_microfrontend_or_basket_url, - get_payment_microfrontend_url_if_configured, - prepare_basket, validate_voucher ) from ecommerce.extensions.offer.constants import DYNAMIC_DISCOUNT_FLAG @@ -68,23 +56,13 @@ get_quantized_benefit_value, get_redirect_to_email_confirmation_if_required ) -from ecommerce.extensions.order.exceptions import AlreadyPlacedOrderException -from ecommerce.extensions.partner.shortcuts import get_partner_for_site -from ecommerce.extensions.payment.constants import CLIENT_SIDE_CHECKOUT_FLAG_NAME from ecommerce.extensions.payment.forms import PaymentForm from ecommerce.programs.utils import get_program -Basket = get_model('basket', 'basket') BasketAttribute = get_model('basket', 'BasketAttribute') -BasketAttributeType = get_model('basket', 'BasketAttributeType') BUNDLE = 'bundle_identifier' ConditionalOffer = get_model('offer', 'ConditionalOffer') -Benefit = get_model('offer', 'Benefit') logger = logging.getLogger(__name__) -Product = get_model('catalogue', 'Product') -StockRecord = get_model('partner', 'StockRecord') -Voucher = get_model('voucher', 'Voucher') -Selector = get_class('partner.strategy', 'Selector') class BasketLogicMixin: @@ -403,213 +381,6 @@ def _deserialize_date(self, date_string): return None -class BasketAddItemsView(BasketLogicMixin, APIView): - """ - View that adds multiple products to a user's basket. - An additional coupon code can be supplied so the offer is applied to the basket. - """ - permission_classes = (LoginRedirectIfUnauthenticated,) - - def get(self, request): - # Send time when this view is called - https://openedx.atlassian.net/browse/REV-984 - properties = {'emitted_at': time.time()} - track_segment_event(request.site, request.user, 'Basket Add Items View Called', properties) - - try: - skus = self._get_skus(request) - products = self._get_products(request, skus) - voucher = None - invalid_code = None - code = request.GET.get('code', None) - try: - voucher = self._get_voucher(request) - except Voucher.DoesNotExist: # pragma: nocover - # Display an error message when an invalid code is passed as a parameter - invalid_code = code - - logger.info('Starting payment flow for user [%s] for products [%s].', request.user.username, skus) - - available_products = self._get_available_products(request, products) - - try: - basket = prepare_basket(request, available_products, voucher) - except AlreadyPlacedOrderException: - return render(request, 'edx/error.html', {'error': _('You have already purchased these products')}) - - self._set_email_preference_on_basket(request, basket) - - # Used basket object from request to allow enterprise offers - # being applied on basket via BasketMiddleware - self.verify_enterprise_needs(request.basket) - if code and not request.basket.vouchers.exists(): - if not (len(available_products) == 1 and available_products[0].is_enrollment_code_product): - # Display an error message when an invalid code is passed as a parameter - invalid_code = code - return self._redirect_response_to_basket_or_payment(request, invalid_code) - - except BadRequestException as e: - return HttpResponseBadRequest(str(e)) - except RedirectException as e: - return e.response - - def _get_skus(self, request): - skus = [escape(sku) for sku in request.GET.getlist('sku')] - if not skus: - raise BadRequestException(_('No SKUs provided.')) - return skus - - def _get_products(self, request, skus): - partner = get_partner_for_site(request) - products = Product.objects.filter(stockrecords__partner=partner, stockrecords__partner_sku__in=skus) - if not products: - raise BadRequestException(_('Products with SKU(s) [{skus}] do not exist.').format(skus=', '.join(skus))) - return products - - def _get_voucher(self, request): - code = request.GET.get('code', None) - return Voucher.objects.get(code=code) if code else None - - def _get_available_products(self, request, products): - unavailable_product_ids = [] - for product in products: - purchase_info = request.strategy.fetch_for_product(product) - if not purchase_info.availability.is_available_to_buy: - logger.warning('Product [%s] is not available to buy.', product.title) - unavailable_product_ids.append(product.id) - - available_products = products.exclude(id__in=unavailable_product_ids) - if not available_products: - raise BadRequestException(_('No product is available to buy.')) - return available_products - - def _set_email_preference_on_basket(self, request, basket): - """ - Associate the user's email opt in preferences with the basket in - order to opt them in later as part of fulfillment - """ - BasketAttribute.objects.update_or_create( - basket=basket, - attribute_type=BasketAttributeType.objects.get(name=EMAIL_OPT_IN_ATTRIBUTE), - defaults={'value_text': request.GET.get('email_opt_in') == 'true'}, - ) - - def _redirect_response_to_basket_or_payment(self, request, invalid_code=None): - redirect_url = get_payment_microfrontend_or_basket_url(request) - redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) - redirect_url = add_invalid_code_message_to_url(redirect_url, invalid_code) - - return HttpResponseRedirect(redirect_url, status=303) - - -class BasketSummaryView(BasketLogicMixin, BasketView): - @newrelic.agent.function_trace() - def get_context_data(self, **kwargs): - context = super(BasketSummaryView, self).get_context_data(**kwargs) - return self._add_to_context_data(context) - - @newrelic.agent.function_trace() - def get(self, request, *args, **kwargs): - basket = request.basket - - try: - self.fire_segment_events(request, basket) - self.verify_enterprise_needs(basket) - self._redirect_to_payment_microfrontend_if_configured(request) - except RedirectException as e: - return e.response - - return super(BasketSummaryView, self).get(request, *args, **kwargs) - - def _redirect_to_payment_microfrontend_if_configured(self, request): - microfrontend_url = get_payment_microfrontend_url_if_configured(request) - if microfrontend_url: - # For now, the enterprise consent form validation is communicated via - # a URL parameter, which must be forwarded via this redirect. - consent_failed_param_to_forward = request.GET.get(CONSENT_FAILED_PARAM) - if consent_failed_param_to_forward: - microfrontend_url = '{}?{}={}'.format( - microfrontend_url, - CONSENT_FAILED_PARAM, - consent_failed_param_to_forward, - ) - redirect_response = HttpResponseRedirect(microfrontend_url) - raise RedirectException(response=redirect_response) - - @newrelic.agent.function_trace() - def _add_to_context_data(self, context): - formset = context.get('formset', []) - lines = context.get('line_list', []) - site_configuration = self.request.site.siteconfiguration - - context_updates, lines_data = self.process_basket_lines(lines) - context.update(context_updates) - context.update(self.process_totals(context)) - - context.update({ - 'analytics_data': prepare_analytics_data( - self.request.user, - site_configuration.segment_key, - ), - 'enable_client_side_checkout': False, - 'sdn_check': site_configuration.enable_sdn_check - }) - - payment_processors = site_configuration.get_payment_processors() - if ( - site_configuration.client_side_payment_processor and - waffle.flag_is_active(self.request, CLIENT_SIDE_CHECKOUT_FLAG_NAME) - ): - payment_processors_data = self._get_payment_processors_data(payment_processors) - context.update(payment_processors_data) - - context.update({ - 'formset_lines_data': list(zip(formset, lines_data)), - 'homepage_url': get_lms_url(''), - 'min_seat_quantity': 1, - 'max_seat_quantity': 100, - 'payment_processors': payment_processors, - 'lms_url_root': site_configuration.lms_url_root, - }) - return context - - @newrelic.agent.function_trace() - def _get_payment_processors_data(self, payment_processors): - """Retrieve information about payment processors for the client side checkout basket. - - Args: - payment_processors (list): List of all available payment processors. - Returns: - A dictionary containing information about the payment processor(s) with which the - 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() - - if payment_processor_class: - payment_processor = payment_processor_class(self.request.site) - current_year = datetime.today().year - - return { - 'client_side_payment_processor': payment_processor, - 'enable_client_side_checkout': True, - 'months': list(range(1, 13)), - 'payment_form': PaymentForm( - user=self.request.user, - request=self.request, - initial={'basket': self.request.basket}, - label_suffix='' - ), - 'paypal_enabled': 'paypal' in (p.NAME for p in payment_processors), - # Assumption is that the credit card duration is 15 years - 'years': list(range(current_year, current_year + 16)), - } - else: - msg = 'Unable to load client-side payment processor [{processor}] for ' \ - 'site configuration [{sc}]'.format(processor=site_configuration.client_side_payment_processor, - sc=site_configuration.id) - raise SiteConfigurationError(msg) - - class CaptureContextApiLogicMixin: # pragma: no cover """ Business logic for the capture context API. @@ -1049,26 +820,6 @@ def _get_voucher(self, code): raise VoucherException() from voucher_no_exist -class VoucherAddView(VoucherAddLogicMixin, BaseVoucherAddView): # pylint: disable=function-redefined - """ - Deprecated: Adds a voucher to the basket. - - Ensure any changes made here are also made to VoucherAddApiView. - """ - def form_valid(self, form): - code = form.cleaned_data['code'] - - try: - self.verify_and_apply_voucher(code) - except RedirectException as e: - return e.response - except VoucherException: - # errors are passed via messages object - pass - - return redirect_to_referrer(self.request, 'basket:summary') - - class VoucherAddApiView(VoucherAddLogicMixin, PaymentApiLogicMixin, APIView): """ Api for adding voucher to a basket. diff --git a/ecommerce/extensions/checkout/tests/test_views.py b/ecommerce/extensions/checkout/tests/test_views.py index fca076dc1eb..bd9fc9f232a 100644 --- a/ecommerce/extensions/checkout/tests/test_views.py +++ b/ecommerce/extensions/checkout/tests/test_views.py @@ -58,8 +58,8 @@ def prepare_basket(self, price, bundle=False): def test_empty_basket(self): """ Verify redirect to basket summary in case of empty basket. """ response = self.client.get(self.path) - expected_url = reverse('basket:summary') - self.assertRedirects(response, expected_url) + expected_url = self.site_configuration.payment_microfrontend_url + self.assertRedirects(response, expected_url, target_status_code=302) def test_non_free_basket(self): """ Verify an exception is raised when the URL is being accessed to with a non-free basket. """ diff --git a/ecommerce/extensions/checkout/views.py b/ecommerce/extensions/checkout/views.py index a2e67b5a2a0..b8b5b20af8c 100644 --- a/ecommerce/extensions/checkout/views.py +++ b/ecommerce/extensions/checkout/views.py @@ -24,6 +24,7 @@ get_lms_program_dashboard_url ) from ecommerce.enterprise.api import fetch_enterprise_learner_data +from ecommerce.extensions.basket.utils import get_payment_microfrontend from ecommerce.enterprise.utils import has_enterprise_offer from ecommerce.extensions.checkout.exceptions import BasketNotFreeError from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin @@ -88,7 +89,7 @@ def get_redirect_url(self, *args, **kwargs): else: # If a user's basket is empty redirect the user to the basket summary # page which displays the appropriate message for empty baskets. - url = reverse('basket:summary') + url = get_payment_microfrontend(request) return url diff --git a/ecommerce/extensions/payment/constants.py b/ecommerce/extensions/payment/constants.py index a393e85ab84..2761af4506f 100644 --- a/ecommerce/extensions/payment/constants.py +++ b/ecommerce/extensions/payment/constants.py @@ -42,18 +42,6 @@ CLIENT_SIDE_CHECKOUT_FLAG_NAME = 'enable_client_side_checkout' -# .. toggle_name: disable_microfrontend_for_basket_page -# .. toggle_type: waffle_flag -# .. toggle_default: False -# .. toggle_description: Allows viewing the old basket page even when using a new micro-frontend based basket page -# .. toggle_category: micro-frontend -# .. toggle_use_cases: open_edx -# .. toggle_creation_date: 2019-10-03 -# .. toggle_expiration_date: 2020-12-31 -# .. toggle_tickets: DEPR-42 -# .. toggle_status: supported -DISABLE_MICROFRONTEND_FOR_BASKET_PAGE_FLAG_NAME = 'disable_microfrontend_for_basket_page' - # Paypal only supports 4 languages, which are prioritized by country. # https://developer.paypal.com/docs/classic/api/locale_codes/ PAYPAL_LOCALES = { diff --git a/ecommerce/extensions/payment/tests/views/test_cybersource.py b/ecommerce/extensions/payment/tests/views/test_cybersource.py index b0bb2dd97bb..612d546fdf6 100644 --- a/ecommerce/extensions/payment/tests/views/test_cybersource.py +++ b/ecommerce/extensions/payment/tests/views/test_cybersource.py @@ -18,7 +18,6 @@ from ecommerce.core.models import User from ecommerce.extensions.api.serializers import OrderSerializer -from ecommerce.extensions.basket.utils import get_payment_microfrontend_or_basket_url from ecommerce.extensions.checkout.utils import get_receipt_page_url from ecommerce.extensions.order.constants import PaymentEventTypeName from ecommerce.extensions.payment.core.sdn import SDNClient @@ -367,7 +366,7 @@ def test_decline(self): request = RequestFactory(SERVER_NAME='testserver.fake').post(self.path, data) request.site = self.site - assert json.loads(response.content)['redirectTo'] == get_payment_microfrontend_or_basket_url(request) + assert json.loads(response.content)['redirectTo'] == self.site_configuration.payment_microfrontend_url # Ensure the basket is frozen basket = Basket.objects.get(pk=basket.pk) @@ -397,7 +396,7 @@ def test_authorized_pending_review_request(self): request = RequestFactory(SERVER_NAME='testserver.fake').post(self.path, data) request.site = self.site - assert json.loads(response.content)['redirectTo'] == get_payment_microfrontend_or_basket_url(request) + assert json.loads(response.content)['redirectTo'] == self.site_configuration.payment_microfrontend_url # Ensure the basket is frozen basket = Basket.objects.get(pk=basket.pk) @@ -435,7 +434,7 @@ def test_authorized_pending_review_request_reversal_failed(self): request = RequestFactory(SERVER_NAME='testserver.fake').post(self.path, data) request.site = self.site - assert json.loads(response.content)['redirectTo'] == get_payment_microfrontend_or_basket_url(request) + assert json.loads(response.content)['redirectTo'] == self.site_configuration.payment_microfrontend_url # Ensure the basket is frozen basket = Basket.objects.get(pk=basket.pk) @@ -517,21 +516,6 @@ def test_post(self, status, body): """ The view should POST to the given URL and return the response. """ self._call_to_apple_pay_and_assert_response(status, body) - @responses.activate - @ddt.data(*itertools.product((True, False), (True, False))) - @ddt.unpack - def test_with_microfrontend(self, request_from_mfe, enable_microfrontend): - self.site.siteconfiguration.enable_microfrontend_for_basket_page = enable_microfrontend - self.site.siteconfiguration.payment_microfrontend_url = 'http://{}'.format(self.payment_microfrontend_domain) - self.site.siteconfiguration.save() - - self._call_to_apple_pay_and_assert_response( - 200, - {'foo': 'bar'}, - request_from_mfe, - request_from_mfe and enable_microfrontend, - ) - def test_post_without_url(self): """ The view should return HTTP 400 if no url parameter is posted. """ response = self.client.post(self.url) diff --git a/ecommerce/extensions/payment/views/cybersource.py b/ecommerce/extensions/payment/views/cybersource.py index 6405cc5b08e..f2e7ed93931 100644 --- a/ecommerce/extensions/payment/views/cybersource.py +++ b/ecommerce/extensions/payment/views/cybersource.py @@ -21,7 +21,7 @@ from ecommerce.extensions.basket.utils import ( add_utm_params_to_url, basket_add_organization_attribute, - get_payment_microfrontend_or_basket_url + get_payment_microfrontend ) from ecommerce.extensions.checkout.mixins import EdxOrderPlacementMixin from ecommerce.extensions.checkout.utils import get_receipt_page_url @@ -470,7 +470,7 @@ def redirect_to_receipt_page(self): }, status=201) def redirect_on_transaction_declined(self): - redirect_url = get_payment_microfrontend_or_basket_url(self.request) + redirect_url = get_payment_microfrontend(self.request) redirect_url = add_utm_params_to_url(redirect_url, list(self.request.GET.items())) return JsonResponse({ 'redirectTo': redirect_url, diff --git a/ecommerce/templates/oscar/basket/basket.html b/ecommerce/templates/oscar/basket/basket.html deleted file mode 100644 index 08ee975cc1c..00000000000 --- a/ecommerce/templates/oscar/basket/basket.html +++ /dev/null @@ -1,73 +0,0 @@ -{% extends 'edx/base.html' %} - -{% load core_extras %} -{% load i18n %} -{% load django_markup %} -{% load static %} - -{% block title %} -{% trans 'Basket' as tmsg %}{{ tmsg | force_escape }} -{% endblock title %} - -{% block navbar %} - {% include 'edx/partials/_student_navbar.html' %} -{% endblock navbar %} - -{% block pre_app_js %} - {# Right now, dynamic discounts only apply to single verified certs #} - {% if formset_lines_data|length == 1 %} - - {% endif %} -{% endblock %} - -{% block javascript %} - - - -{% endblock %} - -{% block skip_link %} - -{% endblock skip_link %} - -{% block content %} -
-
-
- {# Use a partial template so that AJAX can be used to re-render basket #} - {% if basket.is_empty %} - {% block emptybasket %} -
-

{% trans "Your basket is empty" as tmsg %}{{ tmsg | force_escape }}

- {% blocktrans asvar tmsg %} - If you attempted to make a purchase, you have not been charged. Return to your {link_start}{link_middle}{homepage_url}dashboard{link_end} to try - again, or {link_start}{homepage_url}{link_middle}contact {platform_name} Support{link_end}. - {% endblocktrans %} - {% interpolate_html tmsg link_start=''|safe %} -
- {% endblock %} - {% else %} - {% if enable_client_side_checkout %} - {% include 'oscar/basket/partials/client_side_checkout_basket.html' %} - {% else %} - {% include 'oscar/basket/partials/hosted_checkout_basket.html' %} - {% endif %} - {% endif %} -
-
-
-{% endblock content %} - -{% block post_js %} - {# Load payment processor code after the basket app to ensure that all neccessary hooks are in place. #} - {% if enable_client_side_checkout %} - {% include client_side_payment_processor.get_template_name %} - {% endif %} -{% endblock %} diff --git a/ecommerce/templates/oscar/basket/messages/new_total.html b/ecommerce/templates/oscar/basket/messages/new_total.html deleted file mode 100644 index 9fe2b87039e..00000000000 --- a/ecommerce/templates/oscar/basket/messages/new_total.html +++ /dev/null @@ -1,30 +0,0 @@ -{% load i18n %} -{% load django_markup %} -{% load currency_filters %} - -

- {% if basket.is_empty %} - {% trans "Your basket is now empty" as tmsg %}{{ tmsg | force_escape }} - {% else %} - {% if basket.is_tax_known %} - {% blocktrans asvar tmsg %} - {strong_start}We’ve updated your quantity.{strong_end} - {paragraph_start}Your cart includes {num_items} enrollment codes at a total cost of {total}, that you will receive via email.{paragraph_end} - {% endblocktrans %} - {% interpolate_html tmsg num_items=basket.num_items|safe total=basket.total_incl_tax|currency:basket.currency|safe strong_start=''|safe strong_end=''|safe paragraph_start='

'|safe paragraph_end='

'|safe %} - {% else %} - {% blocktrans asvar tmsg %} - {strong_start}We’ve updated your quantity.{strong_end} - {paragraph_start}Your cart includes {num_items} enrollment codes at a total cost of {total}, that you will receive via email.{paragraph_end} - {% endblocktrans %} - {% interpolate_html tmsg num_items=basket.num_items total=basket.total_excl_tax|currency:basket.currency|safe strong_start=''|safe strong_end=''|safe paragraph_start='

'|safe paragraph_end='

'|safe %} - {% endif %} - {% endif %} -

- -{% if include_buttons %} -

- {% trans "View basket" as tmsg %}{{ tmsg | force_escape }} - {% trans "Checkout now" as tmsg %}{{ tmsg | force_escape }} -

-{% endif %} diff --git a/ecommerce/templates/oscar/basket/partials/add_voucher_form.html b/ecommerce/templates/oscar/basket/partials/add_voucher_form.html deleted file mode 100644 index 49a37504031..00000000000 --- a/ecommerce/templates/oscar/basket/partials/add_voucher_form.html +++ /dev/null @@ -1,20 +0,0 @@ -{% load i18n %} - -
-
- {% csrf_token %} -
- - -
-
-
diff --git a/ecommerce/templates/oscar/basket/partials/client_side_checkout_basket.html b/ecommerce/templates/oscar/basket/partials/client_side_checkout_basket.html deleted file mode 100644 index 2972e6b8052..00000000000 --- a/ecommerce/templates/oscar/basket/partials/client_side_checkout_basket.html +++ /dev/null @@ -1,281 +0,0 @@ -{% load i18n %} -{% load currency_filters %} -{% load crispy_forms_tags %} -{% load offer_tags %} -{% load widget_tweaks %} -{% load static %} - -{% include 'oscar/partials/alert_messages.html' %} - -
-
- {% csrf_token %} - {{ formset.management_form }} - -
-

{% trans "in your cart" as tmsg %}{{ tmsg | force_escape }}

-

{% trans "Your purchase contains the following" as tmsg %}{{ tmsg | force_escape }}:

- {% for form, line_data in formset_lines_data %} - {{ form.id }} -
-
- -
-
-

{{ line_data.product_title }}

- {% include 'oscar/basket/partials/seat_type.html' %} -
- {% if line_data.enrollment_code %} -
- -
-
- {% render_field form.quantity class+="quantity form-control" min=min_seat_quantity max=max_seat_quantity title="Quantity" id="input-quantity-field" %} -
- -
- - Max: 100 -
- {% endif %} -
- {% endfor %} -
-
-
-

{% trans "summary" as tmsg %}{{ tmsg | force_escape }}

- {% if is_enrollment_code_purchase %} -
- {% trans "Price" as tmsg %}{{ tmsg | force_escape }} - {% trans "Quantity" as tmsg %}{{ tmsg | force_escape }} - {% trans "Subtotal" as tmsg %}{{ tmsg | force_escape }} -
-
- {{line_price|currency:basket.currency}} - {{basket.num_items}} - {{order_total.incl_tax|currency:basket.currency}} -
- {% else %} -
- {% trans "Price" as tmsg %}{{ tmsg | force_escape }} - - {{basket.total_incl_tax_excl_discounts|currency:basket.currency}} -
- {% endif %} - {% if basket.total_discount %} -
- {% trans "Discounts applied" as tmsg %}{{ tmsg | force_escape }} - - -{{basket.total_discount|currency:basket.currency}} -
- {% endif %} -
- {% block offers %} -
- {% for offer_id, offer in basket.applied_offers.items %} - {% if offer.condition.enterprise_customer_name %} -

- {% filter force_escape %} - {% blocktrans with enterprise_customer_name=offer.condition.enterprise_customer_name benefit=offer.benefit|benefit_discount %} - {{ benefit }} discount provided by {{ enterprise_customer_name }}. - {% endblocktrans %} - {% endfilter %} -

- {% elif offer.condition.name == 'dynamic_discount_condition' %} - {% filter force_escape %} - {% blocktrans with benefit=discount_percent %} - {{ benefit }}% discount for your first upgrade applied. - {% endblocktrans %} - {% endfilter %} - {% endif %} - {% endfor %} -
- {% endblock offers %} -
-
- {% if show_voucher_form %} - {% block vouchers %} - {% if basket.contains_a_voucher %} -
- {% for voucher in basket.vouchers.all %} -

- {% filter force_escape %} - {% blocktrans with voucher_code=voucher.code %} - Coupon {{ voucher_code }} applied for {{ total_benefit }} off - {% endblocktrans %} - {% endfilter %} -

- {% csrf_token %} - -
-

- {% endfor %} -
- {% else %} - {# Hide the entire section if a custom BasketView doesn't pass in a voucher form #} - {% if voucher_form %} -
- - {% include 'oscar/basket/partials/add_voucher_form.html' %} -
- {% endif %} - {% endif %} - {% endblock vouchers %} - {% endif %} -
-
- {% trans "TOTAL" as tmsg %}{{ tmsg | force_escape }} - - {{ order_total.incl_tax|currency:basket.currency }} -
-
- {% if order_details_msg %} -
-

{% trans "order details" as tmsg %}{{ tmsg | force_escape }}

-

{{ order_details_msg | safe}}

-
- {% endif %} -
- - -
- {% csrf_token %} - {% if discount_jwt %} - - {% endif %} - -
- {% if not free_basket %} - {% if paypal_enabled %} -
-
-

{% trans "select payment method" as tmsg %}{{ tmsg | force_escape }}

-
- - - - {# Translators: Do NOT translate the name PayPal. #} - - -
- -
-
-
-
- {% endif %} -
-
-

{% trans "card holder information" as tmsg %}{{ tmsg | force_escape }}

- {% if sdn_check %} - - {% endif %} - {% crispy payment_form %} -
-
- {% endif %} -
-
- {% if not free_basket %} -

{% trans "billing information" as tmsg %}{{ tmsg | force_escape }}

- {% if not paypal_enabled %} - - {% endif %} -
- {# NOTE: The PCI fields should NOT have name attributes by default. This ensures the fields are #} - {# not posted to the server if JavaScript is disabled/fails. The processor-specific JS should #} - {# restore the name attribute to ensure this information is sent to the payment processor. #} -
- - - - -

-
- -
- - - -

-
-

{% trans "Expiration (required)" as tmsg %}{{ tmsg | force_escape }}

-
- -

-
-
- -

-
-
- {% endif %} - -
- {% if free_basket %} - - {% trans "Place Order" as tmsg %}{{ tmsg | force_escape }} - - {% else %} - - {% endif %} -
-
-
-
-
diff --git a/ecommerce/templates/oscar/basket/partials/hosted_checkout_basket.html b/ecommerce/templates/oscar/basket/partials/hosted_checkout_basket.html deleted file mode 100644 index 7a53ab61513..00000000000 --- a/ecommerce/templates/oscar/basket/partials/hosted_checkout_basket.html +++ /dev/null @@ -1,189 +0,0 @@ -{% load i18n %} -{% load django_markup %} -{% load core_extras %} -{% load currency_filters %} -{% load purchase_info_tags %} -{% load widget_tweaks %} - -{% if not is_bulk_purchase %} - {% include 'oscar/partials/alert_messages.html' %} -{% endif %} - -
- {% block basket_form_main %} -
- {% csrf_token %} - {{ formset.management_form }} - - {% for form, line_data in formset_lines_data %} - {% purchase_info_for_line request line_data.line as session %} -
- {% if line_data.seat_type %} -

- {% trans "Earn a valuable certificate to showcase the skills you learn in" as tmsg %}{{ tmsg | force_escape }} -

- {% endif %} -
-
- {{ form.id }} - {{ line_data.product_title|default_if_none:'' }} -
-
-

{{ line_data.product_title }} {% if line_data.course_key %}- {{ line_data.course_key.org }} - ({{ line_data.course_key.run }}) {% endif %}

-

{{ line_data.product_description|default_if_none:'' }}

-
- {% if line_data.enrollment_code %} -
- - {{ line_data.line.price_incl_tax|currency:line_data.line.price_currency }} -
-
- -
-
- {% render_field form.quantity class+="quantity form-control" min=min_seat_quantity %} -
- - -
-
- -
-
- {% endif %} -
- {% if line_data.enrollment_code %} - - {% endif %} - - {% if line_data.line.has_discount %} -
-
- {% filter force_escape %} - {% blocktrans with benefit_value=line_data.benefit_value %} - {{ benefit_value }} off - {% endblocktrans %} - {% endfilter %} -
-
- {{ line_data.line.line_price_incl_tax|currency:line_data.line.price_currency }} -
-
- {% endif %} -
- {{ line_data.line.line_price_incl_tax_incl_discounts|currency:line_data.line.price_currency }} -
-
-
-
- {% endfor %} -
- {% endblock %} - -
-
- {% if show_voucher_form %} - {% block vouchers %} - {% if basket.contains_a_voucher %} -
- {% for voucher in basket.vouchers.all %} -

- {% filter force_escape %} - {% blocktrans with voucher_code=voucher.code %} - Coupon code {{ voucher_code }} applied - {% endblocktrans %} - {% endfilter %} -

- {% csrf_token %} - -
-

- {% endfor %} -
- {% else %} - {# Hide the entire section if a custom BasketView doesn't pass in a voucher form #} - {% if voucher_form %} -
- - {% include 'oscar/basket/partials/add_voucher_form.html' %} -
- {% endif %} - {% endif %} - {% endblock vouchers %} - {% endif %} - -
- {% block order_total %} - {% trans "Total:" as tmsg %}{{ tmsg | force_escape }} - {{ order_total.incl_tax|currency:basket.currency }} - {% endblock %} -
-
-
- -
-
- {# Switch Basket view in between single and bulk purchase items #} - {% if partner_sku %} - - {% endif %} - -
- {% if free_basket %} - - {% trans "Place Order" as tmsg %}{{ tmsg | force_escape }} - - {% else %} - {% for processor in payment_processors %} - - {% endfor %} - {% endif %} -
-
-
- -
-
- - {# Translators: tags will bold the text within. Keep the tags and translate the text within. #} - {% trans "{strong_start}Note:{strong_end} To complete your enrollment, select Checkout or Checkout with PayPal." as tmsg %} - {% interpolate_html tmsg strong_start=''|safe strong_end=''|safe %} -
-
- -
diff --git a/ecommerce/templates/oscar/basket/partials/seat_type.html b/ecommerce/templates/oscar/basket/partials/seat_type.html deleted file mode 100644 index 3b7f699cbec..00000000000 --- a/ecommerce/templates/oscar/basket/partials/seat_type.html +++ /dev/null @@ -1,11 +0,0 @@ -{% load i18n %} - -{% if line_data.seat_type %} -

- {% filter force_escape %} - {% blocktrans with seat_type=line_data.seat_type %} - {{ seat_type }} Certificate - {% endblocktrans %} - {% endfilter %} -

-{% endif %} diff --git a/ecommerce/templates/payment/cybersource.html b/ecommerce/templates/payment/cybersource.html deleted file mode 100644 index e1d00903216..00000000000 --- a/ecommerce/templates/payment/cybersource.html +++ /dev/null @@ -1,23 +0,0 @@ -{% load static %} - - -{# NOTE: Using compress tags here results in the JS not being loaded. #} -{# We have no idea why after multiple hours of investigation. #} - - diff --git a/ecommerce/tests/factories.py b/ecommerce/tests/factories.py index 74e8ca29789..bde78567afa 100644 --- a/ecommerce/tests/factories.py +++ b/ecommerce/tests/factories.py @@ -40,7 +40,8 @@ class Meta: enable_sdn_check = False enable_embargo_check = False enable_partial_program = False - discovery_api_url = 'http://{}.fake/'.format(Faker().domain_name()) + discovery_api_url = f'http://{Faker().domain_name()}.fake/' + payment_microfrontend_url = f'http://{Faker().domain_name()}.fake/' class StockRecordFactory(OscarStockRecordFactory):