diff --git a/pos_financial_surcharge/README.md b/pos_financial_surcharge/README.md
new file mode 100644
index 0000000..3881a98
--- /dev/null
+++ b/pos_financial_surcharge/README.md
@@ -0,0 +1,89 @@
+# Módulo: POS - Recargo Financiero por Tarjeta
+
+Este módulo permite aplicar recargos financieros personalizados según el plan de cuotas elegido por el cliente al pagar con tarjeta en el Punto de Venta (POS) de Odoo 18. Es especialmente útil para operaciones con tarjetas de crédito donde el comercio asume un interés bancario y desea trasladarlo al consumidor.
+
+## Características Principales
+
+- Permite asociar productos de recargo a métodos de pago POS.
+- Soporte para múltiples tarjetas y planes de cuotas (cuotas, coeficiente de recargo, descuentos bancarios).
+- Muestra un popup en el POS para seleccionar tarjeta, lote, cupón y plan de financiación.
+- Calcula y agrega automáticamente el recargo como una línea adicional en el pedido.
+- Configuración visual por método de pago para definir qué tarjetas están permitidas.
+- Validaciones de productos sin impuestos en los recargos.
+
+---
+
+## Instalación
+
+1. Clonar este módulo en el directorio de addons de tu instancia de Odoo:
+ ```bash
+ git clone https://github.com/filoquin/pos_payment.git
+ ```
+
+2. Activar el modo desarrollador en Odoo y habilitar el módulo.
+
+---
+
+## Configuración
+
+### 1. Productos de Recargo
+- Crear un producto tipo "Servicio" con `Disponible en POS` y sin impuestos y para argentina IVA 0% si se factura.
+- Este producto se usará para cargar el importe adicional del plan de financiación.
+
+### 2. Métodos de Pago POS
+- Ir a **Punto de Venta → Configuración → Métodos de pago**.
+- Seleccionar un método de tipo `Terminal` e integrar con `Card financial surcharge`.
+- Asignar:
+ - **Producto de recargo financiero**
+ - **Tarjetas permitidas en POS**
+
+### 3. Tarjetas y Cuotas
+- Crear tarjetas en **Contabilidad → Configuración → Tarjetas**.
+- Asociar planes de cuotas a cada tarjeta con:
+ - Cantidad de cuotas
+ - Coeficiente de recargo
+ - Descuento bancario (opcional)
+
+---
+
+## Uso en el Punto de Venta
+
+1. Al seleccionar un método de pago configurado con recargo, se abre automáticamente un popup.
+2. El usuario debe seleccionar:
+ - Tarjeta
+ - Plan de cuotas
+3. Se calcula el total ajustado y se agrega una línea de recargo si corresponde.
+4. Se guarda una nota de cliente con los datos de la operación.
+
+---
+
+## Detalles Técnicos
+
+- **Modelos Extendidos:**
+ - `pos.payment.method`: Añade campo `bank_charge_prod_id` y `available_cards_ids`.
+ - `account.card`: Filtrado en POS según método de pago.
+ - `account.card.installment`: Cargado como dependencia POS.
+- **Frontend:**
+ - Reemplaza el flujo de pago estándar con un `PaymentInterface` personalizado.
+ - Incluye popup interactivo con OWL.
+- **Integración POS:**
+ - Declaración con `register_payment_method("financial_surcharge", ...)`.
+
+---
+
+## Compatibilidad
+
+- Odoo 18 (Tested)
+- Compatible con POS Web y POS Touch
+- Modo multi-tienda compatible
+
+---
+
+## Créditos
+
+Desarrollado por: Martín Quinteros (Filoquin), Francisco Sulé.
+Especialista funcional y técnico en Odoo para Argentina 🇦🇷
+
+---
+
+
diff --git a/pos_financial_surcharge/__init__.py b/pos_financial_surcharge/__init__.py
new file mode 100644
index 0000000..aee8895
--- /dev/null
+++ b/pos_financial_surcharge/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizards
diff --git a/pos_financial_surcharge/__manifest__.py b/pos_financial_surcharge/__manifest__.py
new file mode 100644
index 0000000..a210c31
--- /dev/null
+++ b/pos_financial_surcharge/__manifest__.py
@@ -0,0 +1,21 @@
+{
+ 'name': 'Pos Financial Surchage',
+ 'version': "18.0.1.0.0",
+ 'category': 'Sales/Point of Sale',
+ 'sequence': 6,
+ 'summary': 'Add pos finanacial surcharge',
+ 'data': [
+ 'views/card_installment_view.xml',
+ 'views/pos_payment_method.xml',
+ 'wizards/res_config_settings_views.xml',
+ ],
+ 'depends': ['point_of_sale', 'card_installment'],
+ 'installable': True,
+ 'assets': {
+ 'point_of_sale._assets_pos': [
+ 'pos_financial_surcharge/static/src/**/*',
+ 'pos_financial_surcharge/static/src/**/**/*',
+ ],
+ },
+ 'license': 'LGPL-3',
+}
diff --git a/pos_financial_surcharge/models/__init__.py b/pos_financial_surcharge/models/__init__.py
new file mode 100644
index 0000000..3867e85
--- /dev/null
+++ b/pos_financial_surcharge/models/__init__.py
@@ -0,0 +1,5 @@
+from . import account_card
+from . import account_card_installment
+from . import pos_session
+from . import pos_payment_method
+from . import res_company
diff --git a/pos_financial_surcharge/models/account_card.py b/pos_financial_surcharge/models/account_card.py
new file mode 100644
index 0000000..558d72c
--- /dev/null
+++ b/pos_financial_surcharge/models/account_card.py
@@ -0,0 +1,30 @@
+from odoo import models, api, fields
+
+
+class AccountCard(models.Model):
+ _inherit = "account.card"
+
+ available_in_pos = fields.Boolean(
+ string='Available in POS',
+ help='Check if you want this card can be used in the Point of Sale.',
+ default=False
+ )
+
+ @api.model
+ def _load_pos_data_domain(self, data):
+ return self.env['account.card']._check_company_domain(data['pos.config']['data'][0]['company_id'])
+ #+ [('available_in_pos', '=', True)]
+
+ @api.model
+ def _load_pos_data_fields(self, config_id):
+ return [
+ 'id', 'name', 'installment_ids'
+ ]
+
+ def _load_pos_data(self, data):
+ domain = self._load_pos_data_domain(data)
+ fields = self._load_pos_data_fields(data['pos.config']['data'][0]['id'])
+ return {
+ 'data': self.search_read(domain, fields, load=False) if domain is not False else [],
+ 'fields': fields,
+ }
diff --git a/pos_financial_surcharge/models/account_card_installment.py b/pos_financial_surcharge/models/account_card_installment.py
new file mode 100644
index 0000000..f01858e
--- /dev/null
+++ b/pos_financial_surcharge/models/account_card_installment.py
@@ -0,0 +1,24 @@
+from odoo import models, api
+
+
+class AccountCard(models.Model):
+ _inherit = "account.card.installment"
+
+ @api.model
+ def _load_pos_data_domain(self, data):
+ #return self.env['account.card']._check_company_domain(data['pos.config']['data'][0]['company_id'])
+ return []
+
+ @api.model
+ def _load_pos_data_fields(self, config_id):
+ return [
+ 'id', 'card_id', 'name', 'divisor', 'installment', 'surcharge_coefficient', 'bank_discount'
+ ]
+
+ def _load_pos_data(self, data):
+ domain = self._load_pos_data_domain(data)
+ fields = self._load_pos_data_fields(data['pos.config']['data'][0]['id'])
+ return {
+ 'data': self.search_read(domain, fields, load=False) if domain is not False else [],
+ 'fields': fields,
+ }
diff --git a/pos_financial_surcharge/models/pos_payment_method.py b/pos_financial_surcharge/models/pos_payment_method.py
new file mode 100644
index 0000000..f088d72
--- /dev/null
+++ b/pos_financial_surcharge/models/pos_payment_method.py
@@ -0,0 +1,21 @@
+import logging
+
+from odoo import models, fields, api
+
+_logger = logging.getLogger(__name__)
+
+
+class PosPaymentMethod(models.Model):
+ _inherit = 'pos.payment.method'
+
+ available_card_ids = fields.Many2many(
+ "account.card", string="Cards",
+ )
+
+
+ def _get_payment_terminal_selection(self):
+ return super()._get_payment_terminal_selection() + [('financial_surcharge', 'Card financial surcharge')]
+
+ @api.model
+ def _load_pos_data_fields(self, config_id):
+ return super()._load_pos_data_fields(config_id) + ['available_card_ids']
\ No newline at end of file
diff --git a/pos_financial_surcharge/models/pos_session.py b/pos_financial_surcharge/models/pos_session.py
new file mode 100644
index 0000000..46f9b1b
--- /dev/null
+++ b/pos_financial_surcharge/models/pos_session.py
@@ -0,0 +1,10 @@
+from odoo import api, fields, models, _, Command
+from odoo.exceptions import AccessDenied, AccessError, UserError, ValidationError
+
+
+class PosSession(models.Model):
+ _inherit = 'pos.session'
+
+ @api.model
+ def _load_pos_data_models(self, config_id):
+ return super()._load_pos_data_models(config_id) + ['account.card', 'account.card.installment']
diff --git a/pos_financial_surcharge/models/res_company.py b/pos_financial_surcharge/models/res_company.py
new file mode 100644
index 0000000..1b5f4bb
--- /dev/null
+++ b/pos_financial_surcharge/models/res_company.py
@@ -0,0 +1,9 @@
+from odoo import api, models
+
+
+class ResCompany(models.Model):
+ _inherit = "res.company"
+
+ @api.model
+ def _load_pos_data_fields(self, config_id):
+ return super()._load_pos_data_fields(config_id) + ['product_surcharge_id']
diff --git a/pos_financial_surcharge/static/src/app/financial_surcharge.js b/pos_financial_surcharge/static/src/app/financial_surcharge.js
new file mode 100644
index 0000000..03fa96d
--- /dev/null
+++ b/pos_financial_surcharge/static/src/app/financial_surcharge.js
@@ -0,0 +1,77 @@
+/** @odoo-module */
+import { _t } from "@web/core/l10n/translation";
+import { PaymentInterface } from "@point_of_sale/app/payment/payment_interface";
+import { ask } from "@point_of_sale/app/store/make_awaitable_dialog";
+import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
+import { FinancialSurchargePopup } from "@pos_financial_surcharge/app/financial_surcharge_popup/financial_surcharge_popup";
+
+
+export class FinancialSurcharge extends PaymentInterface {
+
+ async _get_cards(){
+ const res = []
+ const installment_ids = this.pos.models['account.card.installment'].getAll()
+ const available_card_ids = this.payment_method_id.available_card_ids.map((card)=> card.id);
+ for (const card of this.pos.models['account.card'].getAll().filter((card) =>{
+ return available_card_ids.includes(card.id);
+ })
+ ) {
+ res.push({
+ id: card.id,
+ name: card.name,
+ installments: installment_ids.filter((installment) => {
+ return installment.card_id.id == card.id;
+ }).map((installment) => {
+ return {
+ id: installment.id,
+ name: installment.name,
+ divisor: installment.divisor,
+ installment: installment.installment,
+ surcharge_coefficient: installment.surcharge_coefficient,
+ bank_discount: installment.bank_discount,
+ };
+ }),
+ })
+ }
+ return res;
+ }
+ async send_payment_request(cid) {
+ await super.send_payment_request(...arguments);
+ const line = this.pos.get_order().get_selected_paymentline();
+ try {
+ // During payment creation, user can't cancel the payment intent
+ line.set_payment_status("waitingCapture");
+ return await ask(
+ this.env.services.dialog,
+ {
+ title: 'Select payment intallment ',
+ line: line,
+ cards: await this._get_cards(),
+ order: line.pos_order_id,
+ pos: this.pos,
+ },
+ {},
+ FinancialSurchargePopup
+ ).then((result) => {
+ return result;
+ });
+
+ } catch (error) {
+ console.error(error);
+ this._showMsg('error', "System error");
+ return false;
+ }
+ }
+
+ async send_payment_cancel(order, cid) {
+ await super.send_payment_cancel(order, cid);
+ return true;
+ }
+ // private methods
+ _showMsg(msg, title) {
+ this.env.services.dialog.add(AlertDialog, {
+ title: "Error " + title,
+ body: msg,
+ });
+ }
+}
\ No newline at end of file
diff --git a/pos_financial_surcharge/static/src/app/financial_surcharge_popup/financial_surcharge_popup.js b/pos_financial_surcharge/static/src/app/financial_surcharge_popup/financial_surcharge_popup.js
new file mode 100644
index 0000000..b71b326
--- /dev/null
+++ b/pos_financial_surcharge/static/src/app/financial_surcharge_popup/financial_surcharge_popup.js
@@ -0,0 +1,118 @@
+import { useState } from "@odoo/owl";
+import { ConfirmationDialog, AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
+import { compute_price_force_price_include } from "@point_of_sale/app/models/utils/tax_utils";
+import { _t } from "@web/core/l10n/translation";
+
+export class FinancialSurchargePopup extends ConfirmationDialog {
+ static template = "point_of_sale.FinancialSurchargeConfirmationDialog";
+ static props = {
+ ...ConfirmationDialog.props,
+ line: Object,
+ order: Object,
+ cards: Object,
+ pos: Object,
+ };
+
+ // Función para crear la nota del cliente
+ _createCustomerNote(installment) {
+ const card_name = installment.card_id?.name || "Tarjeta desconocida";
+ const installment_name = installment.name;
+
+ return `Tarjeta: ${card_name}\nCuotas: ${installment_name}`;
+ }
+
+ async _confirm() {
+ if (!this.state.selected_installment) {
+ this._showMsg(_t("You must select an installment"), _t("Error"));
+ return false;
+ }
+
+ const instalments = this.props.line.models['account.card.installment'].getAllBy('id');
+ const surcharge_coefficient = instalments[this.state.selected_installment].surcharge_coefficient;
+ const diff_amount = (this.state.raw_amount * surcharge_coefficient) - this.state.raw_amount;
+
+ // Si no hay diferencia (es decir, no aplica recargo), finalizar el proceso y agregar la nota
+ if (!diff_amount) {
+ this.props.line.set_payment_status("done");
+ await this._addNoteToLastLine(); // Asegurarse de agregar la nota al último producto
+ return this.execButton(this.props.confirm);
+ }
+
+ const product_surcharge_id = this.props.pos.company.product_surcharge_id;
+ const new_price = compute_price_force_price_include(
+ product_surcharge_id.taxes_id,
+ diff_amount,
+ product_surcharge_id,
+ this.props.pos.config._product_default_values,
+ this.props.pos.company,
+ this.props.pos.currency,
+ this.props.pos.models
+ );
+
+ const new_line = await this.props.pos.addLineToCurrentOrder({
+ product_id: this.props.pos.company.product_surcharge_id.id,
+ qty: 1,
+ price_unit: new_price,
+ note: instalments[this.state.selected_installment].name,
+ });
+
+ // Actualizamos el monto de la línea
+ this.props.line.amount = this.state.raw_amount + new_line.price_subtotal_incl;
+ this.props.line.set_payment_status("done");
+
+ // Agregamos la nota al último producto de la orden
+ await this._addNoteToLastLine(); // Asegurarse de agregar la nota al último producto
+
+ return this.execButton(this.props.confirm);
+ }
+
+ // Función para agregar la nota al último producto de la orden
+ async _addNoteToLastLine() {
+ // Obtenemos todas las líneas de la orden
+ const orderlines = this.props.order.get_orderlines();
+ const last_line = orderlines.at(-1); // Accedemos a la última línea de la orden
+
+ if (last_line) {
+ const instalments = this.props.line.models['account.card.installment'].getAllBy('id');
+ const installment = instalments[this.state.selected_installment];
+
+ // Creamos la nota del cliente
+ const customer_note = this._createCustomerNote(installment);
+
+ // Asignamos la nota al último producto de la orden
+ last_line.set_customer_note(customer_note);
+
+ }
+ }
+
+ static defaultProps = {
+ ...ConfirmationDialog.defaultProps,
+ confirmLabel: _t("Confirm Payment"),
+ cancelLabel: _t("Cancel Payment"),
+ title: _t("Card register"),
+ };
+
+ formatCurrency(amount) {
+ return this.env.utils.formatCurrency(amount);
+ }
+
+ setup() {
+ super.setup();
+ this.props.body = _t(" %s", this.props.title);
+ this.amount = this.env.utils.formatCurrency(this.props.line.amount);
+ this.raw_amount = this.props.line.amount;
+ this.cards = this.props.cards;
+ this.state = useState({
+ raw_amount: this.props.line.amount,
+ selected_installment: "",
+
+ });
+ }
+
+ _showMsg(msg, title) {
+ this.env.services.dialog.add(AlertDialog, {
+ title: "Error " + title,
+ body: msg,
+ });
+ }
+}
diff --git a/pos_financial_surcharge/static/src/app/financial_surcharge_popup/financial_surcharge_popup.xml b/pos_financial_surcharge/static/src/app/financial_surcharge_popup/financial_surcharge_popup.xml
new file mode 100644
index 0000000..1df8524
--- /dev/null
+++ b/pos_financial_surcharge/static/src/app/financial_surcharge_popup/financial_surcharge_popup.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+ Amount:
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pos_financial_surcharge/static/src/overrides/models.js b/pos_financial_surcharge/static/src/overrides/models.js
new file mode 100644
index 0000000..a44546a
--- /dev/null
+++ b/pos_financial_surcharge/static/src/overrides/models.js
@@ -0,0 +1,5 @@
+/** @odoo-module */
+import { register_payment_method } from "@point_of_sale/app/store/pos_store";
+import { FinancialSurcharge } from "@pos_financial_surcharge/app/financial_surcharge";
+
+register_payment_method("financial_surcharge", FinancialSurcharge);
diff --git a/pos_financial_surcharge/views/card_installment_view.xml b/pos_financial_surcharge/views/card_installment_view.xml
new file mode 100644
index 0000000..b8069de
--- /dev/null
+++ b/pos_financial_surcharge/views/card_installment_view.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/pos_financial_surcharge/views/pos_payment_method.xml b/pos_financial_surcharge/views/pos_payment_method.xml
new file mode 100644
index 0000000..8848576
--- /dev/null
+++ b/pos_financial_surcharge/views/pos_payment_method.xml
@@ -0,0 +1,14 @@
+
+
+
+ pos.payment.method.inherit.financial.surcharge
+ pos.payment.method
+
+
+
+
+
+
+
+
+
diff --git a/pos_financial_surcharge/wizards/__init__.py b/pos_financial_surcharge/wizards/__init__.py
new file mode 100644
index 0000000..0deb68c
--- /dev/null
+++ b/pos_financial_surcharge/wizards/__init__.py
@@ -0,0 +1 @@
+from . import res_config_settings
diff --git a/pos_financial_surcharge/wizards/res_config_settings.py b/pos_financial_surcharge/wizards/res_config_settings.py
new file mode 100644
index 0000000..81a57e1
--- /dev/null
+++ b/pos_financial_surcharge/wizards/res_config_settings.py
@@ -0,0 +1,10 @@
+from odoo import fields, models
+
+
+class ResConfigSettings(models.TransientModel):
+ _inherit = "res.config.settings"
+
+ product_surcharge_id = fields.Many2one(
+ related="company_id.product_surcharge_id",
+ readonly=False,
+ )
diff --git a/pos_financial_surcharge/wizards/res_config_settings_views.xml b/pos_financial_surcharge/wizards/res_config_settings_views.xml
new file mode 100644
index 0000000..8b50334
--- /dev/null
+++ b/pos_financial_surcharge/wizards/res_config_settings_views.xml
@@ -0,0 +1,20 @@
+
+
+
+ res.config.settings.form.inherit.account.payment
+ res.config.settings
+
+
+
+
+
+
+
+
+
+
diff --git a/pos_mercado_pago_qr/static/src/app/payment_mercado_pago_qr.js b/pos_mercado_pago_qr/static/src/app/payment_mercado_pago_qr.js
new file mode 100644
index 0000000..1a8c08d
--- /dev/null
+++ b/pos_mercado_pago_qr/static/src/app/payment_mercado_pago_qr.js
@@ -0,0 +1,205 @@
+/** @odoo-module */
+import { _t } from "@web/core/l10n/translation";
+import { PaymentInterface } from "@point_of_sale/app/payment/payment_interface";
+import { AlertDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
+
+export class PaymentMercadoPagoQR extends PaymentInterface {
+ async create_payment_qr() {
+ const order = this.pos.get_order();
+ const line = order.get_selected_paymentline();
+ // Build informations for creating a payment intend on Mercado Pago.
+ // Data in "external_reference" are send back with the webhook notification
+ let entropy = Date.now() + Math.random();
+ line.externalReference = `QR_${this.pos.pos_session.id}_${line.payment_method.id}_${line.cid}_${entropy}`;
+ const infos = {
+ title: order.name,
+ notification_url: base_url + '/pos_mercado_pago/notification',
+ description: this.pos.config.company_id[1],
+ total_amount: line.amount,
+ items: [{
+ sku_number: "0001",
+ category: "general",
+ title: this.pos.config.current_session_id[1] + "sale",
+ description: "odoo sale",
+ unit_price: line.amount,
+ quantity: 1,
+ unit_measure: "unit",
+ total_amount: line.amount,
+ }],
+ external_reference: line.externalReference
+ };
+
+ // mp_payment_order_create will call the Mercado Pago api
+ return await this.env.services.orm.silent.call(
+ "pos.payment.method",
+ "mp_payment_order_create",
+ [[line.payment_method_id.id], infos]
+ );
+ }
+
+ async mp_payment_order_get() {
+ const line = this.pos.get_order().get_selected_paymentline();
+ // mp_payment_intent_get will call the Mercado Pago api
+ return await this.env.services.orm.silent.call(
+ "pos.payment.method",
+ "mp_payment_order_get",
+ [[line.payment_method_id.id], line.externalReference]
+ );
+ }
+
+ async cancel_payment_order() {
+ const line = this.pos.get_order().get_selected_paymentline();
+ // mp_payment_order_cancel will call the Mercado Pago api
+ return await this.env.services.orm.silent.call(
+ "pos.payment.method",
+ "mp_payment_order_cancel",
+ [[line.payment_method_id.id], line.externalReference]
+ );
+ }
+
+ async get_payment(payment_id) {
+ const line = this.pos.get_order().get_selected_paymentline();
+ // mp_get_payment_status will call the Mercado Pago api
+ return await this.env.services.orm.silent.call(
+ "pos.payment.method",
+ "mp_get_payment_status",
+ [[line.payment_method_id.id], payment_id]
+ );
+ }
+
+ setup() {
+ super.setup(...arguments);
+ this.webhook_resolver = null;
+ this.payment_intent = {};
+ }
+
+ async send_payment_request(cid) {
+ await super.send_payment_request(...arguments);
+ const line = this.pos.get_order().get_selected_paymentline();
+ try {
+ // During payment creation, user can't cancel the payment intent
+ line.set_payment_status("waitingCapture");
+ // Call Mercado Pago to create a payment intent
+ const payment_order = await this.create_payment_qr();
+ if (! payment_order) {
+ this._showMsg(payment_order.message, "error");
+ return false;
+ }
+ // Payment intent creation successfull, save it
+ this.payment_order = payment_order;
+ // After payment creation, make the payment intent canceling possible
+ line.set_payment_status("waitingCard");
+ // Wait for payment intent status change and return status result
+ return await new Promise((resolve) => {
+ this.webhook_resolver = resolve;
+ });
+ } catch (error) {
+ this._showMsg(error, "System error");
+ return false;
+ }
+ }
+
+ async send_payment_cancel(order, cid) {
+ await super.send_payment_cancel(order, cid);
+ if (!("id" in this.payment_intent)) {
+ return true;
+ }
+ const canceling_status = await this.cancel_payment_intent();
+ if ("error" in canceling_status) {
+ const message =
+ canceling_status.status === 409
+ ? _t("Payment has to be canceled on terminal")
+ : _t("Payment not found (canceled/finished on terminal)");
+ this._showMsg(message, "info");
+ return canceling_status.status !== 409;
+ }
+ return true;
+ }
+
+ async handleMercadoPagoQRWebhook() {
+ const line = this.pos.get_order().get_selected_paymentline();
+ const MAX_RETRY = 5; // Maximum number of retries for the "ON_TERMINAL" BUG
+ const RETRY_DELAY = 1000; // Delay between retries in milliseconds for the "ON_TERMINAL" BUG
+
+ const showMessageAndResolve = (messageKey, status, resolverValue) => {
+ if (!resolverValue) {
+ this._showMsg(messageKey, status);
+ }
+ line.set_payment_status("done");
+ this.webhook_resolver?.(resolverValue);
+ return resolverValue;
+ };
+
+ const handleFinishedPayment = async (paymentIntent) => {
+ if (paymentIntent.state === "CANCELED") {
+ return showMessageAndResolve(_t("Payment has been canceled"), "info", false);
+ }
+ if (["FINISHED", "PROCESSED"].includes(paymentIntent.state)) {
+ const payment = await this.get_payment(paymentIntent.payment.id);
+ if (payment.status === "approved") {
+ return showMessageAndResolve(_t("Payment has been processed"), "info", true);
+ }
+ return showMessageAndResolve(_t("Payment has been rejected"), "info", false);
+ }
+ };
+
+ // No payment intent id means either that the user reload the page or
+ // it is an old webhook -> trash
+ if ("id" in this.payment_intent) {
+ // Call Mercado Pago to get the payment intent status
+ let last_status_payment_intent = await this.mp_payment_order_get();
+ // Bad payment intent id, then it's an old webhook not related with the
+ // current payment intent -> trash
+ if (this.payment_intent.id == last_status_payment_intent.id) {
+ if (
+ ["FINISHED", "PROCESSED", "CANCELED"].includes(last_status_payment_intent.state)
+ ) {
+ return await handleFinishedPayment(last_status_payment_intent);
+ }
+ // BUG Sometimes the Mercado Pago webhook return ON_TERMINAL
+ // instead of CANCELED/FINISHED when we requested a payment status
+ // that was actually canceled/finished by the user on the terminal.
+ // Then the strategy here is to ask Mercado Pago MAX_RETRY times the
+ // payment intent status, hoping going out of this status
+ if (["OPEN", "ON_TERMINAL"].includes(last_status_payment_intent.state)) {
+ return await new Promise((resolve) => {
+ let retry_cnt = 0;
+ const s = setInterval(async () => {
+ last_status_payment_intent =
+ await this.mp_payment_order_get();
+ if (
+ ["FINISHED", "PROCESSED", "CANCELED"].includes(
+ last_status_payment_intent.state
+ )
+ ) {
+ clearInterval(s);
+ resolve(await handleFinishedPayment(last_status_payment_intent));
+ }
+ retry_cnt += 1;
+ if (retry_cnt >= MAX_RETRY) {
+ clearInterval(s);
+ resolve(
+ showMessageAndResolve(
+ _t("Payment status could not be confirmed"),
+ "error",
+ false
+ )
+ );
+ }
+ }, RETRY_DELAY);
+ });
+ }
+ // If the state does not match any of the expected values
+ return showMessageAndResolve(_t("Unknown payment status"), "error", false);
+ }
+ }
+ }
+
+ // private methods
+ _showMsg(msg, title) {
+ this.env.services.dialog.add(AlertDialog, {
+ title: "Mercado Pago " + title,
+ body: msg,
+ });
+ }
+}
diff --git a/pos_mercado_pago_qr/static/src/app/pos_bus.js b/pos_mercado_pago_qr/static/src/app/pos_bus.js
deleted file mode 100644
index 381f21a..0000000
--- a/pos_mercado_pago_qr/static/src/app/pos_bus.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/** @odoo-module */
-import { patch } from "@web/core/utils/patch";
-import { PosBus } from "@point_of_sale/app/bus/pos_bus_service";
-
-patch(PosBus.prototype, {
- // Override
- dispatch(message) {
- super.dispatch(...arguments);
- if (message.type === "MERCADO_PAGO_LATEST_MESSAGE") {
- this.pos
- .getPendingPaymentLine("mercado_pago")
- .payment_method.payment_terminal.handleMercadoPagoWebhook();
- }
- },
-});
diff --git a/pos_mercado_pago_qr/static/src/app/pos_store.js b/pos_mercado_pago_qr/static/src/app/pos_store.js
new file mode 100644
index 0000000..7da6a4e
--- /dev/null
+++ b/pos_mercado_pago_qr/static/src/app/pos_store.js
@@ -0,0 +1,19 @@
+/** @odoo-module */
+import { patch } from "@web/core/utils/patch";
+import { PosStore } from "@point_of_sale/app/store/pos_store";
+
+patch(PosStore.prototype, {
+ // Override
+ async setup() {
+ await super.setup(...arguments);
+ this.data.connectWebSocket("MERCADO_PAGO_LATEST_MESSAGE", (payload) => {
+ if (payload.config_id === this.config.id) {
+ const pendingLine = this.getPendingPaymentLine("mercado_pago");
+
+ if (pendingLine) {
+ pendingLine.payment_method_id.payment_terminal.handleMercadoPagoQRWebhook();
+ }
+ }
+ });
+ },
+});