Commit dc46170f by asadiqbal08 Committed by Chris Dodge

registration code checkout flow

Redirecting to dashboard after free user enrollment.
parent 878eaa9f
...@@ -1368,7 +1368,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa ...@@ -1368,7 +1368,7 @@ class TestInstructorAPILevelsDataDump(ModuleStoreTestCase, LoginEnrollmentTestCa
url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()}) url = reverse('get_purchase_transaction', kwargs={'course_id': self.course.id.to_deprecated_string()})
# using coupon code # using coupon code
resp = self.client.post(reverse('shoppingcart.views.use_coupon'), {'coupon_code': self.coupon_code}) resp = self.client.post(reverse('shoppingcart.views.use_code'), {'code': self.coupon_code})
self.assertEqual(resp.status_code, 200) self.assertEqual(resp.status_code, 200)
self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123') self.cart.purchase(first='FirstNameTesting123', street1='StreetTesting123')
response = self.client.get(url, {}) response = self.client.get(url, {})
......
...@@ -40,6 +40,14 @@ class ItemDoesNotExistAgainstCouponException(InvalidCartItem): ...@@ -40,6 +40,14 @@ class ItemDoesNotExistAgainstCouponException(InvalidCartItem):
pass pass
class RegCodeAlreadyExistException(InvalidCartItem):
pass
class ItemDoesNotExistAgainstRegCodeException(InvalidCartItem):
pass
class ReportException(Exception): class ReportException(Exception):
pass pass
......
...@@ -31,7 +31,9 @@ from xmodule_django.models import CourseKeyField ...@@ -31,7 +31,9 @@ from xmodule_django.models import CourseKeyField
from verify_student.models import SoftwareSecurePhotoVerification from verify_student.models import SoftwareSecurePhotoVerification
from .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException, from .exceptions import (InvalidCartItem, PurchasedCallbackException, ItemAlreadyInCartException,
AlreadyEnrolledInCourseException, CourseDoesNotExistException, CouponAlreadyExistException, ItemDoesNotExistAgainstCouponException) AlreadyEnrolledInCourseException, CourseDoesNotExistException,
CouponAlreadyExistException, ItemDoesNotExistAgainstCouponException,
RegCodeAlreadyExistException, ItemDoesNotExistAgainstRegCodeException)
from microsite_configuration import microsite from microsite_configuration import microsite
...@@ -326,6 +328,25 @@ class CourseRegistrationCode(models.Model): ...@@ -326,6 +328,25 @@ class CourseRegistrationCode(models.Model):
created_by = models.ForeignKey(User, related_name='created_by_user') created_by = models.ForeignKey(User, related_name='created_by_user')
created_at = models.DateTimeField(default=datetime.now(pytz.utc)) created_at = models.DateTimeField(default=datetime.now(pytz.utc))
@classmethod
@transaction.commit_on_success
def free_user_enrollment(cls, cart):
"""
Here we enroll the user free for all courses available in shopping cart
"""
cart_items = cart.orderitem_set.all().select_subclasses()
if cart_items:
for item in cart_items:
CourseEnrollment.enroll(cart.user, item.course_id)
log.info("Enrolled '{0}' in free course '{1}'"
.format(cart.user.email, item.course_id)) # pylint: disable=E1101
item.status = 'purchased'
item.save()
cart.status = 'purchased'
cart.purchase_time = datetime.now(pytz.utc)
cart.save()
class RegistrationCodeRedemption(models.Model): class RegistrationCodeRedemption(models.Model):
""" """
...@@ -336,6 +357,35 @@ class RegistrationCodeRedemption(models.Model): ...@@ -336,6 +357,35 @@ class RegistrationCodeRedemption(models.Model):
redeemed_by = models.ForeignKey(User, db_index=True) redeemed_by = models.ForeignKey(User, db_index=True)
redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True) redeemed_at = models.DateTimeField(default=datetime.now(pytz.utc), null=True)
@classmethod
def add_reg_code_redemption(cls, course_reg_code, order):
"""
add course registration code info into RegistrationCodeRedemption model
"""
cart_items = order.orderitem_set.all().select_subclasses()
for item in cart_items:
if getattr(item, 'course_id'):
if item.course_id == course_reg_code.course_id:
# If another account tries to use a existing registration code before the student checks out, an
# error message will appear.The reg code is un-reusable.
code_redemption = cls.objects.filter(registration_code=course_reg_code)
if code_redemption:
log.exception("Registration code '{0}' already used".format(course_reg_code.code))
raise RegCodeAlreadyExistException
code_redemption = RegistrationCodeRedemption(registration_code=course_reg_code, order=order, redeemed_by=order.user)
code_redemption.save()
item.list_price = item.unit_cost
item.unit_cost = 0
item.save()
log.info("Code '{0}' is used by user {1} against order id '{2}' "
.format(course_reg_code.code, order.user.username, order.id))
return course_reg_code
log.warning("Course item does not exist against registration code '{0}'".format(course_reg_code.code))
raise ItemDoesNotExistAgainstRegCodeException
class SoftDeleteCouponManager(models.Manager): class SoftDeleteCouponManager(models.Manager):
""" Use this manager to get objects that have a is_active=True """ """ Use this manager to get objects that have a is_active=True """
......
...@@ -14,7 +14,8 @@ if settings.FEATURES['ENABLE_SHOPPING_CART']: ...@@ -14,7 +14,8 @@ if settings.FEATURES['ENABLE_SHOPPING_CART']:
url(r'^clear/$', 'clear_cart'), url(r'^clear/$', 'clear_cart'),
url(r'^remove_item/$', 'remove_item'), url(r'^remove_item/$', 'remove_item'),
url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'), url(r'^add/course/{}/$'.format(settings.COURSE_ID_PATTERN), 'add_course_to_cart', name='add_course_to_cart'),
url(r'^use_coupon/$', 'use_coupon'), url(r'^use_code/$', 'use_code'),
url(r'^register_courses/$', 'register_courses'),
) )
if settings.FEATURES.get('ENABLE_PAYMENT_FAKE'): if settings.FEATURES.get('ENABLE_PAYMENT_FAKE'):
......
...@@ -14,8 +14,9 @@ from edxmako.shortcuts import render_to_response ...@@ -14,8 +14,9 @@ from edxmako.shortcuts import render_to_response
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport from shoppingcart.reports import RefundReport, ItemizedPurchaseReport, UniversityRevenueShareReport, CertificateStatusReport
from student.models import CourseEnrollment from student.models import CourseEnrollment
from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException, CouponAlreadyExistException, ItemDoesNotExistAgainstCouponException from .exceptions import ItemAlreadyInCartException, AlreadyEnrolledInCourseException, CourseDoesNotExistException, ReportTypeDoesNotExistException, \
from .models import Order, PaidCourseRegistration, OrderItem, Coupon, CouponRedemption CouponAlreadyExistException, ItemDoesNotExistAgainstCouponException, RegCodeAlreadyExistException, ItemDoesNotExistAgainstRegCodeException
from .models import Order, PaidCourseRegistration, OrderItem, Coupon, CouponRedemption, CourseRegistrationCode, RegistrationCodeRedemption
from .processors import process_postpay_callback, render_purchase_form_html from .processors import process_postpay_callback, render_purchase_form_html
import json import json
...@@ -97,6 +98,11 @@ def clear_cart(request): ...@@ -97,6 +98,11 @@ def clear_cart(request):
coupon_redemption.delete() coupon_redemption.delete()
log.info('Coupon redemption entry removed for user {0} for order {1}'.format(request.user, cart.id)) log.info('Coupon redemption entry removed for user {0} for order {1}'.format(request.user, cart.id))
reg_code_redemption = RegistrationCodeRedemption.objects.filter(redeemed_by=request.user, order=cart.id)
if reg_code_redemption:
reg_code_redemption.delete()
log.info('Registration code redemption entry removed for user {0} for order {1}'.format(request.user, cart.id))
return HttpResponse('Cleared') return HttpResponse('Cleared')
...@@ -111,43 +117,104 @@ def remove_item(request): ...@@ -111,43 +117,104 @@ def remove_item(request):
order_item_course_id = item.paidcourseregistration.course_id order_item_course_id = item.paidcourseregistration.course_id
item.delete() item.delete()
log.info('order item {0} removed for user {1}'.format(item_id, request.user)) log.info('order item {0} removed for user {1}'.format(item_id, request.user))
try: remove_code_redemption(order_item_course_id, item_id, item, request.user)
coupon_redemption = CouponRedemption.objects.get(user=request.user, order=item.order_id)
if order_item_course_id == coupon_redemption.coupon.course_id:
coupon_redemption.delete()
log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'
.format(coupon_redemption.coupon.code, request.user, item_id))
except CouponRedemption.DoesNotExist:
log.debug('Coupon redemption does not exist for order item id={0}.'.format(item_id))
except OrderItem.DoesNotExist: except OrderItem.DoesNotExist:
log.exception('Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(item_id)) log.exception('Cannot remove cart OrderItem id={0}. DoesNotExist or item is already purchased'.format(item_id))
return HttpResponse('OK') return HttpResponse('OK')
def remove_code_redemption(order_item_course_id, item_id, item, user):
"""
If an item removed from shopping cart then we will remove
the corresponding redemption info of coupon/registration code.
"""
try:
# Try to remove redemption information of coupon code, If exist.
coupon_redemption = CouponRedemption.objects.get(user=user, order=item.order_id)
except CouponRedemption.DoesNotExist:
try:
# Try to remove redemption information of registration code, If exist.
reg_code_redemption = RegistrationCodeRedemption.objects.get(redeemed_by=user, order=item.order_id)
except RegistrationCodeRedemption.DoesNotExist:
log.debug('Code redemption does not exist for order item id={0}.'.format(item_id))
else:
if order_item_course_id == reg_code_redemption.registration_code.course_id:
reg_code_redemption.delete()
log.info('Registration code "{0}" redemption entry removed for user "{1}" for order item "{2}"'
.format(reg_code_redemption.registration_code.code, user, item_id))
else:
if order_item_course_id == coupon_redemption.coupon.course_id:
coupon_redemption.delete()
log.info('Coupon "{0}" redemption entry removed for user "{1}" for order item "{2}"'
.format(coupon_redemption.coupon.code, user, item_id))
@login_required @login_required
def use_coupon(request): def use_code(request):
""" """
This method generate discount against valid coupon code and save its entry into coupon redemption table This method may generate the discount against valid coupon code
and save its entry into coupon redemption table
OR
Make the cart item free of cost against valid registration code.
Valid Code can be either coupon or registration code.
""" """
coupon_code = request.POST["coupon_code"] code = request.POST["code"]
try: try:
coupon = Coupon.objects.get(code=coupon_code) coupon = Coupon.objects.get(code=code)
except Coupon.DoesNotExist: except Coupon.DoesNotExist:
return HttpResponseNotFound(_("Discount does not exist against coupon '{0}'.".format(coupon_code))) # If not coupon code then we check that code against course registration code
try:
course_reg = CourseRegistrationCode.objects.get(code=code)
except CourseRegistrationCode.DoesNotExist:
return HttpResponseNotFound(_("Discount does not exist against code '{0}'.".format(code)))
return use_registration_code(course_reg, request.user)
return use_coupon_code(coupon, request.user)
def use_registration_code(course_reg, user):
"""
This method utilize course registration code
"""
try:
cart = Order.get_cart_for_user(user)
RegistrationCodeRedemption.add_reg_code_redemption(course_reg, cart)
except RegCodeAlreadyExistException:
return HttpResponseBadRequest(_("Oops! The code '{0}' you entered is either invalid or expired".format(course_reg.code)))
except ItemDoesNotExistAgainstRegCodeException:
return HttpResponseNotFound(_("Code '{0}' is not valid for any course in the shopping cart.".format(course_reg.code)))
return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json")
def use_coupon_code(coupon, user):
"""
This method utilize course coupon code
"""
if coupon.is_active: if coupon.is_active:
try: try:
cart = Order.get_cart_for_user(request.user) cart = Order.get_cart_for_user(user)
CouponRedemption.add_coupon_redemption(coupon, cart) CouponRedemption.add_coupon_redemption(coupon, cart)
except CouponAlreadyExistException: except CouponAlreadyExistException:
return HttpResponseBadRequest(_("Coupon '{0}' already used.".format(coupon_code))) return HttpResponseBadRequest(_("Coupon '{0}' already used.".format(coupon.code)))
except ItemDoesNotExistAgainstCouponException: except ItemDoesNotExistAgainstCouponException:
return HttpResponseNotFound(_("Coupon '{0}' is not valid for any course in the shopping cart.".format(coupon_code))) return HttpResponseNotFound(_("Coupon '{0}' is not valid for any course in the shopping cart.".format(coupon.code)))
response = HttpResponse(json.dumps({'response': 'success'}), content_type="application/json") return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json")
return response
else: else:
return HttpResponseBadRequest(_("Coupon '{0}' is inactive.".format(coupon_code))) return HttpResponseBadRequest(_("Coupon '{0}' is inactive.".format(coupon.code)))
@login_required
def register_courses(request):
"""
This method enroll the user for available course(s)
in cart on which valid registration code is applied
"""
cart = Order.get_cart_for_user(request.user)
CourseRegistrationCode.free_user_enrollment(cart)
return HttpResponse(json.dumps({'response': 'success'}), content_type="application/json")
@csrf_exempt @csrf_exempt
......
...@@ -63,10 +63,6 @@ ...@@ -63,10 +63,6 @@
height: 35px; height: 35px;
border-bottom: 1px solid #BEBEBE; border-bottom: 1px solid #BEBEBE;
th:nth-child(5),th:first-child{
text-align: center;
width: 120px;
}
th { th {
text-align: left; text-align: left;
border-bottom: 1px solid $border-color-1; border-bottom: 1px solid $border-color-1;
...@@ -75,17 +71,19 @@ ...@@ -75,17 +71,19 @@
width: 100px; width: 100px;
} }
&.u-pr { &.u-pr {
width: 100px; width: 70px;
} }
&.prc { &.prc {
width: 150px; width: 150px;
} }
&.cur { &.cur {
width: 100px; width: 100px;
text-align: center;
} }
&.dsc{ &.dsc{
width: 640px; width: 640px;
padding-right: 50px; padding-right: 50px;
text-align: left;
} }
} }
} }
...@@ -96,22 +94,25 @@ ...@@ -96,22 +94,25 @@
position: relative; position: relative;
line-height: normal; line-height: normal;
span.old-price{ span.old-price{
left: -75px;
position: relative; position: relative;
text-decoration: line-through; text-decoration: line-through;
color: red; color: red;
font-size: 12px; font-size: 12px;
top: -1px; top: -1px;
margin-left: 3px;
} }
} }
td:nth-child(5),td:first-child{ td:nth-child(3){
text-align: center;
}
td:last-child{
width: 50px;
text-align: center; text-align: center;
} }
td:nth-child(2){ td:nth-child(1){
line-height: 22px; line-height: 22px;
padding-right: 50px; text-align: left;
padding-right: 20px;
} }
} }
......
...@@ -13,17 +13,15 @@ ...@@ -13,17 +13,15 @@
<table class="cart-table"> <table class="cart-table">
<thead> <thead>
<tr class="cart-headings"> <tr class="cart-headings">
<th class="qty">${_("Quantity")}</th>
<th class="dsc">${_("Description")}</th> <th class="dsc">${_("Description")}</th>
<th class="u-pr">${_("Unit Price")}</th> <th class="u-pr">${_("Price")}</th>
<th class="prc">${_("Price")}</th>
<th class="cur">${_("Currency")}</th> <th class="cur">${_("Currency")}</th>
<th>&nbsp;</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
% for item in shoppingcart_items: % for item in shoppingcart_items:
<tr class="cart-items"> <tr class="cart-items">
<td>${item.qty}</td>
<td>${item.line_desc}</td> <td>${item.line_desc}</td>
<td> <td>
${"{0:0.2f}".format(item.unit_cost)} ${"{0:0.2f}".format(item.unit_cost)}
...@@ -31,14 +29,12 @@ ...@@ -31,14 +29,12 @@
<span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span> <span class="old-price"> ${"{0:0.2f}".format(item.list_price)}</span>
% endif % endif
</td> </td>
<td>${"{0:0.2f}".format(item.line_cost)}</td>
<td>${item.currency.upper()}</td> <td>${item.currency.upper()}</td>
<td><a data-item-id="${item.id}" class='remove_line_item' href='#'>[x]</a></td> <td><a data-item-id="${item.id}" class='remove_line_item' href='#'>[x]</a></td>
</tr> </tr>
% endfor % endfor
<tr class="always-gray"> <tr class="always-gray">
<td colspan="3"></td> <td colspan="4" valign="middle" class="cart-total" align="right">
<td colspan="3" valign="middle" class="cart-total" align="right">
<b>${_("Total Amount")}: <span> ${"{0:0.2f}".format(amount)} </span> </b> <b>${_("Total Amount")}: <span> ${"{0:0.2f}".format(amount)} </span> </b>
</td> </td>
</tr> </tr>
...@@ -47,11 +43,15 @@ ...@@ -47,11 +43,15 @@
<tfoot> <tfoot>
<tr class="always-white"> <tr class="always-white">
<td colspan="2"> <td colspan="2">
<input type="text" placeholder="Enter coupon code here" name="coupon_code" id="couponCode"> <input type="text" placeholder="Enter code here" name="cart_code" id="code">
<input type="button" value="Use Coupon" id="cart-coupon"> <input type="button" value="Apply Code" id="cart-code">
</td> </td>
<td colspan="4" align="right"> <td colspan="4" align="right">
${form_html} % if amount == 0:
<input type="button" value = "Register" id="register" >
% else:
${form_html}
%endif
</td> </td>
</tr> </tr>
...@@ -76,14 +76,14 @@ ...@@ -76,14 +76,14 @@
}); });
}); });
$('#cart-coupon').click(function(event){ $('#cart-code').click(function(event){
event.preventDefault(); event.preventDefault();
var post_url = "${reverse('shoppingcart.views.use_coupon')}"; var post_url = "${reverse('shoppingcart.views.use_code')}";
$.post(post_url,{ $.post(post_url,{
"coupon_code" : $('#couponCode').val(), "code" : $('#code').val(),
beforeSend: function(xhr, options){ beforeSend: function(xhr, options){
if($('#couponCode').val() == "") { if($('#code').val() == "") {
showErrorMsgs('Must contain a valid coupon code') showErrorMsgs('Must enter a valid code')
xhr.abort(); xhr.abort();
} }
} }
...@@ -101,6 +101,22 @@ ...@@ -101,6 +101,22 @@
}) })
}); });
$('#register').click(function(event){
event.preventDefault();
var post_url = "${reverse('shoppingcart.views.register_courses')}";
$.post(post_url)
.success(function(data) {
window.location.href = "${reverse('dashboard')}";
})
.error(function(data,status) {
if(status=="parsererror"){
location.reload(true);
}else{
showErrorMsgs(data.responseText)
}
})
});
$('#back_input').click(function(){ $('#back_input').click(function(){
history.back(); history.back();
}); });
...@@ -110,5 +126,4 @@ ...@@ -110,5 +126,4 @@
$("#cart-error").html(msg); $("#cart-error").html(msg);
} }
}); });
</script> </script>
\ No newline at end of file
...@@ -86,16 +86,18 @@ ...@@ -86,16 +86,18 @@
${_("Note: items with strikethough like <del>this</del> have been refunded.")} ${_("Note: items with strikethough like <del>this</del> have been refunded.")}
</p> </p>
% endif % endif
% if order.total_cost > 0:
<h2>${_("Billed To:")}</h2> <h2>${_("Billed To:")}</h2>
<p> <p>
${order.bill_to_cardtype} ${_("#:")} ${order.bill_to_ccnum}<br /> ${order.bill_to_cardtype} ${_("#:")} ${order.bill_to_ccnum}<br />
${order.bill_to_first} ${order.bill_to_last}<br /> ${order.bill_to_first} ${order.bill_to_last}<br />
${order.bill_to_street1}<br /> ${order.bill_to_street1}<br />
${order.bill_to_street2}<br /> ${order.bill_to_street2}<br />
${order.bill_to_city}, ${order.bill_to_state} ${order.bill_to_postalcode}<br /> ${order.bill_to_city}, ${order.bill_to_state} ${order.bill_to_postalcode}<br />
${order.bill_to_country.upper()}<br /> ${order.bill_to_country.upper()}<br />
</p> </p>
% endif
</article>
</div> </div>
</section> </section>
</div> </div>
......
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