[REF] payment_authorize: migrate Authorize.Net to the new payment API

This commit also drops the payment with redirection flow in favor of
the direct payment flow only, while preserving the currently used APIs.

See the merge commit for more details.

task-2333030

Co-authored-by: Adrien Horgnies <aho@odoo.com>
This commit is contained in:
Kevin Baptiste
2021-03-30 09:25:51 +02:00
committed by Antoine Vandevenne (anv)
co-authored by Adrien Horgnies
parent 6b42234b8b
commit 660dc0ebaf
19 changed files with 952 additions and 1207 deletions
+6 -6
View File
@@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import controllers
from odoo.addons.payment.models.payment_acquirer import create_missing_journal_for_acquirers
from odoo.addons.payment import reset_payment_provider
from . import models
from odoo.addons.payment import reset_payment_acquirer
from odoo.addons.payment.models.payment_acquirer import create_missing_journals # post-init hook
def uninstall_hook(cr, registry):
reset_payment_provider(cr, registry, 'authorize')
reset_payment_acquirer(cr, registry, 'authorize')
+4 -4
View File
@@ -1,20 +1,20 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'Authorize.Net Payment Acquirer',
'version': '2.0',
'category': 'Accounting/Payment Acquirers',
'sequence': 350,
'summary': 'Payment Acquirer: Authorize.net Implementation',
'version': '1.0',
'description': """Authorize.Net Payment Acquirer""",
'depends': ['payment'],
'data': [
'views/assets.xml',
'views/payment_views.xml',
'views/payment_authorize_templates.xml',
'data/payment_acquirer_data.xml',
],
'installable': True,
'application': True,
'post_init_hook': 'create_missing_journal_for_acquirers',
'post_init_hook': 'create_missing_journals',
'uninstall_hook': 'uninstall_hook',
}
@@ -1,3 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import main
+49 -74
View File
@@ -1,85 +1,60 @@
# -*- coding: utf-8 -*-
import pprint
import logging
from werkzeug import urls, utils
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import http, _
import logging
import pprint
from odoo import _, http
from odoo.exceptions import ValidationError
from odoo.http import request
from odoo.exceptions import ValidationError, UserError
from odoo.addons.payment import utils as payment_utils
_logger = logging.getLogger(__name__)
class AuthorizeController(http.Controller):
_return_url = '/payment/authorize/return/'
_cancel_url = '/payment/authorize/cancel/'
@http.route([
'/payment/authorize/return/',
'/payment/authorize/cancel/',
], type='http', auth='public', csrf=False)
def authorize_form_feedback(self, **post):
_logger.info('Authorize: entering form_feedback with post data %s', pprint.pformat(post))
if post:
request.env['payment.transaction'].sudo().form_feedback(post, 'authorize')
base_url = request.env['ir.config_parameter'].sudo().get_param('web.base.url')
# Authorize.Net is expecting a response to the POST sent by their server.
# This response is in the form of a URL that Authorize.Net will pass on to the
# client's browser to redirect them to the desired location need javascript.
return request.render('payment_authorize.payment_authorize_redirect', {
'return_url': urls.url_join(base_url, "/payment/process")
})
@http.route('/payment/authorize/get_acquirer_info', type='json', auth='public')
def authorize_get_acquirer_info(self, acquirer_id):
""" Return public information on the acquirer.
@http.route(['/payment/authorize/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
def authorize_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
token = False
acquirer = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id')))
try:
if not kwargs.get('partner_id'):
kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
token = acquirer.s2s_process(kwargs)
except ValidationError as e:
message = e.args[0]
if isinstance(message, dict) and 'missing_fields' in message:
if request.env.user._is_public():
message = _("Please sign in to complete the payment.")
# update message if portal mode = b2b
if request.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited', 'False').lower() == 'false':
message += _(" If you don't have any account, ask your salesperson to grant you a portal access. ")
else:
msg = _("The transaction cannot be processed because some contact details are missing or invalid: ")
message = msg + ', '.join(message['missing_fields']) + '. '
message += _("Please complete your profile. ")
return {
'error': message
}
if not token:
res = {
'result': False,
}
return res
res = {
'result': True,
'id': token.id,
'short_name': token.short_name,
'3d_secure': False,
'verified': True, #Authorize.net does a transaction type of Authorization Only
#As Authorize.net already verify this card, we do not verify this card again.
:param int acquirer_id: The acquirer handling the transaction, as a `payment.acquirer` id
:return: Information on the acquirer, namely: the state, login ID and public client key
:rtype: dict
"""
acquirer_sudo = request.env['payment.acquirer'].sudo().browse(acquirer_id).exists()
return {
'state': acquirer_sudo.state,
# The public API key solely used to identify the seller account with Authorize.Net
'login_id': acquirer_sudo.authorize_login,
# The public client key solely used to identify requests from the Accept.js suite
'client_key': acquirer_sudo.authorize_client_key,
}
#token.validate() don't work with Authorize.net.
#Payments made via Authorize.net are settled and allowed to be refunded only on the next day.
#https://account.authorize.net/help/Miscellaneous/FAQ/Frequently_Asked_Questions.htm#Refund
#<quote>The original transaction that you wish to refund must have a status of Settled Successfully.
#You cannot issue refunds against unsettled, voided, declined or errored transactions.</quote>
return res
@http.route(['/payment/authorize/s2s/create'], type='http', auth='public')
def authorize_s2s_create(self, **post):
acquirer_id = int(post.get('acquirer_id'))
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
acquirer.s2s_process(post)
return utils.redirect("/payment/process")
@http.route('/payment/authorize/payment', type='json', auth='public')
def authorize_payment(self, reference, partner_id, access_token, opaque_data):
""" Make a payment request and handle the response.
:param str reference: The reference of the transaction
:param int partner_id: The partner making the transaction, as a `res.partner` id
:param str access_token: The access token used to verify the provided values
:param dict opaque_data: The payment details obfuscated by Authorize.Net
:return: None
"""
# Check that the transaction details have not been altered
if not payment_utils.check_access_token(access_token, reference, partner_id):
raise ValidationError("Authorize.Net: " + _("Received tampered payment request data."))
# Make the payment request to Authorize.Net
tx_sudo = request.env['payment.transaction'].sudo().search([('reference', '=', reference)])
response_content = tx_sudo._authorize_create_transaction_request(opaque_data)
# Handle the payment request response
_logger.info("make payment response:\n%s", pprint.pformat(response_content))
# As the API has no redirection flow, we always know the reference of the transaction.
# Still, we prefer to simulate the matching of the transaction by crafting dummy feedback
# data in order to go through the centralized `_handle_feedback_data` method.
feedback_data = {'reference': tx_sudo.reference, 'response': response_content}
request.env['payment.transaction'].sudo()._handle_feedback_data(
'authorize', feedback_data
)
@@ -1,15 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<odoo noupdate="1">
<record id="payment.payment_acquirer_authorize" model="payment.acquirer">
<field name="name">Authorize.Net</field>
<field name="image_128" type="base64" file="payment_authorize/static/src/img/authorize_icon.png"/>
<field name="provider">authorize</field>
<field name="company_id" ref="base.main_company"/>
<field name="view_template_id" ref="authorize_form"/>
<field name="registration_view_template_id" ref="authorize_s2s_form"/>
</record>
<record id="payment.payment_acquirer_authorize" model="payment.acquirer">
<field name="provider">authorize</field>
<field name="inline_form_view_id" ref="inline_form"/>
<field name="support_authorization">True</field>
<field name="support_fees_computation">False</field>
<field name="support_tokenization">True</field>
<field name="allow_tokenization">True</field>
</record>
</data>
</odoo>
+5 -2
View File
@@ -1,2 +1,5 @@
# -*- coding: utf-8 -*-
from . import payment
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import payment_acquirer
from . import payment_token
from . import payment_transaction
@@ -1,20 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import json
import logging
import requests
import pprint
from uuid import uuid4
from odoo import _
from odoo.exceptions import UserError
from odoo.addons.payment import utils as payment_utils
from odoo.addons.payment.models.payment_acquirer import _partner_split_name
import requests
_logger = logging.getLogger(__name__)
class AuthorizeAPI():
"""Authorize.net Gateway API integration.
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
@@ -23,12 +22,12 @@ class AuthorizeAPI():
- Transaction authorization/capture/voiding
"""
AUTH_ERROR_STATUS = 3
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
:param recordset acquirer: payment.acquirer account that will be contacted
"""
if acquirer.state == 'test':
self.url = 'https://apitest.authorize.net/xml/v1/request.api'
@@ -39,87 +38,48 @@ class AuthorizeAPI():
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')
def _make_request(self, operation, data=None):
request = {
operation: {
'merchantAuthentication': {
'name': self.name,
'transactionKey': self.transaction_key,
},
**(data or {})
}
}
_logger.info("sending request to %s:\n%s", self.url, pprint.pformat(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':
return {
'err_code': messages.get('message')[0].get('code'),
'err_msg': messages.get('message')[0].get('text')
}
return resp
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:
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': operation,
}
# 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.
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
@@ -127,7 +87,7 @@ class AuthorizeAPI():
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.
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
@@ -138,26 +98,18 @@ class AuthorizeAPI():
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._make_request('createCustomerProfileFromTransactionRequest', {
'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)
'Unable to create customer payment profile, data missing from transaction. Transaction_id: %s - Partner_id: %s',
transaction_id, partner,
)
return False
@@ -166,126 +118,118 @@ class AuthorizeAPI():
'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)
response = self._make_request('getCustomerPaymentProfileRequest', {
'customerProfileId': res['profile_id'],
'customerPaymentProfileId': res['payment_profile_id'],
})
res['name'] = response.get('paymentProfile', {}).get('payment', {}).get('creditCard', {}).get('cardNumber')
return res
# Transaction management
def auth_and_capture(self, token, amount, reference):
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 authorize(self, amount, reference, token=None, opaque_data=None):
""" Authorize (without capture) a payment for the given amount.
:param float amount: The amount to pay
:param str reference: The "invoiceNumber" in Authorize.net backend
: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', {
'transactionRequest': {
'transactionType': 'authOnlyTransaction',
'amount': str(amount),
**tx_data,
'order': {
'invoiceNumber': reference[:20],
'description': reference[:255],
},
'customerIP': payment_utils.get_customer_ip_address(),
}
})
return self._format_response(response, 'auth_only')
def auth_and_capture(self, amount, reference, 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 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
: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
"""
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
tx_data = self._prepare_tx_data(token=token, opaque_data=opaque_data)
response = self._make_request('createTransactionRequest', {
'transactionRequest': {
'transactionType': 'authCaptureTransaction',
'amount': str(amount),
**tx_data,
'order': {
'invoiceNumber': reference[:20],
'description': reference[:255],
},
'transactionRequest': {
'transactionType': 'authCaptureTransaction',
'amount': str(amount),
'profile': {
'customerProfileId': token.authorize_profile,
'paymentProfile': {
'paymentProfileId': token.acquirer_ref,
}
},
'order': {
'invoiceNumber': reference[:20],
'description': reference[:255],
}
}
'customerIP': payment_utils.get_customer_ip_address(),
}
}
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'
}
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 authorize(self, token, amount, reference):
"""Authorize a payment for the given amount.
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.acquirer_ref,
}
},
}
else:
return {
'payment': {
'opaqueData': opaque_data,
}
}
Authorize (without capture) a payment for the given payment.token
record for the specified amount with reference as communication.
def _get_transaction_details(self, transaction_id):
""" Return detailed information about a specific transaction. Useful to issue refunds.
: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
:param str transaction_id: transaction id
:return: a dict containing the transaction details
: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'
}
return self._make_request('getTransactionDetailsRequest', {'transId': transaction_id})
def capture(self, transaction_id, amount):
"""Capture a previously authorized payment for the given amount.
@@ -300,102 +244,83 @@ class AuthorizeAPI():
: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._make_request('createTransactionRequest', {
'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'
}
})
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
"""
values = {
'createTransactionRequest': {
"merchantAuthentication": {
"name": self.name,
"transactionKey": self.transaction_key
},
'transactionRequest': {
'transactionType': 'voidTransaction',
'refTransId': transaction_id
}
response = self._make_request('createTransactionRequest', {
'transactionRequest': {
'transactionType': 'voidTransaction',
'refTransId': transaction_id
}
}
})
return self._format_response(response, 'void')
response = self._authorize_request(values)
def refund(self, transaction_id, amount):
"""Refund a previously authorized payment. If the transaction is not settled
yet, it will be voided.
if response and response.get('err_code'):
:param str transaction_id: the id of the authorized transaction in the
Authorize.net backend
:param float amount: transaction amount to refund
:return: a dict containing the response code, transaction id and transaction type
:rtype: dict
"""
tx_details = self._get_transaction_details(transaction_id)
if tx_details and tx_details.get('err_code'):
return {
'x_response_code': self.AUTH_ERROR_STATUS,
'x_response_reason_text': response.get('err_msg')
'x_response_reason_text': tx_details.get('err_msg')
}
return {
'x_response_code': response.get('transactionResponse', {}).get('responseCode'),
'x_trans_id': response.get('transactionResponse', {}).get('transId'),
'x_type': 'void'
}
# Void transaction not yet settled instead of issuing a refund
# (spoiler alert: a refund on a non settled transaction will throw an error)
if tx_details.get('transaction', {}).get('transactionStatus') in ['authorizedPendingCapture', 'capturedPendingSettlement']:
return self.void(transaction_id)
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')
# Acquirer 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.
""" Test Authorize.net communication with a simple credentials check.
:return: True if authentication was successful, else False (or throws an error)
:rtype: bool
:return: The authentication results
:rtype: dict
"""
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
return self._make_request('authenticateTestRequest')
-335
View File
@@ -1,335 +0,0 @@
# coding: utf-8
from werkzeug import urls
from .authorize_request import AuthorizeAPI
import hashlib
import hmac
import logging
import time
from odoo import _, api, fields, models
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
from odoo.tools.float_utils import float_compare, float_repr
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class PaymentAcquirerAuthorize(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(selection_add=[
('authorize', 'Authorize.Net')
], ondelete={'authorize': 'set default'})
authorize_login = fields.Char(string='API Login Id', required_if_provider='authorize', groups='base.group_user')
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', required_if_provider='authorize', groups='base.group_user')
authorize_client_key = fields.Char(string='API Client Key', groups='base.group_user')
@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 action_client_secret(self):
api = AuthorizeAPI(self)
if not api.test_authenticate():
raise UserError(_('Unable to fetch Client Key, make sure the API Login and Transaction Key are correct.'))
self.authorize_client_key = api.get_client_secret()
return True
def _get_feature_support(self):
"""Get advanced feature support by provider.
Each provider should add its technical in the corresponding
key for the following features:
* fees: support payment fees computations
* authorize: support authorizing payment (separates
authorization and capture)
* tokenize: support saving payment data in a payment.tokenize
object
"""
res = super(PaymentAcquirerAuthorize, self)._get_feature_support()
res['authorize'].append('authorize')
res['tokenize'].append('authorize')
return res
def _get_authorize_urls(self, environment):
""" Authorize URLs """
if environment == 'prod':
return {'authorize_form_url': 'https://secure2.authorize.net/gateway/transact.dll'}
else:
return {'authorize_form_url': 'https://test.authorize.net/gateway/transact.dll'}
def _authorize_generate_hashing(self, values):
data = '^'.join([
values['x_login'],
values['x_fp_sequence'],
values['x_fp_timestamp'],
values['x_amount'],
values['x_currency_code']]).encode('utf-8')
return hmac.new(bytes.fromhex(self.authorize_signature_key), data, hashlib.sha512).hexdigest().upper()
def authorize_form_generate_values(self, values):
self.ensure_one()
# State code is only supported in US, use state name by default
# See https://developer.authorize.net/api/reference/
state = values['partner_state'].name if values.get('partner_state') else ''
if values.get('partner_country') and values.get('partner_country') == self.env.ref('base.us', False):
state = values['partner_state'].code if values.get('partner_state') else ''
billing_state = values['billing_partner_state'].name if values.get('billing_partner_state') else ''
if values.get('billing_partner_country') and values.get('billing_partner_country') == self.env.ref('base.us', False):
billing_state = values['billing_partner_state'].code if values.get('billing_partner_state') else ''
base_url = self.get_base_url()
authorize_tx_values = dict(values)
temp_authorize_tx_values = {
'x_login': self.authorize_login,
'x_amount': float_repr(values['amount'], values['currency'].decimal_places if values['currency'] else 2),
'x_show_form': 'PAYMENT_FORM',
'x_type': 'AUTH_CAPTURE' if not self.capture_manually else 'AUTH_ONLY',
'x_method': 'CC',
'x_fp_sequence': '%s%s' % (self.id, int(time.time())),
'x_version': '3.1',
'x_relay_response': 'TRUE',
'x_fp_timestamp': str(int(time.time())),
'x_relay_url': urls.url_join(base_url, AuthorizeController._return_url),
'x_cancel_url': urls.url_join(base_url, AuthorizeController._cancel_url),
'x_currency_code': values['currency'] and values['currency'].name or '',
'address': values.get('partner_address'),
'city': values.get('partner_city'),
'country': values.get('partner_country') and values.get('partner_country').name or '',
'email': values.get('partner_email'),
'zip_code': values.get('partner_zip'),
'first_name': values.get('partner_first_name'),
'last_name': values.get('partner_last_name'),
'phone': values.get('partner_phone'),
'state': state,
'billing_address': values.get('billing_partner_address'),
'billing_city': values.get('billing_partner_city'),
'billing_country': values.get('billing_partner_country') and values.get('billing_partner_country').name or '',
'billing_email': values.get('billing_partner_email'),
'billing_zip_code': values.get('billing_partner_zip'),
'billing_first_name': values.get('billing_partner_first_name'),
'billing_last_name': values.get('billing_partner_last_name'),
'billing_phone': values.get('billing_partner_phone'),
'billing_state': billing_state,
}
temp_authorize_tx_values['returndata'] = authorize_tx_values.pop('return_url', '')
temp_authorize_tx_values['x_fp_hash'] = self._authorize_generate_hashing(temp_authorize_tx_values)
authorize_tx_values.update(temp_authorize_tx_values)
return authorize_tx_values
def authorize_get_form_action_url(self):
self.ensure_one()
environment = 'prod' if self.state == 'enabled' else 'test'
return self._get_authorize_urls(environment)['authorize_form_url']
@api.model
def authorize_s2s_form_process(self, data):
values = {
'opaqueData': data.get('opaqueData'),
'encryptedCardData': data.get('encryptedCardData'),
'acquirer_id': int(data.get('acquirer_id')),
'partner_id': int(data.get('partner_id'))
}
PaymentMethod = self.env['payment.token'].sudo().create(values)
return PaymentMethod
def authorize_s2s_form_validate(self, data):
error = dict()
mandatory_fields = ["opaqueData", "encryptedCardData"]
# Validation
for field_name in mandatory_fields:
if not data.get(field_name):
error[field_name] = 'missing'
return False if error else True
def authorize_test_credentials(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
return transaction.test_authenticate()
class TxAuthorize(models.Model):
_inherit = 'payment.transaction'
_authorize_valid_tx_status = 1
_authorize_pending_tx_status = 4
_authorize_cancel_tx_status = 2
_authorize_error_tx_status = 3
# --------------------------------------------------
# FORM RELATED METHODS
# --------------------------------------------------
@api.model
def _authorize_form_get_tx_from_data(self, data):
""" Given a data dict coming from authorize, verify it and find the related
transaction record. """
reference, description, trans_id, fingerprint = data.get('x_invoice_num'), data.get('x_description'), data.get('x_trans_id'), data.get('x_SHA2_Hash') or data.get('x_MD5_Hash')
if not reference or not trans_id or not fingerprint:
error_msg = _('Authorize: received data with missing reference (%s) or trans_id (%s) or fingerprint (%s)') % (reference, trans_id, fingerprint)
_logger.info(error_msg)
raise ValidationError(error_msg)
tx = self.search(['|', ('reference', '=', reference), ('reference', '=', description)])
if not tx or len(tx) > 1:
error_msg = 'Authorize: received data for x_invoice_num %s and x_description %s' % (reference, description)
if not tx:
error_msg += '; no order found'
else:
error_msg += '; multiple order found'
_logger.info(error_msg)
raise ValidationError(error_msg)
return tx[0]
def _authorize_form_get_invalid_parameters(self, data):
invalid_parameters = []
if self.acquirer_reference and data.get('x_trans_id') != self.acquirer_reference:
invalid_parameters.append(('Transaction Id', data.get('x_trans_id'), self.acquirer_reference))
# check what is buyed
if float_compare(float(data.get('x_amount', '0.0')), self.amount, 2) != 0:
invalid_parameters.append(('Amount', data.get('x_amount'), '%.2f' % self.amount))
return invalid_parameters
def _authorize_form_validate(self, data):
if self.state == 'done':
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
return True
status_code = int(data.get('x_response_code', '0'))
if status_code == self._authorize_valid_tx_status:
if data.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
self.write({
'acquirer_reference': data.get('x_trans_id'),
'date': fields.Datetime.now(),
})
self._set_transaction_done()
elif data.get('x_type').lower() in ['auth_only']:
self.write({'acquirer_reference': data.get('x_trans_id')})
self._set_transaction_authorized()
if self.partner_id and not self.payment_token_id and \
(self.type == 'form_save' or self.acquirer_id.save_token == 'always'):
transaction = AuthorizeAPI(self.acquirer_id)
res = transaction.create_customer_profile_from_tx(self.partner_id, self.acquirer_reference)
if res:
token_id = self.env['payment.token'].create({
'authorize_profile': res.get('profile_id'),
'name': res.get('name'),
'acquirer_ref': res.get('payment_profile_id'),
'acquirer_id': self.acquirer_id.id,
'partner_id': self.partner_id.id,
})
self.payment_token_id = token_id
return True
elif status_code == self._authorize_pending_tx_status:
self.write({'acquirer_reference': data.get('x_trans_id')})
self._set_transaction_pending()
return True
else:
error = data.get('x_response_reason_text')
_logger.info(error)
self.write({
'state_message': error,
'acquirer_reference': data.get('x_trans_id'),
})
self._set_transaction_cancel()
return False
def authorize_s2s_do_transaction(self, **data):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
if not self.payment_token_id.authorize_profile:
raise UserError(_('Invalid token found: the Authorize profile is missing.'
'Please make sure the token has a valid acquirer reference.'))
if not self.acquirer_id.capture_manually:
res = transaction.auth_and_capture(self.payment_token_id, round(self.amount, self.currency_id.decimal_places), self.reference)
else:
res = transaction.authorize(self.payment_token_id, round(self.amount, self.currency_id.decimal_places), self.reference)
return self._authorize_s2s_validate_tree(res)
def authorize_s2s_capture_transaction(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
tree = transaction.capture(self.acquirer_reference or '', round(self.amount, self.currency_id.decimal_places))
return self._authorize_s2s_validate_tree(tree)
def authorize_s2s_void_transaction(self):
self.ensure_one()
transaction = AuthorizeAPI(self.acquirer_id)
tree = transaction.void(self.acquirer_reference or '')
return self._authorize_s2s_validate_tree(tree)
def _authorize_s2s_validate_tree(self, tree):
return self._authorize_s2s_validate(tree)
def _authorize_s2s_validate(self, tree):
if self.state == 'done':
_logger.warning('Authorize: trying to validate an already validated tx (ref %s)' % self.reference)
return True
status_code = int(tree.get('x_response_code', '0'))
if status_code == self._authorize_valid_tx_status:
if tree.get('x_type').lower() in ['auth_capture', 'prior_auth_capture']:
init_state = self.state
self.write({
'acquirer_reference': tree.get('x_trans_id'),
'date': fields.Datetime.now(),
})
self._set_transaction_done()
if init_state != 'authorized':
self.execute_callback()
if tree.get('x_type').lower() == 'auth_only':
self.write({'acquirer_reference': tree.get('x_trans_id')})
self._set_transaction_authorized()
self.execute_callback()
if tree.get('x_type').lower() == 'void':
self._set_transaction_cancel()
return True
elif status_code == self._authorize_pending_tx_status:
self.write({'acquirer_reference': tree.get('x_trans_id')})
self._set_transaction_pending()
return True
else:
error = tree.get('x_response_reason_text')
_logger.info(error)
self.write({
'acquirer_reference': tree.get('x_trans_id'),
})
self._set_transaction_error(msg=error)
return False
class PaymentToken(models.Model):
_inherit = 'payment.token'
authorize_profile = fields.Char(string='Authorize.net Profile ID', help='This contains the unique reference '
'for this partner/payment token combination in the Authorize.net backend')
provider = fields.Selection(string='Provider', related='acquirer_id.provider', readonly=False)
save_token = fields.Selection(string='Save Cards', related='acquirer_id.save_token', readonly=False)
@api.model
def authorize_create(self, values):
if values.get('opaqueData') and values.get('encryptedCardData'):
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
partner = self.env['res.partner'].browse(values['partner_id'])
transaction = AuthorizeAPI(acquirer)
res = transaction.create_customer_profile(partner, values['opaqueData'])
if res.get('profile_id') and res.get('payment_profile_id'):
return {
'authorize_profile': res.get('profile_id'),
'name': values['encryptedCardData'].get('cardNumber'),
'acquirer_ref': res.get('payment_profile_id'),
'verified': True
}
else:
raise ValidationError(_('The Customer Profile creation in Authorize.NET failed.'))
else:
return values
@@ -0,0 +1,94 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from odoo import _, api, fields, models
from odoo.exceptions import UserError
from .authorize_request import AuthorizeAPI
_logger = logging.getLogger(__name__)
class PaymentAcquirer(models.Model):
_inherit = 'payment.acquirer'
provider = fields.Selection(
selection_add=[('authorize', 'Authorize.Net')], ondelete={'authorize': 'set default'})
authorize_login = fields.Char(
string="API Login ID", help="The ID solely used to identify the account with Authorize.Net",
required_if_provider='authorize')
authorize_transaction_key = fields.Char(
string="API Transaction Key", required_if_provider='authorize', groups='base.group_system')
authorize_signature_key = fields.Char(
string="API Signature Key", required_if_provider='authorize', groups='base.group_system')
authorize_client_key = fields.Char(
string="API Client Key",
help="The public client key. To generate directly from Odoo or from Authorize.Net backend.")
# Authorize.Net supports only one currency: "One gateway account is required for each currency"
# See https://community.developer.authorize.net/t5/The-Authorize-Net-Developer-Blog/Authorize-Net-UK-Europe-Update/ba-p/35957
authorize_currency_id = fields.Many2one(
string="Authorize Currency", comodel_name='res.currency', groups='base.group_system')
def action_update_merchant_details(self):
""" Fetch the merchant details to update the client key and the account currency. """
self.ensure_one()
if self.state == 'disabled':
raise UserError(_("This action cannot be performed while the acquirer is disabled."))
authorize_API = AuthorizeAPI(self)
# Validate the API Login ID and Transaction Key
res_content = authorize_API.test_authenticate()
_logger.info("test_authenticate request response:\n%s", pprint.pformat(res_content))
if res_content.get('err_msg'):
raise UserError(_("Failed to authenticate.\n%s", res_content['err_msg']))
# Update the merchant details
res_content = authorize_API.merchant_details()
_logger.info("merchant_details request response:\n%s", pprint.pformat(res_content))
if res_content.get('err_msg'):
raise UserError(_("Could not fetch merchant details:\n%s", res_content['err_msg']))
currency = self.env['res.currency'].search([('name', 'in', res_content.get('currencies'))])
self.authorize_currency_id = currency
self.authorize_client_key = res_content.get('publicClientKey')
@api.model
def _get_compatible_acquirers(self, *args, currency_id=None, **kwargs):
""" Override of payment to unlist Authorize acquirers for unsupported currencies. """
acquirers = super()._get_compatible_acquirers(*args, currency_id=currency_id, **kwargs)
currency = self.env['res.currency'].browse(currency_id).exists()
if currency:
acquirers = acquirers.filtered(
lambda a: a.provider != 'authorize' or currency == a.authorize_currency_id
)
return acquirers
def _get_validation_amount(self):
""" Override of payment to return the amount for Authorize.Net validation operations.
:return: The validation amount
:rtype: float
"""
res = super()._get_validation_amount()
if self.provider != 'authorize':
return res
return 0.01
def _get_validation_currency(self):
""" Override of payment to return the currency for Authorize.Net validation operations.
:return: The validation currency
:rtype: recordset of `res.currency`
"""
res = super()._get_validation_currency()
if self.provider != 'authorize':
return res
return self.authorize_currency_id
@@ -0,0 +1,47 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from odoo import _, fields, models
from odoo.exceptions import UserError
from .authorize_request import AuthorizeAPI
_logger = logging.getLogger(__name__)
class PaymentToken(models.Model):
_inherit = 'payment.token'
authorize_profile = fields.Char(
string="Authorize.Net Profile ID",
help="The unique reference for the partner/token combination in the Authorize.net backend.")
def _handle_deactivation_request(self):
""" Override of payment to request Authorize.Net to delete the token.
Note: self.ensure_one()
:return: None
"""
super()._handle_deactivation_request()
if self.provider != 'authorize':
return
authorize_API = AuthorizeAPI(self.acquirer_id)
res_content = authorize_API.delete_customer_profile(self.authorize_profile)
_logger.info("delete_customer_profile request response:\n%s", pprint.pformat(res_content))
def _handle_reactivation_request(self):
""" Override of payment to raise an error informing that Auth.net tokens cannot be restored.
Note: self.ensure_one()
:return: None
"""
super()._handle_reactivation_request()
if self.provider != 'authorize':
return
raise UserError(_("Saved payment methods cannot be restored once they have been deleted."))
@@ -0,0 +1,243 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
import pprint
from odoo import _, api, models
from odoo.addons.payment import utils as payment_utils
from odoo.exceptions import UserError, ValidationError
from .authorize_request import AuthorizeAPI
_logger = logging.getLogger(__name__)
class PaymentTransaction(models.Model):
_inherit = 'payment.transaction'
def _get_specific_processing_values(self, processing_values):
""" Override of payment to return an access token as acquirer-specific processing values.
Note: self.ensure_one() from `_get_processing_values`
:param dict processing_values: The generic processing values of the transaction
:return: The dict of acquirer-specific processing values
:rtype: dict
"""
res = super()._get_specific_processing_values(processing_values)
if self.provider != 'authorize':
return res
return {
'access_token': payment_utils.generate_access_token(
processing_values['reference'], processing_values['partner_id']
)
}
def _authorize_create_transaction_request(self, opaque_data):
""" Create an Authorize.Net payment transaction request.
Note: self.ensure_one()
:param dict opaque_data: The payment details obfuscated by Authorize.Net
:return:
"""
self.ensure_one()
authorize_API = AuthorizeAPI(self.acquirer_id)
if self.acquirer_id.capture_manually or self.operation == 'validation':
return authorize_API.authorize(self.amount, self.reference, opaque_data=opaque_data)
else:
return authorize_API.auth_and_capture(
self.amount, self.reference, opaque_data=opaque_data
)
def _send_payment_request(self):
""" Override of payment to send a payment request to Authorize.
Note: self.ensure_one()
:return: None
:raise: UserError if the transaction is not linked to a token
"""
super()._send_payment_request()
if self.provider != 'authorize':
return
if not self.token_id.authorize_profile:
raise UserError("Authorize.Net: " + _("The transaction is not linked to a token."))
authorize_API = AuthorizeAPI(self.acquirer_id)
if self.acquirer_id.capture_manually:
res_content = authorize_API.authorize(self.amount, self.reference, token=self.token_id)
_logger.info("authorize request response:\n%s", pprint.pformat(res_content))
else:
res_content = authorize_API.auth_and_capture(
self.amount, self.reference, token=self.token_id
)
_logger.info("auth_and_capture request response:\n%s", pprint.pformat(res_content))
# As the API has no redirection flow, we always know the reference of the transaction.
# Still, we prefer to simulate the matching of the transaction by crafting dummy feedback
# data in order to go through the centralized `_handle_feedback_data` method.
feedback_data = {'reference': self.reference, 'response': res_content}
self._handle_feedback_data('authorize', feedback_data)
@api.model
def _get_tx_from_feedback_data(self, provider, data):
""" Find the transaction based on the feedback data.
:param str provider: The provider of the acquirer that handled the transaction
:param dict data: The feedback data sent by the acquirer
:return: The transaction if found
:rtype: recordset of `payment.transaction`
"""
tx = super()._get_tx_from_feedback_data(provider, data)
if provider != 'authorize':
return tx
reference = data.get('reference')
tx = self.search([('reference', '=', reference), ('provider', '=', 'authorize')])
if not tx:
raise ValidationError(
"Authorize.Net: " + _("No transaction found matching reference %s.", reference)
)
return tx
def _process_feedback_data(self, data):
""" Override of payment to process the transaction based on Authorize data.
Note: self.ensure_one()
:param dict data: The feedback data sent by the provider
:return: None
"""
super()._process_feedback_data(data)
if self.provider != 'authorize':
return
response_content = data.get('response')
self.acquirer_reference = response_content.get('x_trans_id')
status_code = response_content.get('x_response_code', '3')
if status_code == '1': # Approved
status_type = response_content.get('x_type').lower()
if status_type in ('auth_capture', 'prior_auth_capture'):
self._set_done()
if self.tokenize and not self.token_id:
self._authorize_tokenize()
elif status_type == 'auth_only':
self._set_authorized()
if self.tokenize and not self.token_id:
self._authorize_tokenize()
elif status_type == 'void':
self._set_canceled()
elif status_code == '2': # Declined
self._set_canceled()
elif status_code == '4': # Held for Review
self._set_pending()
else: # Error / Unknown code
error_code = response_content.get('x_response_reason_text')
_logger.info(
"received data with invalid status code %s and error code %s",
status_code, error_code
)
self._set_error(
"Authorize.Net: " + _(
"Received data with status code \"%(status)s\" and error code \"%(error)s\"",
status=status_code, error=error_code
)
)
def _authorize_tokenize(self):
""" Create a token for the current transaction.
Note: self.ensure_one()
:return: None
"""
self.ensure_one()
authorize_API = AuthorizeAPI(self.acquirer_id)
cust_profile = authorize_API.create_customer_profile(
self.partner_id, self.acquirer_reference
)
_logger.info("create_customer_profile request response:\n%s", pprint.pformat(cust_profile))
if cust_profile:
token = self.env['payment.token'].create({
'acquirer_id': self.acquirer_id.id,
'name': cust_profile.get('name'),
'partner_id': self.partner_id.id,
'acquirer_ref': cust_profile.get('payment_profile_id'),
'authorize_profile': cust_profile.get('profile_id'),
})
self.write({
'token_id': token.id,
'tokenize': False,
})
_logger.info(
"created token with id %s for partner with id %s", token.id, self.partner_id.id
)
def _send_refund_request(self):
""" Override of payment to send a refund request to Authorize.
Note: self.ensure_one()
:return: None
"""
super()._send_refund_request()
if self.provider != 'authorize':
return
authorize_API = AuthorizeAPI(self.acquirer_id)
rounded_amount = round(self.amount, self.currency_id.decimal_places)
res_content = authorize_API.refund(self.acquirer_reference, rounded_amount)
_logger.info("refund request response:\n%s", pprint.pformat(res_content))
# As the API has no redirection flow, we always know the reference of the transaction.
# Still, we prefer to simulate the matching of the transaction by crafting dummy feedback
# data in order to go through the centralized `_handle_feedback_data` method.
feedback_data = {'reference': self.reference, 'response': res_content}
self._handle_feedback_data('authorize', feedback_data)
def _send_capture_request(self):
""" Override of payment to send a capture request to Authorize.
Note: self.ensure_one()
:return: None
"""
super()._send_capture_request()
if self.provider != 'authorize':
return
authorize_API = AuthorizeAPI(self.acquirer_id)
rounded_amount = round(self.amount, self.currency_id.decimal_places)
res_content = authorize_API.capture(self.acquirer_reference, rounded_amount)
_logger.info("capture request response:\n%s", pprint.pformat(res_content))
# As the API has no redirection flow, we always know the reference of the transaction.
# Still, we prefer to simulate the matching of the transaction by crafting dummy feedback
# data in order to go through the centralized `_handle_feedback_data` method.
feedback_data = {'reference': self.reference, 'response': res_content}
self._handle_feedback_data('authorize', feedback_data)
def _send_void_request(self):
""" Override of payment to send a void request to Authorize.
Note: self.ensure_one()
:return: None
"""
super()._send_void_request()
if self.provider != 'authorize':
return
authorize_API = AuthorizeAPI(self.acquirer_id)
res_content = authorize_API.void(self.acquirer_reference)
_logger.info("void request response:\n%s", pprint.pformat(res_content))
# As the API has no redirection flow, we always know the reference of the transaction.
# Still, we prefer to simulate the matching of the transaction by crafting dummy feedback
# data in order to go through the centralized `_handle_feedback_data` method.
feedback_data = {'reference': self.reference, 'response': res_content}
self._handle_feedback_data('authorize', feedback_data)
@@ -1,160 +1,156 @@
odoo.define('payment_authorize.payment_form', function (require) {
"use strict";
odoo.define('payment_authorize.payment_form', require => {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var PaymentForm = require('payment.payment_form');
const core = require('web.core');
const ajax = require('web.ajax');
var _t = core._t;
const checkoutForm = require('payment.checkout_form');
const manageForm = require('payment.manage_form');
PaymentForm.include({
const _t = core._t;
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
const authorizeMixin = {
/**
* Returns the parameters for the AcceptUI button that AcceptJS will use.
*
* @private
* @param {Object} formData data obtained by getFormData
* @returns {Object} params for the AcceptJS button
*/
_acceptJsParams: function (formData) {
return {
'class': 'AcceptUI d-none',
'data-apiLoginID': formData.login_id,
'data-clientKey': formData.client_key,
'data-billingAddressOptions': '{"show": false, "required": false}',
'data-responseHandler': 'responseHandler'
};
},
/**
* called when clicking on pay now or add payment event to create token for credit card/debit card.
*
* @private
* @param {Event} ev
* @param {DOMElement} checkedRadio
* @param {Boolean} addPmEvent
*/
_createAuthorizeToken: function (ev, $checkedRadio, addPmEvent) {
var self = this;
if (ev.type === 'submit') {
var button = $(ev.target).find('*[type="submit"]')[0]
} else {
var button = ev.target;
}
var acquirerID = this.getAcquirerIdFromRadio($checkedRadio);
var acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
var inputsForm = $('input', acquirerForm);
var formData = self.getFormData(inputsForm);
if (this.options.partnerId === undefined) {
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
}
var AcceptJs = false;
if (formData.acquirer_state === 'enabled') {
AcceptJs = 'https://js.authorize.net/v3/AcceptUI.js';
} else {
AcceptJs = 'https://jstest.authorize.net/v3/AcceptUI.js';
}
window.responseHandler = function (response) {
_.extend(formData, response);
if (response.messages.resultCode === "Error") {
var errorMessage = "";
_.each(response.messages.message, function (message) {
errorMessage += message.code + ": " + message.text;
})
acquirerForm.removeClass('d-none');
return self.displayError(_t('Server Error'), errorMessage);
/**
* Prepare the inline form of Authorize.Net for direct payment.
*
* @override method from payment.payment_form_mixin
* @private
* @param {string} provider - The provider of the selected payment option's acquirer
* @param {number} paymentOptionId - The id of the selected payment option
* @param {string} flow - The online payment flow of the selected payment option
* @return {Promise}
*/
_prepareInlineForm: function (provider, paymentOptionId, flow) {
if (provider !== 'authorize') {
return this._super(...arguments);
}
self._rpc({
route: formData.data_set,
params: formData
}).then (function (data) {
if (addPmEvent) {
if (formData.return_url) {
window.location = formData.return_url;
} else {
window.location.reload();
}
} else {
$checkedRadio.val(data.id);
self.el.submit();
if (flow === 'token') {
return Promise.resolve(); // Don't show the form for tokens
}
this._setPaymentFlow('direct');
let acceptJSUrl = 'https://js.authorize.net/v1/Accept.js';
return this._rpc({
route: '/payment/authorize/get_acquirer_info',
params: {
'acquirer_id': paymentOptionId,
},
}).then(acquirerInfo => {
if (acquirerInfo.state !== 'enabled') {
acceptJSUrl = 'https://jstest.authorize.net/v1/Accept.js';
}
}).guardedCatch(function (error) {
// if the rpc fails, pretty obvious
this.authorizeInfo = acquirerInfo;
}).then(() => {
ajax.loadJS(acceptJSUrl);
}).guardedCatch((error) => {
error.event.preventDefault();
acquirerForm.removeClass('d-none');
self.displayError(
_t('Server Error'),
_t("We are not able to add your payment method at the moment.") +
self._parseError(error)
this._displayError(
_t("Server Error"),
_t("An error occurred when displayed this payment form."),
error.message.data.message
);
});
};
},
if (this.$button === undefined) {
this.$button = $('<button>', this._acceptJsParams(formData));
this.$button.appendTo('body');
}
ajax.loadJS(AcceptJs).then(function () {
self.$button.trigger('click');
});
},
/**
* @override
*/
updateNewPaymentDisplayStatus: function () {
var $checkedRadio = this.$('input[type="radio"]:checked');
/**
* Dispatch the secure data to Authorize.Net.
*
* @override method from payment.payment_form_mixin
* @private
* @param {string} provider - The provider of the payment option's acquirer
* @param {number} paymentOptionId - The id of the payment option handling the transaction
* @param {string} flow - The online payment flow of the transaction
* @return {Promise}
*/
_processPayment: function (provider, paymentOptionId, flow) {
if (provider !== 'authorize' || flow === 'token') {
return this._super(...arguments); // Tokens are handled by the generic flow
}
if ($checkedRadio.length !== 1) {
return;
}
const card = document.getElementById(`o_authorize_card_${paymentOptionId}`);
const month = document.getElementById(`o_authorize_month_${paymentOptionId}`);
const year = document.getElementById(`o_authorize_year_${paymentOptionId}`);
const code = document.getElementById(`o_authorize_code_${paymentOptionId}`);
// hide add token form for authorize
if ($checkedRadio.data('provider') === 'authorize' && this.isNewPaymentRadio($checkedRadio)) {
this.$('[id*="o_payment_add_token_acq_"]').addClass('d-none');
} else {
this._super.apply(this, arguments);
}
},
// Basic form validation
if (!(
card.reportValidity()
&& month.reportValidity()
&& year.reportValidity()
&& code.reportValidity()
)) {
this._enableButton(); // The submit button is disabled at this point, enable it
return Promise.resolve();
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
// Build the authentication and card data objects to be dispatched to Authorized.Net
const secureData = {
authData: {
apiLoginID: this.authorizeInfo.login_id,
clientKey: this.authorizeInfo.client_key,
},
cardData: {
cardNumber: card.value.replace(/ /g, ''), // Remove all spaces
month: month.value,
year: year.value,
cardCode: code.value,
}
};
// Dispatch secure data to Authorize.Net to get a payment nonce in return
return Accept.dispatchData(
secureData, response => this._responseHandler(paymentOptionId, response)
);
},
/**
* @override
*/
payEvent: function (ev) {
ev.preventDefault();
var $checkedRadio = this.$('input[type="radio"]:checked');
/**
* Handle the response from Authorize.Net and initiate the payment.
*
* @private
* @param {number} acquirerId - The id of the selected acquirer
* @param {object} response - The payment nonce returned by Authorized.Net
* @return {Promise}
*/
_responseHandler: function (acquirerId, response) {
if (response.messages.resultCode === 'Error') {
let error = "";
response.messages.message.forEach(msg => error += `${msg.code}: ${msg.text}\n`);
this._displayError(
_t("Server Error"),
_t("We are not able to process your payment."),
error
);
return Promise.resolve();
}
// first we check that the user has selected a authorize as s2s payment method
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'authorize') {
this._createAuthorizeToken(ev, $checkedRadio);
} else {
this._super.apply(this, arguments);
}
},
/**
* @override
*/
addPmEvent: function (ev) {
ev.stopPropagation();
ev.preventDefault();
var $checkedRadio = this.$('input[type="radio"]:checked');
// Create the transaction and retrieve the processing values
return this._rpc({
route: this.txContext.transactionRoute,
params: this._prepareTransactionRouteParams('authorize', acquirerId, 'direct'),
}).then(processingValues => {
// Initiate the payment
return this._rpc({
route: '/payment/authorize/payment',
params: {
'reference': processingValues.reference,
'partner_id': processingValues.partner_id,
'opaque_data': response.opaqueData,
'access_token': processingValues.access_token,
}
}).then(() => window.location = '/payment/status');
}).guardedCatch((error) => {
error.event.preventDefault();
this._displayError(
_t("Server Error"),
_t("We are not able to process your payment."),
error.message.data.message
);
});
},
};
// first we check that the user has selected a authorize as add payment method
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'authorize') {
this._createAuthorizeToken(ev, $checkedRadio, true);
} else {
this._super.apply(this, arguments);
}
},
});
checkoutForm.include(authorizeMixin);
manageForm.include(authorizeMixin);
});
@@ -0,0 +1,15 @@
.o_authorize_form {
label {
margin-top: 0 !important;
}
input[type=number]::-webkit-outer-spin-button,
input[type=number]::-webkit-inner-spin-button {
appearance: none;
margin: 0;
}
input[type=number] {
-moz-appearance: textfield;
}
}
+2 -1
View File
@@ -1,3 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import common
from . import test_authorize
+20
View File
@@ -0,0 +1,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.payment.tests.common import PaymentCommon
class AuthorizeCommon(PaymentCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.authorize = cls._prepare_acquirer('authorize', update_values={
'authorize_login': 'dummy',
'authorize_transaction_key': 'dummy',
'authorize_signature_key': '00000000',
'authorize_currency_id': cls.currency_usd.id,
})
cls.acquirer = cls.authorize
cls.currency = cls.currency_usd
+45 -264
View File
@@ -1,275 +1,56 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from werkzeug import urls
from lxml import objectify
from unittest.mock import patch
import odoo
from odoo.addons.payment.models.payment_acquirer import ValidationError
from odoo.addons.payment.tests.common import PaymentAcquirerCommon
from odoo.addons.payment_authorize.controllers.main import AuthorizeController
from odoo.addons.payment import utils as payment_utils
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tools import mute_logger
@odoo.tests.tagged('post_install', '-at_install')
class AuthorizeCommon(PaymentAcquirerCommon):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
# authorize only support USD in test environment
cls.currency_usd = cls.env['res.currency'].search([('name', '=', 'USD')], limit=1)[0]
# get the authorize account
cls.authorize = cls.env.ref('payment.payment_acquirer_authorize')
cls.authorize.write({
'authorize_login': 'dummy',
'authorize_transaction_key': 'dummy',
'authorize_signature_key': '00000000',
'state': 'test',
})
# Be sure to be in 'capture' mode
# self.authorize.auto_confirm = 'confirm_so'
from .common import AuthorizeCommon
@odoo.tests.tagged('post_install', '-at_install', '-standard', 'external')
class AuthorizeForm(AuthorizeCommon):
@tagged('post_install', '-at_install')
class AuthorizeTest(AuthorizeCommon):
def test_10_Authorize_form_render(self):
self.assertEqual(self.authorize.state, 'test', 'test without test environment')
def test_compatible_acquirers(self):
# Note: in the test common, 'USD' is specified as authorize_currency_id
unsupported_currency = self._prepare_currency('CHF')
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
currency_id=unsupported_currency.id,
company_id=self.company.id)
self.assertNotIn(self.authorize, acquirers)
acquirers = self.env['payment.acquirer']._get_compatible_acquirers(
partner_id=self.partner.id,
currency_id=self.currency_usd.id,
company_id=self.company.id)
self.assertIn(self.authorize, acquirers)
# ----------------------------------------
# Test: button direct rendering
# ----------------------------------------
base_url = self.env['ir.config_parameter'].get_param('web.base.url')
form_values = {
'x_login': self.authorize.authorize_login,
'x_amount': '56.16',
'x_show_form': 'PAYMENT_FORM',
'x_type': 'AUTH_CAPTURE',
'x_method': 'CC',
'x_fp_sequence': '%s%s' % (self.authorize.id, int(time.time())),
'x_version': '3.1',
'x_relay_response': 'TRUE',
'x_fp_timestamp': str(int(time.time())),
'x_relay_url': urls.url_join(base_url, AuthorizeController._return_url),
'x_cancel_url': urls.url_join(base_url, AuthorizeController._cancel_url),
'return_url': None,
'x_currency_code': 'USD',
'x_invoice_num': 'SO004',
'x_first_name': 'Norbert',
'x_last_name': 'Buyer',
'x_company': 'Big Company',
'x_address': 'Huge Street 2/543',
'x_city': 'Sin City',
'x_zip': '1000',
'x_country': 'Belgium',
'x_phone': '0032 12 34 56 78',
'x_email': 'norbert.buyer@example.com',
'x_state': None,
'x_ship_to_first_name': 'Norbert',
'x_ship_to_last_name': 'Buyer',
'x_ship_to_address': 'Huge Street 2/543',
'x_ship_to_city': 'Sin City',
'x_ship_to_zip': '1000',
'x_ship_to_country': 'Belgium',
'x_ship_to_phone': '0032 12 34 56 78',
'x_ship_to_email': 'norbert.buyer@example.com',
'x_ship_to_state': None,
}
def test_processing_values(self):
"""Test custom 'access_token' processing_values for authorize acquirer."""
tx = self.create_transaction(flow='direct')
with mute_logger('odoo.addons.payment.models.payment_transaction'), \
patch(
'odoo.addons.payment.utils.generate_access_token',
new=self._generate_test_access_token
):
processing_values = tx._get_processing_values()
form_values['x_fp_hash'] = self.authorize._authorize_generate_hashing(form_values)
# render the button
res = self.authorize.render('SO004', 56.16, self.currency_usd.id, values=self.buyer_values)
# check form result
tree = objectify.fromstring(res)
with patch(
'odoo.addons.payment.utils.generate_access_token', new=self._generate_test_access_token
):
self.assertTrue(payment_utils.check_access_token(
processing_values['access_token'], self.reference, self.partner.id,
))
data_set = tree.xpath("//input[@name='data_set']")
self.assertEqual(len(data_set), 1, 'Authorize: Found %d "data_set" input instead of 1' % len(data_set))
self.assertEqual(data_set[0].get('data-action-url'), 'https://test.authorize.net/gateway/transact.dll', 'Authorize: wrong data-action-url POST url')
for el in tree.iterfind('input'):
values = list(el.attrib.values())
if values[1] in ['submit', 'x_fp_hash', 'return_url', 'x_state', 'x_ship_to_state', 'data_set']:
continue
self.assertEqual(
values[2],
form_values[values[1]],
'Authorize: wrong value for input %s: received %s instead of %s' % (values[1], values[2], form_values[values[1]])
)
def test_validation(self):
self.assertEqual(self.authorize.authorize_currency_id, self.currency_usd)
self.assertEqual(self.authorize._get_validation_amount(), 0.01)
self.assertEqual(self.authorize._get_validation_currency(), self.currency_usd)
@mute_logger('odoo.addons.payment_authorize.models.payment', 'ValidationError')
def test_20_authorize_form_management(self):
# be sure not to do stupid thing
self.assertEqual(self.authorize.state, 'test', 'test without test environment')
# typical data posted by authorize after client has successfully paid
authorize_post_data = {
'return_url': u'/shop/payment/validate',
# x_MD5_Hash will be empty starting the 28th March 2019
'x_MD5_Hash': u'7934485E1C105940BE854208D10FAB4F',
'x_SHA2_Hash': u'7D3AC844BE8CA3F649AB885A90D22CFE35B850338EC91D1A5ADD819A85FF948A3D777334A18CDE36821DC8F2B42A6E1950C1FF96B52B60F23201483A656195FB',
'x_account_number': u'XXXX0027',
'x_address': u'Huge Street 2/543',
'x_amount': u'320.00',
'x_auth_code': u'E4W7IU',
'x_avs_code': u'Y',
'x_card_type': u'Visa',
'x_cavv_response': u'2',
'x_city': u'Sun City',
'x_company': u'',
'x_country': u'Belgium',
'x_cust_id': u'',
'x_cvv2_resp_code': u'',
'x_description': u'',
'x_duty': u'0.00',
'x_email': u'norbert.buyer@example.com',
'x_fax': u'',
'x_first_name': u'Norbert',
'x_freight': u'0.00',
'x_invoice_num': u'SO004',
'x_last_name': u'Buyer',
'x_method': u'CC',
'x_phone': u'0032 12 34 56 78',
'x_po_num': u'',
'x_response_code': u'1',
'x_response_reason_code': u'1',
'x_response_reason_text': u'This transaction has been approved.',
'x_ship_to_address': u'Huge Street 2/543',
'x_ship_to_city': u'Sun City',
'x_ship_to_company': u'',
'x_ship_to_country': u'Belgium',
'x_ship_to_first_name': u'Norbert',
'x_ship_to_last_name': u'Buyer',
'x_ship_to_state': u'',
'x_ship_to_zip': u'1000',
'x_state': u'',
'x_tax': u'0.00',
'x_tax_exempt': u'FALSE',
'x_test_request': u'false',
'x_trans_id': u'2217460311',
'x_type': u'auth_capture',
'x_zip': u'1000'
}
# should raise error about unknown tx
with self.assertRaises(ValidationError):
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
tx = self.env['payment.transaction'].create({
'amount': 320.0,
'acquirer_id': self.authorize.id,
'currency_id': self.currency_usd.id,
'reference': 'SO004',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# validate it
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
# check state
self.assertEqual(tx.state, 'done', 'Authorize: validation did not put tx into done state')
self.assertEqual(tx.acquirer_reference, authorize_post_data.get('x_trans_id'), 'Authorize: validation did not update tx payid')
tx = self.env['payment.transaction'].create({
'amount': 320.0,
'acquirer_id': self.authorize.id,
'currency_id': self.currency_usd.id,
'reference': 'SO004-2',
'partner_name': 'Norbert Buyer',
'partner_country_id': self.country_france.id})
# simulate an error
authorize_post_data['x_response_code'] = u'3'
self.env['payment.transaction'].form_feedback(authorize_post_data, 'authorize')
# check state
self.assertNotEqual(tx.state, 'done', 'Authorize: erroneous validation did put tx into done state')
@odoo.tests.tagged('post_install', '-at_install', '-standard')
class AuthorizeS2s(AuthorizeCommon):
def test_30_authorize_s2s(self):
# be sure not to do stupid thing
authorize = self.authorize
self.assertEqual(authorize.state, 'test', 'test without test environment')
# add credential
# FIXME: put this test in master-nightly on odoo/odoo + create sandbox account
authorize.write({
'authorize_transaction_key': '',
'authorize_login': '',
})
self.assertTrue(authorize.authorize_test_credentials, 'Authorize.net: s2s authentication failed')
# create payment meethod
payment_token = self.env['payment.token'].create({
'acquirer_id': authorize.id,
'partner_id': self.buyer_id,
'opaqueData': {
'dataDescriptor': 'COMMON.ACCEPT.INAPP.PAYMENT',
'dataValue': '9487801666614876704604'
},
})
# create normal s2s transaction
transaction = self.env['payment.transaction'].create({
'amount': 500,
'acquirer_id': authorize.id,
'type': 'server2server',
'currency_id': self.currency_usd.id,
'reference': 'test_ref_%s' % int(time.time()),
'payment_token_id': payment_token.id,
'partner_id': self.buyer_id,
})
transaction.authorize_s2s_do_transaction()
self.assertEqual(transaction.state, 'done',)
# switch to 'authorize only'
# create authorize only s2s transaction & capture it
self.authorize.capture_manually = True
transaction = self.env['payment.transaction'].create({
'amount': 500,
'acquirer_id': authorize.id,
'type': 'server2server',
'currency_id': self.currency_usd.id,
'reference': 'test_%s' % int(time.time()),
'payment_token_id': payment_token.id,
'partner_id': self.buyer_id,
})
transaction.authorize_s2s_do_transaction()
self.assertEqual(transaction.state, 'authorized')
transaction.action_capture()
self.assertEqual(transaction.state, 'done')
# create authorize only s2s transaction & void it
self.authorize.capture_manually = True
transaction = self.env['payment.transaction'].create({
'amount': 500,
'acquirer_id': authorize.id,
'type': 'server2server',
'currency_id': self.currency_usd.id,
'reference': 'test_%s' % int(time.time()),
'payment_token_id': payment_token.id,
'partner_id': self.buyer_id,
})
transaction.authorize_s2s_do_transaction()
self.assertEqual(transaction.state, 'authorized')
transaction.action_void()
self.assertEqual(transaction.state, 'cancel')
# try charging an unexisting profile
ghost_payment_token = payment_token.copy()
ghost_payment_token.authorize_profile = '99999999999'
# create normal s2s transaction
transaction = self.env['payment.transaction'].create({
'amount': 500,
'acquirer_id': authorize.id,
'type': 'server2server',
'currency_id': self.currency_usd.id,
'reference': 'test_ref_%s' % int(time.time()),
'payment_token_id': ghost_payment_token.id,
'partner_id': self.buyer_id,
})
transaction.authorize_s2s_do_transaction()
self.assertEqual(transaction.state, 'cancel')
def test_token_activation(self):
"""Activation of disabled authorize tokens is forbidden"""
token = self.create_token(active=False)
with self.assertRaises(UserError):
token._handle_reactivation_request()
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr="link[last()]" position="after">
<link rel="stylesheet" type="text/scss" href="/payment_authorize/static/src/scss/payment_authorize.scss"/>
</xpath>
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/payment_authorize/static/src/js/payment_form.js"/>
</xpath>
</template>
</odoo>
@@ -1,71 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<template id="authorize_form">
<div>
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
<input type="hidden" name='x_login' t-att-value='x_login'/>
<input type="hidden" name='x_fp_hash' t-att-value='x_fp_hash'/>
<input type="hidden" name='x_amount' t-att-value='x_amount'/>
<input type="hidden" name='x_show_form' t-att-value="x_show_form"/>
<input type="hidden" name='x_type' t-att-value="x_type"/>
<input type="hidden" name='x_method' t-att-value="x_method"/>
<input type="hidden" name='x_fp_sequence' t-att-value='x_fp_sequence'/>
<input type="hidden" name='x_version' t-att-value="x_version"/>
<input type="hidden" name="x_relay_response" t-att-value="x_relay_response"/>
<input type="hidden" name="x_relay_url" t-att-value="x_relay_url"/>
<input type="hidden" name="x_fp_timestamp" t-att-value="x_fp_timestamp"/>
<input type="hidden" name='return_url' t-att-value="returndata"/>
<input type="hidden" name='x_cancel_url' t-att-value="x_cancel_url"/>
<!--Order Information -->
<input type="hidden" name='x_invoice_num' t-att-value='reference'/>
<input type="hidden" name='x_description' t-att-value='reference'/>
<input type="hidden" name='x_currency_code' t-att-value='x_currency_code'/>
<!-- Billing Information-->
<input type="hidden" name='x_first_name' t-att-value="billing_first_name"/>
<input type="hidden" name='x_last_name' t-att-value="billing_last_name"/>
<input type="hidden" name="x_company" t-att-value="billing_partner_commercial_company_name"/>
<input type="hidden" name='x_address' t-att-value="billing_address"/>
<input type="hidden" name='x_city' t-att-value="billing_city"/>
<input type="hidden" name='x_zip' t-att-value="billing_zip_code"/>
<input type="hidden" name='x_country' t-att-value="billing_country"/>
<input type="hidden" name='x_phone' t-att-value='billing_phone'/>
<input type="hidden" name='x_email' t-att-value="billing_email"/>
<input type="hidden" name='x_state' t-att-value="billing_state"/>
<!-- Shipping Information-->
<input type="hidden" name='x_ship_to_first_name' t-att-value="first_name"/>
<input type="hidden" name='x_ship_to_last_name' t-att-value="last_name"/>
<input type="hidden" name='x_ship_to_address' t-att-value="address"/>
<input type="hidden" name='x_ship_to_city' t-att-value="city"/>
<input type="hidden" name='x_ship_to_zip' t-att-value="zip_code"/>
<input type="hidden" name='x_ship_to_country' t-att-value="country"/>
<input type="hidden" name='x_ship_to_phone' t-att-value='phone'/>
<input type="hidden" name='x_ship_to_email' t-att-value="email"/>
<input type="hidden" name='x_ship_to_state' t-att-value="state"/>
<template id="inline_form">
<div t-attf-id="o_authorize_form_{{acquirer_id}}" class="o_authorize_form">
<div class="form-group">
<label t-attf-for="o_authorize_card_{{acquirer_id}}" class="col-form-label">Card Number</label>
<input type="text" t-attf-id="o_authorize_card_{{acquirer_id}}" required="" maxlength="19" class="form-control"/>
</div>
</template>
<div class="row">
<div class="col-sm-8 form-group">
<label t-attf-for="o_authorize_month_{{acquirer_id}}">Expiration</label>
<div class="input-group">
<input type="number" t-attf-id="o_authorize_month_{{acquirer_id}}" placeholder="MM" min="1" max="12" required="" class="form-control"/>
<input type="number" t-attf-id="o_authorize_year_{{acquirer_id}}" placeholder="YY" min="00" max="99" required="" class="form-control"/>
</div>
</div>
<div class="col-sm-4 form-group">
<label t-attf-for="o_authorize_code_{{acquirer_id}}">Card Code</label>
<input type="number" t-attf-id="o_authorize_code_{{acquirer_id}}" max="999" class="form-control"/>
</div>
</div>
</div>
</template>
<template id="payment_authorize_redirect" name="Payment Authorize">
<script type="text/javascript">
window.location.href = '<t t-esc="return_url"/>';
</script>
</template>
<template id="authorize_s2s_form">
<input type="hidden" name="data_set" value="/payment/authorize/s2s/create_json_3ds"/>
<input type="hidden" name="acquirer_id" t-att-value="id"/>
<input type="hidden" name="acquirer_state" t-att-value="acq.state"/>
<input type="hidden" name="login_id" t-att-value="acq.sudo().authorize_login"/>
<input type="hidden" name="client_key" t-att-value="acq.sudo().authorize_client_key"/>
<input class="d-none" name="csrf_token" t-att-value="request.csrf_token()"/>
<input t-if="return_url" type="hidden" name="return_url" t-att-value="return_url"/>
<input t-if="partner_id" type="hidden" name="partner_id" t-att-value="partner_id"/>
</template>
<template id="assets_frontend" inherit_id="web.assets_frontend">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/payment_authorize/static/src/js/payment_form.js"></script>
</xpath>
</template>
</data>
</odoo>
@@ -1,36 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="acquirer_form_authorize" model="ir.ui.view">
<field name="name">acquirer.form.authorize</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'authorize')]}">
<field name="authorize_login" attrs="{'required':[ ('provider', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
<field name="authorize_transaction_key" password="True" attrs="{'required':[ ('provider', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
<field name="authorize_signature_key" password="True" attrs="{'required':[ ('provider', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
<label for="authorize_client_key"/>
<div>
<field name="authorize_client_key" password="True"/>
<button class="oe_link" icon="fa-refresh" type="object" name="action_client_secret" string="Generate Client Key" />
</div>
<a colspan="2" href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/authorize.html" target="_blank">How to get paid with Authorize.Net</a>
</group>
</xpath>
</field>
</record>
<record id="token_form_authorize_net" model="ir.ui.view">
<field name='name'>payment.token.form</field>
<field name='model'>payment.token</field>
<field name="inherit_id" ref="payment.payment_token_form_view"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="acquirer_ref"]' position='after'>
<field name="authorize_profile" attrs="{'invisible':['|', ('provider', '!=', 'authorize'), ('save_token', '=', 'none')]}"/>
<field name="provider" invisible='1'/>
<field name="save_token" invisible='1'/>
</xpath>
</field>
</record>
<record id="payment_acquirer_form" model="ir.ui.view">
<field name="name">Authorize.Net Acquirer Form</field>
<field name="model">payment.acquirer</field>
<field name="inherit_id" ref="payment.payment_acquirer_form"/>
<field name="arch" type="xml">
<xpath expr='//group[@name="acquirer"]' position='inside'>
<group attrs="{'invisible': [('provider', '!=', 'authorize')]}">
<field name="authorize_login" attrs="{'required':[('provider', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
<field name="authorize_transaction_key" password="True" attrs="{'required':[ ('provider', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
<field name="authorize_signature_key" password="True" attrs="{'required':[ ('provider', '=', 'authorize'), ('state', '!=', 'disabled')]}"/>
<label for="authorize_client_key"/>
<div>
<field name="authorize_client_key" password="True"/>
<button class="oe_link" icon="fa-refresh" type="object"
name="action_update_merchant_details"
string="Generate Client Key"/>
</div>
<a colspan="2" href="https://www.odoo.com/documentation/user/online/ecommerce/shopper_experience/authorize.html" target="_blank">
How to get paid with Authorize.Net
</a>
</group>
</xpath>
<xpath expr="//field[@name='country_ids']" position="after">
<label for="authorize_currency_id" string="Currency" attrs="{'invisible': [('provider', '!=', 'authorize')]}"/>
<div attrs="{'invisible': [('provider', '!=', 'authorize')]}">
<field name="authorize_currency_id"/>
<button class="oe_link" icon="fa-refresh" type="object"
name="action_update_merchant_details"
string="Set Account Currency"/>
</div>
</xpath>
</field>
</record>
<record id="payment_token_form" model="ir.ui.view">
<field name='name'>Authorize.Net Token Form</field>
<field name='model'>payment.token</field>
<field name="inherit_id" ref="payment.payment_token_form"/>
<field name="arch" type="xml">
<xpath expr='//field[@name="acquirer_ref"]' position='after'>
<field name="provider" invisible="1"/>
<field name="authorize_profile" attrs="{'invisible':[('provider', '!=', 'authorize')]}"/>
</xpath>
</field>
</record>
</odoo>