ADR-010: Avisos entre capas de prefondeo (Holding → País → Caja)
Date: 2026-09-13 Status: Proposed — la decisión de fondo (aviso, no bloqueo) la confirmó Carlos el 2026-09-13; queda por confirmar la fórmula del disponible de la Holding (ver Open question al final). Extends: ADR-007 — no lo supersede: lo completa.
Context
ADR-007 definió tres ledgers (Holding → País → Caja), con estas invariantes: la Holding descuenta amount + fee, el País y la Caja descuentan solo amount, mover plata de una capa a la de abajo no descuenta la de arriba (es distribución, no gasto), y el pago valida solo la caja operativa.
De esas decisiones salió una asimetría que nunca se cerró: la capa País → Caja quedó con un aviso cuando se reparte de más (ADR-007 §3, pedido textual de Diego: "como una alerta más que un validador"), pero la capa Holding → País no quedó con nada. No hay validación, no hay aviso, y no existe siquiera la métrica: ninguna consulta del sistema suma country_prefunding_entries a nivel global.
El 2026-09-13 Carlos pidió que el prefondeo del país estuviera "validado por la Holding": que el máximo repartible entre todos los países sea el total de la Holding, y que el máximo fondeable entre todas las cajas de un país sea el prefondeo de ese país. Consultado sobre si eso debía bloquear, respondió: "estoy ok que haya una alerta pero comprobá que funcione".
Esto importa porque la regla ya está violada en silencio. Medición contra la base de desarrollo del 2026-09-13:
| Concepto | Valor |
|---|---|
| Σ asientos Prefondeo Holding | 138.100,00 |
| Σ asientos Prefondeo País (todos los países) | 148.100,00 |
| Σ comisiones de transacciones pagadas | 1.888,90 |
| Disponible de la Holding para repartir | −11.888,90 |
Parte de ese desfase es herencia del propio deploy: el backfill de 2026_08_06_120000_e4b3_crear_prefondeo_pais.php:60-67 copió 1:1 los asientos de la Holding a la capa País, así que el sistema arrancó exactamente en el límite y cualquier asiento posterior lo cruzó.
Decision
1. El control entre capas es un aviso, en las dos fronteras. No bloquea nunca
Se mantiene el criterio de ADR-007 §3 y se extiende hacia arriba: repartir de más nunca impide completar la operación. El movimiento se registra y la respuesta trae un aviso.
La razón es la misma que dio tesorería y sigue valiendo: un sobregiro entre capas es un problema administrativo, no un problema de plata. La transferencia bancaria puede estar en trámite y la plata física puede estar en el cajón. Bloquear la distribución no hace aparecer el dinero; solo frena la operación de mostrador que sí se puede hacer.
Lo que sí cambia respecto de hoy es que el aviso tiene que existir en las dos fronteras y tiene que ser visible, no solo en el JSON de una respuesta.
| Frontera | Operación que lo dispara | Hoy | Con este ADR |
|---|---|---|---|
| Holding → País | Alta de asiento de Prefondeo País | nada | aviso OVER_HOLDING_PREFUNDING |
| País → Caja | FONDEO de caja | aviso OVER_COUNTRY_PREFUNDING | igual, + el AJUSTE positivo |
2. El disponible de cada capa se calcula como saldo propio − suma de los saldos de los hijos
Es el patrón que ya implementa CountryPrefundingService::availableToDistribute() un nivel abajo. Se replica hacia arriba en vez de inventar un criterio nuevo:
F = Σ prefunding_entries.amount (lo que Soterex acreditó a la Holding)
D = Σ country_prefunding_entries.amount (lo que la Holding bajó a TODOS los países)
Ap = Σ transactions.amount WHERE PAID (principales pagados)
Fe = Σ transactions.fee WHERE PAID (comisiones de lo pagado)
holding.globalBalance() = F − (Ap + Fe) [ya existe]
holding.availableToDistribute() = globalBalance − Σ_c país.balance(c)
= [F − Ap − Fe] − [D − Ap]
= F − Fe − D [nuevo]Los principales pagados se cancelan solos. Esa es la propiedad que hace correcta a esta fórmula y no a la intuitiva: un pago no mueve el disponible de la Holding. El número es estable, y un asiento que era válido ayer no se vuelve inválido hoy porque se cobró plata en el mostrador.
Comparar contra globalBalance() a secas sería la alternativa obvia y es la equivocada: ese saldo baja con cada pago, así que el disponible se achicaría justo cuando la plata ya estaba distribuida — se estaría descontando dos veces el mismo principal.
Se mantiene el patrón de ADR-005/007: ningún saldo se almacena, todo se deriva del ledger.
3. Un saldo negativo se muestra, no se esconde
Las tres capas pueden quedar negativas (la Holding y el País por diseño; la Caja no, porque el pago la valida). Un negativo es información contable válida —refleja deuda real entre capas— y tiene que verse como tal: en rojo, con leyenda, en la pantalla de la capa. Hoy PrefondeoPaisView lo hace y PrefondeoView no, que es exactamente la deuda que ADR-007 §1 nota ⑴ dejó anotada y nunca se pagó.
4. El AJUSTE positivo de caja cuenta como distribución
CashBoxController::ajuste() mete plata en la caja con el mismo permiso que el fondeo y hoy no mira el prefondeo del país. Un ajuste de arqueo por sobrante distribuye plata igual que un fondeo, así que dispara el mismo aviso. Los ajustes negativos no: devuelven headroom, no lo consumen.
5. El alta de asiento de Prefondeo País pasa a ser transaccional
CountryPrefundingController::store() hoy no tiene DB::transaction ni lock, a diferencia de pay(), fondeo() y apertura(). Mientras no validaba nada daba igual; con un cálculo de disponible de por medio, dos altas simultáneas leen el mismo número y las dos avisan (o ninguna). Mismo criterio para PrefundingController::store().
Alternatives Considered
- Bloquear con 422 en vez de avisar. Es lo que pedía la lectura literal del pedido del 2026-09-13 ("el máximo que se puede prefondear un país es el total de la holding"). Descartado por el propio Carlos al preguntarle: revierte una decisión que tesorería tomó con fundamento (ADR-007 §3) y reintroduce la fricción que Diego hizo sacar. La variante intermedia que se evaluó —bloquear solo las operaciones de distribución (administrativas, sin cliente esperando) y dejar el pago sin bloquear— sigue disponible si el aviso resulta insuficiente en la práctica: es un cambio de código chico sobre lo que este ADR define, porque el cálculo del disponible es el mismo.
- Comparar contra
globalBalance(). Ver §2: descuenta dos veces el principal pagado. - Materializar los saldos en una tabla. Descartado por ADR-005 y sin razones nuevas para reabrirlo: los tres saldos se derivan del ledger, que es lo que los hace auditables.
- Corregir el −11.888,90 existente con una migración de datos. No se decide acá: es una pregunta contable para tesorería, no una decisión de arquitectura. El aviso lo va a hacer visible, que es el primer paso.
Consequences
- Positive: la capa Holding → País deja de ser un agujero silencioso. Con el aviso, el desfase actual se vuelve visible el día que alguien cargue el próximo asiento, sin necesidad de una auditoría manual.
- Positive: las dos fronteras quedan simétricas y con el mismo mecanismo, lo que hace que la documentación del circuito (deuda que E7 tiene pendiente) se pueda explicar con una sola regla: cada capa avisa cuando reparte más de lo que tiene.
- Negative: el aviso se puede ignorar, y ahora se puede ignorar en dos lugares. El control real sigue siendo la conciliación de tesorería. Es la misma decisión consciente de ADR-007 §3, extendida.
- Negative: aparece un número nuevo en la UI ("disponible para repartir" de la Holding) en un módulo que un usuario nuevo ya tiene que entender con tres saldos. Es más deuda de documentación para E7.
- Neutral: nada de lo que ADR-007 decidió cambia. El pago sigue validando solo la caja operativa, el país sigue admitiendo negativo, y la comisión sigue sin bajar del país ni de la caja.
Open question (necesita confirmación de Carlos antes de implementar §2)
¿El disponible de la Holding es F − Fe − D (lo recomendado acá: lo que entró de Soterex, menos la comisión que la Holding retiene, menos lo ya bajado a los países) o se prefiere una definición donde la comisión no se reserve, es decir F − D?
La diferencia práctica: con F − Fe − D, la comisión que la Holding retiene deja de estar disponible para repartir a los países apenas se paga la transacción — que es lo correcto si esa plata efectivamente se liquida con Soterex a fin de mes. Con F − D, la comisión queda disponible para repartir hasta que alguien la saque a mano.
Recomendación: F − Fe − D, porque es la que se deriva del patrón ya implementado y la que hace que las tres capas cierren entre sí.

