diff --git a/lms/djangoapps/commerce/signals.py b/lms/djangoapps/commerce/signals.py index bf909b84094d4b148df137497b44ea02809c4a40..54e31340532c83e1c7f44fbab5d2c07b6af5c957 100644 --- a/lms/djangoapps/commerce/signals.py +++ b/lms/djangoapps/commerce/signals.py @@ -36,7 +36,7 @@ def handle_refund_order(sender, course_enrollment=None, **kwargs): # the client does not work anonymously, and furthermore, # there's certainly no need to inform Otto about this request. return - refund_seat(course_enrollment) + refund_seat(course_enrollment, change_mode=True) except Exception: # pylint: disable=broad-except # don't assume the signal was fired with `send_robust`. # avoid blowing up other signal handlers by gracefully diff --git a/lms/djangoapps/commerce/tests/test_utils.py b/lms/djangoapps/commerce/tests/test_utils.py index ed9f9a3eaf5d38f3e6c50f4cebc84dfeab8cc60a..3137d71d3d95ba2cc9c4bc86335f34d4f9c0ace3 100644 --- a/lms/djangoapps/commerce/tests/test_utils.py +++ b/lms/djangoapps/commerce/tests/test_utils.py @@ -1,6 +1,5 @@ """Tests of commerce utilities.""" import json -import unittest from urllib import urlencode import ddt @@ -10,14 +9,18 @@ from django.test import TestCase from django.test.client import RequestFactory from django.test.utils import override_settings from mock import patch +from opaque_keys.edx.locator import CourseLocator from waffle.testutils import override_switch from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory from course_modes.models import CourseMode +from course_modes.tests.factories import CourseModeFactory from lms.djangoapps.commerce.models import CommerceConfiguration -from lms.djangoapps.commerce.utils import EcommerceService, refund_entitlement +from lms.djangoapps.commerce.utils import EcommerceService, refund_entitlement, refund_seat +from openedx.core.djangolib.testing.utils import skip_unless_lms from openedx.core.lib.log_utils import audit_log +from student.models import CourseEnrollment from student.tests.factories import (TEST_PASSWORD, UserFactory) # Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection @@ -178,8 +181,10 @@ class EcommerceServiceTests(TestCase): self.assertEqual(url, expected_url) -@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +@ddt.ddt +@skip_unless_lms class RefundUtilMethodTests(ModuleStoreTestCase): + """Tests for Refund Utilities""" shard = 4 def setUp(self): @@ -320,3 +325,55 @@ class RefundUtilMethodTests(ModuleStoreTestCase): call_args = list(mock_send_notification.call_args) assert call_args[0] == (course_entitlement.user, [1]) assert not refund_success + + @httpretty.activate + @ddt.data( + (["verified", "audit"], "audit"), + (["professional"], "professional"), + ) + @ddt.unpack + def test_mode_change_after_refund_seat(self, course_modes, new_mode): + """ + Test if a course seat is refunded student is enrolled into default course mode + unless no default mode available. + """ + course_id = CourseLocator('test_org', 'test_course_number', 'test_run') + CourseMode.objects.all().delete() + for course_mode in course_modes: + CourseModeFactory.create( + course_id=course_id, + mode_slug=course_mode, + mode_display_name=course_mode, + ) + + httpretty.register_uri( + httpretty.POST, + settings.ECOMMERCE_API_URL + 'refunds/', + status=201, + body='[1]', + content_type='application/json' + ) + httpretty.register_uri( + httpretty.PUT, + settings.ECOMMERCE_API_URL + 'refunds/1/process/', + status=200, + body=json.dumps({ + "id": 9, + "created": "2017-12-21T18:23:49.468298Z", + "modified": "2017-12-21T18:24:02.741426Z", + "total_credit_excl_tax": "100.00", + "currency": "USD", + "status": "Complete", + "order": 15, + "user": 5 + }), + content_type='application/json' + ) + enrollment = CourseEnrollment.enroll(self.user, course_id, mode=course_modes[0]) + + refund_success = refund_seat(enrollment, True) + + enrollment = CourseEnrollment.get_or_create_enrollment(self.user, course_id) + + assert refund_success + self.assertEqual(enrollment.mode, new_mode) diff --git a/lms/djangoapps/commerce/utils.py b/lms/djangoapps/commerce/utils.py index 57c1e4b7b58f1eed07dcae47a041578c975f5b17..9d8c69d80e748b32a3acf0ab49cd342942c3dea5 100644 --- a/lms/djangoapps/commerce/utils.py +++ b/lms/djangoapps/commerce/utils.py @@ -10,7 +10,9 @@ from django.conf import settings from django.contrib.auth import get_user_model from django.urls import reverse from django.utils.translation import ugettext as _ +from opaque_keys.edx.keys import CourseKey +from course_modes.models import CourseMode from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, is_commerce_service_configured from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers from openedx.core.djangoapps.theming import helpers as theming_helpers @@ -208,13 +210,14 @@ def refund_entitlement(course_entitlement): return False -def refund_seat(course_enrollment): +def refund_seat(course_enrollment, change_mode=False): """ Attempt to initiate a refund for any orders associated with the seat being unenrolled, using the commerce service. Arguments: course_enrollment (CourseEnrollment): a student enrollment + change_mode (Boolean): change the course mode to free mode or not Returns: A list of the external service's IDs for any refunds that were initiated @@ -244,6 +247,10 @@ def refund_seat(course_enrollment): 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() else: log.info('No refund opened for user [%s], course [%s]', enrollee.id, course_key_str)