ECOM-662 Raise exception in case of DECLINE response.
Adding decline button in fake payment page. In of case ERROR, CANCEL, and DECLINE removing auth_amount from test cybersource2 and payment fake.
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
<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>
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user