diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource.py b/lms/djangoapps/shoppingcart/processors/CyberSource.py deleted file mode 100644 index 1133b6311b..0000000000 --- a/lms/djangoapps/shoppingcart/processors/CyberSource.py +++ /dev/null @@ -1,459 +0,0 @@ -""" -Implementation the CyberSource credit card processor. - -IMPORTANT: CyberSource will deprecate this version of the API ("Hosted Order Page") in September 2014. -We are keeping this implementation in the code-base for now, but we should -eventually replace this module with the newer implementation (in `CyberSource2.py`) - -To enable this implementation, add the following to Django settings: - - CC_PROCESSOR_NAME = "CyberSource" - CC_PROCESSOR = { - "CyberSource": { - "SHARED_SECRET": "", - "MERCHANT_ID": "", - "SERIAL_NUMBER": "", - "PURCHASE_ENDPOINT": "" - } - } - -""" -import binascii -import hmac -import json -import re -import time -from collections import OrderedDict, defaultdict -from decimal import Decimal, InvalidOperation -from hashlib import sha1 -from textwrap import dedent - -from django.conf import settings -from django.utils.translation import ugettext as _ -from six import text_type - -from edxmako.shortcuts import render_to_string -from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers -from shoppingcart.models import Order -from shoppingcart.processors.exceptions import * -from shoppingcart.processors.helpers import get_processor_config - - -def process_postpay_callback(params, **kwargs): - """ - 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. - """ - try: - verify_signatures(params) - 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_decline_html(params)} - except CCProcessorException as error: - return {'success': False, - 'order': None, # due to exception we may not have the order - 'error_html': get_processor_exception_html(error)} - - -def processor_hash(value): - """ - Performs the base64(HMAC_SHA1(key, value)) used by CyberSource Hosted Order Page - """ - shared_secret = get_processor_config().get('SHARED_SECRET', '') - hash_obj = hmac.new(shared_secret.encode('utf-8'), value.encode('utf-8'), sha1) - return binascii.b2a_base64(hash_obj.digest())[:-1] # last character is a '\n', which we don't want - - -def sign(params, signed_fields_key='orderPage_signedFields', full_sig_key='orderPage_signaturePublic'): - """ - params needs to be an ordered dict, b/c cybersource documentation states that order is important. - Reverse engineered from PHP version provided by cybersource - """ - merchant_id = get_processor_config().get('MERCHANT_ID', '') - order_page_version = get_processor_config().get('ORDERPAGE_VERSION', '7') - serial_number = get_processor_config().get('SERIAL_NUMBER', '') - - params['merchantID'] = merchant_id - params['orderPage_timestamp'] = int(time.time() * 1000) - params['orderPage_version'] = order_page_version - params['orderPage_serialNumber'] = serial_number - fields = u",".join(params.keys()) - values = u",".join([u"{0}={1}".format(i, params[i]) for i in params.keys()]) - fields_sig = processor_hash(fields) - values += u",signedFieldsPublicSignature=" + fields_sig - params[full_sig_key] = processor_hash(values) - params[signed_fields_key] = fields - - return params - - -def verify_signatures(params, signed_fields_key='signedFields', full_sig_key='signedDataPublicSignature'): - """ - Verify the signatures accompanying the POST back from Cybersource Hosted Order Page - - returns silently if verified - - raises CCProcessorSignatureException if not verified - """ - signed_fields = params.get(signed_fields_key, '').split(',') - data = u",".join([u"{0}={1}".format(k, params.get(k, '')) for k in signed_fields]) - signed_fields_sig = processor_hash(params.get(signed_fields_key, '')) - data += u",signedFieldsPublicSignature=" + signed_fields_sig - returned_sig = params.get(full_sig_key, '') - if processor_hash(data) != returned_sig: - raise CCProcessorSignatureException() - - -def render_purchase_form_html(cart, **kwargs): - """ - Renders the HTML of the hidden POST form that must be used to initiate a purchase with CyberSource - """ - return render_to_string('shoppingcart/cybersource_form.html', { - 'action': get_purchase_endpoint(), - 'params': get_signed_purchase_params(cart), - }) - - -def get_signed_purchase_params(cart, **kwargs): - return sign(get_purchase_params(cart)) - - -def get_purchase_params(cart): - total_cost = cart.total_cost - amount = "{0:0.2f}".format(total_cost) - cart_items = cart.orderitem_set.all() - params = OrderedDict() - params['amount'] = amount - params['currency'] = cart.currency - params['orderPage_transactionType'] = 'sale' - params['orderNumber'] = "{0:d}".format(cart.id) - - return params - - -def get_purchase_endpoint(): - return get_processor_config().get('PURCHASE_ENDPOINT', '') - - -def payment_accepted(params): - """ - Check that cybersource has accepted the payment - params: a dictionary of POST parameters returned by CyberSource in their post-payment callback - - returns: true if the payment was correctly accepted, for the right amount - false if the payment was not accepted - - raises: CCProcessorDataException if the returned message did not provide required parameters - CCProcessorWrongAmountException if the amount charged is different than the order amount - - """ - #make sure required keys are present and convert their values to the right type - valid_params = {} - for key, key_type in [('orderNumber', int), - ('orderCurrency', str), - ('decision', str)]: - if key not in params: - raise CCProcessorDataException( - _("The payment processor did not return a required parameter: {0}").format(key) - ) - try: - valid_params[key] = key_type(params[key]) - except ValueError: - raise CCProcessorDataException( - _("The payment processor returned a badly-typed value {0} for param {1}.").format(params[key], key) - ) - - try: - order = Order.objects.get(id=valid_params['orderNumber']) - except Order.DoesNotExist: - raise CCProcessorDataException(_("The payment processor accepted an order whose number is not in our system.")) - - if valid_params['decision'] == 'ACCEPT': - try: - # Moved reading of charged_amount here from the valid_params loop above because - # only 'ACCEPT' messages have a 'ccAuthReply_amount' parameter - charged_amt = Decimal(params['ccAuthReply_amount']) - except InvalidOperation: - raise CCProcessorDataException( - _("The payment processor returned a badly-typed value {0} for param {1}.").format( - params['ccAuthReply_amount'], 'ccAuthReply_amount' - ) - ) - - if charged_amt == order.total_cost and valid_params['orderCurrency'] == order.currency: - return {'accepted': True, - 'amt_charged': charged_amt, - 'currency': valid_params['orderCurrency'], - 'order': order} - else: - raise CCProcessorWrongAmountException( - _("The amount charged by the processor {0} {1} is different than the total cost of the order {2} {3}.") - .format( - charged_amt, - valid_params['orderCurrency'], - order.total_cost, - order.currency - ) - ) - else: - return {'accepted': False, - 'amt_charged': 0, - 'currency': 'usd', - 'order': order} - - -def record_purchase(params, order): - """ - Record the purchase and run purchased_callbacks - """ - ccnum_str = params.get('card_accountNumber', '') - first_digit = re.search(r"\d", ccnum_str) - if first_digit: - ccnum = ccnum_str[first_digit.start():] - else: - ccnum = "####" - - order.purchase( - first=params.get('billTo_firstName', ''), - last=params.get('billTo_lastName', ''), - street1=params.get('billTo_street1', ''), - street2=params.get('billTo_street2', ''), - city=params.get('billTo_city', ''), - state=params.get('billTo_state', ''), - country=params.get('billTo_country', ''), - postalcode=params.get('billTo_postalCode', ''), - ccnum=ccnum, - cardtype=CARDTYPE_MAP[params.get('card_cardType', 'UNKNOWN')], - processor_reply_dump=json.dumps(params) - ) - - -def get_processor_decline_html(params): - """Have to parse through the error codes to return a helpful message""" - - # see if we have an override in the site configuration - payment_support_email = configuration_helpers.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL) - - msg = _( - "Sorry! Our payment processor did not accept your payment. " - "The decision they returned was {decision_text}, " - "and the reason was {reason_text}. " - "You were not charged. " - "Please try a different form of payment. " - "Contact us with payment-related questions at {email}." - ) - formatted = msg.format( - decision_text='{}'.format(params['decision']), - reason_text='{code}:{msg}'.format( - code=params['reasonCode'], msg=REASONCODE_MAP[params['reasonCode']], - ), - email=payment_support_email, - ) - return '

{}

'.format(formatted) - - -def get_processor_exception_html(exception): - """Return error HTML associated with exception""" - - # see if we have an override in the site configuration - payment_support_email = configuration_helpers.get_value('payment_support_email', settings.PAYMENT_SUPPORT_EMAIL) - if isinstance(exception, CCProcessorDataException): - msg = _( - "Sorry! Our payment processor sent us back a payment confirmation " - "that had inconsistent data!" - "We apologize that we cannot verify whether the charge went through " - "and take further action on your order." - "The specific error message is: {error_message}. " - "Your credit card may possibly have been charged. " - "Contact us with payment-specific questions at {email}." - ) - formatted = msg.format( - error_message='{msg}'.format( - msg=text_type(exception), - ), - email=payment_support_email, - ) - return '

{}

'.format(formatted) - elif isinstance(exception, CCProcessorWrongAmountException): - msg = _( - "Sorry! Due to an error your purchase was charged for " - "a different amount than the order total! " - "The specific error message is: {error_message}. " - "Your credit card has probably been charged. " - "Contact us with payment-specific questions at {email}." - ) - formatted = msg.format( - error_message='{msg}'.format( - msg=text_type(exception), - ), - email=payment_support_email, - ) - return '

{}

'.format(formatted) - elif isinstance(exception, CCProcessorSignatureException): - msg = _( - "Sorry! Our payment processor sent us back a corrupted message " - "regarding your charge, so we are unable to validate that " - "the message actually came from the payment processor. " - "The specific error message is: {error_message}. " - "We apologize that we cannot verify whether the charge went through " - "and take further action on your order. " - "Your credit card may possibly have been charged. " - "Contact us with payment-specific questions at {email}." - ) - formatted = msg.format( - error_message='{msg}'.format( - msg=text_type(exception), - ), - email=payment_support_email, - ) - return '

{}

'.format(formatted) - - # fallthrough case, which basically never happens - return '

EXCEPTION!

' - - -CARDTYPE_MAP = defaultdict(lambda: "UNKNOWN") -CARDTYPE_MAP.update( - { - '001': 'Visa', - '002': 'MasterCard', - '003': 'American Express', - '004': 'Discover', - '005': 'Diners Club', - '006': 'Carte Blanche', - '007': 'JCB', - '014': 'EnRoute', - '021': 'JAL', - '024': 'Maestro', - '031': 'Delta', - '033': 'Visa Electron', - '034': 'Dankort', - '035': 'Laser', - '036': 'Carte Bleue', - '037': 'Carta Si', - '042': 'Maestro', - '043': 'GE Money UK card' - } -) - -REASONCODE_MAP = defaultdict(lambda: "UNKNOWN REASON") -REASONCODE_MAP.update( - { - '100': _('Successful transaction.'), - '101': _('The request is missing one or more required fields.'), - '102': _('One or more fields in the request contains invalid data.'), - '104': dedent(_( - """ - The merchantReferenceCode sent with this authorization request matches the - merchantReferenceCode of another authorization request that you sent in the last 15 minutes. - Possible fix: retry the payment after 15 minutes. - """)), - '150': _('Error: General system failure. Possible fix: retry the payment after a few minutes.'), - '151': dedent(_( - """ - Error: The request was received but there was a server timeout. - This error does not include timeouts between the client and the server. - Possible fix: retry the payment after some time. - """)), - '152': dedent(_( - """ - Error: The request was received, but a service did not finish running in time - Possible fix: retry the payment after some time. - """)), - '201': _('The issuing bank has questions about the request. Possible fix: retry with another form of payment'), - '202': dedent(_( - """ - Expired card. You might also receive this if the expiration date you - provided does not match the date the issuing bank has on file. - Possible fix: retry with another form of payment - """)), - '203': dedent(_( - """ - General decline of the card. No other information provided by the issuing bank. - Possible fix: retry with another form of payment - """)), - '204': _('Insufficient funds in the account. Possible fix: retry with another form of payment'), - # 205 was Stolen or lost card. Might as well not show this message to the person using such a card. - '205': _('Unknown reason'), - '207': _('Issuing bank unavailable. Possible fix: retry again after a few minutes'), - '208': dedent(_( - """ - Inactive card or card not authorized for card-not-present transactions. - Possible fix: retry with another form of payment - """)), - '210': _('The card has reached the credit limit. Possible fix: retry with another form of payment'), - '211': _('Invalid card verification number. Possible fix: retry with another form of payment'), - # 221 was The customer matched an entry on the processor's negative file. - # Might as well not show this message to the person using such a card. - '221': _('Unknown reason'), - '231': _('Invalid account number. Possible fix: retry with another form of payment'), - '232': dedent(_( - """ - The card type is not accepted by the payment processor. - Possible fix: retry with another form of payment - """)), - '233': _('General decline by the processor. Possible fix: retry with another form of payment'), - '234': _( - "There is a problem with our CyberSource merchant configuration. Please let us know at {0}" - ).format(settings.PAYMENT_SUPPORT_EMAIL), - # reason code 235 only applies if we are processing a capture through the API. so we should never see it - '235': _('The requested amount exceeds the originally authorized amount.'), - '236': _('Processor Failure. Possible fix: retry the payment'), - # reason code 238 only applies if we are processing a capture through the API. so we should never see it - '238': _('The authorization has already been captured'), - # reason code 239 only applies if we are processing a capture or credit through the API, - # so we should never see it - '239': _('The requested transaction amount must match the previous transaction amount.'), - '240': dedent(_( - """ - The card type sent is invalid or does not correlate with the credit card number. - Possible fix: retry with the same card or another form of payment - """)), - # reason code 241 only applies when we are processing a capture or credit through the API, - # so we should never see it - '241': _('The request ID is invalid.'), - # reason code 242 occurs if there was not a previously successful authorization request or - # if the previously successful authorization has already been used by another capture request. - # This reason code only applies when we are processing a capture through the API - # so we should never see it - '242': dedent(_( - """ - You requested a capture through the API, but there is no corresponding, unused authorization record. - """)), - # we should never see 243 - '243': _('The transaction has already been settled or reversed.'), - # reason code 246 applies only if we are processing a void through the API. so we should never see it - '246': dedent(_( - """ - The capture or credit is not voidable because the capture or credit information has already been - submitted to your processor. Or, you requested a void for a type of transaction that cannot be voided. - """)), - # reason code 247 applies only if we are processing a void through the API. so we should never see it - '247': _('You requested a credit for a capture that was previously voided'), - '250': dedent(_( - """ - Error: The request was received, but there was a timeout at the payment processor. - Possible fix: retry the payment. - """)), - '520': dedent(_( - """ - The authorization request was approved by the issuing bank but declined by CyberSource.' - Possible fix: retry with a different form of payment. - """)), - } -) diff --git a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py deleted file mode 100644 index 1e4327f546..0000000000 --- a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource.py +++ /dev/null @@ -1,355 +0,0 @@ -""" -Tests for the CyberSource processor handler -""" -from collections import OrderedDict - -from django.conf import settings -from django.test import TestCase -from django.test.utils import override_settings -from mock import Mock, patch - -from shoppingcart.models import Order, OrderItem -from shoppingcart.processors.CyberSource import ( - REASONCODE_MAP, - get_processor_decline_html, - get_processor_exception_html, - payment_accepted, - process_postpay_callback, - processor_hash, - record_purchase, - render_purchase_form_html, - sign, - verify_signatures -) -from shoppingcart.processors.exceptions import ( - CCProcessorDataException, - CCProcessorException, - CCProcessorSignatureException, - CCProcessorWrongAmountException -) -from shoppingcart.processors.helpers import get_processor_config -from student.tests.factories import UserFactory - -TEST_CC_PROCESSOR_NAME = "CyberSource" -TEST_CC_PROCESSOR = { - 'CyberSource': { - 'SHARED_SECRET': 'secret', - 'MERCHANT_ID': 'edx_test', - 'SERIAL_NUMBER': '12345', - 'ORDERPAGE_VERSION': '7', - 'PURCHASE_ENDPOINT': '', - 'microsites': { - 'test_site': { - 'SHARED_SECRET': 'secret_override', - 'MERCHANT_ID': 'edx_test_override', - 'SERIAL_NUMBER': '12345_override', - 'ORDERPAGE_VERSION': '7', - 'PURCHASE_ENDPOINT': '', - } - } - } -} - - -def fake_site(name, default=None): # pylint: disable=unused-argument - """ - This is a test mocking function to return a site configuration - """ - if name == 'cybersource_config_key': - return 'test_site' - else: - return None - - -@override_settings( - CC_PROCESSOR_NAME=TEST_CC_PROCESSOR_NAME, - CC_PROCESSOR=TEST_CC_PROCESSOR -) -class CyberSourceTests(TestCase): - shard = 4 - - def test_override_settings(self): - self.assertEqual(settings.CC_PROCESSOR['CyberSource']['MERCHANT_ID'], 'edx_test') - self.assertEqual(settings.CC_PROCESSOR['CyberSource']['SHARED_SECRET'], 'secret') - - def test_site_no_override_settings(self): - self.assertEqual(get_processor_config()['MERCHANT_ID'], 'edx_test') - self.assertEqual(get_processor_config()['SHARED_SECRET'], 'secret') - - @patch("openedx.core.djangoapps.site_configuration.helpers.get_value", fake_site) - def test_site_override_settings(self): - self.assertEqual(get_processor_config()['MERCHANT_ID'], 'edx_test_override') - self.assertEqual(get_processor_config()['SHARED_SECRET'], 'secret_override') - - def test_hash(self): - """ - Tests the hash function. Basically just hardcodes the answer. - """ - self.assertEqual(processor_hash('test'), 'GqNJWF7X7L07nEhqMAZ+OVyks1Y=') - self.assertEqual(processor_hash('edx '), '/KowheysqM2PFYuxVKg0P8Flfk4=') - - def test_sign_then_verify(self): - """ - "loopback" test: - Tests the that the verify function verifies parameters signed by the sign function - """ - params = OrderedDict() - params['amount'] = "12.34" - params['currency'] = 'usd' - params['orderPage_transactionType'] = 'sale' - params['orderNumber'] = "567" - - verify_signatures(sign(params), signed_fields_key='orderPage_signedFields', - full_sig_key='orderPage_signaturePublic') - - # if the above verify_signature fails it will throw an exception, so basically we're just - # testing for the absence of that exception. the trivial assert below does that - self.assertEqual(1, 1) - - def test_sign_then_verify_unicode(self): - """ - Similar to the test above, which loops back to the original. - Testing to make sure we can handle unicode parameters - """ - params = { - 'card_accountNumber': '1234', - 'card_cardType': '001', - 'billTo_firstName': u'\u2699', - 'billTo_lastName': u"\u2603", - 'orderNumber': '1', - 'orderCurrency': 'usd', - 'decision': 'ACCEPT', - 'ccAuthReply_amount': '0.00' - } - - verify_signatures(sign(params), signed_fields_key='orderPage_signedFields', - full_sig_key='orderPage_signaturePublic') - # if the above verify_signature fails it will throw an exception, so basically we're just - # testing for the absence of that exception. the trivial assert below does that - self.assertEqual(1, 1) - - def test_verify_exception(self): - """ - Tests that failure to verify raises the proper CCProcessorSignatureException - """ - params = OrderedDict() - params['a'] = 'A' - params['b'] = 'B' - params['signedFields'] = 'A,B' - params['signedDataPublicSignature'] = 'WONTVERIFY' - - with self.assertRaises(CCProcessorSignatureException): - verify_signatures(params) - - def test_get_processor_decline_html(self): - """ - Tests the processor decline html message - """ - DECISION = 'REJECT' - for code, reason in REASONCODE_MAP.iteritems(): - params = { - 'decision': DECISION, - 'reasonCode': code, - } - html = get_processor_decline_html(params) - self.assertIn(DECISION, html) - self.assertIn(reason, html) - self.assertIn(code, html) - self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, html) - - def test_get_processor_exception_html(self): - """ - Tests the processor exception html message - """ - for type in [CCProcessorSignatureException, CCProcessorWrongAmountException, CCProcessorDataException]: - error_msg = "An exception message of with exception type {0}".format(str(type)) - exception = type(error_msg) - html = get_processor_exception_html(exception) - self.assertIn(settings.PAYMENT_SUPPORT_EMAIL, html) - self.assertIn('Sorry!', html) - self.assertIn(error_msg, html) - - # test base case - self.assertIn("EXCEPTION!", get_processor_exception_html(CCProcessorException())) - - def test_record_purchase(self): - """ - Tests record_purchase with good and without returned CCNum - """ - student1 = UserFactory() - student1.save() - student2 = UserFactory() - student2.save() - params_cc = {'card_accountNumber': '1234', 'card_cardType': '001', 'billTo_firstName': student1.first_name} - params_nocc = {'card_accountNumber': '', 'card_cardType': '002', 'billTo_firstName': student2.first_name} - order1 = Order.get_cart_for_user(student1) - order2 = Order.get_cart_for_user(student2) - record_purchase(params_cc, order1) - record_purchase(params_nocc, order2) - self.assertEqual(order1.bill_to_first, student1.first_name) - self.assertEqual(order1.status, 'purchased') - - order2 = Order.objects.get(user=student2) - self.assertEqual(order2.bill_to_first, student2.first_name) - self.assertEqual(order2.status, 'purchased') - - def test_payment_accepted_invalid_dict(self): - """ - Tests exception is thrown when params to payment_accepted don't have required key - or have an bad value - """ - baseline = { - 'orderNumber': '1', - 'orderCurrency': 'usd', - 'decision': 'ACCEPT', - } - wrong = { - 'orderNumber': 'k', - } - # tests for missing key - for key in baseline: - params = baseline.copy() - del params[key] - with self.assertRaises(CCProcessorDataException): - payment_accepted(params) - - # tests for keys with value that can't be converted to proper type - for key in wrong: - params = baseline.copy() - params[key] = wrong[key] - with self.assertRaises(CCProcessorDataException): - payment_accepted(params) - - def test_payment_accepted_order(self): - """ - Tests payment_accepted cases with an order - """ - student1 = UserFactory() - student1.save() - - order1 = Order.get_cart_for_user(student1) - params = { - 'card_accountNumber': '1234', - 'card_cardType': '001', - 'billTo_firstName': student1.first_name, - 'billTo_lastName': u"\u2603", - 'orderNumber': str(order1.id), - 'orderCurrency': 'usd', - 'decision': 'ACCEPT', - 'ccAuthReply_amount': '0.00' - } - - # tests for an order number that doesn't match up - params_bad_ordernum = params.copy() - params_bad_ordernum['orderNumber'] = str(order1.id + 10) - with self.assertRaises(CCProcessorDataException): - payment_accepted(params_bad_ordernum) - - # tests for a reply amount of the wrong type - params_wrong_type_amt = params.copy() - params_wrong_type_amt['ccAuthReply_amount'] = 'ab' - with self.assertRaises(CCProcessorDataException): - payment_accepted(params_wrong_type_amt) - - # tests for a reply amount of the wrong type - params_wrong_amt = params.copy() - params_wrong_amt['ccAuthReply_amount'] = '1.00' - with self.assertRaises(CCProcessorWrongAmountException): - payment_accepted(params_wrong_amt) - - # tests for a not accepted order - params_not_accepted = params.copy() - params_not_accepted['decision'] = "REJECT" - self.assertFalse(payment_accepted(params_not_accepted)['accepted']) - - # finally, tests an accepted order - self.assertTrue(payment_accepted(params)['accepted']) - - @patch('shoppingcart.processors.CyberSource.render_to_string', autospec=True) - def test_render_purchase_form_html(self, render): - """ - Tests the rendering of the purchase form - """ - student1 = UserFactory() - student1.save() - - order1 = Order.get_cart_for_user(student1) - item1 = OrderItem(order=order1, user=student1, unit_cost=1.0, line_cost=1.0) - item1.save() - render_purchase_form_html(order1) - ((template, context), render_kwargs) = render.call_args - - self.assertEqual(template, 'shoppingcart/cybersource_form.html') - self.assertDictContainsSubset({'amount': '1.00', - 'currency': 'usd', - 'orderPage_transactionType': 'sale', - 'orderNumber': str(order1.id)}, - context['params']) - - def test_process_postpay_exception(self): - """ - Tests the exception path of process_postpay_callback - """ - baseline = { - 'orderNumber': '1', - 'orderCurrency': 'usd', - 'decision': 'ACCEPT', - } - # tests for missing key - for key in baseline: - params = baseline.copy() - del params[key] - result = process_postpay_callback(params) - self.assertFalse(result['success']) - self.assertIsNone(result['order']) - self.assertIn('error_msg', result['error_html']) - - @patch('shoppingcart.processors.CyberSource.verify_signatures', Mock(return_value=True)) - def test_process_postpay_accepted(self): - """ - Tests the ACCEPTED path of process_postpay - """ - student1 = UserFactory() - student1.save() - - order1 = Order.get_cart_for_user(student1) - params = { - 'card_accountNumber': '1234', - 'card_cardType': '001', - 'billTo_firstName': student1.first_name, - 'orderNumber': str(order1.id), - 'orderCurrency': 'usd', - 'decision': 'ACCEPT', - 'ccAuthReply_amount': '0.00' - } - result = process_postpay_callback(params) - self.assertTrue(result['success']) - self.assertEqual(result['order'], order1) - order1 = Order.objects.get(id=order1.id) # reload from DB to capture side-effect of process_postpay_callback - self.assertEqual(order1.status, 'purchased') - self.assertFalse(result['error_html']) - - @patch('shoppingcart.processors.CyberSource.verify_signatures', Mock(return_value=True)) - def test_process_postpay_not_accepted(self): - """ - Tests the non-ACCEPTED path of process_postpay - """ - student1 = UserFactory() - student1.save() - - order1 = Order.get_cart_for_user(student1) - params = { - 'card_accountNumber': '1234', - 'card_cardType': '001', - 'billTo_firstName': student1.first_name, - 'orderNumber': str(order1.id), - 'orderCurrency': 'usd', - 'decision': 'REJECT', - 'ccAuthReply_amount': '0.00', - 'reasonCode': '207' - } - result = process_postpay_callback(params) - self.assertFalse(result['success']) - self.assertEqual(result['order'], order1) - self.assertEqual(order1.status, 'cart') - self.assertIn(REASONCODE_MAP['207'], result['error_html']) diff --git a/lms/envs/common.py b/lms/envs/common.py index 3a0c8f38de..33f026c16e 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1131,15 +1131,8 @@ PAYMENT_SUPPORT_EMAIL = 'payment@example.com' ##### Using cybersource by default ##### -CC_PROCESSOR_NAME = 'CyberSource' +CC_PROCESSOR_NAME = 'CyberSource2' CC_PROCESSOR = { - 'CyberSource': { - 'SHARED_SECRET': '', - 'MERCHANT_ID': '', - 'SERIAL_NUMBER': '', - 'ORDERPAGE_VERSION': '7', - 'PURCHASE_ENDPOINT': '', - }, 'CyberSource2': { "PURCHASE_ENDPOINT": '', "SECRET_KEY": '',