Skip to content
Snippets Groups Projects
Unverified Commit f813a56d authored by McKenzie Welter's avatar McKenzie Welter Committed by GitHub
Browse files

Merge pull request #17931 from edx/McKenzieW/learner-3923

mark support (re)issued entitlements as unrefundable
parents 700a4d8e 435c3c63
No related merge requests found
......@@ -35,6 +35,7 @@ class CourseEntitlementSerializer(serializers.ModelSerializer):
'created',
'modified',
'mode',
'refund_locked',
'order_number',
'support_details'
)
......
......@@ -27,6 +27,7 @@ class EntitlementsSerializerTests(ModuleStoreTestCase):
'expired_at': entitlement.expired_at,
'course_uuid': str(entitlement.course_uuid),
'mode': entitlement.mode,
'refund_locked': False,
'enrollment_course_run': None,
'order_number': entitlement.order_number,
'created': entitlement.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
......
......@@ -17,6 +17,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from student.models import CourseEnrollment
......@@ -27,7 +28,7 @@ log = logging.getLogger(__name__)
# Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection
if settings.ROOT_URLCONF == 'lms.urls':
from entitlements.tests.factories import CourseEntitlementFactory
from entitlements.models import CourseEntitlement, CourseEntitlementPolicy
from entitlements.models import CourseEntitlement, CourseEntitlementPolicy, CourseEntitlementSupportDetail
from entitlements.api.v1.serializers import CourseEntitlementSerializer
from entitlements.api.v1.views import set_entitlement_policy
......@@ -606,7 +607,7 @@ class EntitlementViewSetTest(ModuleStoreTestCase):
'support_details': [
{
'unenrolled_run': str(enrollment.course.id),
'action': 'REISSUE',
'action': CourseEntitlementSupportDetail.REISSUE,
'comments': 'Severe illness.'
}
]
......@@ -625,6 +626,74 @@ class EntitlementViewSetTest(ModuleStoreTestCase):
)
assert results == CourseEntitlementSerializer(reinstated_entitlement).data
def test_reinstate_refundable_entitlement(self):
""" Verify that an entitlement that is refundable stays refundable when support reinstates it. """
enrollment = CourseEnrollmentFactory(user=self.user, is_active=True, course=CourseOverviewFactory(start=now()))
fulfilled_entitlement = CourseEntitlementFactory.create(
user=self.user, enrollment_course_run=enrollment
)
assert fulfilled_entitlement.is_entitlement_refundable() is True
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(fulfilled_entitlement.uuid)])
update_data = {
'expired_at': None,
'enrollment_course_run': None,
'support_details': [
{
'unenrolled_run': str(enrollment.course.id),
'action': CourseEntitlementSupportDetail.REISSUE,
'comments': 'Severe illness.'
}
]
}
response = self.client.patch(
url,
data=json.dumps(update_data),
content_type='application/json'
)
assert response.status_code == 200
reinstated_entitlement = CourseEntitlement.objects.get(
uuid=fulfilled_entitlement.uuid
)
assert reinstated_entitlement.refund_locked is False
assert reinstated_entitlement.is_entitlement_refundable() is True
def test_reinstate_unrefundable_entitlement(self):
""" Verify that a no longer refundable entitlement does not become refundable when support reinstates it. """
enrollment = CourseEnrollmentFactory(user=self.user, is_active=True)
expired_entitlement = CourseEntitlementFactory.create(
user=self.user, enrollment_course_run=enrollment, expired_at=datetime.now()
)
assert expired_entitlement.is_entitlement_refundable() is False
url = reverse(self.ENTITLEMENTS_DETAILS_PATH, args=[str(expired_entitlement.uuid)])
update_data = {
'expired_at': None,
'enrollment_course_run': None,
'support_details': [
{
'unenrolled_run': str(enrollment.course.id),
'action': CourseEntitlementSupportDetail.REISSUE,
'comments': 'Severe illness.'
}
]
}
response = self.client.patch(
url,
data=json.dumps(update_data),
content_type='application/json'
)
assert response.status_code == 200
reinstated_entitlement = CourseEntitlement.objects.get(
uuid=expired_entitlement.uuid
)
assert reinstated_entitlement.refund_locked is True
assert reinstated_entitlement.is_entitlement_refundable() is False
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):
......
......@@ -275,6 +275,11 @@ class EntitlementViewSet(viewsets.ModelViewSet):
)
support_details = request.data.pop('support_details', [])
# If a patch request does not explicitly update an entitlement's refundability status, we want to ensure that
# changes made to other attributes of the entitlement do not implicitly change its ability to be refunded.
if request.data.get('refund_locked') is None:
request.data['refund_locked'] = not entitlement.is_entitlement_refundable()
for support_detail in support_details:
support_detail['entitlement'] = entitlement
support_detail['support_user'] = request.user
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.12 on 2018-04-12 12:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('entitlements', '0008_auto_20180328_1107'),
]
operations = [
migrations.AddField(
model_name='courseentitlement',
name='refund_locked',
field=models.BooleanField(default=False),
),
]
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django.db import migrations, models
def backfill_refundability(apps, schema_editor):
CourseEntitlementSupportDetail = apps.get_model('entitlements', 'CourseEntitlementSupportDetail')
for support_detail in CourseEntitlementSupportDetail.objects.all().select_related('entitlement'):
support_detail.entitlement.refund_locked = True
support_detail.entitlement.save()
def revert_backfill(apps, schema_editor):
CourseEntitlementSupportDetail = apps.get_model('entitlements', 'CourseEntitlementSupportDetail')
for support_detail in CourseEntitlementSupportDetail.objects.all().select_related('entitlement'):
support_detail.entitlement.refund_locked = False
support_detail.entitlement.save()
class Migration(migrations.Migration):
dependencies = [
('entitlements', '0009_courseentitlement_refund_locked'),
]
operations = [
migrations.RunPython(backfill_refundability, revert_backfill),
]
......@@ -162,6 +162,7 @@ class CourseEntitlement(TimeStampedModel):
blank=True
)
order_number = models.CharField(max_length=128, null=True)
refund_locked = models.BooleanField(default=False)
_policy = models.ForeignKey(CourseEntitlementPolicy, null=True, blank=True)
@property
......@@ -226,7 +227,7 @@ class CourseEntitlement(TimeStampedModel):
"""
Returns a boolean as to whether or not the entitlement can be refunded based on the entitlement's policy
"""
return self.policy.is_entitlement_refundable(self)
return not self.refund_locked and self.policy.is_entitlement_refundable(self)
def is_entitlement_redeemable(self):
"""
......
......@@ -25,6 +25,7 @@ const postEntitlement = ({ username, courseUuid, mode, action, comments = null }
course_uuid: courseUuid,
user: username,
mode,
refund_locked: true,
support_details: [{
action,
comments,
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment