[IMP] payment_authorize: convert xml request to json

- convert XML format request to JSON
- remove refund method from the request, as there is no use of it,
   we will never validate card as authorize.net validate card by itself,
   so there is no case for the refund
- verify token while creating it
- make verify validity field invisible in case of authorize

task- 2025821
This commit is contained in:
Nikunj Ladava
2019-08-07 06:00:43 +00:00
parent 6fc4c53404
commit 26f3d8465d
2 changed files with 241 additions and 276 deletions
@@ -1,65 +1,17 @@
# -*- coding: utf-8 -*-
import io
import requests
from lxml import etree, objectify
from xml.etree import ElementTree as ET
from uuid import uuid4
import pprint
import json
import logging
import requests
from uuid import uuid4
from odoo import _
from odoo.exceptions import UserError
from odoo.addons.payment.models.payment_acquirer import _partner_split_name
from odoo.exceptions import ValidationError, UserError
from odoo import _
_logger = logging.getLogger(__name__)
XMLNS = 'AnetApi/xml/v1/schema/AnetApiSchema.xsd'
def strip_ns(xml, ns):
"""Strip the provided name from tag names.
:param str xml: xml document
:param str ns: namespace to strip
:rtype: etree._Element
:return: the parsed xml string with the namespace prefix removed
"""
it = ET.iterparse(io.BytesIO(xml))
ns_prefix = '{%s}' % XMLNS
for _, el in it:
if el.tag.startswith(ns_prefix):
el.tag = el.tag[len(ns_prefix):] # strip all Auth.net namespaces
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.
@@ -86,47 +38,18 @@ class AuthorizeAPI():
self.transaction_key = acquirer.authorize_transaction_key
def _authorize_request(self, data):
"""Encode, send and process the request to the Authorize.net API.
_logger.info('_authorize_request: Sending values to URL %s, values:\n%s', self.url, data)
resp = requests.post(self.url, json.dumps(data))
resp.raise_for_status()
resp = json.loads(resp.content)
messages = resp.get('messages')
if messages and messages.get('resultCode') == 'Error':
return {
'err_code': messages.get('message')[0].get('code'),
'err_msg': messages.get('message')[0].get('text')
}
Encodes the xml data and process the response. Note that only a basic
processing is done at this level (namespace cleanup, basic error management).
:param etree._Element data: etree data to process
"""
logged_data = data
data = etree.tostring(data, encoding='utf-8')
for node_to_remove in ['//merchantAuthentication', '//creditCard']:
for node in logged_data.xpath(node_to_remove):
node.getparent().remove(node)
logged_data = str(etree.tostring(logged_data, encoding='utf-8', pretty_print=True)).replace(r'\n', '\n')
_logger.info('_authorize_request: Sending values to URL %s, values:\n%s', self.url, logged_data)
r = requests.post(self.url, data=data, headers={'Content-Type': 'text/xml'})
r.raise_for_status()
response = strip_ns(r.content, XMLNS)
logged_data = etree.XML(r.content)
logged_data = str(etree.tostring(logged_data, encoding='utf-8', pretty_print=True)).replace(r'\n', '\n')
_logger.info('_authorize_request: Values received\n%s', logged_data)
return response
def _base_tree(self, requestType):
"""Create a basic tree containing authentication information.
Create a etree Element of type requestType and appends the Authorize.net
credentials (they are always required).
:param str requestType: the type of request to send to Authorize.net
See http://developer.authorize.net/api/reference
for available types.
:return: basic etree Element of the requested type
containing credentials information
:rtype: etree._Element
"""
root = etree.Element(requestType, xmlns=XMLNS)
auth = etree.SubElement(root, "merchantAuthentication")
etree.SubElement(auth, "name").text = self.name
etree.SubElement(auth, "transactionKey").text = self.transaction_key
return root
return resp
# Customer profiles
def create_customer_profile(self, partner, cardnumber, expiration_date, card_code):
@@ -146,57 +69,51 @@ class AuthorizeAPI():
newly created customer profile and payment profile
:rtype: dict
"""
root = self._base_tree('createCustomerProfileRequest')
profile = etree.SubElement(root, "profile")
# merchantCustomerId is ODOO-{partner.id}-{random hex string} truncated to maximum 20 characters
etree.SubElement(profile, "merchantCustomerId").text = ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20]
etree.SubElement(profile, "email").text = partner.email or ''
payment_profile = etree.SubElement(profile, "paymentProfiles")
etree.SubElement(payment_profile, "customerType").text = 'business' if partner.is_company else 'individual'
billTo = etree.SubElement(payment_profile, "billTo")
if partner.is_company:
etree.SubElement(billTo, "firstName").text = ' '
etree.SubElement(billTo, "lastName").text = partner.name
else:
etree.SubElement(billTo, "firstName").text = _partner_split_name(partner.name)[0]
etree.SubElement(billTo, "lastName").text = _partner_split_name(partner.name)[1]
etree.SubElement(billTo, "address").text = (partner.street or '' + (partner.street2 if partner.street2 else '')) or None
missing_fields = [partner._fields[field].string for field in ['city', 'country_id'] if not partner[field]]
if missing_fields:
raise ValidationError({'missing_fields': missing_fields})
etree.SubElement(billTo, "city").text = partner.city
etree.SubElement(billTo, "state").text = partner.state_id.name or None
etree.SubElement(billTo, "zip").text = partner.zip or ''
etree.SubElement(billTo, "country").text = partner.country_id.name or None
payment = etree.SubElement(payment_profile, "payment")
creditCard = etree.SubElement(payment, "creditCard")
etree.SubElement(creditCard, "cardNumber").text = cardnumber
etree.SubElement(creditCard, "expirationDate").text = expiration_date
etree.SubElement(creditCard, "cardCode").text = card_code
etree.SubElement(root, "validationMode").text = 'liveMode'
response = self._authorize_request(root)
values = {
'createCustomerProfileRequest': {
'merchantAuthentication': {
'name': self.name,
'transactionKey': self.transaction_key
},
'profile': {
'description': ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20],
'email': partner.email or '',
'paymentProfiles': {
'customerType': 'business' if partner.is_company else 'individual',
'billTo': {
'firstName': '' if partner.is_company else _partner_split_name(partner.name)[0],
'lastName': _partner_split_name(partner.name)[1],
'address': (partner.street or '' + (partner.street2 if partner.street2 else '')) or None,
'city': partner.city,
'state': partner.state_id.name or None,
'zip': partner.zip or '',
'country': partner.country_id.name or None
},
'payment': {
'creditCard': {
'cardNumber': cardnumber,
'expirationDate': expiration_date,
'cardCode': card_code
}
}
}
},
'validationMode': 'liveMode'
}
}
# If the user didn't set up authorize.net properly then the response
# won't contain stuff like customerProfileId and accessing text
# will raise a NoneType has no text attribute
msg = response.find('messages')
if msg is not None:
rc = msg.find('resultCode')
if rc is not None and rc.text == 'Error':
err = msg.find('message')
err_code = err.find('code').text
err_msg = err.find('text').text
raise UserError(
"Authorize.net Error:\nCode: %s\nMessage: %s"
% (err_code, err_msg)
)
response = self._authorize_request(values)
res = dict()
res['profile_id'] = response.find('customerProfileId').text
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
return res
if response and response.get('err_code'):
raise UserError(_(
"Authorize.net Error:\nCode: %s\nMessage: %s"
% (response.get('err_code'), response.get('err_msg'))
))
return {
'profile_id': response.get('customerProfileId'),
'payment_profile_id': response.get('customerPaymentProfileIdList')[0]
}
def create_customer_profile_from_tx(self, partner, transaction_id):
"""Create an Auth.net payment/customer profile from an existing transaction.
@@ -218,54 +135,48 @@ class AuthorizeAPI():
last digits of the card number
:rtype: dict
"""
root = self._base_tree('createCustomerProfileFromTransactionRequest')
etree.SubElement(root, "transId").text = transaction_id
customer = etree.SubElement(root, "customer")
# merchantCustomerId is ODOO-{partner.id}-{random hex string} truncated to maximum 20 characters
etree.SubElement(customer, "merchantCustomerId").text = ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20]
etree.SubElement(customer, "email").text = partner.email or ''
response = self._authorize_request(root)
res = dict()
if response.find('customerProfileId') is None: # Warning: do not use bool(etree) as the semantics is very misleading
values = {
'createCustomerProfileFromTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transId': transaction_id,
'customer': {
'merchantCustomerId': ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20],
'email': partner.email or ''
}
}
}
response = self._authorize_request(values)
if not response.get('customerProfileId'):
_logger.warning(
'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s'
% (transaction_id, partner)
)
return res
res['profile_id'] = response.find('customerProfileId').text
res['payment_profile_id'] = response.find('customerPaymentProfileIdList/numericString').text
root_profile = self._base_tree('getCustomerPaymentProfileRequest')
etree.SubElement(root_profile, "customerProfileId").text = res['profile_id']
etree.SubElement(root_profile, "customerPaymentProfileId").text = res['payment_profile_id']
response_profile = self._authorize_request(root_profile)
res['name'] = response_profile.find('paymentProfile/payment/creditCard/cardNumber').text
return res
return False
def credit(self, token, amount, transaction_id):
""" Refund a payment for the given amount.
res = {
'profile_id': response.get('customerProfileId'),
'payment_profile_id': response.get('customerPaymentProfileIdList')[0]
}
:param record token: the payment.token record that must be refunded.
:param str amount: transaction amount
:param str transaction_id: the reference of the transacation that is going to be refunded.
values = {
'getCustomerPaymentProfileRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'customerProfileId': res['profile_id'],
'customerPaymentProfileId': res['payment_profile_id'],
}
}
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
root = self._base_tree('createTransactionRequest')
tx = etree.SubElement(root, "transactionRequest")
etree.SubElement(tx, "transactionType").text = "refundTransaction"
etree.SubElement(tx, "amount").text = str(amount)
payment = etree.SubElement(tx, "payment")
credit_card = etree.SubElement(payment, "creditCard")
idx = token.name.find(' - ')
etree.SubElement(credit_card, "cardNumber").text = token.name[idx-4:idx] # shitty hack, but that's the only way to get the 4 last digits
etree.SubElement(credit_card, "expirationDate").text = "XXXX"
etree.SubElement(tx, "refTransId").text = transaction_id
response = self._authorize_request(root)
res = dict()
res['x_response_code'] = response.find('transactionResponse/responseCode').text
res['x_trans_id'] = transaction_id
res['x_type'] = 'refund'
response = self._authorize_request(values)
res['name'] = response.get('paymentProfile', {}).get('payment', {}).get('creditCard', {}).get('cardNumber')
return res
# Transaction management
@@ -282,27 +193,41 @@ class AuthorizeAPI():
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
root = self._base_tree('createTransactionRequest')
tx = etree.SubElement(root, "transactionRequest")
etree.SubElement(tx, "transactionType").text = "authCaptureTransaction"
etree.SubElement(tx, "amount").text = str(amount)
profile = etree.SubElement(tx, "profile")
etree.SubElement(profile, "customerProfileId").text = token.authorize_profile
payment_profile = etree.SubElement(profile, "paymentProfile")
etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
order = etree.SubElement(tx, "order")
etree.SubElement(order, "invoiceNumber").text = reference[:20]
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'
return res
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'authCaptureTransaction',
'amount': str(amount),
'profile': {
'customerProfileId': token.authorize_profile,
'paymentProfile': {
'paymentProfileId': token.acquirer_ref,
}
},
'order': {
'invoiceNumber': reference[:20]
}
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'auth_capture'
}
def authorize(self, token, amount, reference):
"""Authorize a payment for the given amount.
@@ -317,27 +242,41 @@ class AuthorizeAPI():
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
root = self._base_tree('createTransactionRequest')
tx = etree.SubElement(root, "transactionRequest")
etree.SubElement(tx, "transactionType").text = "authOnlyTransaction"
etree.SubElement(tx, "amount").text = str(amount)
profile = etree.SubElement(tx, "profile")
etree.SubElement(profile, "customerProfileId").text = token.authorize_profile
payment_profile = etree.SubElement(profile, "paymentProfile")
etree.SubElement(payment_profile, "paymentProfileId").text = token.acquirer_ref
order = etree.SubElement(tx, "order")
etree.SubElement(order, "invoiceNumber").text = reference[:20]
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'
return res
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'authOnlyTransaction',
'amount': str(amount),
'profile': {
'customerProfileId': token.authorize_profile,
'paymentProfile': {
'paymentProfileId': token.acquirer_ref,
}
},
'order': {
'invoiceNumber': reference[:20]
}
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'auth_only'
}
def capture(self, transaction_id, amount):
"""Capture a previously authorized payment for the given amount.
@@ -352,22 +291,33 @@ class AuthorizeAPI():
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
root = self._base_tree('createTransactionRequest')
tx = etree.SubElement(root, "transactionRequest")
etree.SubElement(tx, "transactionType").text = "priorAuthCaptureTransaction"
etree.SubElement(tx, "amount").text = str(amount)
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'
return res
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'priorAuthCaptureTransaction',
'refTransId': transaction_id,
'amount': str(amount)
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'prior_auth_capture'
}
def void(self, transaction_id):
"""Void a previously authorized payment.
@@ -378,21 +328,32 @@ class AuthorizeAPI():
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
root = self._base_tree('createTransactionRequest')
tx = etree.SubElement(root, "transactionRequest")
etree.SubElement(tx, "transactionType").text = "voidTransaction"
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'
return res
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'voidTransaction',
'refTransId': transaction_id
}
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'void'
}
# Test
def test_authenticate(self):
@@ -401,9 +362,16 @@ class AuthorizeAPI():
:return: True if authentication was successful, else False (or throws an error)
:rtype: bool
"""
test_auth = self._base_tree('authenticateTestRequest')
response = self._authorize_request(test_auth)
root = objectify.fromstring(response)
if root.find('{ns}messages/{ns}resultCode'.format(ns='{%s}' % XMLNS)) == 'Ok':
return True
return False
values = {
'authenticateTestRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
}
}
response = self._authorize_request(values)
if response and response.get('err_code'):
return False
return True
+9 -12
View File
@@ -27,6 +27,14 @@ class PaymentAcquirerAuthorize(models.Model):
authorize_transaction_key = fields.Char(string='API Transaction Key', required_if_provider='authorize', groups='base.group_user')
authorize_signature_key = fields.Char(string='API Signature Key', groups='base.group_user', compute="_compute_auth_signature_key", inverse="_inverse_auth_signature_key")
@api.onchange('provider', 'check_validity')
def onchange_check_validity(self):
if self.provider == 'authorize' and self.check_validity:
self.check_validity = False
return {'warning': {
'title': _("Warning"),
'message': ('This option is not supported for Authorize.net')}}
def _get_feature_support(self):
"""Get advanced feature support by provider.
@@ -252,9 +260,6 @@ class TxAuthorize(models.Model):
'partner_id': self.partner_id.id,
})
self.payment_token_id = token_id
if self.payment_token_id:
self.payment_token_id.verified = True
return True
elif status_code == self._authorize_pending_tx_status:
self.write({'acquirer_reference': data.get('x_trans_id')})
@@ -291,12 +296,6 @@ class TxAuthorize(models.Model):
res = transaction.authorize(self.payment_token_id, self.amount, self.reference)
return self._authorize_s2s_validate_tree(res)
def authorize_s2s_do_refund(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
res = transaction.credit(self.payment_token_id, self.amount, self.acquirer_reference)
return self._authorize_s2s_validate_tree(res)
def authorize_s2s_capture_transaction(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
@@ -325,9 +324,6 @@ class TxAuthorize(models.Model):
'date': fields.Datetime.now(),
})
if self.payment_token_id:
self.payment_token_id.verified = True
self._set_transaction_done()
if init_state != 'authorized':
@@ -380,6 +376,7 @@ class PaymentToken(models.Model):
'authorize_profile': res.get('profile_id'),
'name': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']),
'acquirer_ref': res.get('payment_profile_id'),
'verified': True
}
else:
raise ValidationError(_('The Customer Profile creation in Authorize.NET failed.'))