From 82a4bc3a6d2aa4c7a44dec3032314236c0f2e203 Mon Sep 17 00:00:00 2001 From: pilarvargas-tecnativa Date: Mon, 20 Apr 2026 16:04:32 +0200 Subject: [PATCH 1/2] [ADD] payment_amazon_pay: Add new module for Amazon Pay TT61807 --- eslint.config.cjs | 4 +- payment_amazon_pay/README.rst | 84 ++++ payment_amazon_pay/__init__.py | 2 + payment_amazon_pay/__manifest__.py | 23 + payment_amazon_pay/controllers/__init__.py | 1 + payment_amazon_pay/controllers/main.py | 97 ++++ .../data/payment_provider_data.xml | 17 + payment_amazon_pay/i18n/es.po | 275 +++++++++++ .../i18n/payment_amazon_pay.pot | 273 +++++++++++ payment_amazon_pay/models/__init__.py | 2 + payment_amazon_pay/models/payment_provider.py | 390 ++++++++++++++++ .../models/payment_transaction.py | 182 ++++++++ payment_amazon_pay/pyproject.toml | 3 + payment_amazon_pay/readme/CONTRIBUTORS.md | 3 + payment_amazon_pay/readme/DESCRIPTION.md | 6 + .../static/description/index.html | 431 ++++++++++++++++++ payment_amazon_pay/static/img/amazon_pay.png | Bin 0 -> 4165 bytes .../static/src/css/amazon_pay_checkout.css | 24 + .../static/src/js/amazon_pay_checkout.js | 15 + .../static/src/js/payment_form.js | 164 +++++++ .../views/payment_amazon_pay_templates.xml | 15 + .../views/payment_provider_views.xml | 41 ++ requirements.txt | 2 + 23 files changed, 2052 insertions(+), 2 deletions(-) create mode 100644 payment_amazon_pay/README.rst create mode 100644 payment_amazon_pay/__init__.py create mode 100644 payment_amazon_pay/__manifest__.py create mode 100644 payment_amazon_pay/controllers/__init__.py create mode 100644 payment_amazon_pay/controllers/main.py create mode 100644 payment_amazon_pay/data/payment_provider_data.xml create mode 100644 payment_amazon_pay/i18n/es.po create mode 100644 payment_amazon_pay/i18n/payment_amazon_pay.pot create mode 100644 payment_amazon_pay/models/__init__.py create mode 100644 payment_amazon_pay/models/payment_provider.py create mode 100644 payment_amazon_pay/models/payment_transaction.py create mode 100644 payment_amazon_pay/pyproject.toml create mode 100644 payment_amazon_pay/readme/CONTRIBUTORS.md create mode 100644 payment_amazon_pay/readme/DESCRIPTION.md create mode 100644 payment_amazon_pay/static/description/index.html create mode 100644 payment_amazon_pay/static/img/amazon_pay.png create mode 100644 payment_amazon_pay/static/src/css/amazon_pay_checkout.css create mode 100644 payment_amazon_pay/static/src/js/amazon_pay_checkout.js create mode 100644 payment_amazon_pay/static/src/js/payment_form.js create mode 100644 payment_amazon_pay/views/payment_amazon_pay_templates.xml create mode 100644 payment_amazon_pay/views/payment_provider_views.xml diff --git a/eslint.config.cjs b/eslint.config.cjs index 0d5731f89a8..506d7b0be36 100644 --- a/eslint.config.cjs +++ b/eslint.config.cjs @@ -152,7 +152,7 @@ const config = [{ strict: ["error", "function"], "use-isnan": "error", - "jsdoc/check-tag-names": "warn", + "jsdoc/check-tag-names": ["warn", { definedTags: ["odoo-module"] }], "jsdoc/check-types": "warn", "jsdoc/require-param-description": "off", "jsdoc/require-return": "off", @@ -191,7 +191,7 @@ const config = [{ }, }, { - files: ["**/*.esm.js"], + files: ["**/*.esm.js", "payment_amazon_pay/static/src/js/payment_form.js"], languageOptions: { ecmaVersion: 2024, diff --git a/payment_amazon_pay/README.rst b/payment_amazon_pay/README.rst new file mode 100644 index 00000000000..2cb95af1e66 --- /dev/null +++ b/payment_amazon_pay/README.rst @@ -0,0 +1,84 @@ +========== +Amazon Pay +========== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:25f860184c83e375186e40eafb6e83beaf5cf4a36a6eaa2b27db8aba953cdcb9 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--payment-lightgray.png?logo=github + :target: https://github.com/OCA/account-payment/tree/18.0/payment_amazon_pay + :alt: OCA/account-payment +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/account-payment-18-0/account-payment-18-0-payment_amazon_pay + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-payment&target_branch=18.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Payment provider integration for `Amazon Pay +v2 `__. + +Supports the EU, NA, and JP regions. Uses the Amazon Pay v2 API (RSA-PSS +signed requests) for checkout session creation, completion, and refunds. +Handles IPN notifications via a dedicated webhook endpoint. + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Tecnativa + +Contributors +------------ + +- `Tecnativa `__: + + - Pilar Vargas + - Cristina Hidalgo + +Maintainers +----------- + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/account-payment `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/payment_amazon_pay/__init__.py b/payment_amazon_pay/__init__.py new file mode 100644 index 00000000000..91c5580fed3 --- /dev/null +++ b/payment_amazon_pay/__init__.py @@ -0,0 +1,2 @@ +from . import controllers +from . import models diff --git a/payment_amazon_pay/__manifest__.py b/payment_amazon_pay/__manifest__.py new file mode 100644 index 00000000000..f0687d19e90 --- /dev/null +++ b/payment_amazon_pay/__manifest__.py @@ -0,0 +1,23 @@ +{ + "name": "Amazon Pay", + "version": "18.0.1.0.0", + "category": "Accounting/Payment Providers", + "summary": "Amazon Pay payment provider for Odoo", + "license": "AGPL-3", + "author": "Tecnativa, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/account-payment", + "depends": ["payment"], + "external_dependencies": {"python": ["cryptography", "requests"]}, + "data": [ + "data/payment_provider_data.xml", + "views/payment_provider_views.xml", + "views/payment_amazon_pay_templates.xml", + ], + "assets": { + "web.assets_frontend": [ + "payment_amazon_pay/static/src/js/payment_form.js", + ], + }, + "installable": True, + "application": False, +} diff --git a/payment_amazon_pay/controllers/__init__.py b/payment_amazon_pay/controllers/__init__.py new file mode 100644 index 00000000000..12a7e529b67 --- /dev/null +++ b/payment_amazon_pay/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/payment_amazon_pay/controllers/main.py b/payment_amazon_pay/controllers/main.py new file mode 100644 index 00000000000..3dc5541c995 --- /dev/null +++ b/payment_amazon_pay/controllers/main.py @@ -0,0 +1,97 @@ +import json +import logging + +from odoo import http +from odoo.exceptions import ValidationError +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +def _get_tx(notification_data): + return ( + request.env["payment.transaction"] + .sudo() + ._get_tx_from_notification_data("amazon_pay", notification_data) + ) + + +class AmazonPayController(http.Controller): + @http.route( + "/payment/amazon_pay/return", + type="http", + auth="public", + methods=["GET"], + csrf=False, + save_session=False, + ) + def amazon_pay_return(self, reference=None, amazonCheckoutSessionId=None, **kwargs): + request.env["payment.transaction"].sudo()._handle_notification_data( + "amazon_pay", + {"reference": reference, "checkoutSessionId": amazonCheckoutSessionId}, + ) + return request.redirect("/payment/status") + + @http.route( + "/payment/amazon_pay/cancel", + type="http", + auth="public", + methods=["GET"], + csrf=False, + save_session=False, + ) + def amazon_pay_cancel(self, reference=None, amazonCheckoutSessionId=None, **kwargs): + request.env["payment.transaction"].sudo()._handle_notification_data( + "amazon_pay", + { + "reference": reference, + "checkoutSessionId": amazonCheckoutSessionId, + "statusDetails": {"state": "Canceled"}, + }, + ) + return request.redirect("/payment/status") + + @http.route( + "/payment/amazon_pay/ipn", + type="http", + auth="public", + methods=["POST"], + csrf=False, + save_session=False, + ) + def amazon_pay_ipn(self, **kwargs): + raw_payload = request.httprequest.get_data(cache=False, as_text=True) or "{}" + try: + payload = json.loads(raw_payload) + except json.JSONDecodeError: + _logger.warning("Invalid Amazon Pay IPN payload: %s", raw_payload) + return request.make_response( + "invalid", headers=[("Content-Type", "text/plain")] + ) + + data = payload.get("Object") or payload + object_type = data.get("ObjectType") or data.get("objectType") + object_id = data.get("ObjectId") or data.get("objectId") + charge_id = data.get("chargeId") + refund_id = data.get("refundId") + + try: + tx = _get_tx({"checkoutSessionId": object_id, "chargeId": charge_id}) + except ValidationError: + return request.make_response( + "ignored", headers=[("Content-Type", "text/plain")] + ) + + if object_type in ("CHARGE", "Charge") and charge_id: + notification_data = tx.provider_id._amazon_pay_get_charge(charge_id) + elif object_type in ("REFUND", "Refund") and refund_id: + notification_data = tx.provider_id._amazon_pay_get_refund(refund_id) + else: + notification_data = tx.provider_id._amazon_pay_get_checkout_session( + object_id or tx.amazon_pay_checkout_session_id + ) + + notification_data.setdefault("reference", tx.reference) + notification_data.setdefault("merchantReferenceId", tx.reference) + tx._handle_notification_data("amazon_pay", notification_data) + return request.make_response("ok", headers=[("Content-Type", "text/plain")]) diff --git a/payment_amazon_pay/data/payment_provider_data.xml b/payment_amazon_pay/data/payment_provider_data.xml new file mode 100644 index 00000000000..22c8896b3c2 --- /dev/null +++ b/payment_amazon_pay/data/payment_provider_data.xml @@ -0,0 +1,17 @@ + + + + Amazon Pay + amazon_pay + + + + + diff --git a/payment_amazon_pay/i18n/es.po b/payment_amazon_pay/i18n/es.po new file mode 100644 index 00000000000..1737694fb35 --- /dev/null +++ b/payment_amazon_pay/i18n/es.po @@ -0,0 +1,275 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * payment_amazon_pay +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-30 08:04+0000\n" +"PO-Revision-Date: 2026-04-30 08:04+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__code__amazon_pay +#: model:payment.method,name:payment_amazon_pay.payment_method_amazon_pay +#: model:payment.provider,name:payment_amazon_pay.payment_provider_amazon_pay +msgid "Amazon Pay" +msgstr "Amazon Pay" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_provider.py:0 +msgid "Amazon Pay API error: %s" +msgstr "Error de la API de Amazon Pay: %s" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_charge_id +msgid "Amazon Pay Charge" +msgstr "Cargo de Amazon Pay" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_charge_permission_id +msgid "Amazon Pay Charge Permission" +msgstr "Permiso de cargo de Amazon Pay" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_checkout_session_id +msgid "Amazon Pay Checkout Session" +msgstr "Sesión de pago de Amazon Pay" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_private_key +msgid "Amazon Pay Checkout v2 private key in PEM format." +msgstr "Clave privada de Amazon Pay Checkout v2 en formato PEM." + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_store_id +msgid "Amazon Pay Checkout v2 store identifier." +msgstr "Identificador de tienda de Amazon Pay Checkout v2." + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_refund_id +msgid "Amazon Pay Refund" +msgstr "Reembolso de Amazon Pay" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay charge ID is missing on the source transaction." +msgstr "Falta el ID de cargo de Amazon Pay en la transacción de origen." + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay declined the payment." +msgstr "Amazon Pay ha rechazado el pago." + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay did not accept the refund request." +msgstr "Amazon Pay no ha aceptado la solicitud de reembolso." + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay did not return a checkout session id." +msgstr "Amazon Pay no ha devuelto un ID de sesión de pago." + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay: could not retrieve the payment status." +msgstr "Amazon Pay: no se ha podido obtener el estado del pago." + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_merchant_id +msgid "Amazon Pay merchant identifier." +msgstr "Identificador de comerciante de Amazon Pay." + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_provider.py:0 +msgid "Amazon Pay private key is missing." +msgstr "Falta la clave privada de Amazon Pay." + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_public_key_id +msgid "Amazon Pay public key ID used to sign requests." +msgstr "ID de clave pública de Amazon Pay utilizado para firmar solicitudes." + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay returned an unexpected state: %s" +msgstr "Amazon Pay ha devuelto un estado inesperado: %s" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay: no transaction found for the received data." +msgstr "Amazon Pay: no se ha encontrado ninguna transacción para los datos recibidos." + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_checkout_language +msgid "Checkout Language" +msgstr "Idioma de pago" + +#. module: payment_amazon_pay +#: model_terms:ir.ui.view,arch_db:payment_amazon_pay.amazon_pay_checkout_button +msgid "You will be redirected to Amazon to sign in and confirm your payment." +msgstr "Serás redirigido a Amazon para iniciar sesión y confirmar el pago." + +#. module: payment_amazon_pay +#: model_terms:ir.ui.view,arch_db:payment_amazon_pay.amazon_pay_checkout_page +msgid "Click the button to complete your payment with Amazon Pay." +msgstr "Haga clic en el botón para completar su pago con Amazon Pay." + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__code +msgid "Code" +msgstr "Código" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__en_gb +msgid "English (UK)" +msgstr "Inglés (Reino Unido)" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__en_us +msgid "English (US)" +msgstr "Inglés (EE. UU.)" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_region__eu +msgid "Europe" +msgstr "Europa" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__fr_fr +msgid "French" +msgstr "Francés" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__de_de +msgid "German" +msgstr "Alemán" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__it_it +msgid "Italian" +msgstr "Italiano" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_region__jp +msgid "Japan" +msgstr "Japón" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__ja_jp +msgid "Japanese" +msgstr "Japonés" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_merchant_id +msgid "Merchant ID" +msgstr "ID de comerciante" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_region__na +msgid "North America" +msgstr "Norteamérica" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_sandbox_simulation_code +msgid "Optional sandbox simulation code used by Amazon Pay." +msgstr "Código de simulación de entorno de pruebas opcional utilizado por Amazon Pay." + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_soft_descriptor +msgid "Optional text displayed on the buyer statement when supported." +msgstr "Texto opcional que se muestra en el extracto del comprador cuando es compatible." + +#. module: payment_amazon_pay +#: model:ir.model,name:payment_amazon_pay.model_payment_provider +msgid "Payment Provider" +msgstr "Proveedor de pago" + +#. module: payment_amazon_pay +#: model:ir.model,name:payment_amazon_pay.model_payment_transaction +msgid "Payment Transaction" +msgstr "Transacción de pago" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_private_key +msgid "Private Key" +msgstr "Clave privada" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_public_key_id +msgid "Public Key ID" +msgstr "ID de clave pública" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_region +msgid "Region" +msgstr "Región" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_sandbox_simulation_code +msgid "Sandbox Simulation Code" +msgstr "Código de simulación de entorno de pruebas" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_soft_descriptor +msgid "Soft Descriptor" +msgstr "Descriptor suave" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__es_es +msgid "Spanish" +msgstr "Español" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_store_id +msgid "Store ID" +msgstr "ID de tienda" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__code +msgid "The technical code of this payment provider." +msgstr "El código técnico de este proveedor de pago." + +#. module: payment_amazon_pay +#: model_terms:ir.ui.view,arch_db:payment_amazon_pay.view_payment_provider_form_amazon_pay +msgid "This provider uses Amazon Pay Checkout v2 in PayOnly mode." +msgstr "Este proveedor utiliza Amazon Pay Checkout v2 en modo PayOnly." + +#. module: payment_amazon_pay +#: model_terms:payment.provider,auth_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "Your payment has been authorized." +msgstr "Su pago ha sido autorizado." + +#. module: payment_amazon_pay +#: model_terms:payment.provider,cancel_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "Your payment has been cancelled." +msgstr "Su pago ha sido cancelado." + +#. module: payment_amazon_pay +#: model_terms:payment.provider,pending_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "" +"Your payment has been successfully processed but is waiting for approval." +msgstr "" +"Su pago se ha procesado correctamente pero está pendiente de aprobación." + +#. module: payment_amazon_pay +#: model_terms:payment.provider,done_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "Your payment has been successfully processed." +msgstr "Su pago se ha procesado correctamente." diff --git a/payment_amazon_pay/i18n/payment_amazon_pay.pot b/payment_amazon_pay/i18n/payment_amazon_pay.pot new file mode 100644 index 00000000000..43cc6781fd2 --- /dev/null +++ b/payment_amazon_pay/i18n/payment_amazon_pay.pot @@ -0,0 +1,273 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * payment_amazon_pay +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-04-30 08:04+0000\n" +"PO-Revision-Date: 2026-04-30 08:04+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__code__amazon_pay +#: model:payment.method,name:payment_amazon_pay.payment_method_amazon_pay +#: model:payment.provider,name:payment_amazon_pay.payment_provider_amazon_pay +msgid "Amazon Pay" +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_provider.py:0 +msgid "Amazon Pay API error: %s" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_charge_id +msgid "Amazon Pay Charge" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_charge_permission_id +msgid "Amazon Pay Charge Permission" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_checkout_session_id +msgid "Amazon Pay Checkout Session" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_private_key +msgid "Amazon Pay Checkout v2 private key in PEM format." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_store_id +msgid "Amazon Pay Checkout v2 store identifier." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_transaction__amazon_pay_refund_id +msgid "Amazon Pay Refund" +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay charge ID is missing on the source transaction." +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay declined the payment." +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay did not accept the refund request." +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay did not return a checkout session id." +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay: could not retrieve the payment status." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_merchant_id +msgid "Amazon Pay merchant identifier." +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_provider.py:0 +msgid "Amazon Pay private key is missing." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_public_key_id +msgid "Amazon Pay public key ID used to sign requests." +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay returned an unexpected state: %s" +msgstr "" + +#. module: payment_amazon_pay +#. odoo-python +#: code:addons/payment_amazon_pay/models/payment_transaction.py:0 +msgid "Amazon Pay: no transaction found for the received data." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_checkout_language +msgid "Checkout Language" +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:ir.ui.view,arch_db:payment_amazon_pay.amazon_pay_checkout_button +msgid "You will be redirected to Amazon to sign in and confirm your payment." +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:ir.ui.view,arch_db:payment_amazon_pay.amazon_pay_checkout_page +msgid "Click the button to complete your payment with Amazon Pay." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__code +msgid "Code" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__en_gb +msgid "English (UK)" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__en_us +msgid "English (US)" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_region__eu +msgid "Europe" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__fr_fr +msgid "French" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__de_de +msgid "German" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__it_it +msgid "Italian" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_region__jp +msgid "Japan" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__ja_jp +msgid "Japanese" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_merchant_id +msgid "Merchant ID" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_region__na +msgid "North America" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_sandbox_simulation_code +msgid "Optional sandbox simulation code used by Amazon Pay." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__amazon_pay_soft_descriptor +msgid "Optional text displayed on the buyer statement when supported." +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model,name:payment_amazon_pay.model_payment_provider +msgid "Payment Provider" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model,name:payment_amazon_pay.model_payment_transaction +msgid "Payment Transaction" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_private_key +msgid "Private Key" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_public_key_id +msgid "Public Key ID" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_region +msgid "Region" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_sandbox_simulation_code +msgid "Sandbox Simulation Code" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_soft_descriptor +msgid "Soft Descriptor" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields.selection,name:payment_amazon_pay.selection__payment_provider__amazon_pay_checkout_language__es_es +msgid "Spanish" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,field_description:payment_amazon_pay.field_payment_provider__amazon_pay_store_id +msgid "Store ID" +msgstr "" + +#. module: payment_amazon_pay +#: model:ir.model.fields,help:payment_amazon_pay.field_payment_provider__code +msgid "The technical code of this payment provider." +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:ir.ui.view,arch_db:payment_amazon_pay.view_payment_provider_form_amazon_pay +msgid "This provider uses Amazon Pay Checkout v2 in PayOnly mode." +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:payment.provider,auth_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "Your payment has been authorized." +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:payment.provider,cancel_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "Your payment has been cancelled." +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:payment.provider,pending_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "" +"Your payment has been successfully processed but is waiting for approval." +msgstr "" + +#. module: payment_amazon_pay +#: model_terms:payment.provider,done_msg:payment_amazon_pay.payment_provider_amazon_pay +msgid "Your payment has been successfully processed." +msgstr "" diff --git a/payment_amazon_pay/models/__init__.py b/payment_amazon_pay/models/__init__.py new file mode 100644 index 00000000000..9afbccd31e8 --- /dev/null +++ b/payment_amazon_pay/models/__init__.py @@ -0,0 +1,2 @@ +from . import payment_provider +from . import payment_transaction diff --git a/payment_amazon_pay/models/payment_provider.py b/payment_amazon_pay/models/payment_provider.py new file mode 100644 index 00000000000..f1fdc48652a --- /dev/null +++ b/payment_amazon_pay/models/payment_provider.py @@ -0,0 +1,390 @@ +import base64 +import hashlib +import json +import uuid +from datetime import datetime, timezone + +import requests +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding +from werkzeug.urls import url_join + +from odoo import fields, models +from odoo.exceptions import ValidationError + + +class PaymentProvider(models.Model): + _inherit = "payment.provider" + + code = fields.Selection( + selection_add=[("amazon_pay", "Amazon Pay")], + ondelete={"amazon_pay": "set default"}, + ) + + amazon_pay_merchant_id = fields.Char( + string="Merchant ID", + required_if_provider="amazon_pay", + help="Amazon Pay merchant identifier.", + ) + amazon_pay_store_id = fields.Char( + string="Store ID", + required_if_provider="amazon_pay", + help="Amazon Pay Checkout v2 store identifier.", + ) + amazon_pay_public_key_id = fields.Char( + string="Public Key ID", + required_if_provider="amazon_pay", + help="Amazon Pay public key ID used to sign requests.", + ) + amazon_pay_private_key = fields.Char( + string="Private Key", + required_if_provider="amazon_pay", + groups="base.group_system", + help="Amazon Pay Checkout v2 private key in PEM format.", + ) + amazon_pay_region = fields.Selection( + selection=[("eu", "Europe"), ("na", "North America"), ("jp", "Japan")], + string="Region", + default="eu", + required=True, + ) + amazon_pay_checkout_language = fields.Selection( + selection=[ + ("es_ES", "Spanish"), + ("en_GB", "English (UK)"), + ("de_DE", "German"), + ("fr_FR", "French"), + ("it_IT", "Italian"), + ("en_US", "English (US)"), + ("ja_JP", "Japanese"), + ], + string="Checkout Language", + default="es_ES", + required=True, + ) + amazon_pay_soft_descriptor = fields.Char( + string="Soft Descriptor", + help="Optional text displayed on the buyer statement when supported.", + ) + amazon_pay_sandbox_simulation_code = fields.Char( + string="Sandbox Simulation Code", + help="Optional sandbox simulation code used by Amazon Pay.", + ) + + def _compute_feature_support_fields(self): + """Override of `payment` to enable additional features.""" + res = super()._compute_feature_support_fields() + amazon_providers = self.filtered(lambda provider: provider.code == "amazon_pay") + amazon_providers.update( + { + "support_express_checkout": False, + "support_manual_capture": False, + "support_refund": "partial", + "support_tokenization": False, + } + ) + return res + + def _get_default_payment_method_codes(self): + self.ensure_one() + default_codes = super()._get_default_payment_method_codes() + if self.code != "amazon_pay": + return default_codes + return {*default_codes, "amazon_pay"} + + def _get_redirect_form_view(self, is_validation=False): + self.ensure_one() + if self.code != "amazon_pay": + return super()._get_redirect_form_view(is_validation=is_validation) + return self.env.ref("payment_amazon_pay.amazon_pay_redirect_form") + + def _get_supported_currencies(self): + self.ensure_one() + if self.code != "amazon_pay": + return super()._get_supported_currencies() + supported_currency_codes = { + "eu": ["EUR", "GBP"], + "na": ["USD"], + "jp": ["JPY"], + }[self.amazon_pay_region] + return ( + self.env["res.currency"] + .with_context(active_test=False) + .search([("name", "in", supported_currency_codes)]) + ) + + def _amazon_pay_get_host(self): + self.ensure_one() + return { + "eu": "pay-api.amazon.eu", + "na": "pay-api.amazon.com", + "jp": "pay-api.amazon.jp", + }[self.amazon_pay_region] + + def _amazon_pay_get_environment(self): + self.ensure_one() + return "sandbox" if self.state == "test" else "live" + + def _amazon_pay_get_api_path(self, endpoint): + self.ensure_one() + return f"/{self._amazon_pay_get_environment()}/v2/{endpoint.lstrip('/')}" + + def _amazon_pay_get_api_url(self, endpoint): + self.ensure_one() + return f"https://{self._amazon_pay_get_host()}{self._amazon_pay_get_api_path(endpoint)}" + + def _amazon_pay_get_region_code(self): + self.ensure_one() + return self.amazon_pay_region + + @staticmethod + def _amazon_pay_format_amount(amount): + return f"{amount:.2f}" + + @staticmethod + def _amazon_pay_normalize_private_key(key): + """Reconstruct PEM format if newlines were stripped.""" + import re + + key = key.strip() + if "\n" not in key and "-----" in key: + match = re.match(r"(-----BEGIN[^-]+-----)([^-]+)(-----END[^-]+-----)", key) + if match: + header, data, footer = match.groups() + data = data.replace(" ", "") + data = "\n".join(data[i : i + 64] for i in range(0, len(data), 64)) + key = f"{header}\n{data}\n{footer}" + return key + + def _amazon_pay_build_headers( + self, method, api_path, payload_text="", idempotency_key=None + ): + self.ensure_one() + if not self.amazon_pay_private_key: + raise ValidationError(self.env._("Amazon Pay private key is missing.")) + + amz_date = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") + payload_hash = hashlib.sha256(payload_text.encode("utf-8")).hexdigest() + headers_to_sign = { + "accept": "application/json", + "content-type": "application/json", + "x-amz-pay-date": amz_date, + "x-amz-pay-host": self._amazon_pay_get_host(), + "x-amz-pay-region": self._amazon_pay_get_region_code(), + } + if idempotency_key: + headers_to_sign["x-amz-pay-idempotency-key"] = idempotency_key + if self.state == "test" and self.amazon_pay_sandbox_simulation_code: + headers_to_sign["x-amz-simulation-code"] = ( + self.amazon_pay_sandbox_simulation_code + ) + + signed_header_names = sorted(headers_to_sign) + canonical_headers = "".join( + f"{name}:{headers_to_sign[name]}\n" for name in signed_header_names + ) + signed_headers = ";".join(signed_header_names) + canonical_request = ( + f"{method.upper()}\n" + f"{api_path}\n" + f"\n" + f"{canonical_headers}\n" + f"{signed_headers}\n" + f"{payload_hash}" + ) + string_to_sign = ( + "AMZN-PAY-RSASSA-PSS-V2\n" + f"{hashlib.sha256(canonical_request.encode('utf-8')).hexdigest()}" + ) + + private_key = serialization.load_pem_private_key( + self._amazon_pay_normalize_private_key(self.amazon_pay_private_key).encode( + "utf-8" + ), + password=None, + ) + signature = private_key.sign( + string_to_sign.encode("utf-8"), + padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=32), + hashes.SHA256(), + ) + authorization = ( + "AMZN-PAY-RSASSA-PSS-V2 " + f"PublicKeyId={self.amazon_pay_public_key_id}, " + f"SignedHeaders={signed_headers}, " + f"Signature={base64.b64encode(signature).decode('ascii')}" + ) + + headers = dict(headers_to_sign) + headers["authorization"] = authorization + return headers + + def _amazon_pay_request(self, method, endpoint, payload=None, idempotency_key=None): + self.ensure_one() + payload = payload or {} + payload_text = ( + "" + if method.upper() == "GET" + else json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + ) + api_path = self._amazon_pay_get_api_path(endpoint) + headers = self._amazon_pay_build_headers( + method, api_path, payload_text=payload_text, idempotency_key=idempotency_key + ) + response = requests.request( + method=method.upper(), + url=self._amazon_pay_get_api_url(endpoint), + data=payload_text or None, + headers=headers, + timeout=60, + ) + try: + data = response.json() if response.text else {} + except ValueError: + data = {"message": response.text} + if response.status_code >= 400: + message = ( + data.get("reasonDescription") or data.get("message") or response.text + ) + raise ValidationError(self.env._("Amazon Pay API error: %s") % message) + data["_http_status"] = response.status_code + return data + + def _amazon_pay_prepare_checkout_payload(self, tx): + self.ensure_one() + tx.ensure_one() + base_url = self.get_base_url() + result_url = url_join( + base_url, f"/payment/amazon_pay/return?reference={tx.reference}" + ) + cancel_url = url_join( + base_url, f"/payment/amazon_pay/cancel?reference={tx.reference}" + ) + payload = { + "storeId": self.amazon_pay_store_id, + "webCheckoutDetails": { + "checkoutMode": "ProcessOrder", + "checkoutResultReturnUrl": result_url, + "checkoutCancelUrl": cancel_url, + }, + "paymentDetails": { + "paymentIntent": "AuthorizeWithCapture", + "chargeAmount": { + "amount": self._amazon_pay_format_amount(tx.amount), + "currencyCode": tx.currency_id.name, + }, + }, + "chargePermissionType": "OneTime", + "merchantMetadata": { + "merchantReferenceId": tx.reference, + "merchantStoreName": (self.company_id.name or self.name or "Store")[ + :50 + ], + "customInformation": tx.reference, + }, + "productType": "PayOnly", + "scopes": ["name", "email", "phoneNumber", "billingAddress"], + } + if self.amazon_pay_soft_descriptor: + payload["paymentDetails"]["softDescriptor"] = ( + self.amazon_pay_soft_descriptor[:16] + ) + return payload + + def _amazon_pay_get_button_payload(self, tx): + """Return config dict for the Amazon Pay Button JS widget. + + Uses AMZN-PAY-RSASSA-PSS (salt=20) as required by the button SDK, + distinct from the API request signing (V2, salt=32). + """ + self.ensure_one() + tx.ensure_one() + payload = self._amazon_pay_prepare_checkout_payload(tx) + payload_json = json.dumps(payload, separators=(",", ":"), ensure_ascii=False) + payload_hash = hashlib.sha256(payload_json.encode()).hexdigest() + string_to_sign = f"AMZN-PAY-RSASSA-PSS\n{payload_hash}" + private_key = serialization.load_pem_private_key( + self._amazon_pay_normalize_private_key(self.amazon_pay_private_key).encode( + "utf-8" + ), + password=None, + ) + signature = private_key.sign( + string_to_sign.encode("utf-8"), + padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=20), + hashes.SHA256(), + ) + checkout_js_url = { + "eu": "https://static-eu.payments-amazon.com/checkout.js", + "na": "https://static-na.payments-amazon.com/checkout.js", + "jp": "https://static-fe.payments-amazon.com/checkout.js", + }[self.amazon_pay_region] + return { + "checkout_js_url": checkout_js_url, + "merchant_id": self.amazon_pay_merchant_id, + "public_key_id": self.amazon_pay_public_key_id, + "ledger_currency": tx.currency_id.name, + "checkout_language": self.amazon_pay_checkout_language or "en_US", + "sandbox": self.state == "test", + "payload_json": payload_json, + "signature": base64.b64encode(signature).decode(), + } + + def _amazon_pay_create_checkout_session(self, tx): + self.ensure_one() + return self._amazon_pay_request( + "POST", + "checkoutSessions", + payload=self._amazon_pay_prepare_checkout_payload(tx), + idempotency_key=uuid.uuid4().hex, + ) + + def _amazon_pay_complete_checkout_session(self, tx, checkout_session_id): + self.ensure_one() + tx.ensure_one() + payload = { + "chargeAmount": { + "amount": self._amazon_pay_format_amount(tx.amount), + "currencyCode": tx.currency_id.name, + }, + } + if self.amazon_pay_soft_descriptor: + payload["softDescriptor"] = self.amazon_pay_soft_descriptor[:16] + return self._amazon_pay_request( + "POST", + f"checkoutSessions/{checkout_session_id}/complete", + payload=payload, + idempotency_key=uuid.uuid4().hex, + ) + + def _amazon_pay_get_checkout_session(self, checkout_session_id): + self.ensure_one() + return self._amazon_pay_request( + "GET", f"checkoutSessions/{checkout_session_id}" + ) + + def _amazon_pay_get_charge(self, charge_id): + self.ensure_one() + return self._amazon_pay_request("GET", f"charges/{charge_id}") + + def _amazon_pay_get_refund(self, refund_id): + self.ensure_one() + return self._amazon_pay_request("GET", f"refunds/{refund_id}") + + def _amazon_pay_create_refund(self, charge_id, amount, currency): + self.ensure_one() + payload = { + "chargeId": charge_id, + "refundAmount": { + "amount": self._amazon_pay_format_amount(amount), + "currencyCode": currency.name, + }, + } + if self.amazon_pay_soft_descriptor: + payload["softDescriptor"] = self.amazon_pay_soft_descriptor[:16] + return self._amazon_pay_request( + "POST", + "refunds", + payload=payload, + idempotency_key=uuid.uuid4().hex, + ) diff --git a/payment_amazon_pay/models/payment_transaction.py b/payment_amazon_pay/models/payment_transaction.py new file mode 100644 index 00000000000..56403738eae --- /dev/null +++ b/payment_amazon_pay/models/payment_transaction.py @@ -0,0 +1,182 @@ +import logging + +from odoo import fields, models +from odoo.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class PaymentTransaction(models.Model): + _inherit = "payment.transaction" + + amazon_pay_checkout_session_id = fields.Char(readonly=True) + amazon_pay_charge_permission_id = fields.Char(readonly=True) + amazon_pay_charge_id = fields.Char(readonly=True) + amazon_pay_refund_id = fields.Char(readonly=True) + + def _get_specific_processing_values(self, processing_values): + res = super()._get_specific_processing_values(processing_values) + if self.provider_code != "amazon_pay": + return res + + btn = self.provider_id._amazon_pay_get_button_payload(self) + return { + **res, + "checkout_js_url": btn["checkout_js_url"], + "amazon_pay_cfg": { + "merchantId": btn["merchant_id"], + "publicKeyId": btn["public_key_id"], + "ledgerCurrency": btn["ledger_currency"], + "checkoutLanguage": btn["checkout_language"], + "productType": "PayOnly", + "placement": "Checkout", + "buttonColor": "Gold", + "sandbox": btn["sandbox"], + "createCheckoutSessionConfig": { + "payloadJSON": btn["payload_json"], + "signature": btn["signature"], + "publicKeyId": btn["public_key_id"], + }, + }, + } + + def _get_tx_from_notification_data(self, provider_code, notification_data): + tx = super()._get_tx_from_notification_data(provider_code, notification_data) + if provider_code != "amazon_pay" or len(tx) == 1: + return tx + + reference = notification_data.get( + "merchantReferenceId" + ) or notification_data.get("reference") + checkout_session_id = notification_data.get("checkoutSessionId") + charge_id = notification_data.get("chargeId") + domain = [("provider_code", "=", "amazon_pay")] + if reference: + domain.append(("reference", "=", reference)) + elif checkout_session_id: + domain.append(("amazon_pay_checkout_session_id", "=", checkout_session_id)) + elif charge_id: + domain.append(("amazon_pay_charge_id", "=", charge_id)) + tx = self.search(domain, limit=1) + if not tx: + raise ValidationError( + self.env._("Amazon Pay: no transaction found for the received data.") + ) + return tx + + def _process_notification_data(self, notification_data): + super()._process_notification_data(notification_data) + if self.provider_code != "amazon_pay": + return + + # No status data yet: this is a return URL callback, complete the session + if not notification_data.get("statusDetails") and not notification_data.get( + "status" + ): + checkout_session_id = ( + notification_data.get("checkoutSessionId") + or self.amazon_pay_checkout_session_id + ) + if not checkout_session_id: + self._set_error( + self.env._("Amazon Pay did not return a checkout session id.") + ) + return + try: + data = self.provider_id._amazon_pay_complete_checkout_session( + self, checkout_session_id + ) + except Exception: + _logger.exception( + "Amazon Pay completion failed for transaction %s", self.reference + ) + try: + data = self.provider_id._amazon_pay_get_checkout_session( + checkout_session_id + ) + except Exception: + _logger.exception( + "Amazon Pay: failed to get checkout session %s", + checkout_session_id, + ) + self._set_error( + self.env._("Amazon Pay: could not retrieve the payment status.") + ) + return + notification_data.update(data) + + self.amazon_pay_checkout_session_id = ( + notification_data.get("checkoutSessionId") + or self.amazon_pay_checkout_session_id + ) + self.amazon_pay_charge_permission_id = ( + notification_data.get("chargePermissionId") + or self.amazon_pay_charge_permission_id + ) + self.amazon_pay_charge_id = ( + notification_data.get("chargeId") or self.amazon_pay_charge_id + ) + self.provider_reference = ( + self.amazon_pay_charge_id + or self.amazon_pay_charge_permission_id + or self.amazon_pay_checkout_session_id + ) + + status_details = notification_data.get("statusDetails") or {} + state = status_details.get("state") or notification_data.get("status") + reason = ( + status_details.get("reasonDescription") + or status_details.get("reasonCode") + or notification_data.get("reasonDescription") + or notification_data.get("reasonCode") + ) + http_status = notification_data.get("_http_status") + + if state == "Completed": + self._set_done() + elif state in ("AuthorizationInitiated", "CaptureInitiated", "Pending", "Open"): + self._set_pending(state_message=reason) + elif state in ("Canceled", "Cancelled"): + self._set_canceled(state_message=reason) + elif state in ("Declined", "Closed"): + self._set_error(reason or self.env._("Amazon Pay declined the payment.")) + elif http_status in (200, 202) and self.amazon_pay_charge_id: + if http_status == 202: + self._set_pending(state_message=reason) + else: + self._set_done() + else: + self._set_error( + reason + or self.env._("Amazon Pay returned an unexpected state: %s") + % (state or "unknown") + ) + + def _send_refund_request(self, amount_to_refund=None): + if self.provider_code != "amazon_pay": + return super()._send_refund_request(amount_to_refund=amount_to_refund) + + source_tx = self.source_transaction_id + if not source_tx or not source_tx.amazon_pay_charge_id: + raise ValidationError( + self.env._("Amazon Pay charge ID is missing on the source transaction.") + ) + + response = self.provider_id._amazon_pay_create_refund( + source_tx.amazon_pay_charge_id, + abs(amount_to_refund or self.amount), + self.currency_id, + ) + self.amazon_pay_refund_id = response.get("refundId") + self.provider_reference = self.amazon_pay_refund_id + status = (response.get("statusDetails") or {}).get("state") or response.get( + "status" + ) + if status in ("RefundInitiated", "Pending"): + self._set_pending(state_message=status) + elif status in ("Completed", "Refunded"): + self._set_done() + else: + self._set_error( + status or self.env._("Amazon Pay did not accept the refund request.") + ) diff --git a/payment_amazon_pay/pyproject.toml b/payment_amazon_pay/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/payment_amazon_pay/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/payment_amazon_pay/readme/CONTRIBUTORS.md b/payment_amazon_pay/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..4aa47fca43d --- /dev/null +++ b/payment_amazon_pay/readme/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +- [Tecnativa](https://www.tecnativa.com): + - Pilar Vargas + - Cristina Hidalgo diff --git a/payment_amazon_pay/readme/DESCRIPTION.md b/payment_amazon_pay/readme/DESCRIPTION.md new file mode 100644 index 00000000000..077af7e6d27 --- /dev/null +++ b/payment_amazon_pay/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +Payment provider integration for [Amazon Pay +v2](https://developer.amazon.com/docs/amazon-pay-api-v2/intro.html). + +Supports the EU, NA, and JP regions. Uses the Amazon Pay v2 API (RSA-PSS +signed requests) for checkout session creation, completion, and refunds. +Handles IPN notifications via a dedicated webhook endpoint. diff --git a/payment_amazon_pay/static/description/index.html b/payment_amazon_pay/static/description/index.html new file mode 100644 index 00000000000..15785ce51dd --- /dev/null +++ b/payment_amazon_pay/static/description/index.html @@ -0,0 +1,431 @@ + + + + + +Amazon Pay + + + +
+

Amazon Pay

+ + +

Beta License: AGPL-3 OCA/account-payment Translate me on Weblate Try me on Runboat

+

Payment provider integration for Amazon Pay +v2.

+

Supports the EU, NA, and JP regions. Uses the Amazon Pay v2 API (RSA-PSS +signed requests) for checkout session creation, completion, and refunds. +Handles IPN notifications via a dedicated webhook endpoint.

+

Table of contents

+ +
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Tecnativa
  • +
+
+
+

Contributors

+
    +
  • Tecnativa:
      +
    • Pilar Vargas
    • +
    • Cristina Hidalgo
    • +
    +
  • +
+
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/account-payment project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/payment_amazon_pay/static/img/amazon_pay.png b/payment_amazon_pay/static/img/amazon_pay.png new file mode 100644 index 0000000000000000000000000000000000000000..7d52f4aaed1e8db1b628b5104779b8a3bd5f2ea7 GIT binary patch literal 4165 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 P00000NkvXXu0mjf4kG^q literal 0 HcmV?d00001 diff --git a/payment_amazon_pay/static/src/css/amazon_pay_checkout.css b/payment_amazon_pay/static/src/css/amazon_pay_checkout.css new file mode 100644 index 00000000000..a3678912b54 --- /dev/null +++ b/payment_amazon_pay/static/src/css/amazon_pay_checkout.css @@ -0,0 +1,24 @@ +body { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + margin: 0; + font-family: sans-serif; + background: #f5f5f5; +} + +.o_amazon_pay_box { + background: white; + padding: 40px; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + text-align: center; + max-width: 400px; +} + +.o_amazon_pay_box p { + color: #555; + margin-bottom: 24px; +} diff --git a/payment_amazon_pay/static/src/js/amazon_pay_checkout.js b/payment_amazon_pay/static/src/js/amazon_pay_checkout.js new file mode 100644 index 00000000000..2b36b61754f --- /dev/null +++ b/payment_amazon_pay/static/src/js/amazon_pay_checkout.js @@ -0,0 +1,15 @@ +/* global amazon, window */ +(function () { + "use strict"; + var cfg = JSON.parse(window.document.getElementById("amazon-pay-cfg").textContent); + function render() { + amazon.Pay.renderButton("#o_amazon_pay_button", cfg); + } + if (typeof amazon === "undefined") { + window.document + .getElementById("o_amazon_pay_sdk") + .addEventListener("load", render); + } else { + render(); + } +})(); diff --git a/payment_amazon_pay/static/src/js/payment_form.js b/payment_amazon_pay/static/src/js/payment_form.js new file mode 100644 index 00000000000..6a289b0f506 --- /dev/null +++ b/payment_amazon_pay/static/src/js/payment_form.js @@ -0,0 +1,164 @@ +/* global document, window */ +/** @odoo-module */ + +import "@payment/js/payment_form"; +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.PaymentForm.include({ + _amazonPayLoadPromise: null, + _amazonPayHidden: false, + + async start() { + await this._super(...arguments); + + // Skip auto-load when inside an iframe (e.g. website editor preview) to + // avoid navigating the preview frame to Amazon's domain on credential errors. + if (window.self !== window.top) return; + + const amazonRadio = this.el.querySelector( + 'input[name="o_payment_radio"][data-provider-code="amazon_pay"]' + ); + if (!amazonRadio) return; + + if (amazonRadio.checked) { + await this._selectPaymentOption({target: amazonRadio}); + } else { + this._amazonPayLoadPromise = this._loadAmazonPayButton(amazonRadio, true); + } + }, + + async _selectPaymentOption(ev) { + const checkedRadio = ev.target; + if (this._getProviderCode(checkedRadio) !== "amazon_pay") { + this._getAmazonWrapper()?.remove(); + const submitBtn = this._getSubmitBtn(); + if (submitBtn) submitBtn.classList.remove("d-none"); + this._amazonPayLoadPromise = null; + await this._super(...arguments); + return; + } + + this._collapseInlineForms(); + // Prevent website_sale_charge_payment_fee from navigating away: that + // module redirects when the selected provider changes, but Amazon Pay + // renders its button inline and must stay on this page. + this.$selectedProvider = checkedRadio; + const submitBtn = this._getSubmitBtn(); + if (submitBtn) submitBtn.classList.add("d-none"); + + const existingWrapper = this._getAmazonWrapper(); + if (existingWrapper && this._amazonPayLoadPromise === null) { + existingWrapper.style.display = ""; + return; + } + + if (this._amazonPayLoadPromise !== null) { + await this._amazonPayLoadPromise; + this._amazonPayLoadPromise = null; + const wrapper = this._getAmazonWrapper(); + if (wrapper) { + wrapper.style.display = ""; + return; + } + } + + this._renderAmazonPayPlaceholder(submitBtn); + await this._loadAmazonPayButton(checkedRadio, false); + }, + + async _loadAmazonPayButton(radio, hidden) { + this._amazonPayHidden = hidden; + this.paymentContext.flow = "redirect"; + this.paymentContext.paymentOptionId = this._getPaymentOptionId(radio); + this.paymentContext.providerCode = "amazon_pay"; + this.paymentContext.paymentMethodCode = this._getPaymentMethodCode(radio); + this.paymentContext.providerId = this._getProviderId(radio); + this.paymentContext.paymentMethodId = this._getPaymentOptionId(radio); + this.paymentContext.tokenizationRequested = false; + + try { + await this._initiatePaymentFlow( + "amazon_pay", + this.paymentContext.paymentOptionId, + this.paymentContext.paymentMethodCode, + "redirect" + ); + } catch { + this._getAmazonWrapper()?.remove(); + if (!hidden) { + const submitBtn = this._getSubmitBtn(); + if (submitBtn) submitBtn.classList.remove("d-none"); + } + } + }, + + _renderAmazonPayPlaceholder(submitBtn) { + this._getAmazonWrapper()?.remove(); + const placeholder = document.createElement("div"); + placeholder.id = "o_amazon_pay_inline"; + placeholder.className = + "w-100 d-flex justify-content-center align-items-center py-3"; + placeholder.innerHTML = + ''; + if (submitBtn?.parentElement) { + submitBtn.parentElement.insertBefore(placeholder, submitBtn); + } + }, + + _processRedirectFlow( + providerCode, + _paymentOptionId, + _paymentMethodCode, + processingValues + ) { + if (providerCode !== "amazon_pay") { + return this._super(...arguments); + } + + const cfg = processingValues.amazon_pay_cfg; + const sdkUrl = processingValues.checkout_js_url; + + this._getAmazonWrapper()?.remove(); + + if (!cfg || !sdkUrl) { + if (!this._amazonPayHidden) this._showInputs(); + return; + } + + const wrapper = document.createElement("div"); + wrapper.id = "o_amazon_pay_inline"; + wrapper.className = "w-100"; + if (this._amazonPayHidden) wrapper.style.display = "none"; + wrapper.innerHTML = '
'; + + const submitBtn = this._getSubmitBtn(); + if (submitBtn?.parentElement) { + submitBtn.parentElement.insertBefore(wrapper, submitBtn); + } else { + (document.querySelector("#o_payment_form") || document.body).appendChild( + wrapper + ); + } + + const renderBtn = () => { + // eslint-disable-next-line no-undef + amazon.Pay.renderButton("#o_amazon_pay_button", cfg); + }; + if (document.querySelector(`script[src="${sdkUrl}"]`)) { + renderBtn(); + } else { + const script = document.createElement("script"); + script.src = sdkUrl; + script.onload = renderBtn; + document.head.appendChild(script); + } + }, + + _getSubmitBtn() { + return document.querySelector('button[name="o_payment_submit_button"]'); + }, + + _getAmazonWrapper() { + return document.getElementById("o_amazon_pay_inline"); + }, +}); diff --git a/payment_amazon_pay/views/payment_amazon_pay_templates.xml b/payment_amazon_pay/views/payment_amazon_pay_templates.xml new file mode 100644 index 00000000000..0bbb5a44d78 --- /dev/null +++ b/payment_amazon_pay/views/payment_amazon_pay_templates.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/payment_amazon_pay/views/payment_provider_views.xml b/payment_amazon_pay/views/payment_provider_views.xml new file mode 100644 index 00000000000..8fcec2b4eee --- /dev/null +++ b/payment_amazon_pay/views/payment_provider_views.xml @@ -0,0 +1,41 @@ + + + + payment.provider.form.amazon.pay + payment.provider + + + + + + + + + + + + +
+ This provider uses Amazon Pay Checkout v2 in PayOnly mode. +
+
+
+
+
+
diff --git a/requirements.txt b/requirements.txt index 27f28b32e74..3fe57660013 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ # generated from manifests external_dependencies +cryptography num2words +requests From 83364af01fc40a5bbdac3dfced2769d1a9513c90 Mon Sep 17 00:00:00 2001 From: juancarlosonate-tecnativa Date: Mon, 29 Jun 2026 17:04:31 +0200 Subject: [PATCH 2/2] [FIX] payment_amazon_pay: don't override the shared payment flow on button preload TT63477 --- payment_amazon_pay/static/src/js/payment_form.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/payment_amazon_pay/static/src/js/payment_form.js b/payment_amazon_pay/static/src/js/payment_form.js index 6a289b0f506..d457c81088a 100644 --- a/payment_amazon_pay/static/src/js/payment_form.js +++ b/payment_amazon_pay/static/src/js/payment_form.js @@ -68,6 +68,7 @@ publicWidget.registry.PaymentForm.include({ async _loadAmazonPayButton(radio, hidden) { this._amazonPayHidden = hidden; + const previousFlow = this.paymentContext.flow; this.paymentContext.flow = "redirect"; this.paymentContext.paymentOptionId = this._getPaymentOptionId(radio); this.paymentContext.providerCode = "amazon_pay"; @@ -77,12 +78,14 @@ publicWidget.registry.PaymentForm.include({ this.paymentContext.tokenizationRequested = false; try { - await this._initiatePaymentFlow( + const flowPromise = this._initiatePaymentFlow( "amazon_pay", this.paymentContext.paymentOptionId, this.paymentContext.paymentMethodCode, "redirect" ); + this.paymentContext.flow = previousFlow; + await flowPromise; } catch { this._getAmazonWrapper()?.remove(); if (!hidden) {