Ir al contenido

Flujo de efectivo

CashFlowManager gestiona el ciclo de vida de la sesión de caja (arqueo): apertura, movimientos manuales, cierre con cuadratura y reasignación de pagos huérfanos. Es la contraparte del módulo de finanzas y se apoya en CashRegisterManager para la asociación con la caja física.

Fuente: repos/tables-sdk/src/core/managers/CashFlowManager.ts.

Es el documento PouchDB de la sesión de caja (type: 'cashflow'). Campos centrales:

CampoTipoDescripción
opening_amountnumberMonto de apertura (caja inicial).
closing_amountnumber?Monto al cierre (igual a declared_cash).
opening_date / closing_datestringMarcas ISO de apertura y cierre.
status'open' | 'closed'Estado de la sesión.
cash_register_idstringCaja registradora asociada.
user_idstringUsuario legacy (apertura o último editor).
movementsCashMovement[]Movimientos manuales embebidos.

Al cerrar se persiste además un snapshot de cuadratura (declared_cash, expected_cash, difference, total_sales, total_cash, total_card, total_transfer, total_tips, excluded_payments_count) y los campos de apertura/cierre (opened_by_user_id, closed_by_user_id, nombres y umbral de descuadre). Todos son opcionales para preservar retrocompatibilidad con sesiones previas.

Movimiento embebido en movements[] (no es un documento PouchDB independiente):

CampoTipoDescripción
idstringIdentificador (nanoid).
amountnumberMonto del movimiento.
operation_type'income' | 'expense'Ingreso o egreso.
datestringMarca ISO.
categoryCashMovementCategory?'sale' | 'tip' | 'paid_in' | 'paid_out'.
subcategoryCashMovementSubcategory?Vocabulario controlado (ver abajo).
payment_methodstring?Método asociado (p. ej. 'cash').
saleIdstring?Pago de venta que originó el movimiento.
user_idstring?Usuario que registró.
notesstring?Observación libre.

category, subcategory y user_id son opcionales por retrocompatibilidad. Las subcategorías están acotadas por PAID_IN_SUBCATEGORIES (deposit, change_replenishment, bank_drop_return, correction, other) y PAID_OUT_SUBCATEGORIES (reimbursement, supplies, vendor_payment, safe_drop, correction, other), exportadas para que la UI reutilice el mismo conjunto.

La sesión vive sobre una caja registradora (cash_register_id). CashRegisterManager resuelve qué caja usar:

  • resolveForArea(areaId): caja asignada al área (area_ids o areaId legacy); si no, la caja default de la compañía; como último recurso, cualquier caja activa.
  • getDefault() / setDefault(registerId): caja default de la compañía.

Al abrir una sesión, openCashFlow setea cash_register_id en el documento y escribe cash_flow_id en la caja (puntero a la sesión abierta); al cerrar, ese puntero se limpia. Para la relación con la jornada operativa ver Turnos.

  1. AperturaopenCashFlow(cashRegisterId, userId, openingAmount, opts?) crea el documento (status: 'open'), enlaza la caja, snapshotea el umbral de descuadre (max_close_difference_amount) desde la config de compañía y adopta pagos huérfanos creados tras el cierre anterior (opening_adopted_payments_count).

  2. Movimientos — durante la sesión abierta se registran ingresos/egresos manuales.

  3. CierrecloseCashFlow(id, declaredCashOrParams) calcula la cuadratura, valida la política de descuadre y persiste el snapshot. La caja queda inmutable.

  4. ImpresiónprintArqueo(cashFlowId, opts?) encola el voucher de arqueo en la cola del printer-daemon (requiere sesión cerrada; advierte si no lo está).

  • registerMovement(cashFlowId, movement) — agrega un CashMovement (asigna id).
  • updateMovement(cashFlowId, movementId, patch, actorUserId?) — corrige un movimiento.
  • deleteMovement(cashFlowId, movementId, actorUserId?) — remueve el movimiento del array.

Las tres operaciones rechazan con CASHFLOW_CLOSED si la caja está cerrada y emiten audit logs (cashflow.movement.create / .update / .delete).

CashFlowManager.computeSummary(cashFlow, payments) (estático) calcula el resumen en vivo combinando movimientos manuales y PaymentDocument enlazados:

  • totalCashIncome / totalExpenses — suma de movements por operation_type.
  • expectedCashInRegisteropening_amount + totalCashIncome - totalExpenses.
  • totalSales y salesByMethod — suma de Payment.amount agrupada por method_code.
  • totalTips y tipsByMethod — suma de tip_amount por método.

Solo se consideran pagos liquidados: SETTLED_PAYMENT_STATUSES = ['paid', 'completed'] (helper isSettledPayment). Los estados no liquidados (pending, processing, failed, refunded) se excluyen.

getSummary(cashFlowId) es el atajo async: carga la sesión, obtiene sus pagos vía BillingManager.getPaymentsByCashFlow y devuelve el CashFlowSummary.

computeCloseSnapshot(cashFlow, payments, params) (estático) construye la cuadratura:

  • Clasifica los pagos liquidados en total_cash / total_card / total_transfer según method_code (acepta variantes en inglés y español: cash/efectivo, transfer/transferencia, card/credit/debit/credito/debito).
  • expected_cash = opening_amount + income(movements) - expense(movements) + other_income - egresos.
  • difference = declared_cash - expected_cash (0 cuadra, > 0 sobrante, < 0 faltante).
  • excluded_payments_count — pagos enlazados no liquidados; no suman a ningún total pero se registran en el audit log al cerrar.

Si la sesión tiene max_close_difference_amount (snapshot de la config al abrir) y |difference| lo excede, closeCashFlow exige close_difference_reason (mínimo 10 caracteres); de lo contrario rechaza con CLOSE_DIFFERENCE_REASON_REQUIRED. El cierre con descuadre aceptado emite cashflow.close.difference_acknowledged.

Pagos creados sin sesión activa (offline, cashFlowId nulo o sentinel '-1', o apuntando a una sesión ya cerrada) se reasignan en dos puntos:

  • openCashFlow adopta los huérfanos de la caja al abrir.
  • recoverOrphanPayments(cashRegisterId) reasigna, a la sesión abierta actual, pagos liquidados dentro de la ventana temporal de esa sesión; para pagos en efectivo sin movimiento de venta crea el sale (y tip) correspondiente de forma idempotente.

recoverOrphanPayments se invoca antes de cada cierre y puede engancharse al evento online del NetworkMonitor con initRecoveryListener. Es idempotente y está protegido contra reentrancia por caja.

CashFlowManager emite entradas vía AuditLogManager: cashflow.open, cashflow.close, cashflow.movement.create / .update / .delete, payment.adopted, cashflow.orphan_payments.recovered, cashflow.close.excluded_payments, cashflow.close.difference_acknowledged y cashflow.close.pending_orders_overridden.