Módulo nuevo de Integración con De UNA para Ecommerce y Punto de Venta

11 de junio de 2026 por
Módulo nuevo de Integración con De UNA para Ecommerce y Punto de Venta
OdooBot
| Sin comentarios aún
  • 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 de account.payment al 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.

    image

    2. Configuración del método de pago POS

    El pos.payment.method recibe una nueva opción de terminal Deuna QR y los campos deuna_provider_id, deuna_qr_type, deuna_pos_code, deuna_format, deuna_expired_time y deuna_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 acepta expiredTime (los demás los rechaza con 409 o los ignora).

    image

    3. Diálogo del QR para el cashier

    Cuando el cashier selecciona el método Deuna, el POS llama /pos_deuna/request_qr y 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.

    image image

    4. Restilizado de la cajita de estado en pantalla de pago

    La cajita nativa .electronic_payment debajo 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).
    image

    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 con deuna_show_qr_on_customer_display.


    7. Webhook handler con auth en cascada

    POST /payment/deuna/webhook recibe las confirmaciones de Deuna.

    Autenticación en cascada:

    1. Si vienen los headers x-api-key/x-api-secret → match del provider por credenciales (preferido).
    2. Si no vienen o no matchean → fallback: se identifica el provider por el transactionId / internalTransactionReference del payload contra la deuna.pos.request que 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).
    3. 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.request pasa de PENDING a SUCCESS por 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 transactionId UUID v4 emitido por Deuna en los últimos minutos.

    image

    8. Polling con short-circuit y respeto al rate-limit

    El cliente POS pollea /pos_deuna/payment_status cada 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 desde deuna.pos.request y 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.payment generado 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ía pos.session._create_split_account_payment usando account.move.origin_payment_id (campo correcto en Odoo 18; en versiones previas era payment_id).

    image

    10. Lifetime del QR alineado a la documentación

    _deuna_qr_lifetime_seconds devuelve el lifetime correcto por caso (verificado contra los PDFs oficiales de Deuna):

    Caso expiredTime enviado a Deuna Lifetime 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_refund aplica:

    • 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_refund del provider se setea a full_only para 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 en deuna.pos.request y pos.payment con previo → 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.py cubre:

    • _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 de expiredTime por qr_type/format, truncado de detail, rechazo de estático + NumericCode.
    • _deuna_extract_qr_artefacts — respuestas full/parcial/None, alias idTransaction, passthrough de data-URL.
    • _deuna_qr_lifetime_seconds — NumericCode 180 s, estático ignora expired_time y 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.py cubre 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

    1. Instalar ambos módulos (trescloud_pos_deuna depende de trescloud_payment_deuna y l10n_ec_pos).
    2. Configurar el provider Deuna (Contabilidad → Providers de Pago → Deuna) con credenciales QA y la URL del webhook en el panel de Deuna.
    3. Crear un método de pago POS con Use Payment Terminal = Deuna QR y vincularlo al provider.
    4. 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.log antes del próximo poll,
      • El siguiente poll logea short-circuit.
    5. Probar un reembolso: devolver la orden, elegir Deuna → confirmar el diálogo → verificar que el reembolso pega a Deuna y la pos.payment original queda REFUNDED.
    6. Cerrar la sesión → abrir uno de los account.payment generados → verificar que el memo termina en - DEUNA<transferNumber>.

    Compatibilidad con la API de Deuna

    Verificado contra los PDFs oficiales:

    • API | Pagos Deuna V2
    • API | Consulta de pagos Deuna | Webhook - Confirmación de pagos Deuna
    • API | Devolución o Cancelación de transacción

    Incluyendo las contradicciones documentadas del campo expiredTime en 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
Iniciar sesión para dejar un comentario