[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
This commit is contained in:
committed by
Damien Bouvy
parent
26f3d8465d
commit
a4f8616a17
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user