Add configurable refund window

Add configuration model for enrollment refunds.

Use order info from otto in refund window calculation

Delete dupe tests. Extend tests to include window tests

Move ecom client from lib to djangoapps in openedx
This commit is contained in:
Bill DeRusha
2015-10-23 17:53:51 -04:00
parent 16ab4f4333
commit 1f77810fad
12 changed files with 513 additions and 104 deletions

View File

@@ -0,0 +1,245 @@
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'EnrollmentRefundConfiguration'
db.create_table('student_enrollmentrefundconfiguration', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('change_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('changed_by', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], null=True, on_delete=models.PROTECT)),
('enabled', self.gf('django.db.models.fields.BooleanField')(default=False)),
('refund_window_microseconds', self.gf('django.db.models.fields.BigIntegerField')(default=1209600000000)),
))
db.send_create_signal('student', ['EnrollmentRefundConfiguration'])
def backwards(self, orm):
# Deleting model 'EnrollmentRefundConfiguration'
db.delete_table('student_enrollmentrefundconfiguration')
models = {
'auth.group': {
'Meta': {'object_name': 'Group'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
'auth.permission': {
'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'student.anonymoususerid': {
'Meta': {'object_name': 'AnonymousUserId'},
'anonymous_user_id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseaccessrole': {
'Meta': {'unique_together': "(('user', 'org', 'course_id', 'role'),)", 'object_name': 'CourseAccessRole'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'org': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'blank': 'True'}),
'role': ('django.db.models.fields.CharField', [], {'max_length': '64', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollment': {
'Meta': {'ordering': "('user', 'course_id')", 'unique_together': "(('user', 'course_id'),)", 'object_name': 'CourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.courseenrollmentallowed': {
'Meta': {'unique_together': "(('email', 'course_id'),)", 'object_name': 'CourseEnrollmentAllowed'},
'auto_enroll': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'student.courseenrollmentattribute': {
'Meta': {'object_name': 'CourseEnrollmentAttribute'},
'enrollment': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'attributes'", 'to': "orm['student.CourseEnrollment']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'namespace': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '255'})
},
'student.dashboardconfiguration': {
'Meta': {'ordering': "('-change_date',)", 'object_name': 'DashboardConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'recent_enrollment_time_delta': ('django.db.models.fields.PositiveIntegerField', [], {'default': '0'})
},
'student.enrollmentrefundconfiguration': {
'Meta': {'ordering': "('-change_date',)", 'object_name': 'EnrollmentRefundConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'refund_window_microseconds': ('django.db.models.fields.BigIntegerField', [], {'default': '1209600000000'})
},
'student.entranceexamconfiguration': {
'Meta': {'unique_together': "(('user', 'course_id'),)", 'object_name': 'EntranceExamConfiguration'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'db_index': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'skip_entrance_exam': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'db_index': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.historicalcourseenrollment': {
'Meta': {'ordering': "(u'-history_date', u'-history_id')", 'object_name': 'HistoricalCourseEnrollment'},
'course_id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'db_index': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}),
u'history_date': ('django.db.models.fields.DateTimeField', [], {}),
u'history_id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
u'history_type': ('django.db.models.fields.CharField', [], {'max_length': '1'}),
u'history_user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.SET_NULL', 'to': "orm['auth.User']"}),
'id': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'blank': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'mode': ('django.db.models.fields.CharField', [], {'default': "'honor'", 'max_length': '100'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "u'+'", 'null': 'True', 'on_delete': 'models.DO_NOTHING', 'to': "orm['auth.User']"})
},
'student.languageproficiency': {
'Meta': {'unique_together': "(('code', 'user_profile'),)", 'object_name': 'LanguageProficiency'},
'code': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user_profile': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'language_proficiencies'", 'to': "orm['student.UserProfile']"})
},
'student.linkedinaddtoprofileconfiguration': {
'Meta': {'ordering': "('-change_date',)", 'object_name': 'LinkedInAddToProfileConfiguration'},
'change_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'on_delete': 'models.PROTECT'}),
'company_identifier': ('django.db.models.fields.TextField', [], {}),
'dashboard_tracking_code': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}),
'enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'trk_partner_name': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '10', 'blank': 'True'})
},
'student.loginfailures': {
'Meta': {'object_name': 'LoginFailures'},
'failure_count': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lockout_until': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.manualenrollmentaudit': {
'Meta': {'object_name': 'ManualEnrollmentAudit'},
'enrolled_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True'}),
'enrolled_email': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'enrollment': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['student.CourseEnrollment']", 'null': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'reason': ('django.db.models.fields.TextField', [], {'null': 'True'}),
'state_transition': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
'time_stamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'})
},
'student.passwordhistory': {
'Meta': {'object_name': 'PasswordHistory'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'time_set': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.pendingemailchange': {
'Meta': {'object_name': 'PendingEmailChange'},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_email': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.pendingnamechange': {
'Meta': {'object_name': 'PendingNameChange'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'new_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
'rationale': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'blank': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.registration': {
'Meta': {'object_name': 'Registration', 'db_table': "'auth_registration'"},
'activation_key': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '32', 'db_index': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
},
'student.userprofile': {
'Meta': {'object_name': 'UserProfile', 'db_table': "'auth_userprofile'"},
'allow_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'bio': ('django.db.models.fields.CharField', [], {'max_length': '3000', 'null': 'True', 'blank': 'True'}),
'city': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'country': ('django_countries.fields.CountryField', [], {'max_length': '2', 'null': 'True', 'blank': 'True'}),
'courseware': ('django.db.models.fields.CharField', [], {'default': "'course.xml'", 'max_length': '255', 'blank': 'True'}),
'gender': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'goals': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'level_of_education': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '6', 'null': 'True', 'blank': 'True'}),
'location': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'mailing_address': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'meta': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '255', 'blank': 'True'}),
'profile_image_uploaded_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'profile'", 'unique': 'True', 'to': "orm['auth.User']"}),
'year_of_birth': ('django.db.models.fields.IntegerField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
},
'student.usersignupsource': {
'Meta': {'object_name': 'UserSignupSource'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'site': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"})
},
'student.userstanding': {
'Meta': {'object_name': 'UserStanding'},
'account_status': ('django.db.models.fields.CharField', [], {'max_length': '31', 'blank': 'True'}),
'changed_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'standing_last_changed_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'standing'", 'unique': 'True', 'to': "orm['auth.User']"})
},
'student.usertestgroup': {
'Meta': {'object_name': 'UserTestGroup'},
'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}),
'users': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.User']", 'db_index': 'True', 'symmetrical': 'False'})
}
}
complete_apps = ['student']

View File

@@ -49,6 +49,7 @@ from xmodule_django.models import CourseKeyField, NoneToEmptyManager
from certificates.models import GeneratedCertificate
from course_modes.models import CourseMode
import lms.lib.comment_client as cc
from openedx.core.djangoapps.commerce.utils import ecommerce_api_client, ECOMMERCE_DATE_FORMAT
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from util.model_utils import emit_field_changed_events, get_changed_fields_dict
from util.query import use_read_replica_if_available
@@ -1374,7 +1375,10 @@ class CourseEnrollment(models.Model):
if GeneratedCertificate.certificate_for_student(self.user, self.course_id) is not None:
return False
#TODO - When Course administrators to define a refund period for paid courses then refundable will be supported. # pylint: disable=fixme
# If it is after the refundable cutoff date they should not be refunded.
refund_cutoff_date = self.refund_cutoff_date()
if refund_cutoff_date and datetime.now() > refund_cutoff_date:
return False
course_mode = CourseMode.mode_for_course(self.course_id, 'verified')
if course_mode is None:
@@ -1382,6 +1386,22 @@ class CourseEnrollment(models.Model):
else:
return True
def refund_cutoff_date(self):
""" Calculate and return the refund window end date. """
try:
attribute = self.attributes.get(namespace='order', name='order_number') # pylint: disable=no-member
except ObjectDoesNotExist:
return None
order_number = attribute.value
order = ecommerce_api_client(self.user).orders(order_number).get()
refund_window_start_date = max(
datetime.strptime(order['date_placed'], ECOMMERCE_DATE_FORMAT),
self.course_overview.start.replace(tzinfo=None)
)
return refund_window_start_date + EnrollmentRefundConfiguration.current().refund_window
@property
def username(self):
return self.user.username
@@ -2024,3 +2044,34 @@ class CourseEnrollmentAttribute(models.Model):
}
for attribute in cls.objects.filter(enrollment=enrollment)
]
class EnrollmentRefundConfiguration(ConfigurationModel):
"""
Configuration for course enrollment refunds.
"""
# TODO: Django 1.8 introduces a DurationField
# (https://docs.djangoproject.com/en/1.8/ref/models/fields/#durationfield)
# for storing timedeltas which uses MySQL's bigint for backing
# storage. After we've completed the Django upgrade we should be
# able to replace this field with a DurationField named
# `refund_window` without having to run a migration or change
# other code.
refund_window_microseconds = models.BigIntegerField(
default=1209600000000,
help_text=_(
"The window of time after enrolling during which users can be granted"
" a refund, represented in microseconds. The default is 14 days."
)
)
@property
def refund_window(self):
"""Return the configured refund window as a `datetime.timedelta`."""
return timedelta(microseconds=self.refund_window_microseconds)
@refund_window.setter
def refund_window(self, refund_window):
"""Set the current refund window to the given timedelta."""
self.refund_window_microseconds = int(refund_window.total_seconds() * 1000000)

