Commit cb6a4db1 by Awais Qureshi

Merge pull request #6430 from edx/awais786/ECOM-662-decline-error

Awais786/ecom 662 decline error
parents f6186aed dfb366af
......@@ -142,6 +142,11 @@ def verify_signatures(params):
if params.get('decision') == u'CANCEL':
raise CCProcessorUserCancelled()
# if the user decline the transaction
# if so, then auth_amount will not be passed back so we can't yet verify signatures
if params.get('decision') == u'DECLINE':
raise CCProcessorUserDeclined()
# Validate the signature to ensure that the message is from CyberSource
# and has not been tampered with.
signed_fields = params.get('signed_field_names', '').split(',')
......@@ -520,6 +525,15 @@ def _get_processor_exception_html(exception):
email=payment_support_email
)
)
elif isinstance(exception, CCProcessorUserDeclined):
return _format_error_html(
_(
u"We're sorry, but this payment was declined. The items in your shopping cart have been saved. "
u"If you have any questions about this transaction, please contact us at {email}."
).format(
email=payment_support_email
)
)
else:
return _format_error_html(
_(
......
......@@ -19,3 +19,8 @@ class CCProcessorWrongAmountException(CCProcessorException):
class CCProcessorUserCancelled(CCProcessorException):
pass
class CCProcessorUserDeclined(CCProcessorException):
"""Transaction declined."""
pass
......@@ -37,6 +37,7 @@ class CyberSource2Test(TestCase):
COST = "10.00"
CALLBACK_URL = "/test_callback_url"
FAILED_DECISIONS = ["DECLINE", "CANCEL", "ERROR"]
def setUp(self):
""" Create a user and an order. """
......@@ -142,7 +143,7 @@ class CyberSource2Test(TestCase):
def test_process_payment_rejected(self):
# Simulate a callback from CyberSource indicating that the payment was rejected
params = self._signed_callback_params(self.order.id, self.COST, self.COST, accepted=False)
params = self._signed_callback_params(self.order.id, self.COST, self.COST, decision='REJECT')
result = process_postpay_callback(params)
# Expect that we get an error message
......@@ -263,7 +264,7 @@ class CyberSource2Test(TestCase):
def _signed_callback_params(
self, order_id, order_amount, paid_amount,
accepted=True, signature=None, card_number='xxxxxxxxxxxx1111',
decision='ACCEPT', signature=None, card_number='xxxxxxxxxxxx1111',
first_name='John'
):
"""
......@@ -281,7 +282,7 @@ class CyberSource2Test(TestCase):
Keyword Args:
accepted (bool): Whether the payment was accepted or rejected.
decision (string): Whether the payment was accepted or rejected or declined.
signature (string): If provided, use this value instead of calculating the signature.
card_numer (string): If provided, use this value instead of the default credit card number.
first_name (string): If provided, the first name of the user.
......@@ -292,9 +293,51 @@ class CyberSource2Test(TestCase):
"""
# Parameters sent from CyberSource to our callback implementation
# These were captured from the CC test server.
signed_field_names = ["transaction_id",
"decision",
"req_access_key",
"req_profile_id",
"req_transaction_uuid",
"req_transaction_type",
"req_reference_number",
"req_amount",
"req_currency",
"req_locale",
"req_payment_method",
"req_override_custom_receipt_page",
"req_bill_to_forename",
"req_bill_to_surname",
"req_bill_to_email",
"req_bill_to_address_line1",
"req_bill_to_address_city",
"req_bill_to_address_state",
"req_bill_to_address_country",
"req_bill_to_address_postal_code",
"req_card_number",
"req_card_type",
"req_card_expiry_date",
"message",
"reason_code",
"auth_avs_code",
"auth_avs_code_raw",
"auth_response",
"auth_amount",
"auth_code",
"auth_trans_ref_no",
"auth_time",
"bill_trans_ref_no",
"signed_field_names",
"signed_date_time"]
# if decision is in FAILED_DECISIONS list then remove auth_amount from
# signed_field_names list.
if decision in self.FAILED_DECISIONS:
signed_field_names.remove("auth_amount")
params = {
# Parameters that change based on the test
"decision": "ACCEPT" if accepted else "REJECT",
"decision": decision,
"req_reference_number": str(order_id),
"req_amount": order_amount,
"auth_amount": paid_amount,
......@@ -307,43 +350,7 @@ class CyberSource2Test(TestCase):
"req_card_expiry_date": "01-2018",
"bill_trans_ref_no": "85080648RYI23S6I",
"req_bill_to_address_state": "MA",
"signed_field_names": ",".join([
"transaction_id",
"decision",
"req_access_key",
"req_profile_id",
"req_transaction_uuid",
"req_transaction_type",
"req_reference_number",
"req_amount",
"req_currency",
"req_locale",
"req_payment_method",
"req_override_custom_receipt_page",
"req_bill_to_forename",
"req_bill_to_surname",
"req_bill_to_email",
"req_bill_to_address_line1",
"req_bill_to_address_city",
"req_bill_to_address_state",
"req_bill_to_address_country",
"req_bill_to_address_postal_code",
"req_card_number",
"req_card_type",
"req_card_expiry_date",
"message",
"reason_code",
"auth_avs_code",
"auth_avs_code_raw",
"auth_response",
"auth_amount",
"auth_code",
"auth_trans_ref_no",
"auth_time",
"bill_trans_ref_no",
"signed_field_names",
"signed_date_time"
]),
"signed_field_names": ",".join(signed_field_names),
"req_payment_method": "card",
"req_transaction_type": "sale",
"auth_code": "888888",
......@@ -370,6 +377,11 @@ class CyberSource2Test(TestCase):
"req_access_key": "abcd12345",
}
# if decision is in FAILED_DECISIONS list then remove the auth_amount from params dict
if decision in self.FAILED_DECISIONS:
del params["auth_amount"]
# Calculate the signature
params['signature'] = signature if signature is not None else self._signature(params)
return params
......@@ -398,3 +410,12 @@ class CyberSource2Test(TestCase):
in params['signed_field_names'].split(u",")
])
)
def test_process_payment_declined(self):
# Simulate a callback from CyberSource indicating that the payment was declined
params = self._signed_callback_params(self.order.id, self.COST, self.COST, decision='DECLINE')
result = process_postpay_callback(params)
# Expect that we get an error message
self.assertFalse(result['success'])
self.assertIn(u"payment was declined", result['error_html'])
......@@ -78,7 +78,7 @@ class PaymentFakeView(View):
"""
new_status = request.body
if new_status not in ["success", "failure"]:
if new_status not in ["success", "failure", "decline"]:
return HttpResponseBadRequest()
else:
......@@ -109,9 +109,17 @@ class PaymentFakeView(View):
"""
Calculate the POST params we want to send back to the client.
"""
if cls.PAYMENT_STATUS_RESPONSE == "success":
decision = "ACCEPT"
elif cls.PAYMENT_STATUS_RESPONSE == "decline":
decision = "DECLINE"
else:
decision = "REJECT"
resp_params = {
# Indicate whether the payment was successful
"decision": "ACCEPT" if cls.PAYMENT_STATUS_RESPONSE == "success" else "REJECT",
"decision": decision,
# Reflect back parameters we were sent by the client
"req_amount": post_params.get('amount'),
......@@ -170,6 +178,13 @@ class PaymentFakeView(View):
'bill_trans_ref_no', 'signed_field_names', 'signed_date_time'
]
# if decision is decline , cancel or error then remove auth_amount from signed_field.
# list and also delete from resp_params dict
if decision in ["DECLINE", "CANCEL", "ERROR"]:
signed_fields.remove('auth_amount')
del resp_params["auth_amount"]
# Add the list of signed fields
resp_params['signed_field_names'] = ",".join(signed_fields)
......@@ -202,13 +217,27 @@ class PaymentFakeView(View):
# Build the context dict used to render the HTML form,
# filling in values for the hidden input fields.
# These will be sent in the POST request to the callback URL.
post_params_success = self.response_post_params(post_params)
# Build the context dict for decline form,
# remove the auth_amount value from here to
# reproduce exact response coming from actual postback call
post_params_decline = self.response_post_params(post_params)
del post_params_decline["auth_amount"]
post_params_decline["decision"] = 'DECLINE'
context_dict = {
# URL to send the POST request to
"callback_url": callback_url,
# POST params embedded in the HTML form
'post_params': self.response_post_params(post_params)
# POST params embedded in the HTML success form
'post_params_success': post_params_success,
# POST params embedded in the HTML decline form
'post_params_decline': post_params_decline
}
return render_to_response('shoppingcart/test/fake_payment_page.html', context_dict)
......@@ -92,6 +92,17 @@ class PaymentFakeViewTest(TestCase):
# Generate shoppingcart signatures
post_params = sign(self.CLIENT_POST_PARAMS)
# Configure the view to declined payments
resp = self.client.put(
'/shoppingcart/payment_fake',
data="decline", content_type='text/plain'
)
self.assertEqual(resp.status_code, 200)
# Check that the decision is "DECLINE"
resp_params = PaymentFakeView.response_post_params(post_params)
self.assertEqual(resp_params.get('decision'), 'DECLINE')
# Configure the view to fail payments
resp = self.client.put(
'/shoppingcart/payment_fake',
......
<html>
<head><title>Payment Form</title></head>
<head><title>Payment Form</title>
</head>
<body>
<p>Payment page</p>
<form name="input" action="${callback_url}" method="post">
% for name, value in post_params.items():
<p>Payment page</p>
<form name="input" action="${callback_url}" method="post">
% for name, value in post_params_success.items():
<input type="hidden" name="${name}" value="${value}">
% endfor
<input type="submit" value="Submit">
</form>
% endfor
<input type="submit" value="Submit">
</form>
<form name="frm_decline" action="${callback_url}" method="post">
% for name, value in post_params_decline.items():
<input type="hidden" name="${name}" value="${value}">
% endfor
<input type="submit" value="Decline" id="decline">
</form>
</body>
</html>
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