Skip to content

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:

  1. setSchemaContext("suc{NNNN}") — resuelto desde sucursal_id del pago
  2. getNumeradorRecibo(ID_TIPO_RECIBO=4)SELECT FOR UPDATE sobre mulcta
  3. insertMovCuenta($mov) — INSERT en ordcta, retorna reciboId
  4. Por cada factura en facturas_json:
    • insertReciboComprobante($reciboId, $factura) — INSERT en recfac
    • updateComprobante($factura->idComprobante, $pago, $saldo) — UPDATE en ordcta
  5. insertMovimientoCaja($cajaSchema, $cuentaBancaria, $reciboId, $monto) — INSERT en movimi
  6. markReciboId($paymentId, $reciboId) — UPDATE portal_payments con guard WHERE recibo_id IS NULL
  7. markReciboAt($paymentId, now()) — timestamp de conciliación exitosa
  8. commit() — ambas conexiones atómicas

Si cualquier paso falla → rollBack() + persist recibo_error en portal_payments.

Diferencias respecto al servicio manual (eliminado)

AspectoPortalReciboCreatorService (eliminado)AutoReconciliacionService (actual)
TriggerHTTP POST del operadorPost-webhook (automático)
caja_idOpcionalObligatorio (portal.recibo.caja_schema)
markReciboAtNo existíaSí — persiste timestamp de conciliación
recibo_errorNo existíaSí — 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):

ColumnaTipoConstraintSignificado
recibo_atTIMESTAMPTZNULLTimestamp de conciliación automática exitosa
recibo_errorTEXTNULLMensaje 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()):

ClaveTipoDefaultDescripción
portal.recibo.cuenta_bancariastringnullNúmero de cuenta bancaria contable para movimi
portal.recibo.caja_schemastringnullSchema 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/*)
  • PortalErpQueryService
  • PortalErpRoutes
  • Tests/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

SuiteTestsStatus
AutoReconciliacionService (unit)Caso feliz, TX2 falla, config incompleta✅ PASS
CtaCteReconciliacionAdapter (unit)insertMovimientoCaja✅ PASS
PagoTicFlowIntegrationTestFlujo webhook → TX1 → TX2✅ PASS
Total portal module464 tests (pre-existentes)✅ 0 new failures

Frontend portal-usuarios

SuiteTestsStatus
useGatewayConfig3 tests (200, 403, degradación graceful)✅ PASS
PagarView guardsBotones deshabilitados si recibo_configured=false✅ PASS
Total276 tests✅ ALL PASS

Ver también