From dfb366afb93235eae1579296b50eda448edf5402 Mon Sep 17 00:00:00 2001 From: Awais Date: Thu, 1 Jan 2015 11:54:39 +0500 Subject: [PATCH] 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. --- .../shoppingcart/processors/CyberSource2.py | 14 +++ .../shoppingcart/processors/exceptions.py | 5 + .../processors/tests/test_CyberSource2.py | 103 +++++++++++------- .../shoppingcart/tests/payment_fake.py | 37 ++++++- .../shoppingcart/tests/test_payment_fake.py | 11 ++ .../shoppingcart/test/fake_payment_page.html | 27 +++-- 6 files changed, 145 insertions(+), 52 deletions(-) diff --git a/lms/djangoapps/shoppingcart/processors/CyberSource2.py b/lms/djangoapps/shoppingcart/processors/CyberSource2.py index b20719ad0a..fb4f5e2528 100644 --- a/lms/djangoapps/shoppingcart/processors/CyberSource2.py +++ b/lms/djangoapps/shoppingcart/processors/CyberSource2.py @@ -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( _( diff --git a/lms/djangoapps/shoppingcart/processors/exceptions.py b/lms/djangoapps/shoppingcart/processors/exceptions.py index 47a51a8b60..fbb8c4ac0a 100644 --- a/lms/djangoapps/shoppingcart/processors/exceptions.py +++ b/lms/djangoapps/shoppingcart/processors/exceptions.py @@ -19,3 +19,8 @@ class CCProcessorWrongAmountException(CCProcessorException): class CCProcessorUserCancelled(CCProcessorException): pass + + +class CCProcessorUserDeclined(CCProcessorException): + """Transaction declined.""" + pass diff --git a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py index 5a12fa5597..34c308f5b3 100644 --- a/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py +++ b/lms/djangoapps/shoppingcart/processors/tests/test_CyberSource2.py @@ -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']) diff --git a/lms/djangoapps/shoppingcart/tests/payment_fake.py b/lms/djangoapps/shoppingcart/tests/payment_fake.py index f2d2a6a2f6..d8d29f0ffa 100644 --- a/lms/djangoapps/shoppingcart/tests/payment_fake.py +++ b/lms/djangoapps/shoppingcart/tests/payment_fake.py @@ -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) diff --git a/lms/djangoapps/shoppingcart/tests/test_payment_fake.py b/lms/djangoapps/shoppingcart/tests/test_payment_fake.py index 17f5df9af5..f087aa4ad4 100644 --- a/lms/djangoapps/shoppingcart/tests/test_payment_fake.py +++ b/lms/djangoapps/shoppingcart/tests/test_payment_fake.py @@ -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', diff --git a/lms/templates/shoppingcart/test/fake_payment_page.html b/lms/templates/shoppingcart/test/fake_payment_page.html index ba488bbdb4..8b870e4a09 100644 --- a/lms/templates/shoppingcart/test/fake_payment_page.html +++ b/lms/templates/shoppingcart/test/fake_payment_page.html @@ -1,12 +1,25 @@ -Payment Form +Payment Form + -

Payment page

-
- % for name, value in post_params.items(): +

Payment page

+ + + % for name, value in post_params_success.items(): - % endfor - -
+ % endfor + + + +
+ % for name, value in post_params_decline.items(): + + % endfor + +
+ + + +