Merge pull request #18979 from edx/feanil/remove_old_cybersource_processor

Remove the cybersource processor that was deprecated in 2014.
This commit is contained in:
Feanil Patel
2018-09-21 10:55:59 -04:00
committed by GitHub
3 changed files with 1 additions and 822 deletions

View File

@@ -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": "<shared secret>",
"MERCHANT_ID": "<merchant ID>",
"SERIAL_NUMBER": "<serial number>",
"PURCHASE_ENDPOINT": "<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='<span class="decision">{}</span>'.format(params['decision']),
reason_text='<span class="reason">{code}:{msg}</span>'.format(
code=params['reasonCode'], msg=REASONCODE_MAP[params['reasonCode']],
),
email=payment_support_email,
)
return '<p class="error_msg">{}</p>'.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='<span class="exception_msg">{msg}</span>'.format(
msg=text_type(exception),
),
email=payment_support_email,
)
return '<p class="error_msg">{}</p>'.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='<span class="exception_msg">{msg}</span>'.format(
msg=text_type(exception),
),
email=payment_support_email,
)
return '<p class="error_msg">{}</p>'.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='<span class="exception_msg">{msg}</span>'.format(
msg=text_type(exception),
),
email=payment_support_email,
)
return '<p class="error_msg">{}</p>'.format(formatted)
# fallthrough case, which basically never happens
return '<p class="error_msg">EXCEPTION!</p>'
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.
""")),
}
)

View File

@@ -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'])

View File

@@ -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": '',