[FIX] payment_authorize: stop raising on 'Error' authorize status
It seems that Authorize.net returns an 'Error' status when transactions fail (note that it can also return an Ok status for different types of failures). This is incoherent with the way the authorize requests are processed, since an Error resultCode will raise a ValidationError, rollbacking the complete cursor transaction. This makes little sense, since there should be a trace of the payment.transaction in the backend (preferably with an explanation for the failure). This fix adds an error processing step to the response handling of authorize.net - while before, it was assumed that an 'error' result meant an issue in communication/setup, now it is treated as an error in payment and the payment.transaction status is set accordingly. I've added a test for this and it was green (pinky promise), but this particular test suite is deactivated (or perhaps only on nightly, I need to check the nightly process with @nim-odoo).
This commit is contained in:
@@ -29,6 +29,34 @@ def strip_ns(xml, ns):
|
||||
return it.root
|
||||
|
||||
|
||||
def error_check(elem):
|
||||
"""Check if the response sent by Authorize.net contains an error.
|
||||
|
||||
Errors can be a failure to try the transaction (in that case, the transasctionResponse
|
||||
is empty, and the meaningful error message will be in message/code) or a failure to process
|
||||
the transaction (in that case, the message/code content will be generic and the actual error
|
||||
message is in transactionResponse/errors/error/errorText).
|
||||
|
||||
:param etree._Element elem: the root element of the response that will be parsed
|
||||
|
||||
:rtype: tuple (bool, str)
|
||||
:return: tuple containnig a boolean indicating if the response should be considered
|
||||
as an error and the most meaningful error message found in it.
|
||||
"""
|
||||
result_code = elem.find('messages/resultCode')
|
||||
msg = 'No meaningful error message found, please check logs or the Authorize.net backend'
|
||||
has_error = result_code is not None and result_code.text == 'Error'
|
||||
if has_error:
|
||||
# accumulate the most meangingful error
|
||||
error = elem.find('transactionResponse/errors/error')
|
||||
error = error if error is not None else elem.find('messages/message')
|
||||
if error is not None:
|
||||
code = error[0].text
|
||||
text = error[1].text
|
||||
msg = '%s: %s' % (code, text)
|
||||
return (has_error, msg)
|
||||
|
||||
|
||||
class AuthorizeAPI():
|
||||
"""Authorize.net Gateway API integration.
|
||||
|
||||
@@ -38,6 +66,9 @@ class AuthorizeAPI():
|
||||
- Customer Profile/Payment Profile creation
|
||||
- Transaction authorization/capture/voiding
|
||||
"""
|
||||
|
||||
AUTH_ERROR_STATUS = 3
|
||||
|
||||
def __init__(self, acquirer):
|
||||
"""Initiate the environment with the acquirer data.
|
||||
|
||||
@@ -63,9 +94,6 @@ class AuthorizeAPI():
|
||||
request.add_header('Content-Type', 'text/xml')
|
||||
response = urlopen(request).read()
|
||||
response = strip_ns(response, XMLNS)
|
||||
if response.find('messages/resultCode').text == 'Error':
|
||||
messages = map(lambda m: m.text, response.findall('messages/message/text'))
|
||||
raise ValidationError(_('Authorize.net Error Message(s):\n %s') % '\n'.join(messages))
|
||||
return response
|
||||
|
||||
def _base_tree(self, requestType):
|
||||
@@ -190,6 +218,11 @@ class AuthorizeAPI():
|
||||
etree.SubElement(order, "invoiceNumber").text = reference
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
(has_error, error_msg) = error_check(response)
|
||||
if has_error:
|
||||
res['x_response_code'] = self.AUTH_ERROR_STATUS
|
||||
res['x_response_reason_text'] = error_msg
|
||||
return res
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'auth_capture'
|
||||
@@ -220,6 +253,11 @@ class AuthorizeAPI():
|
||||
etree.SubElement(order, "invoiceNumber").text = reference
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
(has_error, error_msg) = error_check(response)
|
||||
if has_error:
|
||||
res['x_response_code'] = self.AUTH_ERROR_STATUS
|
||||
res['x_response_reason_text'] = error_msg
|
||||
return res
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'auth_only'
|
||||
@@ -245,6 +283,11 @@ class AuthorizeAPI():
|
||||
etree.SubElement(tx, "refTransId").text = transaction_id
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
(has_error, error_msg) = error_check(response)
|
||||
if has_error:
|
||||
res['x_response_code'] = self.AUTH_ERROR_STATUS
|
||||
res['x_response_reason_text'] = error_msg
|
||||
return res
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'prior_auth_capture'
|
||||
@@ -265,6 +308,11 @@ class AuthorizeAPI():
|
||||
etree.SubElement(tx, "refTransId").text = transaction_id
|
||||
response = self._authorize_request(root)
|
||||
res = dict()
|
||||
(has_error, error_msg) = error_check(response)
|
||||
if has_error:
|
||||
res['x_response_code'] = self.AUTH_ERROR_STATUS
|
||||
res['x_response_reason_text'] = error_msg
|
||||
return res
|
||||
res['x_response_code'] = response.find('transactionResponse/responseCode').text
|
||||
res['x_trans_id'] = response.find('transactionResponse/transId').text
|
||||
res['x_type'] = 'void'
|
||||
|
||||
@@ -209,7 +209,7 @@ class AuthorizeForm(AuthorizeCommon):
|
||||
'acquirer_id': authorize.id,
|
||||
'type': 'server2server',
|
||||
'currency_id': self.currency_usd.id,
|
||||
'reference': 'test_ref_%s' % odoo.fields.Date.today(),
|
||||
'reference': 'test_ref_%s' % int(time.time()),
|
||||
'payment_token_id': payment_token.id,
|
||||
'partner_id': self.buyer_id,
|
||||
|
||||
@@ -251,3 +251,20 @@ class AuthorizeForm(AuthorizeCommon):
|
||||
self.assertEqual(transaction.state, 'authorized')
|
||||
transaction.action_void()
|
||||
self.assertEqual(transaction.state, 'cancel')
|
||||
|
||||
# try charging an unexisting profile
|
||||
ghost_payment_token = payment_token.copy()
|
||||
ghost_payment_token.authorize_profile = '99999999999'
|
||||
# create normal s2s transaction
|
||||
transaction = self.env['payment.transaction'].create({
|
||||
'amount': 500,
|
||||
'acquirer_id': authorize.id,
|
||||
'type': 'server2server',
|
||||
'currency_id': self.currency_usd.id,
|
||||
'reference': 'test_ref_%s' % int(time.time()),
|
||||
'payment_token_id': ghost_payment_token.id,
|
||||
'partner_id': self.buyer_id,
|
||||
|
||||
})
|
||||
transaction.authorize_s2s_do_transaction()
|
||||
self.assertEqual(transaction.state, 'error')
|
||||
|
||||
Reference in New Issue
Block a user