summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--addons/payment_authorize/controllers/main.py13
-rw-r--r--addons/payment_authorize/data/payment_acquirer_data.xml1
-rw-r--r--addons/payment_authorize/models/authorize_request.py28
-rw-r--r--addons/payment_authorize/models/payment.py73
-rw-r--r--addons/payment_authorize/static/src/js/payment_form.js153
-rw-r--r--addons/payment_authorize/views/payment_authorize_templates.xml28
-rw-r--r--addons/payment_authorize/views/payment_views.xml5
7 files changed, 207 insertions, 94 deletions
diff --git a/addons/payment_authorize/controllers/main.py b/addons/payment_authorize/controllers/main.py
index cf6877218db..49f919f4198 100644
--- a/addons/payment_authorize/controllers/main.py
+++ b/addons/payment_authorize/controllers/main.py
@@ -30,19 +30,6 @@ class AuthorizeController(http.Controller):
'return_url': urls.url_join(base_url, "/payment/process")
})
- @http.route(['/payment/authorize/s2s/create_json'], type='json', auth='public')
- def authorize_s2s_create_json(self, **kwargs):
- acquirer_id = int(kwargs.get('acquirer_id'))
- acquirer = request.env['payment.acquirer'].browse(acquirer_id)
- if not kwargs.get('partner_id'):
- kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
- try:
- return acquirer.s2s_process(kwargs).id
- except (ValidationError, UserError) as e:
- return {
- 'error': e.name,
- }
-
@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
diff --git a/addons/payment_authorize/data/payment_acquirer_data.xml b/addons/payment_authorize/data/payment_acquirer_data.xml
index 293bcbad9e0..eda969f9c75 100644
--- a/addons/payment_authorize/data/payment_acquirer_data.xml
+++ b/addons/payment_authorize/data/payment_acquirer_data.xml
@@ -14,6 +14,7 @@
<p>You will be redirected to the Authorize website after clicking on the payment button.</p>]]></field>
<field name='authorize_login'>dummy</field>
<field name="authorize_transaction_key">dummy</field>
+ <field name="authorize_signature_key">dummy</field>
</record>
</data>
diff --git a/addons/payment_authorize/models/authorize_request.py b/addons/payment_authorize/models/authorize_request.py
index 6d8f1465fd7..53f40bfb1af 100644
--- a/addons/payment_authorize/models/authorize_request.py
+++ b/addons/payment_authorize/models/authorize_request.py
@@ -34,6 +34,8 @@ class AuthorizeAPI():
self.url = 'https://apitest.authorize.net/xml/v1/request.api'
else:
self.url = 'https://api.authorize.net/xml/v1/request.api'
+
+ self.environment = acquirer.environment
self.name = acquirer.authorize_login
self.transaction_key = acquirer.authorize_transaction_key
@@ -52,7 +54,7 @@ class AuthorizeAPI():
return resp
# Customer profiles
- def create_customer_profile(self, partner, cardnumber, expiration_date, card_code):
+ 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
@@ -90,15 +92,14 @@ class AuthorizeAPI():
'country': partner.country_id.name or None
},
'payment': {
- 'creditCard': {
- 'cardNumber': cardnumber,
- 'expirationDate': expiration_date,
- 'cardCode': card_code
+ 'opaqueData': {
+ 'dataDescriptor': opaqueData.get('dataDescriptor'),
+ 'dataValue': opaqueData.get('dataValue')
}
}
}
},
- 'validationMode': 'liveMode'
+ 'validationMode': 'liveMode' if self.environment == 'prod' else 'testMode'
}
}
@@ -375,3 +376,18 @@ class AuthorizeAPI():
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
diff --git a/addons/payment_authorize/models/payment.py b/addons/payment_authorize/models/payment.py
index c2d550735fe..fcc70e6682d 100644
--- a/addons/payment_authorize/models/payment.py
+++ b/addons/payment_authorize/models/payment.py
@@ -2,18 +2,15 @@
from werkzeug import urls
from .authorize_request import AuthorizeAPI
-from datetime import datetime
import hashlib
import hmac
import logging
-import string
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.tools.safe_eval import safe_eval
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
@@ -25,7 +22,8 @@ class PaymentAcquirerAuthorize(models.Model):
provider = fields.Selection(selection_add=[('authorize', 'Authorize.Net')])
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', groups='base.group_user', compute="_compute_auth_signature_key", inverse="_inverse_auth_signature_key")
+ 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):
@@ -35,6 +33,13 @@ class PaymentAcquirerAuthorize(models.Model):
'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.
@@ -51,16 +56,6 @@ class PaymentAcquirerAuthorize(models.Model):
res['tokenize'].append('authorize')
return res
- def _compute_auth_signature_key(self):
- ICP = self.env['ir.config_parameter'].sudo()
- for acquirer in self.filtered(lambda a: a.provider == 'authorize'):
- acquirer.authorize_signature_key = ICP.get_param('payment_authorize.signature_key_%s' % acquirer.id)
-
- def _inverse_auth_signature_key(self):
- ICP = self.env['ir.config_parameter'].sudo()
- for acquirer in self.filtered(lambda a: a.provider == 'authorize'):
- ICP.set_param('payment_authorize.signature_key_%s' % acquirer.id, acquirer.authorize_signature_key)
-
def _get_authorize_urls(self, environment):
""" Authorize URLs """
if environment == 'prod':
@@ -76,24 +71,7 @@ class PaymentAcquirerAuthorize(models.Model):
values['x_amount'],
values['x_currency_code']]).encode('utf-8')
- # [BACKWARD COMPATIBILITY, 2nd edition]
- # The signature key is now '128-character hexadecimal format', while the
- # transaction key was only 16-character.
- # One of 2 things should have happened:
- # 1/ the Transaction Key has been replaced with the Signature Key value (patch from March 2019)
- # => Use that to sign, but server-to-server won't work since it uses transaction key
- # as its credentials
- # 2/ the Signature key is a new field (patch from July 2019)
- # => Use that field for the signature
-
- # FORWARD-PORT NOTE: forward part to saas-12.4 but no further
- if len(values['x_trans_key']) == 128 and not self.authorize_signature_key:
- self.authorize_signature_key = values['x_trans_key'] # store in the correct field
- return hmac.new(bytes.fromhex(values['x_trans_key']), data, hashlib.sha512).hexdigest().upper()
- elif self.authorize_signature_key:
- return hmac.new(bytes.fromhex(self.authorize_signature_key), data, hashlib.sha512).hexdigest().upper()
- else:
- return hmac.new(values['x_trans_key'].encode('utf-8'), data, hashlib.md5).hexdigest()
+ return hmac.new(bytes.fromhex(self.authorize_signature_key), data, hashlib.sha512).hexdigest().upper()
def authorize_form_generate_values(self, values):
self.ensure_one()
@@ -110,7 +88,6 @@ class PaymentAcquirerAuthorize(models.Model):
authorize_tx_values = dict(values)
temp_authorize_tx_values = {
'x_login': self.authorize_login,
- 'x_trans_key': self.authorize_transaction_key,
'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',
@@ -143,7 +120,6 @@ class PaymentAcquirerAuthorize(models.Model):
}
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)
- temp_authorize_tx_values.pop('x_trans_key') # We remove this value since it is secret and isn't needed on the form
authorize_tx_values.update(temp_authorize_tx_values)
return authorize_tx_values
@@ -154,11 +130,8 @@ class PaymentAcquirerAuthorize(models.Model):
@api.model
def authorize_s2s_form_process(self, data):
values = {
- 'cc_number': data.get('cc_number'),
- 'cc_holder_name': data.get('cc_holder_name'),
- 'cc_expiry': data.get('cc_expiry'),
- 'cc_cvc': data.get('cc_cvc'),
- 'cc_brand': data.get('cc_brand'),
+ 'opaqueData': data.get('opaqueData'),
+ 'encryptedCardData': data.get('encryptedCardData'),
'acquirer_id': int(data.get('acquirer_id')),
'partner_id': int(data.get('partner_id'))
}
@@ -167,23 +140,11 @@ class PaymentAcquirerAuthorize(models.Model):
def authorize_s2s_form_validate(self, data):
error = dict()
- mandatory_fields = ["cc_number", "cc_cvc", "cc_holder_name", "cc_expiry", "cc_brand"]
+ mandatory_fields = ["opaqueData", "encryptedCardData"]
# Validation
for field_name in mandatory_fields:
if not data.get(field_name):
error[field_name] = 'missing'
- if data['cc_expiry']:
- # FIX we split the date into their components and check if there is two components containing only digits
- # this fixes multiples crashes, if there was no space between the '/' and the components the code was crashing
- # the code was also crashing if the customer was proving non digits to the date.
- cc_expiry = [i.strip() for i in data['cc_expiry'].split('/')]
- if len(cc_expiry) != 2 or any(not i.isdigit() for i in cc_expiry):
- return False
- try:
- if datetime.now().strftime('%y%m') > datetime.strptime('/'.join(cc_expiry), '%m/%y').strftime('%y%m'):
- return False
- except ValueError:
- return False
return False if error else True
def authorize_test_credentials(self):
@@ -364,17 +325,15 @@ class PaymentToken(models.Model):
@api.model
def authorize_create(self, values):
- if values.get('cc_number'):
- values['cc_number'] = values['cc_number'].replace(' ', '')
+ if values.get('opaqueData') and values.get('encryptedCardData'):
acquirer = self.env['payment.acquirer'].browse(values['acquirer_id'])
- expiry = str(values['cc_expiry'][:2]) + str(values['cc_expiry'][-2:])
partner = self.env['res.partner'].browse(values['partner_id'])
transaction = AuthorizeAPI(acquirer)
- res = transaction.create_customer_profile(partner, values['cc_number'], expiry, values['cc_cvc'])
+ 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': 'XXXXXXXXXXXX%s - %s' % (values['cc_number'][-4:], values['cc_holder_name']),
+ 'name': values['encryptedCardData'].get('cardNumber'),
'acquirer_ref': res.get('payment_profile_id'),
'verified': True
}
diff --git a/addons/payment_authorize/static/src/js/payment_form.js b/addons/payment_authorize/static/src/js/payment_form.js
new file mode 100644
index 00000000000..3947111ac1e
--- /dev/null
+++ b/addons/payment_authorize/static/src/js/payment_form.js
@@ -0,0 +1,153 @@
+odoo.define('payment_authorize.payment_form', function (require) {
+"use strict";
+
+var ajax = require('web.ajax');
+var core = require('web.core');
+var PaymentForm = require('payment.payment_form');
+
+var _t = core._t;
+
+PaymentForm.include({
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * 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;
+ }
+ this.disableButton(button);
+ 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_environment === 'prod') {
+ 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');
+ self.enableButton(button);
+ return self.displayError(_t('Server Error'), errorMessage);
+ }
+
+ 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();
+ }
+ }).guardedCatch(function (error) {
+ // if the rpc fails, pretty obvious
+ error.event.preventDefault();
+ acquirerForm.removeClass('d-none');
+ self.enableButton(button);
+ self.displayError(
+ _t('Server Error'),
+ _t("We are not able to add your payment method at the moment.") +
+ error.message.data.message
+ );
+ });
+ };
+
+ if (this.$button === undefined) {
+ var params = {
+ 'class': 'AcceptUI d-none',
+ 'data-apiLoginID': formData.login_id,
+ 'data-clientKey': formData.client_key,
+ 'data-billingAddressOptions': '{"show": false, "required": false}',
+ 'data-responseHandler': 'responseHandler'
+ };
+ this.$button = $('<button>', params);
+ this.$button.appendTo('body');
+ }
+ ajax.loadJS(AcceptJs).then(function () {
+ self.$button.trigger('click');
+ });
+ },
+ /**
+ * @override
+ */
+ updateNewPaymentDisplayStatus: function () {
+ var $checkedRadio = this.$('input[type="radio"]:checked');
+
+ if ($checkedRadio.length !== 1) {
+ return;
+ }
+
+ // 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);
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ payEvent: function (ev) {
+ ev.preventDefault();
+ var $checkedRadio = this.$('input[type="radio"]:checked');
+
+ // 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');
+
+ // 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);
+ }
+ },
+});
+});
diff --git a/addons/payment_authorize/views/payment_authorize_templates.xml b/addons/payment_authorize/views/payment_authorize_templates.xml
index 68149c667cc..f0a8effa896 100644
--- a/addons/payment_authorize/views/payment_authorize_templates.xml
+++ b/addons/payment_authorize/views/payment_authorize_templates.xml
@@ -51,28 +51,20 @@
</template>
<template id="authorize_s2s_form">
- <input type="hidden" name="data_set" data-create-route="/payment/authorize/s2s/create_json_3ds"/>
- <div t-attf-class="row mt8 #{'' if bootstrap_formatting else 'o_card_brand_detail'}">
- <div t-att-class="'form-group col-lg-12' if bootstrap_formatting else 'form-group'">
- <input type="tel" name="cc_number" id="cc_number" class="form-control" placeholder="Card number" data-is-required="true"/>
- <div class="card_placeholder"></div>
- <div class="visa"></div>
- <input type="hidden" name="cc_brand" value=""/>
- </div>
- <div t-att-class="'form-group col-lg-5' if bootstrap_formatting else 'form-group'">
- <input type="text" name="cc_holder_name" id="cc_holder_name" class="form-control" placeholder="Cardholder name" data-is-required="true"/>
- </div>
- <div t-att-class="'form-group col-lg-3' if bootstrap_formatting else 'form-group'">
- <input type="text" name="cc_expiry" id="cc_expiry" class="form-control" maxlength="7" placeholder="Expires (MM / YY)" data-is-required="true"/>
- </div>
- <div t-att-class="'form-group col-lg-4' if bootstrap_formatting else 'form-group'">
- <input type="text" name="cc_cvc" id="cc_cvc" class="form-control" maxlength="4" placeholder="CVC" data-is-required="true"/>
- </div>
- </div>
+ <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_environment" t-att-value="acq.environment"/>
+ <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>
diff --git a/addons/payment_authorize/views/payment_views.xml b/addons/payment_authorize/views/payment_views.xml
index b60385ed9f0..361d1a2903c 100644
--- a/addons/payment_authorize/views/payment_views.xml
+++ b/addons/payment_authorize/views/payment_views.xml
@@ -10,6 +10,11 @@
<field name="authorize_login"/>
<field name="authorize_transaction_key" password="True"/>
<field name="authorize_signature_key" password="True"/>
+ <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>