View File

@@ -0,0 +1,168 @@
"""
Tests for enrollment refund capabilities.
"""
from datetime import datetime, timedelta
import ddt
import httpretty
import logging
import pytz
import unittest
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test.client import Client
from django.test.utils import override_settings
from mock import patch
from student.models import CourseEnrollment, CourseEnrollmentAttribute
from student.tests.factories import UserFactory, CourseModeFactory
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
# These imports refer to lms djangoapps.
# Their testcases are only run under lms.
from certificates.models import CertificateStatuses # pylint: disable=import-error
from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
from openedx.core.djangoapps.commerce.utils import ECOMMERCE_DATE_FORMAT
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
from config_models.models import cache
log = logging.getLogger(__name__)
TEST_API_URL = 'http://www-internal.example.com/api'
TEST_API_SIGNING_KEY = 'edx'
JSON = 'application/json'
@ddt.ddt
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class RefundableTest(SharedModuleStoreTestCase):
"""
Tests for dashboard utility functions
"""
def setUp(self):
""" Setup components used by each refund test."""
super(RefundableTest, self).setUp()
self.course = CourseFactory.create()
self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
self.verified_mode = CourseModeFactory.create(
course_id=self.course.id,
mode_slug='verified',
mode_display_name='Verified',
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
)
self.enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='verified')
self.client = Client()
cache.clear()
def test_refundable(self):
""" Assert base case is refundable"""
self.assertTrue(self.enrollment.refundable())
def test_refundable_expired_verification(self):
""" Assert that enrollment is not refundable if course mode has expired."""
self.verified_mode.expiration_datetime = datetime.now(pytz.UTC) - timedelta(days=1)
self.verified_mode.save()
self.assertFalse(self.enrollment.refundable())
# Assert that can_refund overrides this and allows refund
self.enrollment.can_refund = True
self.assertTrue(self.enrollment.refundable())
def test_refundable_of_purchased_course(self):
""" Assert that courses without a verified mode are not refundable"""
self.client.login(username="jack", password="test")
course = CourseFactory.create()
CourseModeFactory.create(
course_id=course.id,
mode_slug='honor',
min_price=10,
currency='usd',
mode_display_name='honor',
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
)
enrollment = CourseEnrollment.enroll(self.user, course.id, mode='honor')
# TODO: Until we can allow course administrators to define a refund period for paid for courses show_refund_option should be False. # pylint: disable=fixme
self.assertFalse(enrollment.refundable())
resp = self.client.post(reverse('student.views.dashboard', args=[]))
self.assertIn('You will not be refunded the amount you paid.', resp.content)
def test_refundable_when_certificate_exists(self):
""" Assert that enrollment is not refundable once a certificat has been generated."""
self.assertTrue(self.enrollment.refundable())
GeneratedCertificateFactory.create(
user=self.user,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode='verified'
)
self.assertFalse(self.enrollment.refundable())
# Assert that can_refund overrides this and allows refund
self.enrollment.can_refund = True
self.assertTrue(self.enrollment.refundable())
def test_refundable_with_cutoff_date(self):
""" Assert enrollment is refundable before cutoff and not refundable after."""
self.assertTrue(self.enrollment.refundable())
with patch('student.models.CourseEnrollment.refund_cutoff_date') as cutoff_date:
cutoff_date.return_value = datetime.now() - timedelta(days=1)
self.assertFalse(self.enrollment.refundable())
cutoff_date.return_value = datetime.now() + timedelta(days=1)
self.assertTrue(self.enrollment.refundable())
@ddt.data(
(timedelta(days=1), timedelta(days=2), timedelta(days=2), 14),
(timedelta(days=2), timedelta(days=1), timedelta(days=2), 14),
(timedelta(days=1), timedelta(days=2), timedelta(days=2), 1),
(timedelta(days=2), timedelta(days=1), timedelta(days=2), 1),
)
@ddt.unpack
@httpretty.activate
@override_settings(ECOMMERCE_API_SIGNING_KEY=TEST_API_SIGNING_KEY, ECOMMERCE_API_URL=TEST_API_URL)
def test_refund_cutoff_date(self, order_date_delta, course_start_delta, expected_date_delta, days):
"""
Assert that the later date is used with the configurable refund period in calculating the returned cutoff date.
"""
now = datetime.now().replace(microsecond=0)
order_date = now + order_date_delta
course_start = now + course_start_delta
expected_date = now + expected_date_delta
refund_period = timedelta(days=days)
order_number = 'OSCR-1000'
expected_content = '{{"date_placed": "{date}"}}'.format(date=order_date.strftime(ECOMMERCE_DATE_FORMAT))
httpretty.register_uri(
httpretty.GET,
'{url}/orders/{order}/'.format(url=TEST_API_URL, order=order_number),
status=200, body=expected_content,
adding_headers={'Content-Type': JSON}
)
self.enrollment.course_overview.start = course_start
self.enrollment.attributes.add(CourseEnrollmentAttribute( # pylint: disable=no-member
enrollment=self.enrollment,
namespace='order',
name='order_number',
value=order_number
))
with patch('student.models.EnrollmentRefundConfiguration.current') as config:
instance = config.return_value
instance.refund_window = refund_period
self.assertEqual(
self.enrollment.refund_cutoff_date(),
expected_date + refund_period
)
def test_refund_cutoff_date_no_attributes(self):
""" Assert that the None is returned when no order number attribute is found."""
self.assertIsNone(self.enrollment.refund_cutoff_date())

