Appearance
Conciliación Automática — Detalle Técnico
Módulo: Portal de Clientes — Backend Tipo: Technical Estado: Implementado Fecha: 2026-05-12
Arquitectura
La conciliación automática se implementa como TX2 en AutoReconciliacionService, disparado desde el webhook handler post-commit de TX1. El servicio usa el puerto CtaCteReconciliacionPort (sin dependencias directas de los modelos legacy de CtaCte) y un nuevo método insertMovimientoCaja.
WebhookController
└─ PortalPaymentService::applyWebhookResult() [TX1]
└─ commit TX1
└─ AutoReconciliacionService::reconcile($payment) [TX2]
└─ CtaCteReconciliacionPort::getNumeradorRecibo()
└─ CtaCteReconciliacionPort::insertMovCuenta()
└─ CtaCteReconciliacionPort::insertReciboComprobante() × N
└─ CtaCteReconciliacionPort::updateComprobante() × N
└─ CtaCteReconciliacionPort::insertMovimientoCaja()
└─ PortalPaymentRepository::markReciboId() + markReciboAt()AutoReconciliacionService
Archivo: Modules/Portal/Application/Reconciliacion/Services/AutoReconciliacionService.php
Flujo de TX2
TX2 es una transacción atómica sobre conexiones principal + oficial:
setSchemaContext("suc{NNNN}")— resuelto desdesucursal_iddel pagogetNumeradorRecibo(ID_TIPO_RECIBO=4)—SELECT FOR UPDATEsobremulctainsertMovCuenta($mov)— INSERT enordcta, retornareciboId- Por cada factura en
facturas_json:insertReciboComprobante($reciboId, $factura)— INSERT enrecfacupdateComprobante($factura->idComprobante, $pago, $saldo)— UPDATE enordcta
insertMovimientoCaja($cajaSchema, $cuentaBancaria, $reciboId, $monto)— INSERT enmovimimarkReciboId($paymentId, $reciboId)— UPDATE portal_payments con guardWHERE recibo_id IS NULLmarkReciboAt($paymentId, now())— timestamp de conciliación exitosacommit()— ambas conexiones atómicas
Si cualquier paso falla → rollBack() + persist recibo_error en portal_payments.
Diferencias respecto al servicio manual (eliminado)
| Aspecto | PortalReciboCreatorService (eliminado) | AutoReconciliacionService (actual) |
|---|---|---|
| Trigger | HTTP POST del operador | Post-webhook (automático) |
| caja_id | Opcional | Obligatorio (portal.recibo.caja_schema) |
| markReciboAt | No existía | Sí — persiste timestamp de conciliación |
| recibo_error | No existía | Sí — persiste mensaje de fallo de TX2 |
Extensión de CtaCteReconciliacionPort
Archivo: Modules/Portal/Application/Reconciliacion/Ports/CtaCteReconciliacionPort.php
Nuevo método agregado al puerto:
php
public function insertMovimientoCaja(
string $cajaSchema,
string $cuentaBancaria,
int $reciboId,
float $monto
): void;Implementación en el Adapter (CtaCteReconciliacionAdapter.php): Usa MovimientoCaja model directamente (Plan B ADR 6). El adapter inyecta el schema de caja desde el constructor DI — no necesita ConnectionManager::setSchemaContext porque usa una conexión ya configurada con el schema correcto.
Columnas nuevas en portal_payments
Agregadas por migración AlterPortalPaymentsAddReciboColumns (tipo BASE, nivel SUCURSAL/EMPRESA):
| Columna | Tipo | Constraint | Significado |
|---|---|---|---|
recibo_at | TIMESTAMPTZ | NULL | Timestamp de conciliación automática exitosa |
recibo_error | TEXT | NULL | Mensaje del último error de TX2 |
Ambas son nullable. Filas existentes migran con NULL en ambas columnas.
Seeds de data_config
Agregados en migrations/seeds/tenancy/DataConfig.php, dentro del bloque if ($this->isPortalClientesEnabled()):
| Clave | Tipo | Default | Descripción |
|---|---|---|---|
portal.recibo.cuenta_bancaria | string | null | Número de cuenta bancaria contable para movimi |
portal.recibo.caja_schema | string | null | Schema de la caja destino (suc0001caja001) |
Ambas claves se crean vacías. El operador las configura desde la sección Gateway del ERP.
getGatewayConfig() — respuesta extendida
El endpoint GET /backend/config/gateway ahora incluye recibo_configured:
json
{
"gateway_configured": true,
"gateway_name": "paypertic",
"recibo_configured": true
}recibo_configured = true si y solo si portal.recibo.cuenta_bancaria Y portal.recibo.caja_schema están presentes en data_config.
Prerequisito en iniciar pago
PortalPaymentService::iniciarPago() verifica recibo_configured antes de crear la preferencia en el gateway. Si recibo_configured = false:
→ lanza ReciboNotConfiguredException
→ HTTP 422 (controller lo mapea)No es posible iniciar un pago sin esta configuración completa.
Frontend portal-usuarios — Guards de botones
useGatewayConfig() expone isReciboConfigured derivado de recibo_configured:
typescript
// src/core/hooks/useGatewayConfig.ts
export interface GatewayConfigState {
isEnabled: boolean;
isReciboConfigured: boolean;
config: GatewayConfig | null;
}Los botones de iniciar pago en PagarView están deshabilitados cuando isReciboConfigured = false. Un Alert informativo le indica al usuario que los pagos online no están disponibles en este momento.
Eliminación de Phase 5
Los siguientes artefactos fueron eliminados:
Backend:
PortalErpController+ rutas (/backend/portal-erp/pagos/*)PortalErpQueryServicePortalErpRoutesTests/Integration/Portal/PortalErpControllerTest.php
Frontend (bautista-app):
- Directorio
ts/ctacte/PagosPortal/completo - Entrada en
ts/ctacte/config/sidebar.ts - Export en
ts/ctacte/index.ts
Verificación post-eliminación: grep portal-erp → 0 resultados.
Tests
Backend
| Suite | Tests | Status |
|---|---|---|
| AutoReconciliacionService (unit) | Caso feliz, TX2 falla, config incompleta | ✅ PASS |
| CtaCteReconciliacionAdapter (unit) | insertMovimientoCaja | ✅ PASS |
| PagoTicFlowIntegrationTest | Flujo webhook → TX1 → TX2 | ✅ PASS |
| Total portal module | 464 tests (pre-existentes) | ✅ 0 new failures |
Frontend portal-usuarios
| Suite | Tests | Status |
|---|---|---|
| useGatewayConfig | 3 tests (200, 403, degradación graceful) | ✅ PASS |
| PagarView guards | Botones deshabilitados si recibo_configured=false | ✅ PASS |
| Total | 276 tests | ✅ ALL PASS |
Ver también
- Conciliación Automática — Proceso de Negocio
- Configuración de Gateway — Técnico — campo cuentaBancaria
- Schema portal_payments — columnas recibo_at/recibo_error