[IMP] payment_stripe: integrate stripe with new checkout api
There is two payment flow in stripe:
1) Redirection Flow (Form Based):
in this flow, the user will redirect to stripe website for payment
- create a session for a user and redirect the user to stripe with that session
- after successful payment, the user will redirect back to odoo
- get all the details of charge by using payment intent API
- if user have tick save data then the token will be created
2) S2S Flow:
after clicking on pay now button, one popup display with card element
element is provided by a stripe with all validation facilities
After submitting details, payment flow is
- create payment method with card element on stripe
- create a customer with partner email on stripe
- attach customer to the payment method on stripe
- create a token with customer and payment method in odoo
- create payment intent in odoo
- make a request to stripe
- after successful request, payment will be charged for that card
task- 1986267
closes: https://github.com/odoo/odoo/pull/33978
This commit is contained in:
committed by
Damien Bouvy
parent
998ff5e07f
commit
595c0ad8ee
@@ -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):
|
||||
@@ -62,38 +42,3 @@ class StripeController(http.Controller):
|
||||
res['verified'] = token.verified
|
||||
|
||||
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"
|
||||
|
||||
@@ -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,59 @@ 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):
|
||||
self.ensure_one()
|
||||
url = urls.url_join(self._get_stripe_api_url(), url)
|
||||
headers = {'AUTHORIZATION': 'Bearer %s' % self.sudo().stripe_secret_key}
|
||||
resp = requests.post(url, data, headers=headers)
|
||||
resp.raise_for_status()
|
||||
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']
|
||||
|
||||
@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):
|
||||
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('id'),
|
||||
'name': 'XXXXXXXXXXXX%s' % data.get('card', {}).get('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,27 +98,38 @@ 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 _create_stripe_charge(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,
|
||||
'payment_method_types[]': 'card',
|
||||
'customer': acquirer_ref,
|
||||
'off_session': 'recurring',
|
||||
'confirm': True,
|
||||
'payment_method': self.payment_token_id.stripe_payment_method
|
||||
}
|
||||
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('_create_stripe_charge: Sending values to stripe, values:\n%s', pprint.pformat(charge_params))
|
||||
|
||||
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('_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))
|
||||
return res
|
||||
|
||||
@@ -130,9 +138,7 @@ class PaymentTransactionStripe(models.Model):
|
||||
result = self._create_stripe_charge(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 +146,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 +161,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 +192,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'),
|
||||
'id': 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 == 'processing':
|
||||
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,154 @@
|
||||
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({
|
||||
|
||||
//--------------------------------------------------------------------------
|
||||
// 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;
|
||||
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');
|
||||
}
|
||||
|
||||
ajax.loadJS("https://js.stripe.com/v3/").then(function () {
|
||||
var formData = self.getFormData(inputsForm);
|
||||
var stripe = Stripe(formData.stripe_publishable_key);
|
||||
var element = stripe.elements();
|
||||
var card = element.create('card', {hidePostalCode: true});
|
||||
var dialog = new Dialog(self, {
|
||||
title: _t('Card Details'),
|
||||
size: 'medium',
|
||||
technical: false,
|
||||
buttons: [{text: _t('Submit'), classes: 'btn-primary', click: function (ev) {
|
||||
stripe.createPaymentMethod('card', card).then(function (result) {
|
||||
if (result.error) {
|
||||
// Inform the user if there was an error.
|
||||
var errorElement = document.getElementById('card-errors');
|
||||
errorElement.textContent = result.error.message;
|
||||
} else {
|
||||
ev.currentTarget.disabled = true;
|
||||
_.extend(formData, result.paymentMethod);
|
||||
// Send the token to server.
|
||||
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
|
||||
self.enableButton(ev.target);
|
||||
|
||||
self.displayError(
|
||||
_t('Server Error'),
|
||||
_t("We are not able to add your payment method at the moment.") +
|
||||
error.message.data.message
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}}],
|
||||
$content: $(qweb.render('stripe.payment.element')),
|
||||
});
|
||||
dialog.opened().then(function () {
|
||||
self.disableButton(ev.target);
|
||||
card.mount('#card-element');
|
||||
card.addEventListener('change', function (event) {
|
||||
var displayError = document.getElementById('card-errors');
|
||||
displayError.textContent = '';
|
||||
if (event.error) {
|
||||
displayError.textContent = event.error.message;
|
||||
}
|
||||
});
|
||||
});
|
||||
dialog.on('closed', self, function () {
|
||||
self.enableButton(ev.target);
|
||||
});
|
||||
dialog.open();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
updateNewPaymentDisplayStatus: function () {
|
||||
var $checkedRadio = this.$('input[type="radio"]:checked');
|
||||
|
||||
if ($checkedRadio.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hide add token form for stripe
|
||||
if ($checkedRadio.data('provider') === 'stripe' && 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 stripe as s2s payment method
|
||||
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'stripe') {
|
||||
this._createStripeToken(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 stripe as add payment method
|
||||
if ($checkedRadio.length === 1 && this.isNewPaymentRadio($checkedRadio) && $checkedRadio.data('provider') === 'stripe') {
|
||||
this._createStripeToken(ev, $checkedRadio, true);
|
||||
} else {
|
||||
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,28 @@
|
||||
<?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>
|
||||
</t>
|
||||
|
||||
<div t-name="stripe.payment.element">
|
||||
<div id="payment-form">
|
||||
<div id="card-errors" class="m-3 text-danger text-center"/>
|
||||
<div id="card-element" class="m-3"/>
|
||||
</div>
|
||||
</div>
|
||||
</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,18 @@
|
||||
</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"/>
|
||||
</div>
|
||||
<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 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()"/>
|
||||
</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