View File

@@ -3,10 +3,10 @@
Miscellaneous tests for the student app.
"""
from datetime import datetime, timedelta
import ddt
import logging
import pytz
import unittest
import ddt
from django.conf import settings
from django.contrib.auth.models import User, AnonymousUser
@@ -17,7 +17,8 @@ from mock import Mock, patch
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.models import (
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment, unique_id_for_user, LinkedInAddToProfileConfiguration
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment,
unique_id_for_user, LinkedInAddToProfileConfiguration
)
from student.views import (
process_survey_link,
@@ -287,22 +288,6 @@ class DashboardTest(ModuleStoreTestCase):
self.assertFalse(course_mode_info['show_upsell'])
self.assertIsNone(course_mode_info['days_for_upsell'])
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_refundable(self):
verified_mode = CourseModeFactory.create(
course_id=self.course.id,
mode_slug='verified',
mode_display_name='Verified',
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
)
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='verified')
self.assertTrue(enrollment.refundable())
verified_mode.expiration_datetime = datetime.now(pytz.UTC) - timedelta(days=1)
verified_mode.save()
self.assertFalse(enrollment.refundable())
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@patch('courseware.views.log.warning')
@patch.dict('django.conf.settings.FEATURES', {'ENABLE_PAID_COURSE_REGISTRATION': True})
@@ -361,48 +346,6 @@ class DashboardTest(ModuleStoreTestCase):
response = self.client.get(reverse('dashboard'))
self.assertNotIn('You can no longer access this course because payment has not yet been received', response.content)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_refundable_of_purchased_course(self):
self.client.login(username="jack", password="test")
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='honor',
min_price=10,
currency='usd',
mode_display_name='honor',
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
)
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='honor')
# TODO: Until we can allow course administrators to define a refund period for paid for courses show_refund_option should be False. # pylint: disable=fixme
self.assertFalse(enrollment.refundable())
resp = self.client.post(reverse('student.views.dashboard', args=[]))
self.assertIn('You will not be refunded the amount you paid.', resp.content)
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_refundable_when_certificate_exists(self):
CourseModeFactory.create(
course_id=self.course.id,
mode_slug='verified',
mode_display_name='Verified',
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
)
enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode='verified')
self.assertTrue(enrollment.refundable())
GeneratedCertificateFactory.create(
user=self.user,
course_id=self.course.id,
status=CertificateStatuses.downloadable,
mode='verified'
)
self.assertFalse(enrollment.refundable())
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
def test_linked_in_add_to_profile_btn_not_appearing_without_config(self):
# Without linked-in config don't show Add Certificate to LinkedIn button