Targ Apps Docs

Ledger y movimientos

Saldos derivados, ingresos/gastos y transferencias entre cuentas (TAF-012, TAF-016).

Visión general

El dinero no se guarda como saldo mutable. Cada cuenta tiene initialBalance; el saldo visible es esa cifra más la suma firmada de sus movimientos.

transactions[]  +  accounts[]
        ↓
logic/store/ledger.js     (saldos, net worth, income/expense)
logic/store/transactions.js  (alta / edición / baja / transfer pair)
        ↓
FinanceProvider  →  useFinance()

logic/utils/calculations.js reexporta las funciones de ledger.js para no duplicar fórmulas. Los tests viven en test/logic/store/ledger.test.js y test/logic/store/transactions.test.js (node --test).

API de escritura (transactions.js)

El provider no calcula montos: llama a estas funciones puras y luego persiste el resultado.

FunciónRol
prepareAddTransaction(data, accounts)Valida ingreso/gasto y crea la entidad. Rechaza type: "transfer".
prepareAddTransfer(input, accounts, transactions)Valida y crea dos piernas vinculadas.
prepareUpdateTransaction(list, id, patch, accounts)Ingreso/gasto: un patch. Transferencia: sincroniza ambas piernas.
prepareRemoveTransaction(list, id)Devuelve todos los ids a borrar (el par si es transfer).
relatedTransactionIds(list, id)Ids del movimiento y de su pareja.
createTransferPair(input)Factory de las dos piernas (out / in).
transferEndpoints(tx){ fromAccountId, toAccountId } desde cualquiera de las piernas.

addTransfer en useFinance() acepta positional (fromAccountId, toAccountId, amount, date, note) o un objeto con esas claves.

addTransfer({
  fromAccountId: "bank",
  toAccountId: "cash",
  amount: 50,
  date: "2026-08-29",
  note: "cajero",
});

addTransaction({ type: "transfer", accountId, transferAccountId, ... }) delega en addTransfer.

Transferencias (TAF-016)

Una transferencia es dos transacciones con el mismo transferId:

CampoPierna out (origen)Pierna in (destino)
typetransfertransfer
accountIdcuenta origencuenta destino
transferAccountIdcuenta destinocuenta origen
transferLeg"out""in"
categoryId""""

Reglas de validateTransaction / validateTransfer (logic/models.js + logic/validation.js):

  • Monto finito > 0 (cero, negativo y NaN se rechazan).
  • Cuenta requerida; si se pasa accounts, debe existir y no estar archivada.
  • Categoría requerida en ingreso/gasto (no en transfer).
  • Fecha parseable.

Reglas adicionales de validateTransfer:

  • Origen y destino requeridos y distintos.
  • Ambas cuentas deben existir y no estar archivadas.
  • Misma moneda (no hay FX).
  • Fecha parseable.

Saldo insuficiente en origen: warning (El saldo de la cuenta origen es insuficiente.), no bloqueo. El Dashboard no cuenta transferencias como ingreso ni gasto.

Hay un fallback de un solo registro (accountId = origen, transferAccountId = destino, sin transferLeg) para no romper datos viejos. El alta nueva siempre escribe el par.

Reglas de saldo (ledger.js)

signedDeltaForAccount(tx, accountId):

  • income en esa cuenta → +amount
  • expense en esa cuenta → −amount
  • transfer out → −amount; transfer in → +amount
  • registro único de transfer: origen −, destino +
  • monto no finito o ≤ 0 → 0 (no mueve el saldo)

Funciones derivadas:

FunciónResultado
computeAccountBalance(account, txs)initialBalance + deltas
computeBalancesByAccount(accounts, txs)mapa id → saldo
computeNetWorthByCurrency / computeTotalBalanceByCurrencytotales por moneda (nunca se mezclan)
computeIncomeExpense / computeMonthlyIncomeExpense{ income, expense, net } excluyendo transfer
computeTopExpenseCategoriestop gastos del mes (ignora transfers)
getRecentTransactionsúltimas N por fecha (incluye transfers)
transactionTouchesAccountaccountId o transferAccountId
categoriesForType / categoryHasTransactionsfiltros de categoría usados por las páginas

getAccountBalance y balanceByCurrency del store llaman a estas funciones. Archivar una cuenta no borra historial; el KPI de dashboard usa activeAccounts().

Cableado en páginas

Solo handlers e imports; sin rediseño visual.

  • Transactions: tipo transfer en TransactionForm → addTransaction / updateTransaction / removeTransaction (el remove borra las dos piernas). Filtros combinables vía filterTransactions (logic/utils/filterTransactions.js): hookState → filteredTransactions → listState (cadena useMemo) → For({ each: () => listState().groups }) en el DOM (no .map dentro del hijo de Show, que monta una sola vez). Ver Transactions — reactividad de filtros y Chroma — Show vs For.
  • Dashboard: monthlyTotals ignora transfers; filas recientes etiquetan Transferencia.
  • Accounts: transactionTouchesAccount bloquea el hard-delete si hay movimientos.
  • Categories: categoriesForType y categoryHasTransactions.

On this page