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):
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",
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
# 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):
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
a dispatcher to call the callback the proper subclasses, and as such it needs to know about all subclasses.
So please add
a dispatcher to call the callback the proper subclasses, and as such it needs to know about all
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:
#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.purchased_callback()
except (ObjectDoesNotExist, AttributeError):
......@@ -135,13 +137,18 @@ class OrderItem(models.Model):
.format(lc_classname))
pass
# Each entry is a tuple 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')
]
def is_of_subtype(self, cls):
"""
Checks if self is also a type of cls, in addition to being an OrderItem
Uses https://docs.djangoproject.com/en/1.4/topics/db/models/#multi-table-inheritance to test for subclass
"""
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):
......@@ -151,7 +158,16 @@ class PaidCourseRegistration(OrderItem):
course_id = models.CharField(max_length=128, db_index=True)
@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.
Will update the cost if called on an order that already carries the course.
......@@ -164,6 +180,10 @@ class PaidCourseRegistration(OrderItem):
item, created = cls.objects.get_or_create(order=order, user=order.user, course_id=course_id)
item.status = order.status
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.line_cost = cost
item.line_desc = 'Registration for Course: {0}'.format(get_course_about_section(course, "title"))
......@@ -182,9 +202,6 @@ class PaidCourseRegistration(OrderItem):
# 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
# 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)
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):
tags=["org:{0}".format(org),
"course:{0}".format(course_num),
"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
from django.utils.translation import ugettext as _
from mitxmako.shortcuts import render_to_string
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','')
merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID','')
......@@ -21,6 +21,42 @@ serial_number = settings.CC_PROCESSOR['CyberSource'].get('SERIAL_NUMBER','')
orderPage_version = settings.CC_PROCESSOR['CyberSource'].get('ORDERPAGE_VERSION','7')
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):
"""
Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page
......@@ -48,7 +84,7 @@ def sign(params):
return params
def verify(params):
def verify_signatures(params):
"""
Verify the signatures accompanying the POST back from Cybersource Hosted Order Page
"""
......@@ -161,6 +197,18 @@ def record_purchase(params, order):
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.update(
......
......@@ -8,8 +8,32 @@ module = __import__('shoppingcart.processors.' + processor_name,
'render_purchase_form_html'
'payment_accepted',
'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):
"""
Given a dict (or OrderedDict) of parameters to send to the
......@@ -30,14 +54,6 @@ def verify(*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):
"""
Given params returned by the CC processor, check that processor has accepted the payment
......
......@@ -7,5 +7,5 @@ class CCProcessorException(PaymentException):
class CCProcessorDataException(CCProcessorException):
pass
class CCProcessorWrongAmountException(PaymentException):
class CCProcessorWrongAmountException(CCProcessorException):
pass
\ No newline at end of file
......@@ -6,7 +6,6 @@ urlpatterns = patterns('shoppingcart.views', # nopep8
url(r'^add/course/(?P<course_id>[^/]+/[^/]+/[^/]+)/$','add_course_to_cart'),
url(r'^clear/$','clear_cart'),
url(r'^remove_item/$', 'remove_item'),
url(r'^purchased/$', 'purchased'),
url(r'^postpay_accept_callback/$', 'postpay_accept_callback'),
url(r'^postpay_callback/$', 'postpay_callback'), #Both the ~accept and ~reject callback pages are handled here
url(r'^receipt/(?P<ordernum>[0-9]*)/$', 'show_receipt'),
)
\ No newline at end of file
import logging
from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound, HttpResponseForbidden, Http404
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
from django.views.decorators.csrf import csrf_exempt
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 .models import *
from .processors import verify, payment_accepted, render_purchase_form_html, record_purchase
from .processors.exceptions import CCProcessorDataException, CCProcessorWrongAmountException
from .processors import process_postpay_callback, render_purchase_form_html
log = logging.getLogger("shoppingcart")
......@@ -16,20 +17,22 @@ def test(request, course_id):
item1.purchased_callback(request.user.id)
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):
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)
# TODO: Catch 500 here for course that does not exist, period
PaidCourseRegistration.add_to_order(cart, course_id, 200)
return HttpResponse("Added")
if PaidCourseRegistration.part_of_order(cart, course_id):
return HttpResponseNotFound(_('The course {0} is already in your cart.'.format(course_id)))
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
def show_cart(request):
......@@ -62,31 +65,23 @@ def remove_item(request):
return HttpResponse('OK')
@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
and does some other stuff
HANDLES THE ACCEPT AND REVIEW CASES
Receives the POST-back from processor.
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.
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
params = request.POST.dict()
if verify(params):
try:
result = payment_accepted(params)
if result['accepted']:
# ACCEPTED CASE first
record_purchase(params, result['order'])
#render_receipt
result = process_postpay_callback(request)
if result['success']:
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:
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):
"""
......@@ -107,7 +102,7 @@ def show_receipt(request, ordernum):
'order_items': order_items,
'any_refunds': any_refunds})
def show_orders(request):
#def show_orders(request):
"""
Displays all orders of a user
"""
......@@ -59,7 +59,6 @@
%endif
})(this)
</script>
......@@ -93,6 +92,7 @@
<strong>${_("View Courseware")}</strong>
</a>
%endif
%else:
<a href="#" class="register">${_("Register for {course.display_number_with_default}").format(course=course) | h}</a>
......
......@@ -25,7 +25,7 @@
</tbody>
</table>
<!-- <input id="back_input" type="submit" value="Return" /> -->
${form_html}
% else:
<p>${_("You have selected no items for purchase.")}</p>
......@@ -44,6 +44,10 @@
location.reload(true);
});
});
$('#back_input').click(function(){
history.back();
});
});
</script>
......@@ -6,7 +6,11 @@
<%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">
<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