Skip to content

Conciliación Automática de Pagos del Portal

⚠️ DOCUMENTACIÓN RETROSPECTIVA — Generada a partir de código implementado el 2026-06-24

Módulo: Portal de Clientes / CtaCte Tipo: Process Estado: Implementado Fecha: 2026-05-12 — Actualizado 2026-06-24 (hardening: webhook validation, refund reversal, recovery jobs)


Descripción

Cuando un cliente paga desde el portal y el gateway aprueba el pago, el sistema genera el recibo en cuenta corriente de forma automática, sin intervención de ningún operador del ERP.

Este proceso reemplaza íntegramente la anterior Fase 5 (reconciliación manual), que fue eliminada. Ya no existe la vista "Pagos Portal" en CtaCte ni la acción "Generar Recibo".

Valor para el negocio:

  • El saldo del cliente en ctacte se actualiza en el mismo instante en que el gateway aprueba el pago
  • No se requiere disponibilidad de un operador para cerrar el ciclo contable
  • Se elimina el riesgo de pagos aprobados que quedan sin registrar por olvido u operación tardía
  • El movimiento en caja también se genera automáticamente, sin pasos adicionales

Flujo de la Conciliación Automática

Cliente paga → Gateway aprueba → Webhook recibido por backend

TX1: backend registra status='approved' en portal_payments (commit)

TX2 (independiente): AutoReconciliacionService
    1. getNumeradorRecibo() — mulcta SELECT FOR UPDATE
    2. insertMovCuenta() — crea recibo en ordcta (schema CtaCte)
    3. insertReciboComprobante() × N — vincula cada factura en recfac
    4. updateComprobante() × N — actualiza pago/saldo en ordcta
    5. insertMovimientoCaja() — movimi en schema Caja (portal.recibo.caja_schema)
    6. markReciboId() + markReciboAt() en portal_payments

portal_payments.recibo_id = <nuevo recibo> | recibo_at = <timestamp>

TX1 y TX2 son transacciones separadas e independientes. Un fallo de TX2 no revierte TX1: el pago mantiene status='approved' y se registra el error en recibo_error para diagnóstico.

Validación de monto/moneda antes de TX2. Luego del commit de TX1 y antes de disparar TX2, el servicio compara el monto reportado por el webhook con el monto y currency almacenados en la fila bloqueada. Si hay discrepancia, recibo_error se marca con el prefijo AMOUNT_MISMATCH: y TX2 se saltea. El status queda approved. Ver Validación de Monto.


Prerequisitos de Configuración

Para que TX2 funcione, la sucursal debe tener configuradas dos claves en data_config:

ClaveDescripciónConfiguración
portal.recibo.cuenta_bancariaNúmero de cuenta bancaria contable para el movimiento en cajaERP → Configuración del Sistema → Gateway de Pagos → Cuenta Bancaria
portal.recibo.caja_schemaSchema de la caja destino para el movimiento en movimiERP → Configuración del Sistema → Gateway de Pagos

Si cualquiera de las dos claves falta al momento de iniciar un pago, el backend rechaza el inicio con HTTP 422 (ReciboNotConfiguredException). No es posible que un cliente inicie un pago en una sucursal sin esta configuración.


Validación de Monto y Moneda

Antes de disparar TX2, el sistema valida que el webhook informe el monto y la moneda correctos:

VerificaciónRegla
Montoabs(webhook.amount - row.monto) <= 0.001 (epsilon para NUMERIC float)
MonedaComparación case-insensitive. Si el gateway reporta null, la validación se omite (compatibilidad con gateways legacy)

En caso de discrepancia:

  1. recibo_error se marca con AMOUNT_MISMATCH: esperado monto=X currency=Y, recibido amount=A currency=B
  2. TX2 no se ejecuta — el recibo no se genera
  3. status permanece approved
  4. La fila queda excluida del retry automático (retry-recibo-errors.php filtra AMOUNT_MISMATCH a nivel SQL y en servicio)
  5. Requiere intervención humana para resolución

Manejo de Errores de TX2

Si TX2 falla (error en el Port, timeout de BD, etc.):

  1. TX2 hace rollback completo — no quedan recibos huérfanos en ordcta
  2. portal_payments.recibo_error queda con el mensaje del error
  3. portal_payments.recibo_id permanece NULL
  4. portal_payments.status permanece 'approved' (TX1 no se revierte)
  5. El pago puede ser re-conciliado por el job retry-recibo-errors.php (hasta 3 intentos automáticos, excepto filas AMOUNT_MISMATCH)

