diff options
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> |
