This reverts commit 9f2a72ad08.
This commit is contained in:
@@ -43,7 +43,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.utils.translation import ugettext_noop
|
||||
from django_countries.fields import CountryField
|
||||
from edx_django_utils.cache import RequestCache, TieredCache, get_cache_key
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from edx_django_utils import monitoring
|
||||
from edx_rest_api_client.exceptions import SlumberBaseException
|
||||
from eventtracking import tracker
|
||||
@@ -1890,7 +1890,8 @@ class CourseEnrollment(models.Model):
|
||||
def refund_cutoff_date(self):
|
||||
""" Calculate and return the refund window end date. """
|
||||
# NOTE: This is here to avoid circular references
|
||||
from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, ECOMMERCE_DATE_FORMAT
|
||||
|
||||
date_placed = self.get_order_attribute_value('date_placed')
|
||||
|
||||
if not date_placed:
|
||||
@@ -1898,67 +1899,20 @@ class CourseEnrollment(models.Model):
|
||||
if not order_number:
|
||||
return None
|
||||
|
||||
date_placed = self.get_order_attribute_from_ecommerce('date_placed')
|
||||
if not date_placed:
|
||||
return None
|
||||
|
||||
# also save the attribute so that we don't need to call ecommerce again.
|
||||
username = self.user.username
|
||||
enrollment_attributes = get_enrollment_attributes(username, str(self.course_id))
|
||||
enrollment_attributes.append(
|
||||
{
|
||||
"namespace": "order",
|
||||
"name": "date_placed",
|
||||
"value": date_placed,
|
||||
}
|
||||
)
|
||||
set_enrollment_attributes(username, str(self.course_id), enrollment_attributes)
|
||||
|
||||
refund_window_start_date = max(
|
||||
datetime.strptime(date_placed, ECOMMERCE_DATE_FORMAT),
|
||||
self.course_overview.start.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
return refund_window_start_date.replace(tzinfo=UTC) + EnrollmentRefundConfiguration.current().refund_window
|
||||
|
||||
def is_order_voucher_refundable(self):
|
||||
""" Checks if the coupon batch expiration date has passed to determine whether order voucher is refundable. """
|
||||
from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT
|
||||
vouchers = self.get_order_attribute_from_ecommerce('vouchers')
|
||||
if not vouchers:
|
||||
return False
|
||||
voucher_end_datetime_str = vouchers[0]['end_datetime']
|
||||
voucher_expiration_date = datetime.strptime(voucher_end_datetime_str, ECOMMERCE_DATE_FORMAT).replace(tzinfo=UTC)
|
||||
return datetime.now(UTC) < voucher_expiration_date
|
||||
|
||||
def get_order_attribute_from_ecommerce(self, attribute_name):
|
||||
"""
|
||||
Fetches the order details from ecommerce to return the value of the attribute passed as argument.
|
||||
|
||||
Arguments:
|
||||
attribute_name (str): The name of the attribute that you want to fetch from response e:g 'number' or
|
||||
'vouchers', etc.
|
||||
|
||||
Returns:
|
||||
(str | array | None): Returns the attribute value if it exists, returns None if the order doesn't exist or
|
||||
attribute doesn't exist in the response.
|
||||
"""
|
||||
|
||||
# NOTE: This is here to avoid circular references
|
||||
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client
|
||||
order_number = self.get_order_attribute_value('order_number')
|
||||
if not order_number:
|
||||
return None
|
||||
|
||||
# check if response is already cached
|
||||
cache_key = get_cache_key(user_id=self.user.id, order_number=order_number)
|
||||
cached_response = TieredCache.get_cached_response(cache_key)
|
||||
if cached_response.is_found:
|
||||
order = cached_response.value
|
||||
else:
|
||||
try:
|
||||
# response is not cached, so make a call to ecommerce to fetch order details
|
||||
order = ecommerce_api_client(self.user).orders(order_number).get()
|
||||
date_placed = order['date_placed']
|
||||
# also save the attribute so that we don't need to call ecommerce again.
|
||||
username = self.user.username
|
||||
enrollment_attributes = get_enrollment_attributes(username, str(self.course_id))
|
||||
enrollment_attributes.append(
|
||||
{
|
||||
"namespace": "order",
|
||||
"name": "date_placed",
|
||||
"value": date_placed,
|
||||
}
|
||||
)
|
||||
set_enrollment_attributes(username, str(self.course_id), enrollment_attributes)
|
||||
except HttpClientError:
|
||||
log.warning(
|
||||
"Encountered HttpClientError while getting order details from ecommerce. "
|
||||
@@ -1977,12 +1931,12 @@ class CourseEnrollment(models.Model):
|
||||
"Order={number} and user {user}".format(number=order_number, user=self.user.id))
|
||||
return None
|
||||
|
||||
cache_time_out = getattr(settings, 'ECOMMERCE_ORDERS_API_CACHE_TIMEOUT', 3600)
|
||||
TieredCache.set_all_tiers(cache_key, order, cache_time_out)
|
||||
try:
|
||||
return order[attribute_name]
|
||||
except KeyError:
|
||||
return None
|
||||
refund_window_start_date = max(
|
||||
datetime.strptime(date_placed, ECOMMERCE_DATE_FORMAT),
|
||||
self.course_overview.start.replace(tzinfo=None)
|
||||
)
|
||||
|
||||
return refund_window_start_date.replace(tzinfo=UTC) + EnrollmentRefundConfiguration.current().refund_window
|
||||
|
||||
def get_order_attribute_value(self, attr_name):
|
||||
""" Get and return course enrollment order attribute's value."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Tests for enrollment refund capabilities.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
import logging
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
@@ -17,7 +17,6 @@ from django.conf import settings
|
||||
from django.test.client import Client
|
||||
from django.test.utils import override_settings
|
||||
from django.urls import reverse
|
||||
from edx_django_utils.cache import TieredCache, get_cache_key
|
||||
|
||||
# These imports refer to lms djangoapps.
|
||||
# Their testcases are only run under lms.
|
||||
@@ -166,91 +165,10 @@ class RefundableTest(SharedModuleStoreTestCase):
|
||||
|
||||
assert expected_date_placed_attr in CourseEnrollmentAttribute.get_enrollment_attributes(self.enrollment)
|
||||
|
||||
@ddt.data(
|
||||
(datetime.now(pytz.UTC) + timedelta(days=1), True),
|
||||
(datetime.now(pytz.UTC) - timedelta(days=1), False),
|
||||
(datetime.now(pytz.UTC) - timedelta(minutes=5), False),
|
||||
)
|
||||
@ddt.unpack
|
||||
@httpretty.activate
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL)
|
||||
def test_is_order_voucher_refundable(self, voucher_expiration_date, expected):
|
||||
"""
|
||||
Assert that the correct value is returned based on voucher expiration date.
|
||||
"""
|
||||
voucher_expiration_date_str = voucher_expiration_date.strftime(ECOMMERCE_DATE_FORMAT)
|
||||
response = json.dumps({"vouchers": [{"end_datetime": voucher_expiration_date_str}]})
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/',
|
||||
status=200, body=response,
|
||||
adding_headers={'Content-Type': JSON}
|
||||
)
|
||||
|
||||
self.enrollment.attributes.create(
|
||||
enrollment=self.enrollment,
|
||||
namespace='order',
|
||||
name='order_number',
|
||||
value=self.ORDER_NUMBER
|
||||
)
|
||||
assert self.enrollment.is_order_voucher_refundable() == expected
|
||||
|
||||
def test_refund_cutoff_date_no_attributes(self):
|
||||
""" Assert that the None is returned when no order number attribute is found."""
|
||||
assert self.enrollment.refund_cutoff_date() is None
|
||||
|
||||
@httpretty.activate
|
||||
@override_settings(ECOMMERCE_API_URL=TEST_API_URL)
|
||||
def test_is_order_voucher_refundable_no_attributes(self, ):
|
||||
""" Assert that False is returned when no order number or vouchers attribute is found in response."""
|
||||
# no order number attribute
|
||||
assert self.enrollment.is_order_voucher_refundable() is False
|
||||
|
||||
# no voucher information in orders api response
|
||||
response = json.dumps({"vouchers": []})
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/',
|
||||
status=200, body=response,
|
||||
adding_headers={'Content-Type': JSON}
|
||||
)
|
||||
|
||||
self.enrollment.attributes.create(
|
||||
enrollment=self.enrollment,
|
||||
namespace='order',
|
||||
name='order_number',
|
||||
value=self.ORDER_NUMBER
|
||||
)
|
||||
assert self.enrollment.is_order_voucher_refundable() is False
|
||||
|
||||
response = json.dumps({"vouchers": None})
|
||||
httpretty.register_uri(
|
||||
httpretty.GET,
|
||||
f'{TEST_API_URL}/orders/{self.ORDER_NUMBER}/',
|
||||
status=200, body=response,
|
||||
adding_headers={'Content-Type': JSON}
|
||||
)
|
||||
assert self.enrollment.is_order_voucher_refundable() is False
|
||||
|
||||
@patch('openedx.core.djangoapps.commerce.utils.ecommerce_api_client')
|
||||
def test_get_order_attribute_from_ecommerce(self, mock_ecommerce_api_client):
|
||||
"""
|
||||
Assert that the get_order_attribute_from_ecommerce method returns order details if it's already cached,
|
||||
without calling ecommerce.
|
||||
"""
|
||||
order_details = {"number": self.ORDER_NUMBER, "vouchers": [{"end_datetime": '2025-09-25T00:00:00Z'}]}
|
||||
cache_key = get_cache_key(user_id=self.user.id, order_number=self.ORDER_NUMBER)
|
||||
TieredCache.set_all_tiers(cache_key, order_details, 60)
|
||||
|
||||
self.enrollment.attributes.create(
|
||||
enrollment=self.enrollment,
|
||||
namespace='order',
|
||||
name='order_number',
|
||||
value=self.ORDER_NUMBER
|
||||
)
|
||||
assert self.enrollment.get_order_attribute_from_ecommerce("vouchers") == order_details["vouchers"]
|
||||
mock_ecommerce_api_client.assert_not_called()
|
||||
|
||||
@patch('openedx.core.djangoapps.commerce.utils.ecommerce_api_client')
|
||||
def test_refund_cutoff_date_with_date_placed_attr(self, mock_ecommerce_api_client):
|
||||
"""
|
||||
|
||||
@@ -706,12 +706,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
|
||||
if enrollment.is_paid_course()
|
||||
)
|
||||
|
||||
# Checks if a course enrollment redeemed using a voucher is refundable
|
||||
enrolled_courses_voucher_refundable = frozenset(
|
||||
enrollment.course_id for enrollment in course_enrollments
|
||||
if enrollment.is_order_voucher_refundable()
|
||||
)
|
||||
|
||||
# If there are *any* denied reverifications that have not been toggled off,
|
||||
# we'll display the banner
|
||||
denied_banner = any(item.display for item in reverifications["denied"])
|
||||
@@ -781,7 +775,6 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
|
||||
'logout_url': reverse('logout'),
|
||||
'platform_name': platform_name,
|
||||
'enrolled_courses_either_paid': enrolled_courses_either_paid,
|
||||
'enrolled_courses_voucher_refundable': enrolled_courses_voucher_refundable,
|
||||
'provider_states': [],
|
||||
'courses_requirements_not_met': courses_requirements_not_met,
|
||||
'nav_hidden': True,
|
||||
|
||||
@@ -3936,7 +3936,6 @@ SOCIAL_PLATFORMS = {
|
||||
ECOMMERCE_PUBLIC_URL_ROOT = 'http://localhost:8002'
|
||||
ECOMMERCE_API_URL = 'http://localhost:8002/api/v2'
|
||||
ECOMMERCE_API_TIMEOUT = 5
|
||||
ECOMMERCE_ORDERS_API_CACHE_TIMEOUT = 3600
|
||||
ECOMMERCE_SERVICE_WORKER_USERNAME = 'ecommerce_worker'
|
||||
ECOMMERCE_API_SIGNING_KEY = 'SET-ME-PLEASE'
|
||||
|
||||
|
||||
@@ -722,9 +722,6 @@ DEFAULT_MOBILE_AVAILABLE = ENV_TOKENS.get(
|
||||
# Enrollment API Cache Timeout
|
||||
ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT = ENV_TOKENS.get('ENROLLMENT_COURSE_DETAILS_CACHE_TIMEOUT', 60)
|
||||
|
||||
# Ecommerce Orders API Cache Timeout
|
||||
ECOMMERCE_ORDERS_API_CACHE_TIMEOUT = ENV_TOKENS.get('ECOMMERCE_ORDERS_API_CACHE_TIMEOUT', 3600)
|
||||
|
||||
if FEATURES.get('ENABLE_COURSEWARE_SEARCH') or \
|
||||
FEATURES.get('ENABLE_DASHBOARD_SEARCH') or \
|
||||
FEATURES.get('ENABLE_COURSE_DISCOVERY') or \
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
return properties;
|
||||
}
|
||||
|
||||
function setDialogAttributes(isPaidCourse, isCourseVoucherRefundable, certNameLong,
|
||||
function setDialogAttributes(isPaidCourse, certNameLong,
|
||||
courseNumber, courseName, enrollmentMode, showRefundOption, courseKey) {
|
||||
var diagAttr = {};
|
||||
|
||||
@@ -99,9 +99,6 @@
|
||||
} else if (enrollmentMode !== 'verified') {
|
||||
diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from {courseName} ' +
|
||||
'({courseNumber})?');
|
||||
} else if (showRefundOption && !isCourseVoucherRefundable) {
|
||||
diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the verified ' +
|
||||
'{certNameLong} track of {courseName} ({courseNumber})?');
|
||||
} else if (showRefundOption) {
|
||||
diagAttr['data-track-info'] = gettext('Are you sure you want to unenroll from the verified ' +
|
||||
'{certNameLong} track of {courseName} ({courseNumber})?');
|
||||
@@ -137,7 +134,6 @@
|
||||
});
|
||||
$('.action-unenroll').click(function(event) {
|
||||
var isPaidCourse = $(event.target).data('course-is-paid-course') === 'True',
|
||||
isCourseVoucherRefundable = $(event.target).data('is-course-voucher-refundable') === 'True',
|
||||
certNameLong = $(event.target).data('course-cert-name-long'),
|
||||
enrollmentMode = $(event.target).data('course-enrollment-mode'),
|
||||
courseNumber = $(event.target).data('course-number'),
|
||||
@@ -153,7 +149,7 @@
|
||||
});
|
||||
request.success(function(data, textStatus, xhr) {
|
||||
if (xhr.status === 200) {
|
||||
dialogMessageAttr = setDialogAttributes(isPaidCourse, isCourseVoucherRefundable, certNameLong,
|
||||
dialogMessageAttr = setDialogAttributes(isPaidCourse, certNameLong,
|
||||
courseNumber, courseName, enrollmentMode, data.course_refundable_status, courseKey);
|
||||
|
||||
$('#track-info').empty();
|
||||
|
||||
@@ -210,14 +210,13 @@ from common.djangoapps.student.models import CourseEnrollment
|
||||
credit_status = credit_statuses.get(session_id)
|
||||
course_mode_info = all_course_modes.get(session_id)
|
||||
is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid)
|
||||
is_course_voucher_refundable = (session_id in enrolled_courses_voucher_refundable)
|
||||
course_verification_status = verification_status_by_course.get(session_id, {})
|
||||
course_requirements = courses_requirements_not_met.get(session_id)
|
||||
related_programs = inverted_programs.get(six.text_type(entitlement.course_uuid if is_unfulfilled_entitlement else session_id))
|
||||
show_consent_link = (session_id in consent_required_courses)
|
||||
resume_button_url = resume_button_urls[dashboard_index]
|
||||
%>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_voucher_refundable=is_course_voucher_refundable, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' />
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, entitlement_expiration_date=entitlement_expiration_date, entitlement_expired_at=entitlement_expired_at, show_courseware_link=show_courseware_link, cert_status=cert_status, can_refund_entitlement=can_refund_entitlement, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name, resume_button_url=resume_button_url, partner_managed_enrollment=partner_managed_enrollment' />
|
||||
% endfor
|
||||
% if show_load_all_courses_link:
|
||||
<br/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_refund_entitlement, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_voucher_refundable, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name, resume_button_url, partner_managed_enrollment" expression_filter="h"/>
|
||||
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, entitlement_expiration_date, entitlement_expired_at, show_courseware_link, cert_status, can_refund_entitlement, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name, resume_button_url, partner_managed_enrollment" expression_filter="h"/>
|
||||
|
||||
<%!
|
||||
import datetime
|
||||
@@ -266,7 +266,6 @@ from lms.djangoapps.experiments.utils import UPSELL_TRACKING_FLAG
|
||||
data-dashboard-index="${dashboard_index}"
|
||||
data-course-refund-url="${course_refund_url}"
|
||||
data-course-is-paid-course="${is_paid_course}"
|
||||
data-is-course-voucher-refundable="${is_course_voucher_refundable}"
|
||||
data-course-cert-name-long="${cert_name_long}"
|
||||
data-course-enrollment-mode="${enrollment.mode}">
|
||||
${_('Unenroll')}
|
||||
|
||||
@@ -97,8 +97,6 @@ def refund_order_voucher(sender, course_enrollment, skip_refund=False, **kwargs)
|
||||
return
|
||||
if not course_enrollment.refundable():
|
||||
return
|
||||
if not course_enrollment.is_order_voucher_refundable():
|
||||
return
|
||||
if not EnterpriseCourseEnrollment.objects.filter(
|
||||
enterprise_customer_user__user_id=course_enrollment.user_id,
|
||||
course_id=str(course_enrollment.course.id)
|
||||
|
||||
@@ -133,42 +133,28 @@ class EnterpriseSupportSignals(SharedModuleStoreTestCase):
|
||||
|
||||
return enrollment
|
||||
|
||||
@patch('common.djangoapps.student.models.CourseEnrollment.is_order_voucher_refundable')
|
||||
@ddt.data(
|
||||
(True, True, 2, True, False), # test if skip_refund
|
||||
(False, True, 20, True, False), # test refundable time passed
|
||||
(False, False, 2, True, False), # test not enterprise enrollment
|
||||
(False, True, 2, False, False), # test order voucher expiration date has already passed
|
||||
(False, True, 2, True, True), # success: no skip_refund, is enterprise enrollment, coupon voucher is refundable
|
||||
# and is still in refundable window.
|
||||
(True, True, 2, False), # test if skip_refund
|
||||
(False, True, 20, False), # test refundable time passed
|
||||
(False, False, 2, False), # test not enterprise enrollment
|
||||
(False, True, 2, True), # success: no skip_refund, is enterprise enrollment and still in refundable window.
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_refund_order_voucher(
|
||||
self,
|
||||
skip_refund,
|
||||
enterprise_enrollment_exists,
|
||||
no_of_days_placed,
|
||||
order_voucher_refundable,
|
||||
api_called,
|
||||
mock_is_order_voucher_refundable
|
||||
):
|
||||
def test_refund_order_voucher(self, skip_refund, enterprise_enrollment_exists, no_of_days_placed, api_called):
|
||||
"""Test refund_order_voucher signal"""
|
||||
mock_is_order_voucher_refundable.return_value = order_voucher_refundable
|
||||
enrollment = self._create_enrollment_to_refund(no_of_days_placed, enterprise_enrollment_exists)
|
||||
with patch('openedx.features.enterprise_support.signals.ecommerce_api_client') as mock_ecommerce_api_client:
|
||||
enrollment.update_enrollment(is_active=False, skip_refund=skip_refund)
|
||||
assert mock_ecommerce_api_client.called == api_called
|
||||
|
||||
@patch('common.djangoapps.student.models.CourseEnrollment.is_order_voucher_refundable')
|
||||
@ddt.data(
|
||||
(HttpClientError, 'INFO'),
|
||||
(HttpServerError, 'ERROR'),
|
||||
(Exception, 'ERROR'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_refund_order_voucher_with_client_errors(self, mock_error, log_level, mock_is_order_voucher_refundable):
|
||||
def test_refund_order_voucher_with_client_errors(self, mock_error, log_level):
|
||||
"""Test refund_order_voucher signal client_error"""
|
||||
mock_is_order_voucher_refundable.return_value = True
|
||||
enrollment = self._create_enrollment_to_refund()
|
||||
with patch('openedx.features.enterprise_support.signals.ecommerce_api_client') as mock_ecommerce_api_client:
|
||||
client_instance = mock_ecommerce_api_client.return_value
|
||||
|
||||
Reference in New Issue
Block a user