diff --git a/common/djangoapps/course_modes/tests/test_views.py b/common/djangoapps/course_modes/tests/test_views.py index 522540944726..997fb678fef9 100644 --- a/common/djangoapps/course_modes/tests/test_views.py +++ b/common/djangoapps/course_modes/tests/test_views.py @@ -149,6 +149,50 @@ def test_no_id_redirect_otto(self): self.assertRedirects(response, '/test_basket/add/?sku=TEST', fetch_redirect_response=False) ecomm_test_utils.update_commerce_config(enabled=False) + def test_verified_mode_response_contains_course_run_key(self): + # Create only the verified mode and enroll the user + CourseModeFactory.create( + mode_slug='verified', + course_id=self.course_that_started.id, + min_price=149, + sku="dummy" + ) + CourseEnrollmentFactory( + is_active=True, + course_id=self.course_that_started.id, + user=self.user + ) + + # Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out. + with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True): + with patch(GATING_METHOD_NAME, return_value=True): + with patch(CDL_METHOD_NAME, return_value=True): + with patch("common.djangoapps.course_modes.views.EcommerceService.is_enabled", return_value=True): + url = reverse('course_modes_choose', args=[str(self.course_that_started.id)]) + response = self.client.get(url) + self.assertContains(response, "&course_run_key=") + self.assertContains(response, self.course_that_started.id) + + def test_response_without_verified_sku_does_not_contain_course_run_key(self): + CourseModeFactory.create( + mode_slug='verified', + course_id=self.course_that_started.id, + ) + CourseEnrollmentFactory( + is_active=True, + course_id=self.course_that_started.id, + user=self.user + ) + + # Value Prop TODO (REV-2378): remove waffle flag from tests once the new Track Selection template is rolled out. + with override_waffle_flag(VALUE_PROP_TRACK_SELECTION_FLAG, active=True): + with patch(GATING_METHOD_NAME, return_value=True): + with patch(CDL_METHOD_NAME, return_value=True): + with patch("common.djangoapps.course_modes.views.EcommerceService.is_enabled", return_value=True): + url = reverse('course_modes_choose', args=[str(self.course_that_started.id)]) + response = self.client.get(url) + self.assertNotContains(response, "&course_run_key=") + @httpretty.activate @ddt.data( '', diff --git a/common/djangoapps/course_modes/views.py b/common/djangoapps/course_modes/views.py index 7322483b8d8d..269961424d04 100644 --- a/common/djangoapps/course_modes/views.py +++ b/common/djangoapps/course_modes/views.py @@ -191,6 +191,7 @@ def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable= "content_gating_enabled": gated_content, "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment(request.user, course), "search_courses_url": urljoin(settings.MKTG_URLS.get('ROOT'), '/search?tab=course'), + "course_run_key": course_id, } context.update( get_experiment_user_metadata_context( @@ -236,7 +237,7 @@ def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable= if verified_mode.sku: context["use_ecommerce_payment_flow"] = ecommerce_service.is_enabled(request.user) - context["ecommerce_payment_page"] = ecommerce_service.payment_page_url() + context["ecommerce_payment_page"] = ecommerce_service.get_add_to_basket_url() context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index 2b6a2d2f740a..96c5c1e2c35c 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -11,6 +11,7 @@ from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings +from edx_toggles.toggles.testutils import override_waffle_flag from opaque_keys.edx.locator import CourseLocator from waffle.testutils import override_switch @@ -20,9 +21,11 @@ from common.djangoapps.student.tests.factories import TEST_PASSWORD, UserFactory from lms.djangoapps.commerce.models import CommerceConfiguration from lms.djangoapps.commerce.utils import EcommerceService, refund_entitlement, refund_seat +from lms.djangoapps.commerce.waffle import ENABLE_TRANSITION_TO_COORDINATOR_CHECKOUT from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.lib.log_utils import audit_log -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore.tests.django_utils import \ + ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order # Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection @@ -183,6 +186,27 @@ def test_get_checkout_page_url_with_enterprise_catalog_uuid(self, skus, enterpri assert url == expected_url + @override_settings(COMMERCE_COORDINATOR_URL_ROOT='http://coordinator_url') + @override_settings(ECOMMERCE_PUBLIC_URL_ROOT='http://ecommerce_url') + @ddt.data( + {'coordinator_flag_active': True}, + {'coordinator_flag_active': False} + ) + @ddt.unpack + def test_get_add_to_basket_url(self, coordinator_flag_active): + with override_waffle_flag(ENABLE_TRANSITION_TO_COORDINATOR_CHECKOUT, active=coordinator_flag_active): + + ecommerce_service = EcommerceService() + result = ecommerce_service.get_add_to_basket_url() + + if coordinator_flag_active: + expected_url = 'http://coordinator_url/lms/redirect/' + else: + expected_url = 'http://ecommerce_url/test_basket/add/' + + self.assertIsNotNone(result) + self.assertEqual(result, expected_url) + @ddt.ddt @skip_unless_lms diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 7c73aae883a5..c5cb4315f635 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -14,6 +14,7 @@ from opaque_keys.edx.keys import CourseKey from common.djangoapps.course_modes.models import CourseMode +from lms.djangoapps.commerce.waffle import should_redirect_to_commerce_coordinator_checkout from openedx.core.djangoapps.commerce.utils import ( get_ecommerce_api_base_url, get_ecommerce_api_client, @@ -43,6 +44,7 @@ def is_account_activation_requirement_disabled(): class EcommerceService: """ Helper class for ecommerce service integration. """ + def __init__(self): self.config = CommerceConfiguration.current() @@ -103,6 +105,16 @@ def payment_page_url(self): """ return self.get_absolute_ecommerce_url(self.config.basket_checkout_page) + def get_add_to_basket_url(self): + """ Return the URL for the payment page based on the waffle switch. + + Example: + http://localhost/enabled_service_api_path + """ + if should_redirect_to_commerce_coordinator_checkout(): + return urljoin(settings.COMMERCE_COORDINATOR_URL_ROOT, settings.COORDINATOR_CHECKOUT_REDIRECT_PATH) + return self.payment_page_url() + def get_checkout_page_url(self, *skus, **kwargs): """ Construct the URL to the ecommerce checkout page and include products. diff --git a/lms/djangoapps/commerce/waffle.py b/lms/djangoapps/commerce/waffle.py new file mode 100644 index 000000000000..e1ee6f26456d --- /dev/null +++ b/lms/djangoapps/commerce/waffle.py @@ -0,0 +1,29 @@ +""" +Configuration for features of Commerce App +""" +from edx_toggles.toggles import WaffleFlag + +# Namespace for Commerce waffle flags. +WAFFLE_FLAG_NAMESPACE = "commerce" + +# .. toggle_name: commerce.transition_to_coordinator.checkout +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Allows to redirect checkout to Commerce Coordinator API +# .. toggle_use_cases: temporary +# .. toggle_creation_date: 2023-11-22 +# .. toggle_target_removal_date: TBA +# .. toggle_tickets: SONIC-99 +# .. toggle_status: supported +ENABLE_TRANSITION_TO_COORDINATOR_CHECKOUT = WaffleFlag( + f"{WAFFLE_FLAG_NAMESPACE}.transition_to_coordinator.checkout", + __name__, +) + + +def should_redirect_to_commerce_coordinator_checkout(): + """ + Redirect learners to Commerce coordinator checkout. + + """ + return ENABLE_TRANSITION_TO_COORDINATOR_CHECKOUT.is_enabled() diff --git a/lms/envs/common.py b/lms/envs/common.py index f738f57e6381..3bc762636b98 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -4225,6 +4225,10 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring ECOMMERCE_SERVICE_WORKER_USERNAME = 'ecommerce_worker' ECOMMERCE_API_SIGNING_KEY = 'SET-ME-PLEASE' +# E-Commerce Commerce Coordinator Configuration +COMMERCE_COORDINATOR_URL_ROOT = 'http://localhost:8000' +COORDINATOR_CHECKOUT_REDIRECT_PATH = '/lms/redirect/' + # Exam Service EXAMS_SERVICE_URL = 'http://localhost:18740/api/v1' diff --git a/lms/templates/course_modes/choose.html b/lms/templates/course_modes/choose.html index 7dc2a00b412a..78b6dcc26ebc 100644 --- a/lms/templates/course_modes/choose.html +++ b/lms/templates/course_modes/choose.html @@ -45,7 +45,8 @@ $('button[name=verified_mode]').click(function(e){ e.preventDefault(); window.location.href = '${ecommerce_payment_page | n, js_escaped_string}?sku=' + - encodeURIComponent('${sku | n, js_escaped_string}'); + encodeURIComponent('${sku | n, js_escaped_string}') + + '&course_run_key=' + encodeURIComponent('${course_run_key | n, js_escaped_string}'); }); % endif }); diff --git a/lms/templates/course_modes/track_selection.html b/lms/templates/course_modes/track_selection.html index d1afba0d5c3d..557bd18844ca 100644 --- a/lms/templates/course_modes/track_selection.html +++ b/lms/templates/course_modes/track_selection.html @@ -21,7 +21,8 @@ $('button[name=verified_mode]').click(function(e){ e.preventDefault(); window.location.href = '${ecommerce_payment_page | n, js_escaped_string}?sku=' + - encodeURIComponent('${sku | n, js_escaped_string}'); + encodeURIComponent('${sku | n, js_escaped_string}') + + '&course_run_key=' + encodeURIComponent('${course_run_key | n, js_escaped_string}'); }); }); % endif