[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:
committed by
Antoine Vandevenne (anv)
co-authored by
Adrien Horgnies
parent
6b42234b8b
commit
660dc0ebaf
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user