Commit 63940141 by Julia Hansbrough

End-to-end refunding with tests

parent 045e69f3
......@@ -2,7 +2,7 @@
Add and create new modes for running courses on this particular LMS
"""
import pytz
from datetime import datetime, date
from datetime import datetime
from django.db import models
from collections import namedtuple
......@@ -101,17 +101,6 @@ class CourseMode(models.Model):
modes = cls.modes_for_course(course_id)
return min(mode.min_price for mode in modes if mode.currency == currency)
@classmethod
def refund_expiration_date(cls, course_id, mode_slug):
"""
Returns the expiration date for verified certificate refunds. After this date, refunds are
no longer possible. Note that this is currently set to be identical to the expiration date for
verified cert signups, but this could be changed in the future
"""
print "TODO fix this"
return date(1990, 1, 1)
#return cls.mode_for_course(course_id,mode_slug).expiration_date
def __unicode__(self):
return u"{} : {}, min={}, prices={}".format(
self.course_id, self.mode_slug, self.min_price, self.suggested_prices
......
from course_modes.models import CourseMode
from factory import DjangoModelFactory
import datetime
# Factories don't have __init__ methods, and are self documenting
# pylint: disable=W0232
......@@ -12,4 +11,3 @@ class CourseModeFactory(DjangoModelFactory):
mode_display_name = 'audit course'
min_price = 0
currency = 'usd'
expiration_date = datetime.date(1990, 1, 1)
......@@ -5,7 +5,7 @@ when you run "manage.py test".
Replace this with more appropriate tests for your application.
"""
from datetime import datetime, date, timedelta
from datetime import datetime, timedelta
import pytz
from django.test import TestCase
......@@ -20,7 +20,6 @@ class CourseModeModelTest(TestCase):
def setUp(self):
self.course_id = 'TestCourse'
CourseMode.objects.all().delete()
#todo use different default date
def create_mode(self, mode_slug, mode_name, min_price=0, suggested_prices='', currency='usd'):
"""
......@@ -113,9 +112,3 @@ class CourseModeModelTest(TestCase):
modes = CourseMode.modes_for_course('second_test_course')
self.assertEqual([CourseMode.DEFAULT_MODE], modes)
def test_refund_expiration_date(self):
self.create_mode('verified', 'Verified Certificate')
modes = CourseMode.modes_for_course(self.course_id)
mode = Mode(u'verified', u'Verified Certificate', 0, '', 'usd')
self.assertEqual(CourseMode.refund_expiration_date(self.course_id, 'verified'), date(1990, 1, 1))
......@@ -35,6 +35,7 @@ from student.tests.factories import UserFactory, CourseModeFactory
from student.tests.test_email import mock_render_to_string
import shoppingcart
from shoppingcart.models import CertificateItem
COURSE_1 = 'edX/toy/2012_Fall'
COURSE_2 = 'edx/full/6.002_Spring_2012'
......@@ -435,15 +436,20 @@ class CertificateItemTest(ModuleStoreTestCase):
COURSE_ORG = "EDX"
def setUp(self):
# Create course
# Create course, user, and enroll them as a verified student
self.req_factory = RequestFactory()
self.course = CourseFactory.create(org=self.COURSE_ORG, display_name=self.COURSE_NAME, number=self.COURSE_SLUG)
self.assertIsNotNone(self.course)
self.user = User.objects.create(username="test", email="test@test.org")
CourseEnrollment.enroll(self.user, self.course.id, mode='verified')
# Student is verified and paid; we should be able to refund them
def test_unenroll_and_refund(self):
request = self.req_factory.post(reverse('change_enrollment'), {'course_id': self.course.id, 'enrollment_action': 'unenroll'})
request.user = self.user
response = change_enrollment(request)
self.assertEqual(response.status_code, 200)
# add more later; see if this even works
self.assertFalse(CourseEnrollment.is_enrolled(self.user,self.course.id))
target_certs = CertificateItem.objects.filger(course_id=self.course.id, user_id=self.user, status='refunded')
self.assertTrue(target_certs[0].status == 'refunded')
......@@ -2,7 +2,6 @@
Student Views
"""
import datetime
from datetime import date
import json
import logging
import random
......@@ -51,6 +50,8 @@ from verify_student.models import SoftwareSecurePhotoVerification
from certificates.models import CertificateStatuses, certificate_status_for_student
from shoppingcart.models import CertificateItem
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
......@@ -66,7 +67,6 @@ import external_auth.views
from bulk_email.models import Optout, CourseAuthorization
import shoppingcart
from shoppingcart.models import (Order, OrderItem, CertificateItem)
import track.views
......@@ -302,7 +302,6 @@ def dashboard(request):
# exist (because the course IDs have changed). Still, we don't delete those
# enrollments, because it could have been a data push snafu.
courses = []
refund_status = []
for enrollment in CourseEnrollment.enrollments_for_user(user):
try:
courses.append((course_from_id(enrollment.course_id), enrollment))
......@@ -343,7 +342,7 @@ def dashboard(request):
verification_status, verification_msg = SoftwareSecurePhotoVerification.user_status(user)
show_refund_option_for = frozenset(course.id for course, _enrollment in courses
if (has_access(request.user, course, 'refund') and (_enrollment.mode == "verified")))
if (has_access(request.user, course, 'refund') and (_enrollment.mode == "verified")))
# get info w.r.t ExternalAuthMap
external_auth_map = None
......@@ -351,6 +350,7 @@ def dashboard(request):
external_auth_map = ExternalAuthMap.objects.get(user=user)
except ExternalAuthMap.DoesNotExist:
pass
context = {'courses': courses,
'course_optouts': course_optouts,
'message': message,
......@@ -432,8 +432,6 @@ def change_enrollment(request):
.format(user.username, course_id))
return HttpResponseBadRequest(_("Course id is invalid"))
course = course_from_id(course_id)
if not has_access(user, course, 'enroll'):
return HttpResponseBadRequest(_("Enrollment is closed"))
......@@ -475,41 +473,39 @@ def change_enrollment(request):
elif action == "unenroll":
try:
course = course_from_id(course_id)
except ItemNotFoundError:
log.warning("User {0} tried to unenroll from non-existent course {1}"
.format(user.username, course_id))
return HttpResponseBadRequest(_("Course id is invalid"))
course = course_from_id(course_id)
verified = CourseEnrollment.enrollment_mode_for_user(user, course_id)
# did they sign up for verified certs?
if(verified):
# If the user is allowed a refund, do so
if has_access(user, course, 'refund'):
subject = _("[Refund] User-Requested Refund")
# todo: make this reference templates/student/refund_email.html
message = "Important info here."
to_email = [settings.PAYMENT_SUPPORT_EMAIL]
from_email = "support@edx.org"
try:
send_mail(subject, message, from_email, to_email, fail_silently=False)
except:
log.warning('Unable to send reimbursement request to billing', exc_info=True)
js['value'] = _('Could not send reimbursement request.')
return HttpResponse(json.dumps(js))
enrollment_mode = CourseEnrollment.enrollment_mode_for_user(user, course_id)
# did they sign up for verified certs?
if(enrollment_mode=='verified'):
# If the user is allowed a refund, do so
if has_access(user, course, 'refund'):
subject = _("[Refund] User-Requested Refund")
# todo: make this reference templates/student/refund_email.html
message = "Important info here."
to_email = [settings.PAYMENT_SUPPORT_EMAIL]
from_email = "support@edx.org"
try:
send_mail(subject, message, from_email, to_email, fail_silently=False)
except:
log.warning('Unable to send reimbursement request to billing', exc_info=True)
js['value'] = _('Could not send reimbursement request.')
return HttpResponse(json.dumps(js))
# email has been sent, let's deal with the order now
CertificateItem.refund_cert(user, course_id)
CourseEnrollment.unenroll(user, course_id)
org, course_num, run = course_id.split("/")
dog_stats_api.increment(
"common.student.unenrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)]
)
return HttpResponse()
CourseEnrollment.unenroll(user, course_id)
org, course_num, run = course_id.split("/")
dog_stats_api.increment(
"common.student.unenrollment",
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"run:{0}".format(run)]
)
return HttpResponse()
except CourseEnrollment.DoesNotExist:
return HttpResponseBadRequest(_("You are not enrolled in this course"))
except ItemNotFoundError:
log.warning("User {0} tried to unenroll from non-existent course {1}".format(user.username, course_id))
return HttpResponseBadRequest(_("Course id is invalid"))
else:
return HttpResponseBadRequest(_("Enrollment action is invalid"))
......@@ -924,7 +920,7 @@ def create_account(request, post_override=None):
subject = ''.join(subject.splitlines())
message = render_to_string('emails/activation_email.txt', d)
# don't send email if we are doing load testing or random user generation for some reason
# dont send email if we are doing load testing or random user generation for some reason
if not (settings.MITX_FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING')):
try:
if settings.MITX_FEATURES.get('REROUTE_ACTIVATION_EMAIL'):
......
......@@ -2,7 +2,7 @@
Ideally, it will be the only place that needs to know about any special settings
like DISABLE_START_DATES"""
import logging
from datetime import datetime, timedelta, date
from datetime import datetime, timedelta
from functools import partial
from django.conf import settings
......
......@@ -114,12 +114,12 @@ class AccessTestCase(TestCase):
one_day_extra = datetime.timedelta(days=1)
# User is allowed to receive refund if it is within two weeks of course start date
c = Mock(enrollment_start=(today-one_day_extra), id='edX/tests/Whenever')
c = Mock(enrollment_start=(today - one_day_extra), id='edX/tests/Whenever')
self.assertTrue(access._has_access_course_desc(u, c, 'refund'))
c = Mock(enrollment_start=(today-grace_period), id='edX/test/Whenever')
c = Mock(enrollment_start=(today - grace_period), id='edX/test/Whenever')
self.assertTrue(access._has_access_course_desc(u, c, 'refund'))
# After two weeks, user may no longer receive a refund
c = Mock(enrollment_start=(today-grace_period-one_day_extra), id='edX/test/Whenever')
c = Mock(enrollment_start=(today - grace_period - one_day_extra), id='edX/test/Whenever')
self.assertFalse(access._has_access_course_desc(u, c, 'refund'))
......@@ -9,7 +9,7 @@ from boto.exception import BotoServerError # this is a super-class of SESError
from django.db import models
from django.conf import settings
from django.core.exceptions import (ObjectDoesNotExist, MultipleObjectsReturned)
from django.core.exceptions import ObjectDoesNotExist
from django.core.mail import send_mail
from django.contrib.auth.models import User
from django.utils.translation import ugettext as _
......@@ -405,18 +405,21 @@ class CertificateItem(OrderItem):
@classmethod
def refund_cert(cls, target_user, target_course_id):
"""
When refunded, this should find a verified certificate purchase for target_user in target_course_id, change that
certificate's status to "refunded", save that result, and return the refunded certificate.
Note the actual mechanics of refunding money occurs elsewhere; this simply changes the relevant certificate's
status for the refund.
"""
try:
target_cert = CertificateItem.objects.get(course_id=target_course_id, user_id=target_user, status='purchased', mode='verified')
# If there's duplicate entries, just grab the first one and refund it (though in most cases we should only get one)
target_certs = CertificateItem.objects.filter(course_id=target_course_id, user_id=target_user, status='purchased', mode='verified')
target_cert = target_certs[0]
target_cert.status = 'refunded'
# todo return success
target_cert.save()
return target_cert
except MultipleObjectsReturned:
# this seems like a thing that shouldn't happen
log.exception("Multiple entries for single verified cert found")
# but we can recover; select one item and refund it
# todo
except ObjectDoesNotExist:
# todo log properly
except IndexError or ObjectDoesNotExist:
log.exception("No certificate found")
# handle the exception
......
......@@ -368,7 +368,6 @@ class CertificateItemTest(ModuleStoreTestCase):
cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
cart.purchase()
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id)
# now that it's there, let's try refunding it
order = CertificateItem.refund_cert(target_user=self.user, target_course_id=self.course_id)
self.assertEquals(order.status, 'refunded')
......@@ -383,5 +382,4 @@ class CertificateItemTest(ModuleStoreTestCase):
cart = Order.get_cart_for_user(user=self.user)
CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified')
cart.purchase()
enrollment = CourseEnrollment.objects.get(user=self.user, course_id=self.course_id)
self.assertRaises(MultipleObjectsReturned)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment