diff options
| author | Nikunj Ladava <nla@odoo.com> | 2019-07-03 11:18:57 +0000 |
|---|---|---|
| committer | Damien Bouvy <dbo@odoo.com> | 2019-08-07 11:27:44 +0000 |
| commit | a4f8616a178662434964de5fbee4c35637b9e1bc (patch) | |
| tree | e5e873fa1e1b17c644672598f19c69e4cc4a681d /addons/payment_authorize | |
| parent | 26f3d8465d3c6f1da80c18605d5fa8a953feb779 (diff) | |
[IMP] payment_authorize: integration with accept js
add accept js of authorize.net to make s2s flow pci compliance
after clicking on pay now button, one popup display with card inputs
popup is provided by a authorize with all validation facilities
After submitting details, payment flow is
- get the temp token information from authorize
- create a token with that temp token information in odoo
- make a request to authorize for charge
- after successful request, payment will be charged for that card
task- 2025821
Diffstat (limited to 'addons/payment_authorize')
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> |
