From cf9d42772fdf080f878653f3838dbbf98de0447f Mon Sep 17 00:00:00 2001 From: Jason Bau Date: Fri, 9 Aug 2013 11:01:32 -0700 Subject: [PATCH] factor out cybersource processor from cart --- lms/djangoapps/shoppingcart/models.py | 3 +- .../shoppingcart/processors/CyberSource.py | 81 +++++++++++++++++++ .../shoppingcart/processors/__init__.py | 34 ++++++++ lms/djangoapps/shoppingcart/views.py | 73 ++--------------- lms/envs/aws.py | 2 +- lms/envs/common.py | 16 ++-- .../shoppingcart/cybersource_form.html | 6 ++ lms/templates/shoppingcart/list.html | 7 +- 8 files changed, 140 insertions(+), 82 deletions(-) create mode 100644 lms/djangoapps/shoppingcart/processors/CyberSource.py create mode 100644 lms/djangoapps/shoppingcart/processors/__init__.py create mode 100644 lms/templates/shoppingcart/cybersource_form.html diff --git a/lms/djangoapps/shoppingcart/models.py b/lms/djangoapps/shoppingcart/models.py index f9da7082e1..0bf4e3934e 100644 --- a/lms/djangoapps/shoppingcart/models.py +++ b/lms/djangoapps/shoppingcart/models.py @@ -4,6 +4,7 @@ from datetime import datetime from django.db import models from django.core.exceptions import ObjectDoesNotExist from django.contrib.auth.models import User +from courseware.courses import course_image_url, get_course_about_section from student.views import course_from_id from student.models import CourseEnrollmentAllowed, CourseEnrollment from statsd import statsd @@ -152,7 +153,7 @@ class PaidCourseRegistration(OrderItem): item.qty = 1 item.unit_cost = cost item.line_cost = cost - item.line_desc = "Registration for Course {0}".format(course_id) + item.line_desc = 'Registration for Course: {0}'.format(get_course_about_section(course, "title")) item.currency = currency item.save() return item diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource.py b/lms/djangoapps/shoppingcart/processors/CyberSource.py new file mode 100644 index 0000000000..98026e6a84 --- /dev/null +++ b/lms/djangoapps/shoppingcart/processors/CyberSource.py @@ -0,0 +1,81 @@ +### Implementation of support for the Cybersource Credit card processor +### The name of this file should be used as the key of the dict in the CC_PROCESSOR setting + +import time +import hmac +import binascii +from collections import OrderedDict +from hashlib import sha1 +from django.conf import settings +from mitxmako.shortcuts import render_to_string + +shared_secret = settings.CC_PROCESSOR['CyberSource'].get('SHARED_SECRET','') +merchant_id = settings.CC_PROCESSOR['CyberSource'].get('MERCHANT_ID','') +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 hash(value): + """ + Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page + """ + hash_obj = hmac.new(shared_secret, value, sha1) + return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want + + +def sign(params): + """ + params needs to be an ordered dict, b/c cybersource documentation states that order is important. + Reverse engineered from PHP version provided by cybersource + """ + params['merchantID'] = merchant_id + params['orderPage_timestamp'] = int(time.time()*1000) + params['orderPage_version'] = orderPage_version + params['orderPage_serialNumber'] = serial_number + fields = ",".join(params.keys()) + values = ",".join(["{0}={1}".format(i,params[i]) for i in params.keys()]) + fields_sig = hash(fields) + values += ",signedFieldsPublicSignature=" + fields_sig + params['orderPage_signaturePublic'] = hash(values) + params['orderPage_signedFields'] = fields + + return params + +def verify(params): + """ + Verify the signatures accompanying the POST back from Cybersource Hosted Order Page + """ + signed_fields = params.get('signedFields', '').split(',') + data = ",".join(["{0}={1}".format(k, params.get(k, '')) for k in signed_fields]) + signed_fields_sig = hash(params.get('signedFields', '')) + data += ",signedFieldsPublicSignature=" + signed_fields_sig + returned_sig = params.get('signedDataPublicSignature','') + if not returned_sig: + return False + return hash(data) == returned_sig + +def render_purchase_form_html(cart, user): + total_cost = cart.total_cost + amount = "{0:0.2f}".format(total_cost) + cart_items = cart.orderitem_set.all() + params = OrderedDict() + params['comment'] = 'Stanford OpenEdX Purchase' + params['amount'] = amount + params['currency'] = cart.currency + params['orderPage_transactionType'] = 'sale' + params['orderNumber'] = "{0:d}".format(cart.id) + params['billTo_email'] = user.email + idx=1 + for item in cart_items: + prefix = "item_{0:d}_".format(idx) + params[prefix+'productSKU'] = "{0:d}".format(item.id) + params[prefix+'quantity'] = item.qty + params[prefix+'productName'] = item.line_desc + params[prefix+'unitPrice'] = item.unit_cost + params[prefix+'taxAmount'] = "0.00" + signed_param_dict = sign(params) + + return render_to_string('shoppingcart/cybersource_form.html', { + 'action': purchase_endpoint, + 'params': signed_param_dict, + }) \ No newline at end of file diff --git a/lms/djangoapps/shoppingcart/processors/__init__.py b/lms/djangoapps/shoppingcart/processors/__init__.py new file mode 100644 index 0000000000..de567976be --- /dev/null +++ b/lms/djangoapps/shoppingcart/processors/__init__.py @@ -0,0 +1,34 @@ +from django.conf import settings + +### Now code that determines, using settings, which actual processor implementation we're using. +processor_name = settings.CC_PROCESSOR.keys()[0] +module = __import__('shoppingcart.processors.' + processor_name, + fromlist=['sign', 'verify', 'render_purchase_form_html']) + +def sign(*args, **kwargs): + """ + Given a dict (or OrderedDict) of parameters to send to the + credit card processor, signs them in the manner expected by + the processor + + Returns a dict containing the signature + """ + return module.sign(*args, **kwargs) + +def verify(*args, **kwargs): + """ + Given a dict (or OrderedDict) of parameters to returned by the + credit card processor, verifies them in the manner specified by + the processor + + Returns a boolean + """ + 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) diff --git a/lms/djangoapps/shoppingcart/views.py b/lms/djangoapps/shoppingcart/views.py index d81de1a68c..00e6db0e7d 100644 --- a/lms/djangoapps/shoppingcart/views.py +++ b/lms/djangoapps/shoppingcart/views.py @@ -1,26 +1,14 @@ import logging -import random -import time -import hmac -import binascii -from hashlib import sha1 -from collections import OrderedDict -from django.conf import settings from django.http import HttpResponse, HttpResponseRedirect from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.decorators import login_required from mitxmako.shortcuts import render_to_response from .models import * +from .processors import verify, render_purchase_form_html log = logging.getLogger("shoppingcart") -shared_secret = settings.CYBERSOURCE.get('SHARED_SECRET','') -merchant_id = settings.CYBERSOURCE.get('MERCHANT_ID','') -serial_number = settings.CYBERSOURCE.get('SERIAL_NUMBER','') -orderPage_version = settings.CYBERSOURCE.get('ORDERPAGE_VERSION','7') - - def test(request, course_id): item1 = PaidCourseRegistration(course_id, 200) item1.purchased_callback(request.user.id) @@ -47,26 +35,11 @@ def show_cart(request): total_cost = cart.total_cost amount = "{0:0.2f}".format(total_cost) cart_items = cart.orderitem_set.all() - params = OrderedDict() - params['comment'] = 'Stanford OpenEdX Purchase' - params['amount'] = amount - params['currency'] = cart.currency - params['orderPage_transactionType'] = 'sale' - params['orderNumber'] = "{0:d}".format(cart.id) - params['billTo_email'] = request.user.email - idx=1 - for item in cart_items: - prefix = "item_{0:d}_".format(idx) - params[prefix+'productSKU'] = "{0:d}".format(item.id) - params[prefix+'quantity'] = item.qty - params[prefix+'productName'] = item.line_desc - params[prefix+'unitPrice'] = item.unit_cost - params[prefix+'taxAmount'] = "0.00" - signed_param_dict = cybersource_sign(params) + form_html = render_purchase_form_html(cart, request.user) return render_to_response("shoppingcart/list.html", {'shoppingcart_items': cart_items, 'amount': amount, - 'params': signed_param_dict, + 'form_html': form_html, }) @login_required @@ -89,47 +62,11 @@ def remove_item(request): @csrf_exempt def receipt(request): """ - Receives the POST-back from Cybersource and performs the validation and displays a receipt + Receives the POST-back from processor and performs the validation and displays a receipt and does some other stuff """ - if cybersource_verify(request.POST): + if verify(request.POST.dict()): return HttpResponse("Validated") else: return HttpResponse("Not Validated") -def cybersource_hash(value): - """ - Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page - """ - hash_obj = hmac.new(shared_secret, value, sha1) - return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want - - -def cybersource_sign(params): - """ - params needs to be an ordered dict, b/c cybersource documentation states that order is important. - Reverse engineered from PHP version provided by cybersource - """ - params['merchantID'] = merchant_id - params['orderPage_timestamp'] = int(time.time()*1000) - params['orderPage_version'] = orderPage_version - params['orderPage_serialNumber'] = serial_number - fields = ",".join(params.keys()) - values = ",".join(["{0}={1}".format(i,params[i]) for i in params.keys()]) - fields_sig = cybersource_hash(fields) - values += ",signedFieldsPublicSignature=" + fields_sig - params['orderPage_signaturePublic'] = cybersource_hash(values) - params['orderPage_signedFields'] = fields - - return params - -def cybersource_verify(params): - signed_fields = params.get('signedFields', '').split(',') - data = ",".join(["{0}={1}".format(k, params.get(k, '')) for k in signed_fields]) - signed_fields_sig = cybersource_hash(params.get('signedFields', '')) - data += ",signedFieldsPublicSignature=" + signed_fields_sig - returned_sig = params.get('signedDataPublicSignature','') - if not returned_sig: - return False - return cybersource_hash(data) == returned_sig - diff --git a/lms/envs/aws.py b/lms/envs/aws.py index cc0e956b0c..f7c6db39f9 100644 --- a/lms/envs/aws.py +++ b/lms/envs/aws.py @@ -191,7 +191,7 @@ if SEGMENT_IO_LMS_KEY: MITX_FEATURES['SEGMENT_IO_LMS'] = ENV_TOKENS.get('SEGMENT_IO_LMS', False) -CYBERSOURCE = AUTH_TOKENS.get('CYBERSOURCE', CYBERSOURCE) +CC_PROCESSOR = AUTH_TOKENS.get('CC_PROCESSOR', CC_PROCESSOR) SECRET_KEY = AUTH_TOKENS['SECRET_KEY'] diff --git a/lms/envs/common.py b/lms/envs/common.py index 420068f7bd..d066259fe5 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -431,12 +431,16 @@ ZENDESK_URL = None ZENDESK_USER = None ZENDESK_API_KEY = None -##### CyberSource Payment parameters ##### -CYBERSOURCE = { - 'SHARED_SECRET': '', - 'MERCHANT_ID' : '', - 'SERIAL_NUMBER' : '', - 'ORDERPAGE_VERSION': '7', +##### shoppingcart Payment ##### +##### Using cybersource by default ##### +CC_PROCESSOR = { + 'CyberSource' : { + 'SHARED_SECRET': '', + 'MERCHANT_ID' : '', + 'SERIAL_NUMBER' : '', + 'ORDERPAGE_VERSION': '7', + 'PURCHASE_ENDPOINT': '', + } } ################################# open ended grading config ##################### diff --git a/lms/templates/shoppingcart/cybersource_form.html b/lms/templates/shoppingcart/cybersource_form.html new file mode 100644 index 0000000000..b29ea79aa1 --- /dev/null +++ b/lms/templates/shoppingcart/cybersource_form.html @@ -0,0 +1,6 @@ +
+ % for pk, pv in params.iteritems(): + + % endfor + +
diff --git a/lms/templates/shoppingcart/list.html b/lms/templates/shoppingcart/list.html index 7c3e7052ae..35623d8b5b 100644 --- a/lms/templates/shoppingcart/list.html +++ b/lms/templates/shoppingcart/list.html @@ -25,12 +25,7 @@ -
- % for pk, pv in params.iteritems(): - - % endfor - -
+ ${form_html} % else:

You have selected no items for purchase.

% endif