fix: Corrects issue with refund logic and certificates.
[MICROBA-1307] Before this change a user would not be auto refunded if they had a certificate in a course with any status. This had unintended consequences. This change updates the logic to only block auto refund for statuses that we do not want to refund on such as downloadable.
This commit is contained in:
@@ -18,7 +18,8 @@ from common.djangoapps.course_modes.models import CourseMode
|
||||
from common.djangoapps.entitlements.utils import is_course_run_entitlement_fulfillable
|
||||
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentException
|
||||
from common.djangoapps.util.date_utils import strftime_localized
|
||||
from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from lms.djangoapps.certificates import api as certificates_api
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.commerce.utils import refund_entitlement
|
||||
from openedx.core.djangoapps.catalog.utils import get_course_uuid_for_course
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
@@ -95,8 +96,11 @@ class CourseEntitlementPolicy(models.Model):
|
||||
return False
|
||||
|
||||
if entitlement.enrollment_course_run:
|
||||
if GeneratedCertificate.certificate_for_student(
|
||||
entitlement.user_id, entitlement.enrollment_course_run.course_id) is not None:
|
||||
certificate = certificates_api.get_certificate_for_user_id(
|
||||
entitlement.user,
|
||||
entitlement.enrollment_course_run.course_id
|
||||
)
|
||||
if certificate and not CertificateStatuses.is_refundable_status(certificate.status):
|
||||
return False
|
||||
|
||||
# This is >= because a days_until_expiration 0 means that the expiration day has not fully passed yet
|
||||
|
||||
@@ -189,14 +189,14 @@ class TestModels(TestCase):
|
||||
|
||||
def test_is_entitlement_regainable(self):
|
||||
"""
|
||||
Test that the entitlement is not expired when created now, and is expired when created20 days
|
||||
Test that the entitlement is not expired when created now, and is expired when created 20 days
|
||||
ago with a policy that sets the expiration period to 14 days
|
||||
"""
|
||||
entitlement = CourseEntitlementFactory.create(enrollment_course_run=self.enrollment)
|
||||
assert entitlement.is_entitlement_regainable() is True
|
||||
|
||||
# Create and associate a GeneratedCertificate for a user and course and make sure it isn't regainable
|
||||
GeneratedCertificateFactory(
|
||||
certificate = GeneratedCertificateFactory(
|
||||
user=entitlement.user,
|
||||
course_id=entitlement.enrollment_course_run.course_id,
|
||||
mode=MODES.verified,
|
||||
@@ -205,6 +205,11 @@ class TestModels(TestCase):
|
||||
|
||||
assert entitlement.is_entitlement_regainable() is False
|
||||
|
||||
certificate.status = CertificateStatuses.notpassing
|
||||
certificate.save()
|
||||
|
||||
assert entitlement.is_entitlement_regainable() is True
|
||||
|
||||
# Create a date 20 days in the past (greater than the policy expire period of 14 days)
|
||||
# and apply it to both the entitlement and the course
|
||||
past_datetime = now() - timedelta(days=20)
|
||||
|
||||
@@ -66,7 +66,7 @@ from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE, ENROLLMENT_T
|
||||
from common.djangoapps.track import contexts, segment
|
||||
from common.djangoapps.util.model_utils import emit_field_changed_events, get_changed_fields_dict
|
||||
from common.djangoapps.util.query import use_read_replica_if_available
|
||||
from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.courseware.models import (
|
||||
CourseDynamicUpgradeDeadlineConfiguration,
|
||||
DynamicUpgradeDeadlineConfiguration,
|
||||
@@ -1835,7 +1835,7 @@ class CourseEnrollment(models.Model):
|
||||
"""Changes this `CourseEnrollment` record's mode to `mode`. Saves immediately."""
|
||||
self.update_enrollment(mode=mode)
|
||||
|
||||
def refundable(self, user_already_has_certs_for=None):
|
||||
def refundable(self):
|
||||
"""
|
||||
For paid/verified certificates, students may always receive a refund if
|
||||
this CourseEnrollment's `can_refund` attribute is not `None` (that
|
||||
@@ -1848,11 +1848,6 @@ class CourseEnrollment(models.Model):
|
||||
* We are not past the refund cutoff date
|
||||
* There exists a 'verified' CourseMode for this course.
|
||||
|
||||
Arguments:
|
||||
`user_already_has_certs_for` (set of `CourseKey`):
|
||||
An optional param that is a set of `CourseKeys` that the user
|
||||
has already been issued certificates in.
|
||||
|
||||
Returns:
|
||||
bool: Whether is CourseEnrollment can be refunded.
|
||||
"""
|
||||
@@ -1865,13 +1860,16 @@ class CourseEnrollment(models.Model):
|
||||
if getattr(self, 'can_refund', None) is not None:
|
||||
return True
|
||||
|
||||
# If the student has already been given a certificate they should not be refunded
|
||||
if user_already_has_certs_for is not None:
|
||||
if self.course_id in user_already_has_certs_for:
|
||||
return False
|
||||
else:
|
||||
if GeneratedCertificate.certificate_for_student(self.user, self.course_id) is not None:
|
||||
return False
|
||||
# Due to circular import issues this import was placed close to usage. To move this to the
|
||||
# top of the file would require a large scale refactor of the refund code.
|
||||
import lms.djangoapps.certificates.api
|
||||
# If the student has already been given a certificate in a non refundable status they should not be refunded
|
||||
certificate = lms.djangoapps.certificates.api.get_certificate_for_user_id(
|
||||
self.user,
|
||||
self.course_id
|
||||
)
|
||||
if certificate and not CertificateStatuses.is_refundable_status(certificate.status):
|
||||
return False
|
||||
|
||||
# If it is after the refundable cutoff date they should not be refunded.
|
||||
refund_cutoff_date = self.refund_cutoff_date()
|
||||
|
||||
@@ -25,7 +25,6 @@ from common.djangoapps.course_modes.tests.factories import CourseModeFactory
|
||||
from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAttribute, EnrollmentRefundConfiguration
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from lms.djangoapps.certificates.data import CertificateStatuses
|
||||
from lms.djangoapps.certificates.models import GeneratedCertificate
|
||||
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
|
||||
from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT
|
||||
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
|
||||
@@ -88,7 +87,7 @@ class RefundableTest(SharedModuleStoreTestCase):
|
||||
|
||||
assert self.enrollment.refundable()
|
||||
|
||||
GeneratedCertificateFactory.create(
|
||||
certificate = GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
status=CertificateStatuses.downloadable,
|
||||
@@ -96,15 +95,18 @@ class RefundableTest(SharedModuleStoreTestCase):
|
||||
)
|
||||
|
||||
assert not self.enrollment.refundable()
|
||||
assert not self.enrollment.\
|
||||
refundable(user_already_has_certs_for=GeneratedCertificate.course_ids_with_certs_for_user(self.user))
|
||||
|
||||
# Certificates that are not in the PASSED_STATUSES should allow a refund
|
||||
certificate.status = CertificateStatuses.notpassing
|
||||
certificate.save()
|
||||
|
||||
assert self.enrollment.refundable()
|
||||
|
||||
# Assert that can_refund overrides this and allows refund
|
||||
certificate.status = CertificateStatuses.downloadable
|
||||
certificate.save()
|
||||
self.enrollment.can_refund = True
|
||||
assert self.enrollment.refundable()
|
||||
assert self.enrollment.refundable(
|
||||
user_already_has_certs_for=GeneratedCertificate.course_ids_with_certs_for_user(self.user)
|
||||
)
|
||||
|
||||
@patch('common.djangoapps.student.models.CourseEnrollment.refund_cutoff_date')
|
||||
def test_refundable_with_cutoff_date(self, cutoff_date):
|
||||
|
||||
Reference in New Issue
Block a user