summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorNikunj Ladava <nla@odoo.com>2019-07-03 11:18:57 +0000
committerDamien Bouvy <dbo@odoo.com>2019-08-07 11:27:44 +0000
commita4f8616a178662434964de5fbee4c35637b9e1bc (patch)
treee5e873fa1e1b17c644672598f19c69e4cc4a681d /addons
parent26f3d8465d3c6f1da80c18605d5fa8a953feb779 (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')
-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>