mark support (re)issued entitlements as unrefundable

This commit is contained in:
McKenzie Welter
2018-04-10 12:54:31 -04:00
parent c3bb21c714
commit 435c3c6338
8 changed files with 132 additions and 3 deletions

View File

@@ -35,6 +35,7 @@ class CourseEntitlementSerializer(serializers.ModelSerializer):
'created', 'created',
'modified', 'modified',
'mode', 'mode',
'refund_locked',
'order_number', 'order_number',
'support_details' 'support_details'
) )

View File

@@ -27,6 +27,7 @@ class EntitlementsSerializerTests(ModuleStoreTestCase):
'expired_at': entitlement.expired_at, 'expired_at': entitlement.expired_at,
'course_uuid': str(entitlement.course_uuid), 'course_uuid': str(entitlement.course_uuid),
'mode': entitlement.mode, 'mode': entitlement.mode,
'refund_locked': False,
'enrollment_course_run': None, 'enrollment_course_run': None,
'order_number': entitlement.order_number, 'order_number': entitlement.order_number,
'created': entitlement.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'created': entitlement.created.strftime('%Y-%m-%dT%H:%M:%S.%fZ'),

View File

@@ -17,6 +17,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
from course_modes.models import CourseMode from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory 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.schedules.tests.factories import ScheduleFactory
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from student.models import CourseEnrollment 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 # Entitlements is not in CMS' INSTALLED_APPS so these imports will error during test collection
if settings.ROOT_URLCONF == 'lms.urls': if settings.ROOT_URLCONF == 'lms.urls':
from entitlements.tests.factories import CourseEntitlementFactory 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.serializers import CourseEntitlementSerializer
from entitlements.api.v1.views import set_entitlement_policy from entitlements.api.v1.views import set_entitlement_policy
@@ -606,7 +607,7 @@ class EntitlementViewSetTest(ModuleStoreTestCase):
'support_details': [ 'support_details': [
{ {
'unenrolled_run': str(enrollment.course.id), 'unenrolled_run': str(enrollment.course.id),
'action': 'REISSUE', 'action': CourseEntitlementSupportDetail.REISSUE,
'comments': 'Severe illness.' 'comments': 'Severe illness.'
} }
] ]
@@ -625,6 +626,74 @@ class EntitlementViewSetTest(ModuleStoreTestCase):
) )
assert results == CourseEntitlementSerializer(reinstated_entitlement).data 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') @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase): class EntitlementEnrollmentViewSetTest(ModuleStoreTestCase):

View File

@@ -275,6 +275,11 @@ class EntitlementViewSet(viewsets.ModelViewSet):
) )
support_details = request.data.pop('support_details', []) 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: for support_detail in support_details:
support_detail['entitlement'] = entitlement support_detail['entitlement'] = entitlement
support_detail['support_user'] = request.user support_detail['support_user'] = request.user

View File

@@ -0,0 +1,20 @@
# -*- 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),
),
]

View File

@@ -0,0 +1,31 @@
# -*- 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),
]

View File

@@ -162,6 +162,7 @@ class CourseEntitlement(TimeStampedModel):
blank=True blank=True
) )
order_number = models.CharField(max_length=128, null=True) order_number = models.CharField(max_length=128, null=True)
refund_locked = models.BooleanField(default=False)
_policy = models.ForeignKey(CourseEntitlementPolicy, null=True, blank=True) _policy = models.ForeignKey(CourseEntitlementPolicy, null=True, blank=True)
@property @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 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): def is_entitlement_redeemable(self):
""" """

View File

@@ -25,6 +25,7 @@ const postEntitlement = ({ username, courseUuid, mode, action, comments = null }
course_uuid: courseUuid, course_uuid: courseUuid,
user: username, user: username,
mode, mode,
refund_locked: true,
support_details: [{ support_details: [{
action, action,
comments, comments,