Skip to content

Commit

Permalink
feat: waffle refunds for commerce-coordinator
Browse files Browse the repository at this point in the history
# feat: waffle refunds for commerce-coordinator

Based on approved: #34438

However CLA is invalid now for initial creator so a new PR was required.
  • Loading branch information
grmartin authored Apr 12, 2024
2 parents d98164f + 294c6a7 commit 81e781c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 12 deletions.
4 changes: 2 additions & 2 deletions lms/djangoapps/commerce/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 107 additions & 6 deletions lms/djangoapps/commerce/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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__)

Expand Down Expand Up @@ -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)

Expand All @@ -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
Expand Down
24 changes: 22 additions & 2 deletions lms/djangoapps/commerce/waffle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
7 changes: 5 additions & 2 deletions lms/envs/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down

0 comments on commit 81e781c

Please sign in to comment.