359 lines
14 KiB
Python
359 lines
14 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import json
|
|
import logging
|
|
import pprint
|
|
|
|
from uuid import uuid4
|
|
|
|
import requests
|
|
|
|
from odoo.addons.payment import utils as payment_utils
|
|
|
|
_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, provider):
|
|
"""Initiate the environment with the provider data.
|
|
|
|
:param recordset provider: payment.provider account that will be contacted
|
|
"""
|
|
if provider.state == 'enabled':
|
|
self.url = 'https://api.authorize.net/xml/v1/request.api'
|
|
else:
|
|
self.url = 'https://apitest.authorize.net/xml/v1/request.api'
|
|
|
|
self.state = provider.state
|
|
self.name = provider.authorize_login
|
|
self.transaction_key = provider.authorize_transaction_key
|
|
|
|
def _make_request(self, operation, data=None):
|
|
request = {
|
|
operation: {
|
|
'merchantAuthentication': {
|
|
'name': self.name,
|
|
'transactionKey': self.transaction_key,
|
|
},
|
|
**(data or {})
|
|
}
|
|
}
|
|
logged_request = {operation: data or {}}
|
|
|
|
_logger.info("sending request to %s:\n%s", self.url, pprint.pformat(logged_request))
|
|
response = requests.post(self.url, json.dumps(request), timeout=60)
|
|
response.raise_for_status()
|
|
response = json.loads(response.content)
|
|
_logger.info("response received:\n%s", pprint.pformat(response))
|
|
|
|
messages = response.get('messages')
|
|
if messages and messages.get('resultCode') == 'Error':
|
|
err_msg = messages.get('message')[0]['text']
|
|
|
|
tx_errors = response.get('transactionResponse', {}).get('errors')
|
|
if tx_errors:
|
|
if err_msg:
|
|
err_msg += '\n'
|
|
err_msg += '\n'.join([e.get('errorText', '') for e in tx_errors])
|
|
|
|
return {
|
|
'err_code': messages.get('message')[0].get('code'),
|
|
'err_msg': err_msg,
|
|
}
|
|
|
|
return response
|
|
|
|
def _format_response(self, response, operation):
|
|
if response and response.get('err_code'):
|
|
return {
|
|
'x_response_code': self.AUTH_ERROR_STATUS,
|
|
'x_response_reason_text': response.get('err_msg')
|
|
}
|
|
else:
|
|
tx_response = response.get('transactionResponse', {})
|
|
return {
|
|
'x_response_code': tx_response.get('responseCode'),
|
|
'x_trans_id': tx_response.get('transId'),
|
|
'x_type': operation,
|
|
'payment_method_code': tx_response.get('accountType'),
|
|
}
|
|
|
|
# Customer profiles
|
|
def create_customer_profile(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 card number 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
|
|
"""
|
|
response = self._make_request('createCustomerProfileFromTransactionRequest', {
|
|
'transId': transaction_id,
|
|
'customer': {
|
|
'merchantCustomerId': ('ODOO-%s-%s' % (partner.id, uuid4().hex[:8]))[:20],
|
|
'email': partner.email or ''
|
|
}
|
|
})
|
|
|
|
if not response.get('customerProfileId'):
|
|
_logger.warning(
|
|
"unable to create customer payment profile, data missing from transaction with "
|
|
"id %(tx_id)s, partner id: %(partner_id)s",
|
|
{
|
|
'tx_id': transaction_id,
|
|
'partner_id': partner,
|
|
},
|
|
)
|
|
return False
|
|
|
|
res = {
|
|
'profile_id': response.get('customerProfileId'),
|
|
'payment_profile_id': response.get('customerPaymentProfileIdList')[0]
|
|
}
|
|
|
|
response = self._make_request('getCustomerPaymentProfileRequest', {
|
|
'customerProfileId': res['profile_id'],
|
|
'customerPaymentProfileId': res['payment_profile_id'],
|
|
})
|
|
|
|
payment = response.get('paymentProfile', {}).get('payment', {})
|
|
if 'creditCard' in payment:
|
|
# Authorize.net pads the card and account numbers with X's.
|
|
res['payment_details'] = payment.get('creditCard', {}).get('cardNumber')[-4:]
|
|
else:
|
|
res['payment_details'] = payment.get('bankAccount', {}).get('accountNumber')[-4:]
|
|
return res
|
|
|
|
def delete_customer_profile(self, profile_id):
|
|
"""Delete a customer profile
|
|
|
|
:param str profile_id: the id of the customer profile in the Authorize.net backend
|
|
|
|
:return: a dict containing the response code
|
|
:rtype: dict
|
|
"""
|
|
response = self._make_request("deleteCustomerProfileRequest", {'customerProfileId': profile_id})
|
|
return self._format_response(response, 'deleteCustomerProfile')
|
|
|
|
#=== Transaction management ===#
|
|
def _prepare_authorization_transaction_request(self, transaction_type, tx_data, tx):
|
|
# The billTo parameter is required for new ACH transactions (transactions without a payment.token),
|
|
# but is not allowed for transactions with a payment.token.
|
|
bill_to = {}
|
|
if 'profile' not in tx_data:
|
|
if tx.partner_id.is_company:
|
|
split_name = '', tx.partner_name
|
|
else:
|
|
split_name = payment_utils.split_partner_name(tx.partner_name)
|
|
# max lengths are defined by the Authorize API
|
|
bill_to = {
|
|
'billTo': {
|
|
'firstName': split_name[0][:50],
|
|
'lastName': split_name[1][:50], # lastName is always required
|
|
'company': tx.partner_name[:50] if tx.partner_id.is_company else '',
|
|
'address': tx.partner_address,
|
|
'city': tx.partner_city,
|
|
'state': tx.partner_state_id.name or '',
|
|
'zip': tx.partner_zip,
|
|
'country': tx.partner_country_id.name or '',
|
|
}
|
|
}
|
|
|
|
# These keys have to be in the order defined in
|
|
# https://apitest.authorize.net/xml/v1/schema/AnetApiSchema.xsd
|
|
return {
|
|
'transactionRequest': {
|
|
'transactionType': transaction_type,
|
|
'amount': str(tx.amount),
|
|
**tx_data,
|
|
'order': {
|
|
'invoiceNumber': tx.reference[:20],
|
|
'description': tx.reference[:255],
|
|
},
|
|
'customer': {
|
|
'email': tx.partner_email or '',
|
|
},
|
|
**bill_to,
|
|
'customerIP': payment_utils.get_customer_ip_address(),
|
|
}
|
|
}
|
|
|
|
def authorize(self, tx, token=None, opaque_data=None):
|
|
""" Authorize (without capture) a payment for the given amount.
|
|
|
|
:param recordset tx: The transaction of the payment, as a `payment.transaction` record
|
|
:param recordset token: The token of the payment method to charge, as a `payment.token`
|
|
record
|
|
:param dict opaque_data: The payment details obfuscated by Authorize.Net
|
|
:return: a dict containing the response code, transaction id and transaction type
|
|
:rtype: dict
|
|
"""
|
|
tx_data = self._prepare_tx_data(token=token, opaque_data=opaque_data)
|
|
response = self._make_request(
|
|
'createTransactionRequest',
|
|
self._prepare_authorization_transaction_request('authOnlyTransaction', tx_data, tx)
|
|
)
|
|
return self._format_response(response, 'auth_only')
|
|
|
|
def auth_and_capture(self, tx, token=None, opaque_data=None):
|
|
"""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 recordset tx: The transaction of the payment, as a `payment.transaction` record
|
|
:param record token: the payment.token record that must be charged
|
|
:param str opaque_data: the transaction opaque_data obtained from Authorize.net
|
|
|
|
:return: a dict containing the response code, transaction id and transaction type
|
|
:rtype: dict
|
|
"""
|
|
tx_data = self._prepare_tx_data(token=token, opaque_data=opaque_data)
|
|
response = self._make_request(
|
|
'createTransactionRequest',
|
|
self._prepare_authorization_transaction_request('authCaptureTransaction', tx_data, tx)
|
|
)
|
|
|
|
result = self._format_response(response, '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 _prepare_tx_data(self, token=None, opaque_data=False):
|
|
"""
|
|
:param token: The token of the payment method to charge, as a `payment.token` record
|
|
:param dict opaque_data: The payment details obfuscated by Authorize.Net
|
|
"""
|
|
assert (token or opaque_data) and not (token and opaque_data), "Exactly one of token or opaque_data must be specified"
|
|
if token:
|
|
token.ensure_one()
|
|
return {
|
|
'profile': {
|
|
'customerProfileId': token.authorize_profile,
|
|
'paymentProfile': {
|
|
'paymentProfileId': token.provider_ref,
|
|
}
|
|
},
|
|
}
|
|
else:
|
|
return {
|
|
'payment': {
|
|
'opaqueData': opaque_data,
|
|
}
|
|
}
|
|
|
|
def get_transaction_details(self, transaction_id):
|
|
""" Return detailed information about a specific transaction. Useful to issue refunds.
|
|
|
|
:param str transaction_id: transaction id
|
|
:return: a dict containing the transaction details
|
|
:rtype: dict
|
|
"""
|
|
return self._make_request('getTransactionDetailsRequest', {'transId': transaction_id})
|
|
|
|
def capture(self, transaction_id, amount):
|
|
"""Capture a previously authorized payment for the given amount.
|
|
|
|
Capture a previously 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
|
|
"""
|
|
response = self._make_request('createTransactionRequest', {
|
|
'transactionRequest': {
|
|
'transactionType': 'priorAuthCaptureTransaction',
|
|
'amount': str(amount),
|
|
'refTransId': transaction_id,
|
|
}
|
|
})
|
|
return self._format_response(response, '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
|
|
"""
|
|
response = self._make_request('createTransactionRequest', {
|
|
'transactionRequest': {
|
|
'transactionType': 'voidTransaction',
|
|
'refTransId': transaction_id
|
|
}
|
|
})
|
|
return self._format_response(response, 'void')
|
|
|
|
def refund(self, transaction_id, amount, tx_details):
|
|
"""Refund a previously authorized payment. If the transaction is not settled
|
|
yet, it will be voided.
|
|
|
|
:param str transaction_id: the id of the authorized transaction in the
|
|
Authorize.net backend
|
|
:param float amount: transaction amount to refund
|
|
:param dict tx_details: The transaction details from `get_transaction_details()`.
|
|
:return: a dict containing the response code, transaction id and transaction type
|
|
:rtype: dict
|
|
"""
|
|
card = tx_details.get('transaction', {}).get('payment', {}).get('creditCard', {}).get('cardNumber')
|
|
response = self._make_request('createTransactionRequest', {
|
|
'transactionRequest': {
|
|
'transactionType': 'refundTransaction',
|
|
'amount': str(amount),
|
|
'payment': {
|
|
'creditCard': {
|
|
'cardNumber': card,
|
|
'expirationDate': 'XXXX',
|
|
}
|
|
},
|
|
'refTransId': transaction_id,
|
|
}
|
|
})
|
|
return self._format_response(response, 'refund')
|
|
|
|
# Provider configuration: fetch authorize_client_key & currencies
|
|
def merchant_details(self):
|
|
""" Retrieves the merchant details and generate a new public client key if none exists.
|
|
|
|
:return: Dictionary containing the merchant details
|
|
:rtype: dict"""
|
|
return self._make_request('getMerchantDetailsRequest')
|
|
|
|
# Test
|
|
def test_authenticate(self):
|
|
""" Test Authorize.net communication with a simple credentials check.
|
|
|
|
:return: The authentication results
|
|
:rtype: dict
|
|
"""
|
|
return self._make_request('authenticateTestRequest')
|