[IMP] payment_stripe: strong customer authentication
With the PSD2 directive coming into effect on September 14th, it was time to switch our Stripe integration to use newer APIs that support Strong Customer Authentication (SCA for short). This meant switching from the old stripe.js implementation for 'redirection' flow to the Stripe Checkout API and from the Charge API to the newer SetupIntent API for s2s flows. This feature branch introduce these changes as well as some slight changes in the payment module (mainly functional improvement that were noticed when developing this, nothing major and no change in the flow/model of the basic payment models). Note that the way our checkout flow is built for s2s transactions prevents us from using the PaymentIntent API (which would be a better fit that the SetupIntent in some cases) - to use that API, we would need the payment transaction to be created before the token, which is not the case in our current flows. This will probably be changed in a future task with a more flexible checkout flow. In the mean time, the downside of using SetupIntent instead of PaymentIntent means that from the bank's point of view, the authorization of the card and the payment are 2 different requests - this could lead to a higher rejection rate in SCA flows and possibly to payments that stay 'pending'. There is no way to know for sure before PSD2 goes into full effect though, and this implementation should normally suffice.I would advise using the 'redirection' flow most of the time though, since it is clearly the best fit between Odoo and the Stripe APIs at the moment. Task 1986267 closes odoo/odoo#33978 Signed-off-by: Damien Bouvy (dbo) <dbo@odoo.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import logging
|
||||
from unicodedata import normalize
|
||||
import psycopg2
|
||||
|
||||
from odoo import http, _
|
||||
@@ -131,6 +132,7 @@ class WebsitePayment(http.Controller):
|
||||
def pay(self, reference='', order_id=None, amount=False, currency_id=None, acquirer_id=None, **kw):
|
||||
env = request.env
|
||||
user = env.user.sudo()
|
||||
reference = normalize('NFKD', reference).encode('ascii','ignore').decode('utf-8')
|
||||
|
||||
# Default values
|
||||
values = {
|
||||
@@ -188,7 +190,7 @@ class WebsitePayment(http.Controller):
|
||||
})
|
||||
|
||||
values['acquirers'] = [acq for acq in acquirers if acq.payment_flow in ['form', 's2s']]
|
||||
values['pms'] = request.env['payment.token'].search([('acquirer_id', 'in', acquirers.filtered(lambda x: x.payment_flow == 's2s').ids)])
|
||||
values['pms'] = request.env['payment.token'].search([('acquirer_id', 'in', acquirers.ids)])
|
||||
|
||||
return request.render('payment.pay', values)
|
||||
|
||||
@@ -246,7 +248,7 @@ class WebsitePayment(http.Controller):
|
||||
'currency_id': int(currency_id),
|
||||
'partner_id': partner_id,
|
||||
'payment_token_id': pm_id,
|
||||
'type': 'form_save' if token.acquirer_id.save_token != 'none' and partner_id else 'form',
|
||||
'type': 'server2server',
|
||||
'return_url': return_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ var _t = core._t;
|
||||
publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
|
||||
selector: '.o_payment_form',
|
||||
events: {
|
||||
'submit': 'async onSubmit',
|
||||
'click #o_payment_form_pay': 'async payEvent',
|
||||
'click #o_payment_form_add_pm': 'addPmEvent',
|
||||
'click button[name="delete_pm"]': 'deletePmEvent',
|
||||
@@ -21,10 +22,12 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
|
||||
* @override
|
||||
*/
|
||||
start: function () {
|
||||
this.options = _.extend(this.$el.data(), this.options);
|
||||
this.updateNewPaymentDisplayStatus();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
return this._super.apply(this, arguments);
|
||||
var self = this;
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
self.options = _.extend(self.$el.data(), self.options);
|
||||
self.updateNewPaymentDisplayStatus();
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
@@ -147,7 +150,11 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
|
||||
var form = this.el;
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
var self = this;
|
||||
var button = ev.target;
|
||||
if (ev.type === 'submit') {
|
||||
var button = $(ev.target).find('*[type="submit"]')[0]
|
||||
} else {
|
||||
var button = ev.target;
|
||||
}
|
||||
|
||||
// first we check that the user has selected a payment method
|
||||
if (checked_radio.length === 1) {
|
||||
@@ -322,7 +329,11 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
|
||||
ev.preventDefault();
|
||||
var checked_radio = this.$('input[type="radio"]:checked');
|
||||
var self = this;
|
||||
var button = ev.target;
|
||||
if (ev.type === 'submit') {
|
||||
var button = $(ev.target).find('*[type="submit"]')[0]
|
||||
} else {
|
||||
var button = ev.target;
|
||||
}
|
||||
|
||||
// we check if the user has selected a 'add a new payment' option
|
||||
if (checked_radio.length === 1 && this.isNewPaymentRadio(checked_radio[0])) {
|
||||
@@ -425,6 +436,25 @@ publicWidget.registry.PaymentForm = publicWidget.Widget.extend({
|
||||
);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Called when submitting the form (e.g. through the Return key).
|
||||
* We need to check whether we are paying or adding a new pm and dispatch
|
||||
* to the correct method.
|
||||
*
|
||||
* @private
|
||||
* @param {Event} ev
|
||||
*/
|
||||
onSubmit: function(ev) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
var button = $(ev.target).find('*[type="submit"]')[0]
|
||||
if (button.id === 'o_payment_form_pay') {
|
||||
return this.payEvent(ev);
|
||||
} else if (button.id === 'o_payment_form_add_pm') {
|
||||
return this.addPmEvent(ev);
|
||||
}
|
||||
return;
|
||||
},
|
||||
/**
|
||||
* Called when clicking on a button to delete a payment method.
|
||||
*
|
||||
|
||||
@@ -29,7 +29,8 @@
|
||||
t-att-data-access-token="access_token or ''"
|
||||
t-att-data-partner-id="partner_id"
|
||||
t-att-data-callback-method="callback_method or ''"
|
||||
t-att-data-order-id="order_id or ''">
|
||||
t-att-data-order-id="order_id or ''"
|
||||
t-att-data-mode="mode">
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<input type="hidden" t-if="prepare_tx_url" name="prepare_tx_url" t-att-value="prepare_tx_url"/>
|
||||
<input type="hidden" t-if="order_id" name="order_id" t-att-value="order_id"/>
|
||||
@@ -152,7 +153,7 @@
|
||||
<t t-if="show_manage_btn">
|
||||
<a class="btn btn-link mb8 mt8" href="/my/payment_method">Manage your payment methods</a>
|
||||
</t>
|
||||
<button t-if="mode == 'manage' and list(filter(lambda x: x.payment_flow == 's2s', acquirers))" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<button t-if="mode == 'manage' and list(filter(lambda x: x.payment_flow == 's2s', acquirers))" type="submit" id="o_payment_form_add_pm" class="btn btn-primary btn-lg mb8 mt8">
|
||||
<i class="fa fa-plus-circle"/> Add new card
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,41 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
import pprint
|
||||
import werkzeug
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import request
|
||||
from odoo.addons.payment.controllers.portal import PaymentProcessing
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StripeController(http.Controller):
|
||||
_success_url = '/payment/stripe/success'
|
||||
_cancel_url = '/payment/stripe/cancel'
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create_json'], type='json', auth='public')
|
||||
def stripe_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)
|
||||
return acquirer.s2s_process(kwargs).id
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create'], type='http', auth='public')
|
||||
def stripe_s2s_create(self, **post):
|
||||
acquirer_id = int(post.get('acquirer_id'))
|
||||
acquirer = request.env['payment.acquirer'].browse(acquirer_id)
|
||||
error = None
|
||||
try:
|
||||
acquirer.s2s_process(post)
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
|
||||
return_url = post.get('return_url', '/')
|
||||
if error:
|
||||
separator = '?' if werkzeug.urls.url_parse(return_url).query == '' else '&'
|
||||
return_url += '{}{}'.format(separator, werkzeug.urls.url_encode({'error': error}))
|
||||
|
||||
return werkzeug.utils.redirect(return_url)
|
||||
@http.route(['/payment/stripe/success', '/payment/stripe/cancel'], type='http', auth='public')
|
||||
def stripe_success(self, **kwargs):
|
||||
request.env['payment.transaction'].sudo().form_feedback(kwargs, 'stripe')
|
||||
return werkzeug.utils.redirect('/payment/process')
|
||||
|
||||
@http.route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
|
||||
def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
|
||||
@@ -63,37 +43,8 @@ class StripeController(http.Controller):
|
||||
|
||||
return res
|
||||
|
||||
@http.route(['/payment/stripe/create_charge'], type='json', auth='public')
|
||||
def stripe_create_charge(self, **post):
|
||||
""" Create a payment transaction
|
||||
|
||||
Expects the result from the user input from checkout.js popup"""
|
||||
TX = request.env['payment.transaction']
|
||||
tx = None
|
||||
if post.get('tx_ref'):
|
||||
tx = TX.sudo().search([('reference', '=', post['tx_ref'])])
|
||||
if not tx:
|
||||
tx_id = (post.get('tx_id') or request.session.get('sale_transaction_id') or
|
||||
request.session.get('website_payment_tx_id'))
|
||||
tx = TX.sudo().browse(int(tx_id))
|
||||
if not tx:
|
||||
raise werkzeug.exceptions.NotFound()
|
||||
|
||||
stripe_token = post['token']
|
||||
response = None
|
||||
if tx.type == 'form_save' and tx.partner_id:
|
||||
payment_token_id = request.env['payment.token'].sudo().create({
|
||||
'acquirer_id': tx.acquirer_id.id,
|
||||
'partner_id': tx.partner_id.id,
|
||||
'stripe_token': stripe_token
|
||||
})
|
||||
tx.payment_token_id = payment_token_id
|
||||
response = tx._create_stripe_charge(acquirer_ref=payment_token_id.acquirer_ref, email=stripe_token['email'])
|
||||
else:
|
||||
response = tx._create_stripe_charge(tokenid=stripe_token['id'], email=stripe_token['email'])
|
||||
_logger.info('Stripe: entering form_feedback with post data %s', pprint.pformat(response))
|
||||
if response:
|
||||
request.env['payment.transaction'].sudo().with_context(lang=None).form_feedback(response, 'stripe')
|
||||
# add the payment transaction into the session to let the page /payment/process to handle it
|
||||
PaymentProcessing.add_payment_transaction(tx)
|
||||
return "/payment/process"
|
||||
@http.route('/payment/stripe/s2s/create_setup_intent', type='json', auth='public', csrf=False)
|
||||
def stripe_s2s_create_setup_intent(self, acquirer_id, **kwargs):
|
||||
acquirer = request.env['payment.acquirer'].browse(int(acquirer_id))
|
||||
res = acquirer._create_setup_intent(kwargs)
|
||||
return res.get('client_secret')
|
||||
|
||||
@@ -3,19 +3,15 @@
|
||||
import logging
|
||||
import requests
|
||||
import pprint
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
from odoo.tools.float_utils import float_round
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
from odoo.addons.payment.models.payment_acquirer import ValidationError
|
||||
from odoo.addons.payment_stripe.controllers.main import StripeController
|
||||
|
||||
# Force the API version to avoid breaking in case of update on Stripe side
|
||||
# cf https://stripe.com/docs/api#versioning
|
||||
# changelog https://stripe.com/docs/upgrades#api-changelog
|
||||
STRIPE_HEADERS = {'Stripe-Version': '2016-03-07'}
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# The following currencies are integer only, see https://stripe.com/docs/currencies#zero-decimal
|
||||
INT_CURRENCIES = [
|
||||
@@ -38,58 +34,87 @@ class PaymentAcquirerStripe(models.Model):
|
||||
|
||||
def stripe_form_generate_values(self, tx_values):
|
||||
self.ensure_one()
|
||||
stripe_tx_values = dict(tx_values)
|
||||
temp_stripe_tx_values = {
|
||||
'company': self.company_id.name,
|
||||
'amount': tx_values['amount'], # Mandatory
|
||||
'currency': tx_values['currency'].name, # Mandatory anyway
|
||||
'currency_id': tx_values['currency'].id, # same here
|
||||
'address_line1': tx_values.get('partner_address'), # Any info of the partner is not mandatory
|
||||
'address_city': tx_values.get('partner_city'),
|
||||
'address_country': tx_values.get('partner_country') and tx_values.get('partner_country').name or '',
|
||||
'email': tx_values.get('partner_email'),
|
||||
'address_zip': tx_values.get('partner_zip'),
|
||||
'name': tx_values.get('partner_name'),
|
||||
'phone': tx_values.get('partner_phone'),
|
||||
}
|
||||
|
||||
stripe_tx_values.update(temp_stripe_tx_values)
|
||||
return stripe_tx_values
|
||||
base_url = self.env['ir.config_parameter'].sudo().get_param('web.base.url')
|
||||
stripe_session_data = {
|
||||
'payment_method_types[]': 'card',
|
||||
'line_items[][amount]': int(tx_values['amount'] if tx_values['currency'].name in INT_CURRENCIES else float_round(tx_values['amount'] * 100, 2)),
|
||||
'line_items[][currency]': tx_values['currency'].name,
|
||||
'line_items[][quantity]': 1,
|
||||
'line_items[][name]': tx_values['reference'],
|
||||
'client_reference_id': tx_values['reference'],
|
||||
'success_url': urls.url_join(base_url, StripeController._success_url) + '?reference=%s' % tx_values['reference'],
|
||||
'cancel_url': urls.url_join(base_url, StripeController._cancel_url) + '?reference=%s' % tx_values['reference'],
|
||||
'customer_email': tx_values['partner_email'],
|
||||
}
|
||||
tx_values['session_id'] = self._create_stripe_session(stripe_session_data)
|
||||
|
||||
return tx_values
|
||||
|
||||
def _stripe_request(self, url, data=False, method='POST'):
|
||||
self.ensure_one()
|
||||
url = urls.url_join(self._get_stripe_api_url(), url)
|
||||
headers = {
|
||||
'AUTHORIZATION': 'Bearer %s' % self.sudo().stripe_secret_key,
|
||||
'Stripe-Version': '2019-05-16', # SetupIntent need a specific version
|
||||
}
|
||||
resp = requests.request(method, url, data=data, headers=headers)
|
||||
try:
|
||||
resp.raise_for_status()
|
||||
except:
|
||||
_logger.error(resp.text)
|
||||
raise
|
||||
return resp.json()
|
||||
|
||||
def _create_stripe_session(self, kwargs):
|
||||
self.ensure_one()
|
||||
resp = self._stripe_request('checkout/sessions', kwargs)
|
||||
if resp.get('payment_intent') and kwargs.get('client_reference_id'):
|
||||
tx = self.env['payment.transaction'].sudo().search([('reference', '=', kwargs['client_reference_id'])])
|
||||
tx.stripe_payment_intent = resp['payment_intent']
|
||||
return resp['id']
|
||||
|
||||
def _create_setup_intent(self, kwargs):
|
||||
self.ensure_one()
|
||||
params = {
|
||||
'usage': 'off_session',
|
||||
}
|
||||
_logger.info('_stripe_create_setup_intent: Sending values to stripe, values:\n%s', pprint.pformat(params))
|
||||
|
||||
res = self._stripe_request('setup_intents', params)
|
||||
|
||||
_logger.info('_stripe_create_setup_intent: Values received:\n%s', pprint.pformat(res))
|
||||
return res
|
||||
|
||||
@api.model
|
||||
def _get_stripe_api_url(self):
|
||||
return 'api.stripe.com/v1'
|
||||
return 'https://api.stripe.com/v1/'
|
||||
|
||||
@api.model
|
||||
def stripe_s2s_form_process(self, data):
|
||||
last4 = data.get('card', {}).get('last4')
|
||||
if not last4:
|
||||
# PM was created with a setup intent, need to get last4 digits through
|
||||
# yet another call -_-
|
||||
acquirer_id = self.env['payment.acquirer'].browse(int(data['acquirer_id']))
|
||||
pm = data.get('payment_method')
|
||||
res = acquirer_id._stripe_request('payment_methods/%s' % pm, data=False, method='GET')
|
||||
last4 = res.get('card', {}).get('last4', '****')
|
||||
|
||||
payment_token = self.env['payment.token'].sudo().create({
|
||||
'cc_number': data['cc_number'],
|
||||
'cc_holder_name': data['cc_holder_name'],
|
||||
'cc_expiry': data['cc_expiry'],
|
||||
'cc_brand': data['cc_brand'],
|
||||
'cvc': data['cvc'],
|
||||
'acquirer_id': int(data['acquirer_id']),
|
||||
'partner_id': int(data['partner_id'])
|
||||
'partner_id': int(data['partner_id']),
|
||||
'stripe_payment_method': data.get('payment_method'),
|
||||
'name': 'XXXXXXXXXXXX%s' % last4,
|
||||
'acquirer_ref': data.get('customer')
|
||||
})
|
||||
return payment_token
|
||||
|
||||
def stripe_s2s_form_validate(self, data):
|
||||
self.ensure_one()
|
||||
|
||||
# mandatory fields
|
||||
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand"]:
|
||||
if not data.get(field_name):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _get_feature_support(self):
|
||||
"""Get advanced feature support by provider.
|
||||
|
||||
Each provider should add its technical in the corresponding
|
||||
key for the following features:
|
||||
* fees: support payment fees computations
|
||||
* authorize: support authorizing payment (separates
|
||||
authorization and capture)
|
||||
* tokenize: support saving payment data in a payment.tokenize
|
||||
object
|
||||
"""
|
||||
@@ -101,38 +126,45 @@ class PaymentAcquirerStripe(models.Model):
|
||||
class PaymentTransactionStripe(models.Model):
|
||||
_inherit = 'payment.transaction'
|
||||
|
||||
def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
|
||||
api_url_charge = 'https://%s/charges' % (self.acquirer_id._get_stripe_api_url())
|
||||
stripe_payment_intent = fields.Char(string='Stripe Payment Intent ID', readonly=True)
|
||||
|
||||
def form_feedback(self, data, acquirer_name):
|
||||
if data.get('reference') and acquirer_name == 'stripe':
|
||||
transaction = self.env['payment.transaction'].search([('reference', '=', data['reference'])])
|
||||
|
||||
url = 'payment_intents/%s' % transaction.stripe_payment_intent
|
||||
resp = transaction.acquirer_id._stripe_request(url)
|
||||
if resp.get('charges') and resp.get('charges').get('total_count'):
|
||||
resp = resp.get('charges').get('data')[0]
|
||||
|
||||
data.update(resp)
|
||||
_logger.info('Stripe: entering form_feedback with post data %s' % pprint.pformat(data))
|
||||
return super(PaymentTransactionStripe, self).form_feedback(data, acquirer_name)
|
||||
|
||||
def _stripe_create_payment_intent(self, acquirer_ref=None, email=None):
|
||||
charge_params = {
|
||||
'amount': int(self.amount if self.currency_id.name in INT_CURRENCIES else float_round(self.amount * 100, 2)),
|
||||
'currency': self.currency_id.name,
|
||||
'metadata[reference]': self.reference,
|
||||
'description': self.reference,
|
||||
'currency': self.currency_id.name.lower(),
|
||||
'setup_future_usage': 'off_session',
|
||||
'confirm': True,
|
||||
'payment_method': self.payment_token_id.stripe_payment_method,
|
||||
'customer': self.payment_token_id.acquirer_ref,
|
||||
}
|
||||
if acquirer_ref:
|
||||
charge_params['customer'] = acquirer_ref
|
||||
if tokenid:
|
||||
charge_params['card'] = str(tokenid)
|
||||
if email:
|
||||
charge_params['receipt_email'] = email.strip()
|
||||
_logger.info('_stripe_create_payment_intent: Sending values to stripe, values:\n%s', pprint.pformat(charge_params))
|
||||
|
||||
_logger.info('_create_stripe_charge: Sending values to URL %s, values:\n%s', api_url_charge, pprint.pformat(charge_params))
|
||||
r = requests.post(api_url_charge,
|
||||
auth=(self.acquirer_id.stripe_secret_key, ''),
|
||||
params=charge_params,
|
||||
headers=STRIPE_HEADERS)
|
||||
res = r.json()
|
||||
_logger.info('_create_stripe_charge: Values received:\n%s', pprint.pformat(res))
|
||||
res = self.acquirer_id._stripe_request('payment_intents', charge_params)
|
||||
if res.get('charges') and res.get('charges').get('total_count'):
|
||||
res = res.get('charges').get('data')[0]
|
||||
|
||||
_logger.info('_stripe_create_payment_intent: Values received:\n%s', pprint.pformat(res))
|
||||
return res
|
||||
|
||||
def stripe_s2s_do_transaction(self, **kwargs):
|
||||
self.ensure_one()
|
||||
result = self._create_stripe_charge(acquirer_ref=self.payment_token_id.acquirer_ref, email=self.partner_email)
|
||||
result = self._stripe_create_payment_intent(acquirer_ref=self.payment_token_id.acquirer_ref, email=self.partner_email)
|
||||
return self._stripe_s2s_validate_tree(result)
|
||||
|
||||
|
||||
def _create_stripe_refund(self):
|
||||
api_url_refund = 'https://%s/refunds' % (self.acquirer_id._get_stripe_api_url())
|
||||
|
||||
refund_params = {
|
||||
'charge': self.acquirer_reference,
|
||||
@@ -140,13 +172,10 @@ class PaymentTransactionStripe(models.Model):
|
||||
'metadata[reference]': self.reference,
|
||||
}
|
||||
|
||||
_logger.info('_create_stripe_refund: Sending values to URL %s, values:\n%s', api_url_refund, pprint.pformat(refund_params))
|
||||
r = requests.post(api_url_refund,
|
||||
auth=(self.acquirer_id.stripe_secret_key, ''),
|
||||
params=refund_params,
|
||||
headers=STRIPE_HEADERS)
|
||||
res = r.json()
|
||||
_logger.info('_create_stripe_refund: Sending values to stripe URL, values:\n%s', pprint.pformat(refund_params))
|
||||
res = self.acquirer_id._stripe_request('refunds', refund_params)
|
||||
_logger.info('_create_stripe_refund: Values received:\n%s', pprint.pformat(res))
|
||||
|
||||
return res
|
||||
|
||||
def stripe_s2s_do_refund(self, **kwargs):
|
||||
@@ -158,11 +187,11 @@ class PaymentTransactionStripe(models.Model):
|
||||
def _stripe_form_get_tx_from_data(self, data):
|
||||
""" Given a data dict coming from stripe, verify it and find the related
|
||||
transaction record. """
|
||||
reference = data.get('metadata', {}).get('reference')
|
||||
reference = data.get('reference')
|
||||
if not reference:
|
||||
stripe_error = data.get('error', {}).get('message', '')
|
||||
_logger.error('Stripe: invalid reply received from stripe API, looks like '
|
||||
'the transaction failed. (error: %s)', stripe_error or 'n/a')
|
||||
'the transaction failed. (error: %s)', stripe_error or 'n/a')
|
||||
error_msg = _("We're sorry to report that the transaction has failed.")
|
||||
if stripe_error:
|
||||
error_msg += " " + (_("Stripe gave us the following info about the problem: '%s'") %
|
||||
@@ -189,111 +218,78 @@ class PaymentTransactionStripe(models.Model):
|
||||
return True
|
||||
|
||||
status = tree.get('status')
|
||||
tx_id = tree.get('id')
|
||||
vals = {
|
||||
'date': fields.datetime.now(),
|
||||
'acquirer_reference': tx_id,
|
||||
}
|
||||
if status == 'succeeded':
|
||||
self.write({
|
||||
'date': fields.datetime.now(),
|
||||
'acquirer_reference': tree.get('id'),
|
||||
})
|
||||
self.write(vals)
|
||||
self._set_transaction_done()
|
||||
self.execute_callback()
|
||||
if self.type == 'form_save':
|
||||
s2s_data = {
|
||||
'customer': tree.get('customer'),
|
||||
'payment_method': tree.get('payment_method'),
|
||||
'card': tree.get('payment_method_details').get('card'),
|
||||
'acquirer_id': self.acquirer_id.id,
|
||||
'partner_id': self.partner_id.id
|
||||
}
|
||||
token = self.acquirer_id.stripe_s2s_form_process(s2s_data)
|
||||
self.payment_token_id = token.id
|
||||
if self.payment_token_id:
|
||||
self.payment_token_id.verified = True
|
||||
return True
|
||||
if status in ('processing', 'requires_action'):
|
||||
self.write(vals)
|
||||
self._set_transaction_pending()
|
||||
return True
|
||||
else:
|
||||
error = tree['error']['message']
|
||||
error = tree.get('failure_message')
|
||||
_logger.warn(error)
|
||||
self.sudo().write({
|
||||
'state_message': error,
|
||||
'acquirer_reference': tree.get('id'),
|
||||
'date': fields.datetime.now(),
|
||||
})
|
||||
vals.update({'state_message': error})
|
||||
self.write(vals)
|
||||
self._set_transaction_cancel()
|
||||
return False
|
||||
|
||||
def _stripe_form_get_invalid_parameters(self, data):
|
||||
invalid_parameters = []
|
||||
reference = data['metadata']['reference']
|
||||
if reference != self.reference:
|
||||
invalid_parameters.append(('Reference', reference, self.reference))
|
||||
if data.get('amount') != int(self.amount if self.currency_id.name in INT_CURRENCIES else float_round(self.amount * 100, 2)):
|
||||
invalid_parameters.append(('Amount', data.get('amount'), self.amount * 100))
|
||||
if data.get('currency').upper() != self.currency_id.name:
|
||||
invalid_parameters.append(('Currency', data.get('currency'), self.currency_id.name))
|
||||
if data.get('payment_intent') and data.get('payment_intent') != self.stripe_payment_intent:
|
||||
invalid_parameters.append(('Payment Intent', data.get('payment_intent'), self.stripe_payment_intent))
|
||||
return invalid_parameters
|
||||
|
||||
def _stripe_form_validate(self, data):
|
||||
def _stripe_form_validate(self, data):
|
||||
return self._stripe_s2s_validate_tree(data)
|
||||
|
||||
|
||||
class PaymentTokenStripe(models.Model):
|
||||
_inherit = 'payment.token'
|
||||
|
||||
stripe_payment_method = fields.Char('Payment Method ID')
|
||||
|
||||
@api.model
|
||||
def stripe_create(self, values):
|
||||
token = values.get('stripe_token')
|
||||
description = None
|
||||
payment_acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
|
||||
# when asking to create a token on Stripe servers
|
||||
if values.get('cc_number'):
|
||||
url_token = 'https://%s/tokens' % payment_acquirer._get_stripe_api_url()
|
||||
payment_params = {
|
||||
'card[number]': values['cc_number'].replace(' ', ''),
|
||||
'card[exp_month]': str(values['cc_expiry'][:2]),
|
||||
'card[exp_year]': str(values['cc_expiry'][-2:]),
|
||||
'card[cvc]': values['cvc'],
|
||||
'card[name]': values['cc_holder_name'],
|
||||
if values.get('stripe_payment_method') and not values.get('acquirer_ref'):
|
||||
partner_id = self.env['res.partner'].browse(values.get('partner_id'))
|
||||
payment_acquirer = self.env['payment.acquirer'].browse(values.get('acquirer_id'))
|
||||
|
||||
# create customer to stipe
|
||||
customer_data = {
|
||||
'email': partner_id.email
|
||||
}
|
||||
r = requests.post(url_token,
|
||||
auth=(payment_acquirer.stripe_secret_key, ''),
|
||||
params=payment_params,
|
||||
headers=STRIPE_HEADERS)
|
||||
token = r.json()
|
||||
description = values['cc_holder_name']
|
||||
else:
|
||||
partner_id = self.env['res.partner'].browse(values['partner_id'])
|
||||
description = 'Partner: %s (id: %s)' % (partner_id.name, partner_id.id)
|
||||
|
||||
if not token:
|
||||
raise UserError(_("Stripe: no payment token was provided or the token creation failed."))
|
||||
|
||||
res = self._stripe_create_customer(token, description, payment_acquirer.id)
|
||||
|
||||
# pop credit card info to info sent to create
|
||||
for field_name in ["cc_number", "cvc", "cc_holder_name", "cc_expiry", "cc_brand", "stripe_token"]:
|
||||
values.pop(field_name, None)
|
||||
return res
|
||||
|
||||
|
||||
def _stripe_create_customer(self, token, description=None, acquirer_id=None):
|
||||
if token.get('error'):
|
||||
_logger.error('payment.token.stripe_create_customer: Token error:\n%s', pprint.pformat(token['error']))
|
||||
raise Exception(token['error']['message'])
|
||||
|
||||
if token['object'] != 'token':
|
||||
_logger.error('payment.token.stripe_create_customer: Cannot create a customer for object type "%s"', token.get('object'))
|
||||
raise Exception('We are unable to process your credit card information.')
|
||||
|
||||
if token['type'] != 'card':
|
||||
_logger.error('payment.token.stripe_create_customer: Cannot create a customer for token type "%s"', token.get('type'))
|
||||
raise Exception('We are unable to process your credit card information.')
|
||||
|
||||
payment_acquirer = self.env['payment.acquirer'].browse(acquirer_id or self.acquirer_id.id)
|
||||
url_customer = 'https://%s/customers' % payment_acquirer._get_stripe_api_url()
|
||||
|
||||
customer_params = {
|
||||
'source': token['id'],
|
||||
'description': description or token["card"]["name"]
|
||||
}
|
||||
|
||||
r = requests.post(url_customer,
|
||||
auth=(payment_acquirer.stripe_secret_key, ''),
|
||||
params=customer_params,
|
||||
headers=STRIPE_HEADERS)
|
||||
customer = r.json()
|
||||
|
||||
if customer.get('error'):
|
||||
_logger.error('payment.token.stripe_create_customer: Customer error:\n%s', pprint.pformat(customer['error']))
|
||||
raise Exception(customer['error']['message'])
|
||||
|
||||
values = {
|
||||
'acquirer_ref': customer['id'],
|
||||
'name': 'XXXXXXXXXXXX%s - %s' % (token['card']['last4'], customer_params["description"])
|
||||
}
|
||||
cust_resp = payment_acquirer._stripe_request('customers', customer_data)
|
||||
|
||||
# link customer with payment method
|
||||
api_url_payment_method = 'payment_methods/%s/attach' % values['stripe_payment_method']
|
||||
method_data = {
|
||||
'customer': cust_resp.get('id')
|
||||
}
|
||||
payment_acquirer._stripe_request(api_url_payment_method, method_data)
|
||||
return {
|
||||
'acquirer_ref': cust_resp['id'],
|
||||
}
|
||||
return values
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
odoo.define('payment_stripe.payment_form', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dialog = require('web.Dialog');
|
||||
var PaymentForm = require('payment.payment_form');
|
||||
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
|
||||
|
||||
PaymentForm.include({
|
||||
|
||||
willStart: function () {
|
||||
return this._super.apply(this, arguments).then(function () {
|
||||
return ajax.loadJS("https://js.stripe.com/v3/");
|
||||
})
|
||||
},
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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
|
||||
*/
|
||||
_createStripeToken: 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);
|
||||
if (this.options.partnerId === undefined) {
|
||||
console.warn('payment_form: unset partner_id when adding new token; things could go wrong');
|
||||
}
|
||||
|
||||
var formData = self.getFormData(inputsForm);
|
||||
var stripe = this.stripe;
|
||||
var card = this.stripe_card_element;
|
||||
if (card._invalid) {
|
||||
return;
|
||||
}
|
||||
return this._rpc({
|
||||
route: '/payment/stripe/s2s/create_setup_intent',
|
||||
params: {'acquirer_id': formData.acquirer_id}
|
||||
}).then(function(intent_secret){
|
||||
return stripe.handleCardSetup(intent_secret, card);
|
||||
}).then(function(result) {
|
||||
if (result.error) {
|
||||
return Promise.reject({"message": {"data": { "message": result.error.message}}});
|
||||
} else {
|
||||
_.extend(formData, {"payment_method": result.setupIntent.payment_method});
|
||||
return self._rpc({
|
||||
route: formData.data_set,
|
||||
params: formData,
|
||||
})
|
||||
}
|
||||
}).then(function(result) {
|
||||
if (addPmEvent) {
|
||||
if (formData.return_url) {
|
||||
window.location = formData.return_url;
|
||||
} else {
|
||||
window.location.reload();
|
||||
}
|
||||
} else {
|
||||
$checkedRadio.val(result.id);
|
||||
self.el.submit();
|
||||
}
|
||||
}).guardedCatch(function (error) {
|
||||
// if the rpc fails, pretty obvious
|
||||
self.enableButton(button);
|
||||
self.displayError(
|
||||
_t('Unable to save card'),
|
||||
_t("We are not able to add your payment method at the moment. ") +
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
},
|
||||
/**
|
||||
* called when clicking a Stripe radio if configured for s2s flow; instanciates the card and bind it to the widget.
|
||||
*
|
||||
* @private
|
||||
* @param {DOMElement} checkedRadio
|
||||
*/
|
||||
_bindStripeCard: function ($checkedRadio) {
|
||||
var acquirerID = this.getAcquirerIdFromRadio($checkedRadio);
|
||||
var acquirerForm = this.$('#o_payment_add_token_acq_' + acquirerID);
|
||||
var inputsForm = $('input', acquirerForm);
|
||||
var formData = this.getFormData(inputsForm);
|
||||
var stripe = Stripe(formData.stripe_publishable_key);
|
||||
var element = stripe.elements();
|
||||
var card = element.create('card', {hidePostalCode: true});
|
||||
card.mount('#card-element');
|
||||
card.on('ready', function(ev) {
|
||||
card.focus();
|
||||
});
|
||||
card.addEventListener('change', function (event) {
|
||||
var displayError = document.getElementById('card-errors');
|
||||
displayError.textContent = '';
|
||||
if (event.error) {
|
||||
displayError.textContent = event.error.message;
|
||||
}
|
||||
});
|
||||
this.stripe = stripe;
|
||||
this.stripe_card_element = card;
|
||||
},
|
||||
/**
|
||||
* destroys the card element and any stripe instance linked to the widget.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
_unbindStripeCard: function () {
|
||||
if (this.stripe_card_element) {
|
||||
this.stripe_card_element.destroy();
|
||||
}
|
||||
this.stripe = undefined;
|
||||
this.stripe_card_element = undefined;
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
updateNewPaymentDisplayStatus: function () {
|
||||
var $checkedRadio = this.$('input[type="radio"]:checked');
|
||||
|
||||
if ($checkedRadio.length !== 1) {
|
||||
return;
|
||||
}
|
||||
var provider = $checkedRadio.data('provider')
|
||||
if (provider === 'stripe') {
|
||||
// always re-init stripe (in case of multiple acquirers for stripe, make sure the stripe instance is using the right key)
|
||||
this._unbindStripeCard();
|
||||
if (this.isNewPaymentRadio($checkedRadio)) {
|
||||
this._bindStripeCard($checkedRadio);
|
||||
}
|
||||
}
|
||||
return 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 stripe as s2s payment method
|
||||
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'stripe') {
|
||||
return this._createStripeToken(ev, $checkedRadio);
|
||||
} else {
|
||||
return 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 stripe as add payment method
|
||||
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'stripe') {
|
||||
return this._createStripeToken(ev, $checkedRadio, true);
|
||||
} else {
|
||||
return this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1,141 +1,81 @@
|
||||
odoo.define('payment_stripe.stripe', function(require) {
|
||||
"use strict";
|
||||
odoo.define('payment_stripe.stripe', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var _t = core._t;
|
||||
var qweb = core.qweb;
|
||||
ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
|
||||
// The following currencies are integer only, see
|
||||
// https://stripe.com/docs/currencies#zero-decimal
|
||||
var int_currencies = [
|
||||
'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYG',
|
||||
'RWF', 'KRW', 'VUV', 'VND', 'XOF'
|
||||
];
|
||||
var qweb = core.qweb;
|
||||
var _t = core._t;
|
||||
|
||||
if ($.blockUI) {
|
||||
// our message needs to appear above the modal dialog
|
||||
$.blockUI.defaults.baseZ = 2147483647; //same z-index as StripeCheckout
|
||||
$.blockUI.defaults.css.border = '0';
|
||||
$.blockUI.defaults.css["background-color"] = '';
|
||||
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
|
||||
}
|
||||
var stripeHandler;
|
||||
function getStripeHandler()
|
||||
{
|
||||
if (stripeHandler) {
|
||||
return stripeHandler;
|
||||
}
|
||||
var handler = stripeHandler = StripeCheckout.configure({
|
||||
key: $("input[name='stripe_key']").val(),
|
||||
image: $("input[name='stripe_image']").val(),
|
||||
locale: 'auto',
|
||||
token: function(token, args) {
|
||||
handler.isTokenGenerate = true;
|
||||
if ($.blockUI) {
|
||||
var msg = _t("Just one more second, confirming your payment...");
|
||||
$.blockUI({
|
||||
'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' +
|
||||
' <br />' + msg +
|
||||
'</h2>'
|
||||
});
|
||||
}
|
||||
ajax.jsonRpc("/payment/stripe/create_charge", 'call', {
|
||||
tokenid: token.id, // TBE TODO: for backward compatibility, remove on master
|
||||
email: token.email, // TBE TODO: for backward compatibility, remove on master
|
||||
token: token,
|
||||
amount: $("input[name='amount']").val(),
|
||||
acquirer_id: $("#acquirer_stripe").val(),
|
||||
currency: $("input[name='currency']").val(),
|
||||
invoice_num: $("input[name='invoice_num']").val(),
|
||||
tx_ref: $("input[name='invoice_num']").val(),
|
||||
return_url: $("input[name='return_url']").val()
|
||||
}).then(function(data){
|
||||
handler.isTokenGenerate = false;
|
||||
window.location.href = data;
|
||||
if ($.blockUI) {
|
||||
$.unblockUI();
|
||||
}
|
||||
}).guardedCatch(function(data){
|
||||
var msg = data && data.data && data.data.message;
|
||||
var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')}));
|
||||
wizard.appendTo($('body')).modal({'keyboard': true});
|
||||
if ($.blockUI) {
|
||||
$.unblockUI();
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
return handler;
|
||||
}
|
||||
ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);
|
||||
|
||||
require('web.dom_ready');
|
||||
if (!$('.o_payment_form').length) {
|
||||
return Promise.reject("DOM doesn't contain '.o_payment_form'");
|
||||
}
|
||||
if ($.blockUI) {
|
||||
// our message needs to appear above the modal dialog
|
||||
$.blockUI.defaults.baseZ = 2147483647; //same z-index as StripeCheckout
|
||||
$.blockUI.defaults.css.border = '0';
|
||||
$.blockUI.defaults.css["background-color"] = '';
|
||||
$.blockUI.defaults.overlayCSS["opacity"] = '0.9';
|
||||
}
|
||||
|
||||
var observer = new MutationObserver(function(mutations, observer) {
|
||||
for(var i=0; i<mutations.length; ++i) {
|
||||
for(var j=0; j<mutations[i].addedNodes.length; ++j) {
|
||||
if(mutations[i].addedNodes[j].tagName.toLowerCase() === "form" && mutations[i].addedNodes[j].getAttribute('provider') == 'stripe') {
|
||||
display_stripe_form($(mutations[i].addedNodes[j]));
|
||||
}
|
||||
require('web.dom_ready');
|
||||
if (!$('.o_payment_form').length) {
|
||||
return Promise.reject("DOM doesn't contain '.o_payment_form'");
|
||||
}
|
||||
|
||||
var observer = new MutationObserver(function (mutations, observer) {
|
||||
for (var i = 0; i < mutations.length; ++i) {
|
||||
for (var j = 0; j < mutations[i].addedNodes.length; ++j) {
|
||||
if (mutations[i].addedNodes[j].tagName.toLowerCase() === "form" && mutations[i].addedNodes[j].getAttribute('provider') === 'stripe') {
|
||||
_redirectToStripeCheckout($(mutations[i].addedNodes[j]));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function displayError(message) {
|
||||
var wizard = $(qweb.render('stripe.error', {'msg': message || _t('Payment error')}));
|
||||
wizard.appendTo($('body')).modal({'keyboard': true});
|
||||
if ($.blockUI) {
|
||||
$.unblockUI();
|
||||
}
|
||||
$("#o_payment_form_pay").removeAttr('disabled');
|
||||
}
|
||||
|
||||
|
||||
function display_stripe_form(provider_form) {
|
||||
// Open Checkout with further options
|
||||
var payment_form = $('.o_payment_form');
|
||||
if(!payment_form.find('i').length)
|
||||
payment_form.append('<i class="fa fa-spinner fa-spin"/>');
|
||||
payment_form.attr('disabled','disabled');
|
||||
|
||||
var payment_tx_url = payment_form.find('input[name="prepare_tx_url"]').val();
|
||||
var access_token = $("input[name='access_token']").val() || $("input[name='token']").val() || '';
|
||||
|
||||
var get_input_value = function(name) {
|
||||
return provider_form.find('input[name="' + name + '"]').val();
|
||||
}
|
||||
|
||||
var acquirer_id = parseInt(provider_form.find('#acquirer_stripe').val());
|
||||
var amount = parseFloat(get_input_value("amount") || '0.0');
|
||||
var currency = get_input_value("currency");
|
||||
var email = get_input_value("email");
|
||||
var invoice_num = get_input_value("invoice_num");
|
||||
var merchant = get_input_value("merchant");
|
||||
|
||||
// Search if the user wants to save the credit card information
|
||||
var form_save_token = false;
|
||||
var acquirer_form = $('#o_payment_form_acq_' + acquirer_id);
|
||||
if (acquirer_form.length) {
|
||||
form_save_token = acquirer_form.find('input[name="o_payment_form_save_token"]').prop('checked');
|
||||
}
|
||||
|
||||
ajax.jsonRpc(payment_tx_url, 'call', {
|
||||
acquirer_id: acquirer_id,
|
||||
access_token: access_token,
|
||||
save_token: form_save_token,
|
||||
}).then(function(data) {
|
||||
var $pay_stripe = $('#pay_stripe').detach();
|
||||
try { provider_form[0].innerHTML = data; } catch (e) {}
|
||||
// Restore 'Pay Now' button HTML since data might have changed it.
|
||||
$(provider_form[0]).find('#pay_stripe').replaceWith($pay_stripe);
|
||||
}).then(function () {
|
||||
getStripeHandler().open({
|
||||
name: merchant,
|
||||
description: invoice_num,
|
||||
email: email,
|
||||
currency: currency,
|
||||
amount: _.contains(int_currencies, currency) ? amount : amount * 100,
|
||||
});
|
||||
function _redirectToStripeCheckout(providerForm) {
|
||||
// Open Checkout with further options
|
||||
if ($.blockUI) {
|
||||
var msg = _t("Just one more second, We are redirecting you to Stripe...");
|
||||
$.blockUI({
|
||||
'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' +
|
||||
' <br />' + msg +
|
||||
'</h2>'
|
||||
});
|
||||
}
|
||||
|
||||
$.getScript("https://checkout.stripe.com/checkout.js", function(data, textStatus, jqxhr) {
|
||||
observer.observe(document.body, {childList: true});
|
||||
display_stripe_form($('form[provider="stripe"]'));
|
||||
var paymentForm = $('.o_payment_form');
|
||||
if (!paymentForm.find('i').length) {
|
||||
paymentForm.append('<i class="fa fa-spinner fa-spin"/>');
|
||||
paymentForm.attr('disabled', 'disabled');
|
||||
}
|
||||
|
||||
var _getStripeInputValue = function (name) {
|
||||
return providerForm.find('input[name="' + name + '"]').val();
|
||||
};
|
||||
|
||||
var stripe = Stripe(_getStripeInputValue('stripe_key'));
|
||||
|
||||
stripe.redirectToCheckout({
|
||||
sessionId: _getStripeInputValue('session_id')
|
||||
}).then(function (result) {
|
||||
if (result.error) {
|
||||
displayError(result.error.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.getScript("https://js.stripe.com/v3/", function (data, textStatus, jqxhr) {
|
||||
observer.observe(document.body, {childList: true});
|
||||
_redirectToStripeCheckout($('form[provider="stripe"]'));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates id="template" xml:space="preserve">
|
||||
<t t-name="stripe.error">
|
||||
<div role="dialog" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Error</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<t t-esc="msg"></t>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<a role="button" href="#" class="btn btn-link btn-sm" data-dismiss="modal">Close</a>
|
||||
</footer>
|
||||
<t t-name="stripe.error">
|
||||
<div role="dialog" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<header class="modal-header">
|
||||
<h4 class="modal-title">Error</h4>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">&times;</button>
|
||||
</header>
|
||||
<main class="modal-body">
|
||||
<t t-esc="msg"></t>
|
||||
</main>
|
||||
<footer class="modal-footer">
|
||||
<a role="button" href="#" class="btn btn-link btn-sm" data-dismiss="modal">Close</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -3,23 +3,7 @@
|
||||
<data>
|
||||
<template id="stripe_form">
|
||||
<input type="hidden" name="data_set" t-att-data-action-url="tx_url" data-remove-me=""/>
|
||||
<input type="hidden" name='stripe_image' t-att-value="acquirer.stripe_image_url"/>
|
||||
<input type="hidden" name='merchant' t-att-value='acquirer.company_id.name'/>
|
||||
<input type="hidden" name='amount' t-att-value="amount"/>
|
||||
<input type="hidden" name='return_url' t-att-value='returndata'/>
|
||||
<!--Order Information -->
|
||||
<input type="hidden" name='invoice_num' t-att-value='reference'/>
|
||||
<input type="hidden" name='currency' t-att-value='currency'/>
|
||||
<input type="hidden" name='currency_id' t-att-value='currency_id'/>
|
||||
<!-- Billing Information-->
|
||||
<input type="hidden" name='name' t-att-value='name'/>
|
||||
<input type="hidden" name='address_line1' t-att-value='address_line1'/>
|
||||
<input type="hidden" name='address_city' t-att-value='address_city'/>
|
||||
<input type="hidden" name='address_zip' t-att-value='address_zip'/>
|
||||
<input type="hidden" name='address_country' t-att-value='address_country'/>
|
||||
<input type="hidden" name='phone' t-att-value='phone'/>
|
||||
<input type="hidden" name='email' t-att-value='email'/>
|
||||
<input id="acquirer_stripe" type="hidden" name="acquirer" t-att-value="acquirer.id"/>
|
||||
<input type='hidden' name='session_id' t-att-value='session_id'/>
|
||||
<input type="hidden" name="stripe_key" t-att-value="acquirer.stripe_publishable_key"/>
|
||||
<script type="text/javascript">
|
||||
odoo.define(function (require) {
|
||||
@@ -30,29 +14,23 @@
|
||||
</template>
|
||||
|
||||
<template id="stripe_s2s_form">
|
||||
<input type="hidden" name="data_set" data-create-route="/payment/stripe/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="cvc" id="cvc" class="form-control" maxlength="4" placeholder="CVC" data-is-required="true"/>
|
||||
</div>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<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"/>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div class="clearfix"/>
|
||||
<input type="hidden" name="data_set" value="/payment/stripe/s2s/create_json_3ds"/>
|
||||
<input type="hidden" name="acquirer_id" t-att-value="id"/>
|
||||
<input type="hidden" name="stripe_publishable_key" t-att-value="acq.sudo().stripe_publishable_key"/>
|
||||
<input type="hidden" name="currency_id" t-att-value="currency_id"/>
|
||||
<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"/>
|
||||
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
|
||||
<div id="payment-form">
|
||||
<div id="card-element" class="m-3"/>
|
||||
<div id="card-errors" class="m-3 text-danger"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="assets_frontend" inherit_id="web.assets_frontend">
|
||||
<xpath expr="script[last()]" position="after">
|
||||
<script type="text/javascript" src="/payment_stripe/static/src/js/payment_form.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
Reference in New Issue
Block a user