diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/payment_authorize/models/authorize_request.py | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/payment_authorize/models/authorize_request.py')
| -rw-r--r-- | addons/payment_authorize/models/authorize_request.py | 401 |
1 files changed, 401 insertions, 0 deletions
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py new file mode 100644 index 00000000..78db4977 --- /dev/null +++ b/addons/payment_authorize/models/authorize_request.py @@ -0,0 +1,401 @@ +# -*- 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 |
