# -*- coding: utf-8 -*- 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 _logger = logging.getLogger(__name__) class AuthorizeAPI(): """Authorize.net Gateway API integration. This class allows contacting the Authorize.net API with simple operation requests. It implements a *very limited* subset of the complete API (http://developer.authorize.net/api/reference); namely: - Customer Profile/Payment Profile creation - Transaction authorization/capture/voiding """ AUTH_ERROR_STATUS = 3 def __init__(self, acquirer): """Initiate the environment with the acquirer data. :param record acquirer: payment.acquirer account that will be contacted """ if acquirer.state == 'test': self.url = 'https://apitest.authorize.net/xml/v1/request.api' else: self.url = 'https://api.authorize.net/xml/v1/request.api' self.state = acquirer.state self.name = acquirer.authorize_login self.transaction_key = acquirer.authorize_transaction_key def _authorize_request(self, data): _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) _logger.info("_authorize_request: Received response:\n%s", resp) 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') } return resp # Customer profiles def create_customer_profile(self, partner, opaqueData): """Create a payment and customer profile in the Authorize.net backend. Creates a customer profile for the partner/credit card combination and links a corresponding payment profile to it. Note that a single partner in the Odoo database can have multiple customer profiles in Authorize.net (i.e. a customer profile is created for every res.partner/payment.token couple). :param record partner: the res.partner record of the customer :param str cardnumber: cardnumber in string format (numbers only, no separator) :param str expiration_date: expiration date in 'YYYY-MM' string format :param str card_code: three- or four-digit verification number :return: a dict containing the profile_id and payment_profile_id of the newly created customer profile and payment profile :rtype: dict """ 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, 'phoneNumber': partner.phone or '', }, 'payment': { 'opaqueData': { 'dataDescriptor': opaqueData.get('dataDescriptor'), 'dataValue': opaqueData.get('dataValue') } } } }, 'validationMode': 'liveMode' if self.state == 'enabled' else 'testMode' } } response = self._authorize_request(values) 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. Creates a customer profile for the partner/credit card combination and links a corresponding payment profile to it. Note that a single partner in the Odoo database can have multiple customer profiles in Authorize.net (i.e. a customer profile is created for every res.partner/payment.token couple). Note that this function makes 2 calls to the authorize api, since we need to obtain a partial cardnumber to generate a meaningful payment.token name. :param record partner: the res.partner record of the customer :param str transaction_id: id of the authorized transaction in the Authorize.net backend :return: a dict containing the profile_id and payment_profile_id of the newly created customer profile and payment profile as well as the last digits of the card number :rtype: dict """ 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 False res = { 'profile_id': response.get('customerProfileId'), 'payment_profile_id': response.get('customerPaymentProfileIdList')[0] } values = { 'getCustomerPaymentProfileRequest': { "merchantAuthentication": { "name": self.name, "transactionKey": self.transaction_key }, 'customerProfileId': res['profile_id'], 'customerPaymentProfileId': res['payment_profile_id'], } } response = self._authorize_request(values) res['name'] = response.get('paymentProfile', {}).get('payment', {}).get('creditCard', {}).get('cardNumber') return res # Transaction management def auth_and_capture(self, token, amount, reference): """Authorize and capture a payment for the given amount. Authorize and immediately capture a payment for the given payment.token record for the specified amount with reference as communication. :param record token: the payment.token record that must be charged :param str amount: transaction amount (up to 15 digits with decimal point) :param str reference: used as "invoiceNumber" in the Authorize.net backend :return: a dict containing the response code, transaction id and transaction type :rtype: dict """ 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], 'description': reference[:255], } } } } 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') } result = { 'x_response_code': response.get('transactionResponse', {}).get('responseCode'), 'x_trans_id': response.get('transactionResponse', {}).get('transId'), 'x_type': 'auth_capture' } errors = response.get('transactionResponse', {}).get('errors') if errors: result['x_response_reason_text'] = '\n'.join([e.get('errorText') for e in errors]) return result def authorize(self, token, amount, reference): """Authorize a payment for the given amount. Authorize (without capture) a payment for the given payment.token record for the specified amount with reference as communication. :param record token: the payment.token record that must be charged :param str amount: transaction amount (up to 15 digits with decimal point) :param str reference: used as "invoiceNumber" in the Authorize.net backend :return: a dict containing the response code, transaction id and transaction type :rtype: dict """ 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], 'description': reference[:255], } } } } 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. Capture a previsouly authorized payment. Note that the amount is required even though we do not support partial capture. :param str transaction_id: id of the authorized transaction in the Authorize.net backend :param str amount: transaction amount (up to 15 digits with decimal point) :return: a dict containing the response code, transaction id and transaction type :rtype: dict """ values = { 'createTransactionRequest': { "merchantAuthentication": { "name": self.name, "transactionKey": self.transaction_key }, 'transactionRequest': { 'transactionType': 'priorAuthCaptureTransaction', 'amount': str(amount), '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': 'prior_auth_capture' } def void(self, transaction_id): """Void a previously authorized payment. :param str transaction_id: the id of the authorized transaction in the Authorize.net backend :return: a dict containing the response code, transaction id and transaction type :rtype: dict """ 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): """Test Authorize.net communication with a simple credentials check. :return: True if authentication was successful, else False (or throws an error) :rtype: bool """ 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 # Client Key def get_client_secret(self): """ Create a client secret that will be needed for the AcceptJS integration. """ values = { "getMerchantDetailsRequest": { "merchantAuthentication": { "name": self.name, "transactionKey": self.transaction_key, } } } response = self._authorize_request(values) client_secret = response.get('publicClientKey') return client_secret