diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index 96c5c1e2c35c..6c793433dff6 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -200,12 +200,12 @@ def test_get_add_to_basket_url(self, coordinator_flag_active): result = ecommerce_service.get_add_to_basket_url() if coordinator_flag_active: - expected_url = 'http://coordinator_url/lms/redirect/' + expected_url = 'http://coordinator_url/lms/payment_page_redirect/' else: expected_url = 'http://ecommerce_url/test_basket/add/' self.assertIsNotNone(result) - self.assertEqual(result, expected_url) + self.assertEqual(expected_url, result) @ddt.ddt diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index c5cb4315f635..a37cd2ab36b2 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -11,10 +11,9 @@ from django.contrib.auth import get_user_model from django.urls import reverse from django.utils.translation import gettext as _ -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 common.djangoapps.student.models import CourseEnrollmentAttribute from openedx.core.djangoapps.commerce.utils import ( get_ecommerce_api_base_url, get_ecommerce_api_client, @@ -24,6 +23,10 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers from .models import CommerceConfiguration +from .waffle import ( # lint-amnesty, pylint: disable=invalid-django-waffle-import + should_redirect_to_commerce_coordinator_checkout, + should_redirect_to_commerce_coordinator_refunds, +) log = logging.getLogger(__name__) @@ -252,6 +255,10 @@ def refund_seat(course_enrollment, change_mode=False): course_key_str = str(course_enrollment.course_id) enrollee = course_enrollment.user + if should_redirect_to_commerce_coordinator_refunds(): + if _refund_in_commerce_coordinator(course_enrollment, change_mode): + return + service_user = User.objects.get(username=settings.ECOMMERCE_SERVICE_WORKER_USERNAME) api_client = get_ecommerce_api_client(service_user) @@ -274,16 +281,110 @@ def refund_seat(course_enrollment, change_mode=False): mode=course_enrollment.mode, user=enrollee, ) - if change_mode and CourseMode.can_auto_enroll(course_id=CourseKey.from_string(course_key_str)): - course_enrollment.update_enrollment(mode=CourseMode.auto_enroll_mode(course_id=course_key_str), - is_active=False, skip_refund=True) - course_enrollment.save() + if change_mode: + _auto_enroll(course_enrollment) else: log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str) return refund_ids +def _refund_in_commerce_coordinator(course_enrollment, change_mode): + """ + Helper function to perform refund in Commerce Coordinator. + + Parameters: + course_enrollment (CourseEnrollment): the enrollment to refund. + change_mode (bool): whether the enrollment should be auto-enrolled into + the default course mode after refund. + + Returns: + bool: True if refund was performed. False if refund is not applicable + to Commerce Coordinator. + """ + enrollment_source_system = course_enrollment.get_order_attribute_value("source_system") + course_key_str = str(course_enrollment.course_id) + + # Commerce Coordinator enrollments will have an orders.source_system enrollment attribute. + # Redirect to Coordinator only if the source_system is safelisted as Coordinator's in settings. + + if enrollment_source_system and enrollment_source_system in settings.COMMERCE_COORDINATOR_REFUND_SOURCE_SYSTEMS: + log.info('Redirecting refund to Commerce Coordinator for user [%s], course [%s]...', + course_enrollment.user_id, course_key_str) + + # Re-use Ecommerce API client factory to build an API client for Commerce Coordinator... + service_user = get_user_model().objects.get( + username=settings.COMMERCE_COORDINATOR_SERVICE_WORKER_USERNAME + ) + api_client = get_ecommerce_api_client(service_user) + refunds_url = urljoin( + settings.COMMERCE_COORDINATOR_URL_ROOT, + settings.COMMERCE_COORDINATOR_REFUND_PATH + ) + + # Build request, raising exception if Coordinator returns non-200. + enrollment_attributes = CourseEnrollmentAttribute.get_enrollment_attributes(course_enrollment) + + try: + api_client.post( + refunds_url, + json={ + 'course_id': course_key_str, + 'username': course_enrollment.username, + 'enrollment_attributes': enrollment_attributes + } + ).raise_for_status() + + except Exception as exc: # pylint: disable=broad-except + # Catch any possible exceptions from the Commerce Coordinator service to ensure we fail gracefully + log.exception( + "Unexpected exception while attempting to refund user in Coordinator [%s], " + "course key [%s] message: [%s]", + course_enrollment.username, + course_key_str, + str(exc) + ) + + # Refund was successfully sent to Commerce Coordinator + log.info('Refund successfully sent to Commerce Coordinator for user [%s], course [%s].', + course_enrollment.user_id, course_key_str) + if change_mode: + _auto_enroll(course_enrollment) + return True + else: + # Refund was not meant to be sent to Commerce Coordinator + log.info('Continuing refund without Commerce Coordinator redirect for user [%s], course [%s]...', + course_enrollment.user_id, course_key_str) + return False + + +def _auto_enroll(course_enrollment): + """ + Helper method to update an enrollment to a default course mode. + + Arguments: + course_enrollment (CourseEnrollment): The course_enrollment to update. + + Returns: + bool: True if auto-enroll is successful. False if auto-enroll is not applicable. + """ + enrollment_course_id = course_enrollment.course_id + + if CourseMode.can_auto_enroll(course_id=enrollment_course_id): + auto_enroll_mode = CourseMode.auto_enroll_mode(course_id=enrollment_course_id) + course_enrollment.update_enrollment(mode=auto_enroll_mode, is_active=False, skip_refund=True) + course_enrollment.save() + log.info( + 'Auto-enrolled user [%s], course [%s] in mode [%s].', + course_enrollment.user_id, + enrollment_course_id, + auto_enroll_mode + ) + return True + else: + return False + + def _process_refund(refund_ids, api_client, mode, user, always_notify=False): """ Helper method to process a refund for a given course_product. This method assumes that the User has already diff --git a/lms/djangoapps/commerce/waffle.py b/lms/djangoapps/commerce/waffle.py index e1ee6f26456d..a36586a52d9b 100644 --- a/lms/djangoapps/commerce/waffle.py +++ b/lms/djangoapps/commerce/waffle.py @@ -20,10 +20,30 @@ __name__, ) +# .. toggle_name: commerce.transition_to_coordinator.refund +# .. toggle_implementation: WaffleFlag +# .. toggle_default: False +# .. toggle_description: Allows to redirect refunds to Commerce Coordinator API +# .. toggle_use_cases: temporary +# .. toggle_creation_date: 2024-03-26 +# .. toggle_target_removal_date: TBA +# .. toggle_tickets: SONIC-382 +# .. toggle_status: supported +ENABLE_TRANSITION_TO_COORDINATOR_REFUNDS = WaffleFlag( + f"{WAFFLE_FLAG_NAMESPACE}.transition_to_coordinator.refunds", + __name__, +) + def should_redirect_to_commerce_coordinator_checkout(): """ - Redirect learners to Commerce coordinator checkout. - + Redirect learners to Commerce Coordinator checkout. """ return ENABLE_TRANSITION_TO_COORDINATOR_CHECKOUT.is_enabled() + + +def should_redirect_to_commerce_coordinator_refunds(): + """ + Redirect learners to Commerce Coordinator refunds. + """ + return ENABLE_TRANSITION_TO_COORDINATOR_REFUNDS.is_enabled() diff --git a/lms/envs/common.py b/lms/envs/common.py index c45679e4b9bb..27e689035204 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -4263,8 +4263,11 @@ def _make_locale_paths(settings): # pylint: disable=missing-function-docstring 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/' +COMMERCE_COORDINATOR_URL_ROOT = 'http://localhost:8140' +COMMERCE_COORDINATOR_REFUND_PATH = '/lms/refund/' +COMMERCE_COORDINATOR_REFUND_SOURCE_SYSTEMS = ('SET-ME-PLEASE',) +COMMERCE_COORDINATOR_SERVICE_WORKER_USERNAME = 'commerce_coordinator_worker' +COORDINATOR_CHECKOUT_REDIRECT_PATH = '/lms/payment_page_redirect/' # Exam Service EXAMS_SERVICE_URL = 'http://localhost:18740/api/v1'