Commit a4f5f4e4 by Jason Bau Committed by Diana Huang

about page changes, refactor processor reply handling

parent 0b8f4144
...@@ -338,6 +338,7 @@ class CourseFields(object): ...@@ -338,6 +338,7 @@ class CourseFields(object):
show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True) show_timezone = Boolean(help="True if timezones should be shown on dates in the courseware", scope=Scope.settings, default=True)
enrollment_domain = String(help="External login method associated with user accounts allowed to register in course", enrollment_domain = String(help="External login method associated with user accounts allowed to register in course",
scope=Scope.settings) scope=Scope.settings)
enrollment_cost = Dict(scope=Scope.settings, default={'currency':'usd', 'cost':0})
# An extra property is used rather than the wiki_slug/number because # An extra property is used rather than the wiki_slug/number because
# there are courses that change the number for different runs. This allows # there are courses that change the number for different runs. This allows
......
xdescribe("A jsinput has:", function () {
beforeEach(function () {
$('#fixture').remove();
$.ajax({
async: false,
url: 'mainfixture.html',
success: function(data) {
$('body').append($(data));
}
});
});
describe("The jsinput constructor", function(){
var iframe1 = $(document).find('iframe')[0];
var testJsElem = jsinputConstructor({
id: 1,
elem: iframe1,
passive: false
});
it("Returns an object", function(){
expect(typeof(testJsElem)).toEqual('object');
});
it("Adds the object to the jsinput array", function() {
expect(jsinput.exists(1)).toBe(true);
});
describe("The returned object", function() {
it("Has a public 'update' method", function(){
expect(testJsElem.update).toBeDefined();
});
it("Returns an 'update' that is idempotent", function(){
var orig = testJsElem.update();
for (var i = 0; i++; i < 5) {
expect(testJsElem.update()).toEqual(orig);
}
});
it("Changes the parent's inputfield", function() {
testJsElem.update();
});
});
});
describe("The walkDOM functions", function() {
walkDOM();
it("Creates (at least) one object per iframe", function() {
jsinput.arr.length >= 2;
});
it("Does not create multiple objects with the same id", function() {
while (jsinput.arr.length > 0) {
var elem = jsinput.arr.pop();
expect(jsinput.exists(elem.id)).toBe(false);
}
});
});
})
...@@ -123,11 +123,13 @@ class OrderItem(models.Model): ...@@ -123,11 +123,13 @@ class OrderItem(models.Model):
Unfortunately the QuerySet used determines the class to be OrderItem, and not its most specific Unfortunately the QuerySet used determines the class to be OrderItem, and not its most specific
subclasses. That means this parent class implementation of purchased_callback needs to act as subclasses. That means this parent class implementation of purchased_callback needs to act as
a dispatcher to call the callback the proper subclasses, and as such it needs to know about all subclasses. a dispatcher to call the callback the proper subclasses, and as such it needs to know about all
So please add possible subclasses.
So keep ORDER_ITEM_SUBTYPES up-to-date
""" """
for classname, lc_classname in ORDER_ITEM_SUBTYPES: for cls, lc_classname in ORDER_ITEM_SUBTYPES.iteritems():
try: try:
#Uses https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance to test subclass
sub_instance = getattr(self,lc_classname) sub_instance = getattr(self,lc_classname)
sub_instance.purchased_callback() sub_instance.purchased_callback()
except (ObjectDoesNotExist, AttributeError): except (ObjectDoesNotExist, AttributeError):
...@@ -135,13 +137,18 @@ class OrderItem(models.Model): ...@@ -135,13 +137,18 @@ class OrderItem(models.Model):
.format(lc_classname)) .format(lc_classname))
pass pass
# Each entry is a tuple of ('ModelName', 'lower_case_model_name') def is_of_subtype(self, cls):
# See https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance for """
# PLEASE KEEP THIS LIST UP_TO_DATE WITH THE SUBCLASSES OF OrderItem Checks if self is also a type of cls, in addition to being an OrderItem
ORDER_ITEM_SUBTYPES = [ Uses https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance to test for subclass
('PaidCourseRegistration', 'paidcourseregistration') """
] if cls not in ORDER_ITEM_SUBTYPES:
return False
try:
getattr(self, ORDER_ITEM_SUBTYPES[cls])
return True
except (ObjectDoesNotExist, AttributeError):
return False
class PaidCourseRegistration(OrderItem): class PaidCourseRegistration(OrderItem):
...@@ -151,7 +158,16 @@ class PaidCourseRegistration(OrderItem): ...@@ -151,7 +158,16 @@ class PaidCourseRegistration(OrderItem):
course_id = models.CharField(max_length=128, db_index=True) course_id = models.CharField(max_length=128, db_index=True)
@classmethod @classmethod
def add_to_order(cls, order, course_id, cost, currency='usd'): def part_of_order(cls, order, course_id):
"""
Is the course defined by course_id in the order?
"""
return course_id in [item.paidcourseregistration.course_id
for item in order.orderitem_set.all()
if item.is_of_subtype(PaidCourseRegistration)]
@classmethod
def add_to_order(cls, order, course_id, cost=None, currency=None):
""" """
A standardized way to create these objects, with sensible defaults filled in. A standardized way to create these objects, with sensible defaults filled in.
Will update the cost if called on an order that already carries the course. Will update the cost if called on an order that already carries the course.
...@@ -164,6 +180,10 @@ class PaidCourseRegistration(OrderItem): ...@@ -164,6 +180,10 @@ class PaidCourseRegistration(OrderItem):
item, created = cls.objects.get_or_create(order=order, user=order.user, course_id=course_id) item, created = cls.objects.get_or_create(order=order, user=order.user, course_id=course_id)
item.status = order.status item.status = order.status
item.qty = 1 item.qty = 1
if cost is None:
cost = course.enrollment_cost['cost']
if currency is None:
currency = course.enrollment_cost['currency']
item.unit_cost = cost item.unit_cost = cost
item.line_cost = cost item.line_cost = cost
item.line_desc = 'Registration for Course: {0}'.format(get_course_about_section(course, "title")) item.line_desc = 'Registration for Course: {0}'.format(get_course_about_section(course, "title"))
...@@ -182,9 +202,6 @@ class PaidCourseRegistration(OrderItem): ...@@ -182,9 +202,6 @@ class PaidCourseRegistration(OrderItem):
# throw errors if it doesn't # throw errors if it doesn't
# use get_or_create here to gracefully handle case where the user is already enrolled in the course, for # use get_or_create here to gracefully handle case where the user is already enrolled in the course, for
# whatever reason. # whatever reason.
# Don't really need to create CourseEnrollmentAllowed object, but doing it for bookkeeping and consistency
# with rest of codebase.
CourseEnrollmentAllowed.objects.get_or_create(email=self.user.email, course_id=self.course_id, auto_enroll=True)
CourseEnrollment.objects.get_or_create(user=self.user, course_id=self.course_id) CourseEnrollment.objects.get_or_create(user=self.user, course_id=self.course_id)
log.info("Enrolled {0} in paid course {1}, paid ${2}".format(self.user.email, self.course_id, self.line_cost)) log.info("Enrolled {0} in paid course {1}, paid ${2}".format(self.user.email, self.course_id, self.line_cost))
...@@ -193,3 +210,11 @@ class PaidCourseRegistration(OrderItem): ...@@ -193,3 +210,11 @@ class PaidCourseRegistration(OrderItem):
tags=["org:{0}".format(org), tags=["org:{0}".format(org),
"course:{0}".format(course_num), "course:{0}".format(course_num),
"run:{0}".format(run)]) "run:{0}".format(run)])
# Each entry is a dictionary of ModelName: 'lower_case_model_name'
# See https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance for
# PLEASE KEEP THIS LIST UP_TO_DATE WITH THE SUBCLASSES OF OrderItem
ORDER_ITEM_SUBTYPES = {
PaidCourseRegistration: 'paidcourseregistration',
}
\ No newline at end of file
...@@ -13,7 +13,7 @@ from django.conf import settings ...@@ -13,7 +13,7 @@ from django.conf import settings
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from shoppingcart.models import Order from shoppingcart.models import Order
from .exceptions import CCProcessorDataException, CCProcessorWrongAmountException from .exceptions import CCProcessorException, CCProcessorDataException, CCProcessorWrongAmountException
shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET','') shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET','')
merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID','') merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID','')
...@@ -21,6 +21,42 @@ serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER','') ...@@ -21,6 +21,42 @@ serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER','')
orderPage_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION','7') orderPage_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION','7')
purchase_endpoint = settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT','') purchase_endpoint = settings.CC_PROCESSOR['CyberSource'].get('PURCHASE_ENDPOINT','')
def process_postpay_callback(request):
"""
The top level call to this module, basically
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external Hosted Order Page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
"""
params = request.POST.dict()
if verify_signatures(params):
try:
result = payment_accepted(params)
if result['accepted']:
# SUCCESS CASE first, rest are some sort of oddity
record_purchase(params, result['order'])
return {'success': True,
'order': result['order'],
'error_html': ''}
else:
return {'success': False,
'order': result['order'],
'error_html': get_processor_error_html(params)}
except CCProcessorException as e:
return {'success': False,
'order': None, #due to exception we may not have the order
'error_html': get_exception_html(params, e)}
else:
return {'success': False,
'order': None,
'error_html': get_signature_error_html(params)}
def hash(value): def hash(value):
""" """
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
...@@ -48,7 +84,7 @@ def sign(params): ...@@ -48,7 +84,7 @@ def sign(params):
return params return params
def verify(params): def verify_signatures(params):
""" """
Verify the signatures accompanying the POST back from Cybersource Hosted Order Page Verify the signatures accompanying the POST back from Cybersource Hosted Order Page
""" """
...@@ -161,6 +197,18 @@ def record_purchase(params, order): ...@@ -161,6 +197,18 @@ def record_purchase(params, order):
processor_reply_dump=json.dumps(params) processor_reply_dump=json.dumps(params)
) )
def get_processor_error_html(params):
"""Have to parse through the error codes for all the other cases"""
return "<p>ERROR!</p>"
def get_exception_html(params, exp):
"""Return error HTML associated with exception"""
return "<p>EXCEPTION!</p>"
def get_signature_error_html(params):
"""Return error HTML associated with signature failure"""
return "<p>EXCEPTION!</p>"
CARDTYPE_MAP = defaultdict(lambda:"UNKNOWN") CARDTYPE_MAP = defaultdict(lambda:"UNKNOWN")
CARDTYPE_MAP.update( CARDTYPE_MAP.update(
......
...@@ -8,8 +8,32 @@ module = __import__('shoppingcart.processors.' + processor_name, ...@@ -8,8 +8,32 @@ module = __import__('shoppingcart.processors.' + processor_name,
'render_purchase_form_html' 'render_purchase_form_html'
'payment_accepted', 'payment_accepted',
'record_purchase', 'record_purchase',
'process_postpay_callback',
]) ])
def render_purchase_form_html(*args, **kwargs):
"""
The top level call to this module to begin the purchase.
Given a shopping cart,
Renders the HTML form for display on user's browser, which POSTS to Hosted Processors
Returns the HTML as a string
"""
return module.render_purchase_form_html(*args, **kwargs)
def process_postpay_callback(*args, **kwargs):
"""
The top level call to this module after the purchase.
This function is handed the callback request after the customer has entered the CC info and clicked "buy"
on the external payment page.
It is expected to verify the callback and determine if the payment was successful.
It returns {'success':bool, 'order':Order, 'error_html':str}
If successful this function must have the side effect of marking the order purchased and calling the
purchased_callbacks of the cart items.
If unsuccessful this function should not have those side effects but should try to figure out why and
return a helpful-enough error message in error_html.
"""
return module.process_postpay_callback(*args, **kwargs)
def sign(*args, **kwargs): def sign(*args, **kwargs):
""" """
Given a dict (or OrderedDict) of parameters to send to the Given a dict (or OrderedDict) of parameters to send to the
...@@ -30,14 +54,6 @@ def verify(*args, **kwargs): ...@@ -30,14 +54,6 @@ def verify(*args, **kwargs):
""" """
return module.sign(*args, **kwargs) return module.sign(*args, **kwargs)
def render_purchase_form_html(*args, **kwargs):
"""
Given a shopping cart,
Renders the HTML form for display on user's browser, which POSTS to Hosted Processors
Returns the HTML as a string
"""
return module.render_purchase_form_html(*args, **kwargs)
def payment_accepted(*args, **kwargs): def payment_accepted(*args, **kwargs):
""" """
Given params returned by the CC processor, check that processor has accepted the payment Given params returned by the CC processor, check that processor has accepted the payment
......
...@@ -7,5 +7,5 @@ class CCProcessorException(PaymentException): ...@@ -7,5 +7,5 @@ class CCProcessorException(PaymentException):
class CCProcessorDataException(CCProcessorException): class CCProcessorDataException(CCProcessorException):
pass pass
class CCProcessorWrongAmountException(PaymentException): class CCProcessorWrongAmountException(CCProcessorException):
pass pass
\ No newline at end of file
...@@ -6,7 +6,6 @@ urlpatterns = patterns('shoppingcart.views', # nopep8 ...@@ -6,7 +6,6 @@ urlpatterns = patterns('shoppingcart.views', # nopep8
url(r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$','add_course_to_cart'), url(r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$','add_course_to_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'^purchased/$', 'purchased'), url(r'^postpay_callback/$', 'postpay_callback'), #Both the ~accept and ~reject callback pages are handled here
url(r'^postpay_accept_callback/$', 'postpay_accept_callback'),
url(r'^receipt/(?P<ordernum>[0-9]*)/$', 'show_receipt'), url(r'^receipt/(?P<ordernum>[0-9]*)/$', 'show_receipt'),
) )
\ No newline at end of file
import logging import logging
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponseForbidden, Http404
from django.http import HttpResponse, HttpResponseRedirect, Http404 from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from student.models import CourseEnrollment
from xmodule.modulestore.exceptions import ItemNotFoundError
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from .models import * from .models import *
from .processors import verify, payment_accepted, render_purchase_form_html, record_purchase from .processors import process_postpay_callback, render_purchase_form_html
from .processors.exceptions import CCProcessorDataException, CCProcessorWrongAmountException
log = logging.getLogger("shoppingcart") log = logging.getLogger("shoppingcart")
...@@ -16,20 +17,22 @@ def test(request, course_id): ...@@ -16,20 +17,22 @@ def test(request, course_id):
item1.purchased_callback(request.user.id) item1.purchased_callback(request.user.id)
return HttpResponse('OK') return HttpResponse('OK')
@login_required
def purchased(request):
#verify() -- signatures, total cost match up, etc. Need error handling code (
# If verify fails probaly need to display a contact email/number)
cart = Order.get_cart_for_user(request.user)
cart.purchase()
return HttpResponseRedirect('/')
@login_required
def add_course_to_cart(request, course_id): def add_course_to_cart(request, course_id):
if not request.user.is_authenticated():
return HttpResponseForbidden(_('You must be logged-in to add to a shopping cart'))
cart = Order.get_cart_for_user(request.user) cart = Order.get_cart_for_user(request.user)
# TODO: Catch 500 here for course that does not exist, period if PaidCourseRegistration.part_of_order(cart, course_id):
PaidCourseRegistration.add_to_order(cart, course_id, 200) return HttpResponseNotFound(_('The course {0} is already in your cart.'.format(course_id)))
return HttpResponse("Added") if CourseEnrollment.objects.filter(user=request.user, course_id=course_id).exists():
return HttpResponseNotFound(_('You are already registered in course {0}.'.format(course_id)))
try:
PaidCourseRegistration.add_to_order(cart, course_id)
except ItemNotFoundError:
return HttpResponseNotFound(_('The course you requested does not exist.'))
if request.method == 'GET':
return HttpResponseRedirect(reverse('shoppingcart.views.show_cart'))
return HttpResponse(_("Course added to cart."))
@login_required @login_required
def show_cart(request): def show_cart(request):
...@@ -62,31 +65,23 @@ def remove_item(request): ...@@ -62,31 +65,23 @@ def remove_item(request):
return HttpResponse('OK') return HttpResponse('OK')
@csrf_exempt @csrf_exempt
def postpay_accept_callback(request): def postpay_callback(request):
""" """
Receives the POST-back from processor and performs the validation and displays a receipt Receives the POST-back from processor.
and does some other stuff Mainly this calls the processor-specific code to check if the payment was accepted, and to record the order
if it was, and to generate an error page.
HANDLES THE ACCEPT AND REVIEW CASES If successful this function should have the side effect of changing the "cart" into a full "order" in the DB.
The cart can then render a success page which links to receipt pages.
If unsuccessful the order will be left untouched and HTML messages giving more detailed error info will be
returned.
""" """
# TODO: Templates and logic for all error cases and the REVIEW CASE result = process_postpay_callback(request)
params = request.POST.dict() if result['success']:
if verify(params): return HttpResponseRedirect(reverse('shoppingcart.views.show_receipt', args=[result['order'].id]))
try:
result = payment_accepted(params)
if result['accepted']:
# ACCEPTED CASE first
record_purchase(params, result['order'])
#render_receipt
return HttpResponseRedirect(reverse('shoppingcart.views.show_receipt', args=[result['order'].id]))
else:
return HttpResponse("CC Processor has not accepted the payment.")
except CCProcessorWrongAmountException:
return HttpResponse("Charged the wrong amount, contact our user support")
except CCProcessorDataException:
return HttpResponse("Exception: the processor returned invalid data")
else: else:
return HttpResponse("There has been a communication problem blah blah. Not Validated") return render_to_response('shoppingcart.processor_error.html', {'order':result['order'],
'error_html': result['error_html']})
def show_receipt(request, ordernum): def show_receipt(request, ordernum):
""" """
...@@ -107,7 +102,7 @@ def show_receipt(request, ordernum): ...@@ -107,7 +102,7 @@ def show_receipt(request, ordernum):
'order_items': order_items, 'order_items': order_items,
'any_refunds': any_refunds}) 'any_refunds': any_refunds})
def show_orders(request): #def show_orders(request):
""" """
Displays all orders of a user Displays all orders of a user
""" """
...@@ -59,7 +59,6 @@ ...@@ -59,7 +59,6 @@
%endif %endif
})(this) })(this)
</script> </script>
...@@ -93,6 +92,7 @@ ...@@ -93,6 +92,7 @@
<strong>${_("View Courseware")}</strong> <strong>${_("View Courseware")}</strong>
</a> </a>
%endif %endif
%else: %else:
<a href="#" class="register">${_("Register for {course.display_number_with_default}").format(course=course) | h}</a> <a href="#" class="register">${_("Register for {course.display_number_with_default}").format(course=course) | h}</a>
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
</tbody> </tbody>
</table> </table>
<!-- <input id="back_input" type="submit" value="Return" /> -->
${form_html} ${form_html}
% else: % else:
<p>${_("You have selected no items for purchase.")}</p> <p>${_("You have selected no items for purchase.")}</p>
...@@ -44,6 +44,10 @@ ...@@ -44,6 +44,10 @@
location.reload(true); location.reload(true);
}); });
}); });
$('#back_input').click(function(){
history.back();
});
}); });
</script> </script>
...@@ -6,7 +6,11 @@ ...@@ -6,7 +6,11 @@
<%block name="title"><title>${_("Receipt for Order")} ${order.id}</title></%block> <%block name="title"><title>${_("Receipt for Order")} ${order.id}</title></%block>
% if notification is not UNDEFINED:
<section class="notification">
${notification}
</section>
% endif
<section class="container cart-list"> <section class="container cart-list">
<p><h1>${_(settings.PLATFORM_NAME + " (" + settings.SITE_NAME + ")" + " Electronic Receipt")}</h1></p> <p><h1>${_(settings.PLATFORM_NAME + " (" + settings.SITE_NAME + ")" + " Electronic Receipt")}</h1></p>
......
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