[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:
Damien Bouvy
2017-11-27 08:27:53 +01:00
parent a609541f18
commit fc69b37cf7
2 changed files with 69 additions and 4 deletions
@@ -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')