El campo recibo_at solo se setea cuando TX2 completa exitosamente.


Reglas de Negocio

RN-001: Disparo post-webhook TX2 se dispara siempre que TX1 commitea con status='approved' y la validación de monto/moneda pasa. No hay intervención humana.

RN-002: TX2 independiente de TX1 Un fallo de TX2 no revierte el registro del pago aprobado. El sistema prioriza registrar la aprobación del gateway sobre el éxito de la acreditación contable.

RN-003: Schema de CtaCte desde sucursal_idSchemaResolver::resolveSchemaName(sucursalId) resuelve el schema: si sucursalId = null usa public; si sucursalId = N usa suc000N.

RN-004: Vendedor siempre null Los recibos generados por auto-reconciliación no tienen vendedor asociado (cven = NULL). Es el estándar para pagos externos.

RN-005: Prerequisito verificado al iniciar pagogetGatewayConfig() expone recibo_configured: true/false. El portal deshabilita los botones de pago si recibo_configured = false. El backend también lanza ReciboNotConfiguredException al intentar iniciar un pago sin configuración completa.

RN-006: Mapeo estricto de estados del gatewayPagoTicAdapter::mapStatus() mapea charged_back → REJECTED, expired → CANCELLED, in_mediation/mediation → REVIEW. Un estado desconocido lanza UnknownGatewayStatusException (no degrada silenciosamente a PENDING). El controlador registra CRITICAL log + errorbri y retorna HTTP 200 para evitar reintentos del gateway.

RN-007: Errores transitorios de BD retornan HTTP 503\PDOException en el webhook controlador retorna HTTP 503 (en lugar de 200), habilitando reintentos del gateway cuando la BD está momentáneamente no disponible.

RN-008: Cancelar/devolver con lock de filaPortalPaymentService::cancelar() y ::devolver() adquieren SELECT FOR UPDATE antes de escribir el estado, re-validando la transición bajo el lock. Cierra la race condition cancelar-vs-webhook.

RN-009: Reversión contable en devolver Cuando devolver() se ejecuta sobre un pago con recibo_id no-nulo, el servicio revierte las entradas contables en ctacte (ordcta + recfac) y el movimiento de caja dentro de la misma transacción. Ver Reversión de Recibo.

RN-010: AMOUNT_MISMATCH nunca se reintenta automáticamente Filas con recibo_error prefijado AMOUNT_MISMATCH: son excluidas a nivel SQL por el job de retry y por un guard de defensa en profundidad en RetryReciboErrorsService. Siempre requieren intervención humana.


Reversión de Recibo

Cuando un pago aprobado se devuelve y tiene recibo_id set, PortalReciboReversalService::revertirSinTransaccion() se ejecuta dentro de la TX de devolver. Pasos:

  1. Obtener snapshot de recfac por recibo_id
  2. Si recfac está vacío → ctacte ya revertido (idempotencia), saltar
  3. Borrar filas de recfac (hijos antes que padre)
  4. Restaurar saldo de cada comprobante en ordcta desde el snapshot de recfac.saldo
  5. Borrar fila de ordcta (header del recibo)
  6. Reversión de caja (ADR-B condicional):
    • Leer gateway_response.recibo_movimi {id, nrocaj} — persistido al crear el movimiento de caja en TX2
    • Caja abierta: DELETE movimi directamente
    • Caja cerrada (o recibo_movimi null → legacy): INSERT de un contra-movimiento es='S' en la caja abierta actual

El db_origen por comprobante se resuelve desde facturas_json (snapshot en iniciar). Default 'oficial'.


Estado de portal_payments después del flujo

Estadorecibo_idrecibo_atrecibo_errorSignificado
approvedNULLNULLNULLTX2 aún no ejecutada (o en ejecución)
approved<id><timestamp>NULLConciliación automática exitosa
approvedNULLNULLAMOUNT_MISMATCH: ...Monto reportado no coincide — requiere intervención humana
approvedNULLNULL<otro mensaje>TX2 falló — pendiente de recovery automático (hasta 3 reintentos)

Diferencia con la Reconciliación Manual (eliminada)

AspectoManual (Fase 5 — eliminado)Automático (actual)
TriggerAcción del operador ERPPost-webhook
Intervención humanaRequeridaNo requerida
Vista ERP"Pagos Portal" en CtaCteNo existe
Demora hasta acreditaciónVariable (horas/días)Segundos
Movimiento en cajaOpcional, manualAutomático vía caja_schema

Ver también


⚠️ NOTA IMPORTANTE: Validar con stakeholders antes de considerar final.