Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: waffle refunds for commerce-coordinator #34438

Closed
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 108 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
pshiu marked this conversation as resolved.
Show resolved Hide resolved
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,111 @@ 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)
)
return False
grmartin marked this conversation as resolved.
Show resolved Hide resolved

# 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 succesful. False if auto-enroll is not applicable.
grmartin marked this conversation as resolved.
Show resolved Hide resolved
"""
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 @@ -4254,8 +4254,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'
pshiu marked this conversation as resolved.
Show resolved Hide resolved
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
Loading