- Riesgo: bajo
- Tipo: nueva funcionalidad
-
Descripción:
Agrega las mejoras declaradas: https://www.trescloud.com/web#id=11598&cids=1&menu_id=955&action=459&active_id=1759&model=project.task&view_type=form
- Número Ticket/Tarea: #000000
- Riesgo: medio
- Tipo: nueva funcionalidad
-
Descripción:
Resumen
Se implementó la integración de pagos con Deuna en Odoo 18 en dos módulos siguiendo la convención de los providers nativos (Stripe / Xendit):
trescloud_payment_deuna— provider base: cliente API, webhook handler, flujo de checkout web, página de visualización del QR y helpers compartidos.trescloud_pos_deuna— integración POS sobre el provider base: UI para cashier, polling, flujo de reembolso, mirror al customer-facing display y enriquecimiento del memo deaccount.paymental cierre de sesión.
Ambos módulos consumen la Merchant API v2 de Deuna (
/payment/request,/payment/info,/payment/cancel,/payment/refund) y reciben los webhooks de confirmación en un endpoint público.
1. Configuración del provider
Se agrega un nuevo
payment.provider(code = "deuna") con API Key, API Secret,pointOfSale, URLs base (QA/PROD) y los settings de QR para el flujo web (tipo / formato / lifetime). La URL que se debe registrar en el panel de Deuna es:https://<dominio>/payment/deuna/webhook.
2. Configuración del método de pago POS
El
pos.payment.methodrecibe una nueva opción de terminalDeuna QRy los camposdeuna_provider_id,deuna_qr_type,deuna_pos_code,deuna_format,deuna_expired_timeydeuna_show_qr_on_customer_display. El campo de lifetime se muestra solo cuando aplica (dinámico sin NumericCode), porque es el único caso donde Deuna aceptaexpiredTime(los demás los rechaza con 409 o los ignora).
3. Diálogo del QR para el cashier
Cuando el cashier selecciona el método Deuna, el POS llama
/pos_deuna/request_qry abre un modal con:- Logos TresCloud × Deuna en cabecera con gradiente,
- Monto y referencia interna,
- QR con marco animado de gradiente, bloque de NumericCode si aplica, botón de deeplink,
- Tres dots animados como indicador de espera,
- Contador regresivo que refleja el lifetime real del QR (180 s para NumericCode, 15 min default para dinámico, 30 min cap para estático),
- Botón "Cancelar pago".
El contador se pone rojo y parpadea bajo 30 s. Al expirar, el POS auto-cancela tanto local como en Deuna.
4. Restilizado de la cajita de estado en pantalla de pago
La cajita nativa
.electronic_paymentdebajo de una línea Deuna se restilizó con esquinas redondeadas, gradiente de marca y color contextual:- Azul (idle / esperando),
- Verde (pago exitoso),
- Rojo (retry / failed / expired).
5. Diálogo de confirmación de reembolso
Los reembolsos en POS antes se ejecutaban al instante cuando el cashier elegía Deuna como método de devolución. Ahora se abre un diálogo de confirmación con la misma identidad visual del modal del QR, mostrando monto, número de comprobante (
transferNumber) y referencia interna del pago original. El reembolso solo se dispara si el cashier confirma.
6. Mirror al customer-facing display
Si el POS está configurado con un display de cliente (local, remoto o IoT Box), el QR y el monto se duplican vía el canal estándar
qrPaymentData. Controlado por método de pago condeuna_show_qr_on_customer_display.
7. Webhook handler con auth en cascada
POST /payment/deuna/webhookrecibe las confirmaciones de Deuna.Autenticación en cascada:
- Si vienen los headers
x-api-key/x-api-secret→ match del provider por credenciales (preferido). - Si no vienen o no matchean → fallback: se identifica el provider por el
transactionId/internalTransactionReferencedel payload contra ladeuna.pos.requestque nosotros emitimos. Necesario porque la documentación oficial de Deuna no especifica que envíen headers de auth en el webhook (en PRO llegan sin credenciales, comprobado por logs). - Si no se identifica nada → 401 con contexto completo en el log.
Verificado en producción:
- Webhook llega desde la IP de Deuna PRO (
44.208.102.192) con HTTP 200. - La
deuna.pos.requestpasa dePENDINGaSUCCESSpor el webhook. - El siguiente poll del cashier hace short-circuit y no consume cuota del límite 3 TPM de Deuna.
El fallback no abre brecha de seguridad: para envenenar un pago el atacante necesitaría adivinar un
transactionIdUUID v4 emitido por Deuna en los últimos minutos.
8. Polling con short-circuit y respeto al rate-limit
El cliente POS pollea
/pos_deuna/payment_statuscada 25 s (2.4 TPM, dentro del límite duro de 3 TPM de Deuna). Cuando el webhook ya confirmó la transacción, el polling lee el estado desdedeuna.pos.requesty responde sin pegar a Deuna, conservando la cuota.
9. Enriquecimiento del memo al cierre de sesión
Al cierre de la sesión POS, cada
account.paymentgenerado a partir de un pago Deuna recibe en su memo el sufijo- DEUNA<transferNumber>, dejando el asiento contable trazable al comprobante real de Deuna. Implementado víapos.session._create_split_account_paymentusandoaccount.move.origin_payment_id(campo correcto en Odoo 18; en versiones previas erapayment_id).
10. Lifetime del QR alineado a la documentación
_deuna_qr_lifetime_secondsdevuelve el lifetime correcto por caso (verificado contra los PDFs oficiales de Deuna):Caso expiredTimeenviado a DeunaLifetime local Dinámico + formato 0/1/2 Sí (1–720 min, default 15) El valor configurado Dinámico + NumericCode (3/4/5) No (Deuna lo ignora) Fijo 3 min Estático + formato 0/1/2 No (Deuna responde 409 si se envía) Cap de 30 min (cota visible al cashier; el POS auto-cancela al expirar) El contador del modal ahora refleja el lifetime real en lugar de mostrar siempre el valor del campo configurado — antes los QR estático y dinámico mostraban 15:00 igual porque se aplicaba el campo sin distinguir.
11. Reglas de seguridad para reembolsos
_deuna_request_refundaplica:- Solo reembolsos por monto total (la API de Deuna rechaza parciales),
- La orden original debe tener factura registrada en EC (
l10n_ec_invoice_number); si no, se rechaza con mensaje claro (sin factura no hay nota de crédito a la cual referenciar), - El pago Deuna original debe existir y no estar ya
REFUNDED, - Mapea errores de Deuna (400/404/409/500) a mensajes accionables en español.
El flag
support_refunddel provider se setea afull_onlypara que el botón de reembolso del backend respete el contrato del API.
12. Logging estructurado
Todos los flujos se etiquetan con prefijos grep-eables:
[DEUNA-WEBHOOK]— webhook entrante, con peer IP, estado de credenciales (matched|absent|mismatch), provider id, status, transactionId, transferNumber y monto.[DEUNA-POLL]— ciclo de poll del cashier, incluyendo avisos de short-circuit cuando el webhook ya confirmó.[DEUNA-SYNC source=webhook|polling]— cada transición de estado endeuna.pos.requestypos.paymentconprevio → nuevo.
La fuente se propaga vía
with_context(deuna_sync_source=...)para que el mismo helper loguee la etiqueta correcta sin importar quién lo llame.
13. Tests
tests/test_deuna_helpers.pycubre:_deuna_normalize_reference— strip de prefijos de orden, truncado a 19 chars, blanks, sin dígitos, evita colisiones entre órdenes._deuna_build_request_payload— campos requeridos, reglas deexpiredTimepor qr_type/format, truncado de detail, rechazo de estático + NumericCode._deuna_extract_qr_artefacts— respuestas full/parcial/None, aliasidTransaction, passthrough de data-URL._deuna_qr_lifetime_seconds— NumericCode 180 s, estático ignoraexpired_timey queda fijo en 30 min, dinámico cae a default 15 min cuando el campo está vacío._deuna_request_refund— sin tx_id, rechazo de parcial, happy-path de monto completo, falla del API mapeada a resultado no-OK.
tests/test_deuna_transaction.pycubre el flujo web completo (request → webhook → transiciones de estado).
Archivos modificados
trescloud_payment_deuna/ const.py # Constantes centralizadas, reglas de lifetime controllers/main.py # /payment/deuna/{qr,cancel,webhook} + post-processing data/payment_*.xml # Provider + method seed records models/payment_provider.py # Cliente API, helpers, validación models/payment_transaction.py # Flujo de checkout web static/src/js/deuna_qr_page.js # Contador para la página web del QR views/payment_provider_views.xml # Formulario del provider views/payment_deuna_templates.xml # Template de la página de QR web tests/ # Tests del provider + transaction trescloud_pos_deuna/ controllers/main.py # /pos_deuna/{request_qr,payment_status,refund,cancel_qr} models/payment_provider.py # Sync del webhook a deuna.pos.request + pos.payment models/pos_payment_method.py # Config del terminal Deuna models/pos_payment.py # Campos Deuna en pos.payment models/pos_session.py # Sufijo de memo en account.payment al cierre models/deuna_pos_request.py # Log de auditoría server-side de cada QR emitido static/src/js/deuna_payment.js # Patch del PaymentScreen + diálogos QR y refund static/src/xml/deuna_payment.xml # Templates OWL static/src/scss/deuna_payment.scss# Estilos de marca + restilizado del status box views/pos_payment_method_views.xml views/deuna_pos_request_views.xml security/ir.model.access.csv
Cómo probar
- Instalar ambos módulos (
trescloud_pos_deunadepende detrescloud_payment_deunayl10n_ec_pos). - Configurar el provider Deuna (Contabilidad → Providers de Pago → Deuna) con credenciales QA y la URL del webhook en el panel de Deuna.
- Crear un método de pago POS con
Use Payment Terminal = Deuna QRy vincularlo al provider. - Abrir una sesión POS, hacer una venta, validar el pago Deuna desde la app del cliente → verificar:
- El modal del QR aparece con contador,
- La cajita de estado pasa a verde al confirmarse,
- El log del webhook aparece en
sh.logantes del próximo poll, - El siguiente poll logea
short-circuit.
- Probar un reembolso: devolver la orden, elegir Deuna → confirmar el diálogo → verificar que el reembolso pega a Deuna y la
pos.paymentoriginal quedaREFUNDED. - Cerrar la sesión → abrir uno de los
account.paymentgenerados → verificar que el memo termina en- DEUNA<transferNumber>.
Compatibilidad con la API de Deuna
Verificado contra los PDFs oficiales:
API | Pagos Deuna V2API | Consulta de pagos Deuna | Webhook - Confirmación de pagos DeunaAPI | Devolución o Cancelación de transacción
Incluyendo las contradicciones documentadas del campo
expiredTimeen QR estático (la doc lista rango 1–30 min en una sección y "no debe enviarse" en otra) — seguimos el camino conservador (nunca lo enviamos para estático) y documentamos el porqué en los comentarios del código.
- Número Ticket/Tarea: #000000
Módulo nuevo de Integración con De UNA para Ecommerce y Punto de Venta