Appearance
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:
| Clave | Descripción | Configuración |
|---|---|---|
portal.recibo.cuenta_bancaria | Número de cuenta bancaria contable para el movimiento en caja | ERP → Configuración del Sistema → Gateway de Pagos → Cuenta Bancaria |
portal.recibo.caja_schema | Schema de la caja destino para el movimiento en movimi | ERP → 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ón | Regla |
|---|---|
| Monto | abs(webhook.amount - row.monto) <= 0.001 (epsilon para NUMERIC float) |
| Moneda | Comparación case-insensitive. Si el gateway reporta null, la validación se omite (compatibilidad con gateways legacy) |
En caso de discrepancia:
recibo_errorse marca conAMOUNT_MISMATCH: esperado monto=X currency=Y, recibido amount=A currency=B- TX2 no se ejecuta — el recibo no se genera
statuspermaneceapproved- La fila queda excluida del retry automático (
retry-recibo-errors.phpfiltraAMOUNT_MISMATCHa nivel SQL y en servicio) - Requiere intervención humana para resolución
Manejo de Errores de TX2
Si TX2 falla (error en el Port, timeout de BD, etc.):
- TX2 hace rollback completo — no quedan recibos huérfanos en
ordcta portal_payments.recibo_errorqueda con el mensaje del errorportal_payments.recibo_idpermanece NULLportal_payments.statuspermanece'approved'(TX1 no se revierte)- El pago puede ser re-conciliado por el job
retry-recibo-errors.php(hasta 3 intentos automáticos, excepto filasAMOUNT_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:
- Obtener snapshot de
recfacporrecibo_id - Si
recfacestá vacío → ctacte ya revertido (idempotencia), saltar - Borrar filas de
recfac(hijos antes que padre) - Restaurar saldo de cada comprobante en
ordctadesde el snapshot derecfac.saldo - Borrar fila de
ordcta(header del recibo) - 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 movimidirectamente - Caja cerrada (o
recibo_moviminull → legacy): INSERT de un contra-movimientoes='S'en la caja abierta actual
- Leer
El db_origen por comprobante se resuelve desde facturas_json (snapshot en iniciar). Default 'oficial'.
Estado de portal_payments después del flujo
| Estado | recibo_id | recibo_at | recibo_error | Significado |
|---|---|---|---|---|
approved | NULL | NULL | NULL | TX2 aún no ejecutada (o en ejecución) |
approved | <id> | <timestamp> | NULL | Conciliación automática exitosa |
approved | NULL | NULL | AMOUNT_MISMATCH: ... | Monto reportado no coincide — requiere intervención humana |
approved | NULL | NULL | <otro mensaje> | TX2 falló — pendiente de recovery automático (hasta 3 reintentos) |
Diferencia con la Reconciliación Manual (eliminada)
| Aspecto | Manual (Fase 5 — eliminado) | Automático (actual) |
|---|---|---|
| Trigger | Acción del operador ERP | Post-webhook |
| Intervención humana | Requerida | No requerida |
| Vista ERP | "Pagos Portal" en CtaCte | No existe |
| Demora hasta acreditación | Variable (horas/días) | Segundos |
| Movimiento en caja | Opcional, manual | Automático vía caja_schema |
Ver también
- Conciliación Automática — Detalle Técnico
- Jobs de Recuperación de Pagos — sweep y retry cron jobs
- Configuración de Gateway — prerequisito de configuración
- Feature Flag del Portal — control de habilitación del módulo
⚠️ NOTA IMPORTANTE: Validar con stakeholders antes de considerar final.