From 26f3d8465d3c6f1da80c18605d5fa8a953feb779 Mon Sep 17 00:00:00 2001 From: Nikunj Ladava Date: Mon, 8 Jul 2019 12:20:02 +0000 Subject: [PATCH] [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 --- .../models/authorize_request.py | 496 ++++++++---------- addons/payment_authorize/models/payment.py | 21 +- 2 files changed, 241 insertions(+), 276 deletions(-) diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py index e3e65648ee0..6d8f1465fd7 100644 --- a/addons/payment_authorize/models/authorize_request.py +++ b/addons/payment_authorize/models/authorize_request.py @@ -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 diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py index 1385e9ea237..c2d550735fe 100644 --- a/addons/payment_authorize/models/payment.py +++ b/addons/payment_authorize/models/payment.py @@ -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.'))