Odoo18-Base/addons/payment_authorize/models/authorize_request.py
2025-01-06 10:57:38 +07:00

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