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.'))