diff --git a/addons/payment_stripe_sca/__init__.py b/addons/payment_stripe_sca/__init__.py
new file mode 100644
index 00000000000..bb0aae7ee63
--- /dev/null
+++ b/addons/payment_stripe_sca/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import controllers
+from . import models
diff --git a/addons/payment_stripe_sca/__manifest__.py b/addons/payment_stripe_sca/__manifest__.py
new file mode 100644
index 00000000000..6a2a0cac556
--- /dev/null
+++ b/addons/payment_stripe_sca/__manifest__.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+# ____________________
+# / \
+# | DO NOT FORWARD |
+# | PORT FURTHER |
+# | THAN 12.4 |
+# \____________________/
+# ! !
+# ! !
+# L_ !
+# / _)!
+# / /__L
+# _____/ (____)
+# (____)
+# _____ (____)
+# \_(____)
+# ! !
+# ! !
+# \__/
+#
+# Starting with 12.5, the payment_stripe module contains these changes OoB
+
+{
+ 'name': 'Stripe Payment Acquirer - Strong Customer Authentication Update',
+ 'category': 'Hidden',
+ 'summary': 'Payment Acquirer: Stripe Implementation for the EU PSD2',
+ 'version': '1.0',
+ 'description': """Stripe Payment Acquirer - Strong Customer Authentication Update""",
+ 'depends': ['payment_stripe'],
+ 'auto_install': True,
+ 'data': [
+ 'views/assets.xml',
+ 'views/payment_templates.xml',
+ ],
+ 'images': ['static/description/icon.png'],
+}
diff --git a/addons/payment_stripe_sca/controllers/__init__.py b/addons/payment_stripe_sca/controllers/__init__.py
new file mode 100644
index 00000000000..7036966b1ce
--- /dev/null
+++ b/addons/payment_stripe_sca/controllers/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import main
\ No newline at end of file
diff --git a/addons/payment_stripe_sca/controllers/main.py b/addons/payment_stripe_sca/controllers/main.py
new file mode 100644
index 00000000000..a3037125301
--- /dev/null
+++ b/addons/payment_stripe_sca/controllers/main.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+import pprint
+import werkzeug
+
+from odoo.http import route, request
+from odoo.addons.payment_stripe.controllers.main import StripeController
+
+_logger = logging.getLogger(__name__)
+
+
+class StripeControllerSCA(StripeController):
+ _success_url = '/payment/stripe/success'
+ _cancel_url = '/payment/stripe/cancel'
+
+ @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')
+
+ @route(['/payment/stripe/s2s/create_json_3ds'], type='json', auth='public', csrf=False)
+ def stripe_s2s_create_json_3ds(self, verify_validity=False, **kwargs):
+ if not kwargs.get('partner_id'):
+ kwargs = dict(kwargs, partner_id=request.env.user.partner_id.id)
+ token = request.env['payment.acquirer'].browse(int(kwargs.get('acquirer_id'))).s2s_process(kwargs)
+
+ if not token:
+ res = {
+ 'result': False,
+ }
+ return res
+
+ res = {
+ 'result': True,
+ 'id': token.id,
+ 'short_name': token.short_name,
+ '3d_secure': False,
+ 'verified': False,
+ }
+
+ if verify_validity != False:
+ token.validate()
+ res['verified'] = token.verified
+
+ return res
+
+ @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')
+
+
+ @route('/payment/stripe/s2s/process_payment_intent', type='json')
+ def stripe_s2s_process_payment_intent(self, **post):
+ return request.env['payment.transaction'].sudo().form_feedback(post, 'stripe')
+
+
+ # These routes are deprecated, let's remove them for security's sake
+ def stripe_s2s_create_json(self, **post):
+ raise werkzeug.exceptions.NotFound()
+
+ def stripe_s2s_create(self, **post):
+ raise werkzeug.exceptions.NotFound()
+
+ def stripe_create_charge(self, **post):
+ raise werkzeug.exceptions.NotFound()
diff --git a/addons/payment_stripe_sca/models/__init__.py b/addons/payment_stripe_sca/models/__init__.py
new file mode 100644
index 00000000000..df1092a4fa3
--- /dev/null
+++ b/addons/payment_stripe_sca/models/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+from . import payment
\ No newline at end of file
diff --git a/addons/payment_stripe_sca/models/payment.py b/addons/payment_stripe_sca/models/payment.py
new file mode 100644
index 00000000000..453f3a2678b
--- /dev/null
+++ b/addons/payment_stripe_sca/models/payment.py
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import logging
+import requests
+import pprint
+from werkzeug import urls
+
+from odoo import api, models, fields, _
+from odoo.exceptions import ValidationError
+from odoo.addons.payment_stripe_sca.controllers.main import StripeControllerSCA as StripeController
+from odoo.addons.payment_stripe.models.payment import INT_CURRENCIES
+from odoo.tools.float_utils import float_round
+
+_logger = logging.getLogger(__name__)
+
+
+class PaymentAcquirerStripeSCA(models.Model):
+ _inherit = "payment.acquirer"
+
+ def stripe_form_generate_values(self, tx_values):
+ self.ensure_one()
+
+ 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 "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(
+ {
+ "acquirer_id": int(data["acquirer_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):
+ return True
+
+
+class PaymentTransactionStripeSCA(models.Model):
+ _inherit = "payment.transaction"
+
+ stripe_payment_intent = fields.Char(
+ string="Stripe Payment Intent ID", readonly=True
+ )
+ stripe_payment_intent_secret = fields.Char(string='Stripe Payment Intent Secret', readonly=True)
+
+ def _get_json_fields(self):
+ res = super()._get_json_fields()
+ res.append('stripe_payment_intent_secret')
+ return res
+
+ def _get_processing_info(self):
+ res = super()._get_processing_info()
+ if self.acquirer_id.provider == 'stripe':
+ stripe_info = {
+ 'stripe_payment_intent': self.stripe_payment_intent,
+ 'stripe_payment_intent_secret': self.stripe_payment_intent_secret,
+ 'stripe_publishable_key': self.acquirer_id.stripe_publishable_key,
+ }
+ res.update(stripe_info)
+ return res
+
+ def _create_stripe_charge(self, acquirer_ref=None, tokenid=None, email=None):
+ raise NotImplementedError(
+ "This method can no longer be used with the payment_stripe_sca module."
+ )
+
+ 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)
+ )
+ # note: luckily, the base stripe module did not override this method, avoiding
+ # me from using a context key to avoid this call in the parent model
+ return super(PaymentTransactionStripeSCA, self).form_feedback(data, acquirer_name)
+
+ def _stripe_create_payment_intent(self, acquirer_ref=None, email=None):
+ if self.stripe_payment_intent:
+ _logger.info(
+ "_stripe_create_payment_intent: trying to create an intent when one already exists (tx #%s), refetching values for intent %s",
+ self.id, self.stripe_payment_intent
+ )
+ res = self.acquirer_id._stripe_request("payment_intents/%s" % self.stripe_payment_intent, method="GET")
+ _logger.info(
+ "_stripe_create_payment_intent: Values received:\n%s", pprint.pformat(res)
+ )
+ return res
+ 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.lower(),
+ "confirm": True,
+ "off_session": True,
+ "payment_method": self.payment_token_id.stripe_payment_method,
+ "customer": self.payment_token_id.acquirer_ref,
+ "description": self.reference,
+ }
+ if not self.env.context.get('off_session'):
+ charge_params.update(setup_future_usage='off_session', off_session=False)
+ _logger.info(
+ "_stripe_create_payment_intent: 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(
+ "_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._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):
+ refund_params = {
+ "charge": self.acquirer_reference,
+ "amount": int(
+ float_round(self.amount * 100, 2)
+ ), # by default, stripe refund the full amount (we don't really need to specify the value)
+ "metadata[reference]": self.reference,
+ }
+
+ _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
+
+ @api.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("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",
+ )
+ 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'")
+ % stripe_error
+ )
+ error_msg += " " + _(
+ "Perhaps the problem can be solved by double-checking your "
+ "credit card details, or contacting your bank?"
+ )
+ raise ValidationError(error_msg)
+
+ tx = self.search([("reference", "=", reference)])
+ if not tx:
+ error_msg = _("Stripe: no order found for reference %s") % reference
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ elif len(tx) > 1:
+ error_msg = _("Stripe: %s orders found for reference %s") % (
+ len(tx),
+ reference,
+ )
+ _logger.error(error_msg)
+ raise ValidationError(error_msg)
+ return tx[0]
+
+ def _stripe_s2s_validate_tree(self, tree):
+ self.ensure_one()
+ if self.state not in ("draft", "pending"):
+ _logger.info(
+ "Stripe: trying to validate an already validated tx (ref %s)",
+ self.reference,
+ )
+ return True
+
+ status = tree.get("status")
+ tx_id = tree.get("id")
+ tx_secret = tree.get("client_secret")
+ vals = {"date": fields.datetime.now(), "acquirer_reference": tx_id, "stripe_payment_intent": tx_id, "stripe_payment_intent_secret": tx_secret}
+ if status == "succeeded":
+ 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.get("failure_message")
+ _logger.warn(error)
+ vals.update({"state_message": error})
+ self.write(vals)
+ self._set_transaction_cancel()
+ return False
+
+ def _stripe_form_get_invalid_parameters(self, data):
+ invalid_parameters = []
+ 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
+
+
+class PaymentTokenStripeSCA(models.Model):
+ _inherit = "payment.token"
+
+ stripe_payment_method = fields.Char("Payment Method ID")
+
+ @api.model
+ def stripe_create(self, values):
+ 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}
+ 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
+
+ def _stripe_create_customer(self, token, description=None, acquirer_id=None):
+ raise NotImplementedError(
+ "This method can no longer be used with the payment_stripe_sca module."
+ )
+
diff --git a/addons/payment_stripe_sca/static/description/icon.png b/addons/payment_stripe_sca/static/description/icon.png
new file mode 100644
index 00000000000..497b2d65bd9
Binary files /dev/null and b/addons/payment_stripe_sca/static/description/icon.png differ
diff --git a/addons/payment_stripe_sca/static/description/icon.svg b/addons/payment_stripe_sca/static/description/icon.svg
new file mode 100644
index 00000000000..f169bb0d45e
--- /dev/null
+++ b/addons/payment_stripe_sca/static/description/icon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/addons/payment_stripe_sca/static/description/index.html b/addons/payment_stripe_sca/static/description/index.html
new file mode 100644
index 00000000000..284cee7c413
--- /dev/null
+++ b/addons/payment_stripe_sca/static/description/index.html
@@ -0,0 +1,64 @@
+
+ The PSD2 - the second edition of the Payment Services Directive - is coming into full effect in
+ the European Union (EU) and the European Economic Area (EEA) on September 14th, 2019. This
+ directive may indirectly impact your business by requiring stronger authentication mechanisms
+ for online payments.
+ You are impacted by this change if all of these statements applies to your business:
+
+ This directive does not bind you legally to do anything in particular regarding
+ your business processes - it applies to payment processors, acquirers and banks.
+ Legally, you don't have to do anything to comply with this directive - it does not
+ apply to you.
+
+ You might however be indirectly impacted because some online payment flows will
+ change for customers that pay online - they might need to authenticate themselves during
+ a transaction that previously didn't require it. This is why Odoo is adapting some of
+ its integration with payment acquirers to avoid disruptions in your business flows.
+
+ If you use Stripe and are impacted by the PSD2 directive, then simply installing this
+ module will avoid problems for your online customers during their payment flow. No extra
+ configuration is needed once the module is installed.
+ If you don't install this update, some of your customers may be unable to pay you.
+ There is no reason not to install it if you are impacted by the directive. While not required, installing this module remains a good idea nonetheless since
+ it will switch your instance to newer - and thus more future-proof - APIs of the Stripe
+ payment acquirer.
+ The interface displayed to your customers will change when they pay you online. If your Stripe acquirer is configured with the payment flow 'Redirection to the acquirer website'
+ in Odoo, the small payment overlay will be replaced by a full-screen interface.
+ If your Stripe acquirer is configured with the payment flow 'Payment from Odoo'
+ in Odoo, the credit card input form will look slightly different.
+ The rest of the payment experience remains the same.Stripe - Strong Customer Authentication
+ Add support for strong customer authentication as part of the EU PSD2
+ Am I impacted by the PSD2 directive?
+
+
+
+ What should I do if I am impacted?
+ What happens if I don't install this update?
+ I'm not impacted by the PSD2, should I install this module anyway?
+ What will changes for my online customers
+
+
+
' +
+ '