From d8b07ebacd2cbf223bff8d6c4656fe32a82ff9a1 Mon Sep 17 00:00:00 2001 From: Sameen Fatima <55431213+sameenfatima78@users.noreply.github.com> Date: Wed, 19 May 2021 18:06:21 +0500 Subject: [PATCH] Revert "ENT-4095: Handle coupon expiration date scenario in LMS (#27539)" (#27675) This reverts commit 9f2a72ad08a002ccf8ccaf14070e293792775a7f. --- common/djangoapps/student/models.py | 88 +++++-------------- .../djangoapps/student/tests/test_refunds.py | 84 +----------------- common/djangoapps/student/views/dashboard.py | 7 -- lms/envs/common.py | 1 - lms/envs/production.py | 3 - lms/static/js/dashboard/legacy.js | 8 +- lms/templates/dashboard.html | 3 +- .../dashboard/_dashboard_course_listing.html | 3 +- .../features/enterprise_support/signals.py | 2 - .../enterprise_support/tests/test_signals.py | 26 ++---- 10 files changed, 32 insertions(+), 193 deletions(-) diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py index 48b05287f5..bde8b1e062 100644 --- a/common/djangoapps/student/models.py +++ b/common/djangoapps/student/models.py @@ -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.""" diff --git a/common/djangoapps/student/tests/test_refunds.py b/common/djangoapps/student/tests/test_refunds.py index 0c3e4ff16f..aa100ef2ad 100644 --- a/common/djangoapps/student/tests/test_refunds.py +++ b/common/djangoapps/student/tests/test_refunds.py @@ -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): """ diff --git a/common/djangoapps/student/views/dashboard.py b/common/djangoapps/student/views/dashboard.py index c6d31c588a..0eead5ab38 100644 --- a/common/djangoapps/student/views/dashboard.py +++ b/common/djangoapps/student/views/dashboard.py @@ -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, diff --git a/lms/envs/common.py b/lms/envs/common.py index 281a5cf189..a4b51690c8 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -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' diff --git a/lms/envs/production.py b/lms/envs/production.py index 4639bbeaaf..2723ea9980 100644 --- a/lms/envs/production.py +++ b/lms/envs/production.py @@ -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 \ diff --git a/lms/static/js/dashboard/legacy.js b/lms/static/js/dashboard/legacy.js index 966dc4c93d..29f7ccb777 100644 --- a/lms/static/js/dashboard/legacy.js +++ b/lms/static/js/dashboard/legacy.js @@ -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(); diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html index b0351a41cb..5369df1551 100644 --- a/lms/templates/dashboard.html +++ b/lms/templates/dashboard.html @@ -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:
diff --git a/lms/templates/dashboard/_dashboard_course_listing.html b/lms/templates/dashboard/_dashboard_course_listing.html index 95a67035f8..1a0e733822 100644 --- a/lms/templates/dashboard/_dashboard_course_listing.html +++ b/lms/templates/dashboard/_dashboard_course_listing.html @@ -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')} diff --git a/openedx/features/enterprise_support/signals.py b/openedx/features/enterprise_support/signals.py index 454141c100..84d321c985 100644 --- a/openedx/features/enterprise_support/signals.py +++ b/openedx/features/enterprise_support/signals.py @@ -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) diff --git a/openedx/features/enterprise_support/tests/test_signals.py b/openedx/features/enterprise_support/tests/test_signals.py index b101ebf0b5..9143e0e355 100644 --- a/openedx/features/enterprise_support/tests/test_signals.py +++ b/openedx/features/enterprise_support/tests/test_signals.py @@ -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