Link Search Menu Expand Document

Auditoría de calidad — 2026-07-02

Auditor: agente Claude (worktree aislado). Convenciones: severidad alta/media/baja; estado corregido o solo reportado.

1. URLs con backslash en frontend/lib/api-client.ts

Estado: verificado — NO presente en esta rama.

Se buscó exhaustivamente (rg '\\\\' sobre frontend/lib/api-client.ts y rg '`[^`]*\\api' sobre todo frontend/): cero backslashes en el archivo y ninguna URL de template string con \api en el frontend. getGoals (línea 1832) y deleteGoal (línea 1876) ya usan /api/goals. El bug reportado no existe en la base de este worktree (rama worktree-agent-af41565ba5c6ba88f, base dd0a4d8). Si en main u otra rama aún existe, el fix es reemplazar \ por / en esos template strings.

2. Fecha de hoy calculada en UTC en el frontend

La app define todayCO() en frontend/lib/utils.ts justamente para evitar new Date().toISOString(), que después de las 7 p.m. hora Colombia devuelve el día siguiente.

# Archivo:línea Descripción Severidad Estado
2.1 frontend/app/budgets/page.tsx:126,172 Fecha de inicio del formulario de presupuesto inicializada con new Date().toISOString().slice(0,10) (UTC) media corregidotodayCO()
2.2 frontend/app/agent/page.tsx:373 Fallback de fecha del borrador de transacción usaba fecha UTC media corregidotodayCO()
2.3 frontend/app/pending/page.tsx:275 Resaltado de “hoy” en el calendario de pendientes comparaba contra fecha UTC (después de 7 p.m. marca el día equivocado) media corregidotodayCO()
2.4 frontend/app/goals/page.tsx:41,113,127 Mismo patrón (new Date().toISOString().slice(0,10)) para formStartDate media corregidotodayCO() (aplicado en la sesión principal al integrar la auditoría, tsc --noEmit limpio)

Notas no-bug: start.toISOString().split('T')[0] en agent/page.tsx:376,400 opera sobre fechas ancladas a T12:00:00 local, por lo que no cruza de día en Colombia (UTC-5); los new Date().toISOString() de api-client.ts son timestamps (created_at, etc.), no fecha-de-hoy, y son correctos.

Backend: rg 'date\.today\(\)|datetime\.now\(\)|datetime\.utcnow\(\)' sobre backend/app/ solo encuentra menciones en el docstring de app/core/timezone.py. Limpio.

Verificación adicional: todas las URLs de api-client.ts usan ${API_URL}/api/... o ${API_URL}/health; no hay rutas malformadas.

3. Multi-tenant / ownership por profile_id

Patrón correcto verificado en budgets.py, categories.py, rules.py, taxes.py, export.py, subscriptions.py (obtienen get_active_profile_id(db) y validan obj.profile_id != active_profile_id → 404). notifications.py filtra por user.id vía servicio. TransactionEngine (usado por reports.py para pay_pending/pay_overdue) se auto-escopa con self.profile_id y get_transaction valida pertenencia. analytics/queries/cashflow/budget_service resuelven el perfil activo internamente.

# Archivo:línea Descripción Severidad Estado
3.1 backend/app/routers/payees.py (todo el router) + backend/app/models/payee.py Payee NO tiene columna profile_id: los beneficiarios son globales. Con auth JWT multi-tenant, cualquier usuario autenticado puede listar, leer, editar y borrar los payees de TODOS los demás usuarios (GET/PUT/DELETE /payees/{id} solo valida is_deleted). alta CORREGIDO (sesión principal, 2026-07-02): migración d4e6f8a0b2c1 agrega profile_id con backfill (perfil dominante por transacciones; clona y remapea si varios perfiles usan el mismo payee; huérfanos → perfil más antiguo) y reemplaza el unique global de name por unique(profile_id, name). Router, TransactionEngine, agente, importador, nlp_llm y export ahora filtran por perfil activo. Tests: tests/test_payees_multitenant.py (3 casos). Merge de heads de alembic en f0a1b2c3d4e5 (la rama breb_key venía en paralelo).
3.2 backend/app/routers/conversations.py:16-29 ConversationMessage tampoco está escopado por usuario; hay mitigación parcial (S-8: el session_id debe empezar con el fingerprint de 8 chars del user.id, pero session_id == "default" es accesible por todos, y el fingerprint es adivinable si se conoce el UUID). El propio código lo reconoce como follow-up. media solo reportado — requiere migración (columna user_id)

4. Signos de pasivos y Decimal vs float

Los modelos usan Numeric(18,4) / Decimal para todos los montos (account.balance, transaction.amount, budget.amount, goal.*) — correcto. float() aparece casi solo en fronteras de serialización (respuestas JSON), lo cual es aceptable.

# Archivo:línea Descripción Severidad Estado
4.1 backend/app/services/analytics.py:149-159 summary(): una cuenta de pasivo con balance POSITIVO (p. ej. tarjeta de crédito sobrepagada) se excluye tanto de activos como de pasivos → el patrimonio neto ignora ese saldo a favor. debts_status() (queries.py:102) es consistente al omitirla como deuda, pero el neto queda incompleto. media solo reportado — decisión de producto (¿sumar a activos o mostrar como pasivo negativo?)
4.2 backend/app/services/transaction_engine.py:1057-1128 _process_installments: el cronograma de cuotas se calcula en float y cada cuota se redondea a 2 decimales por separado → la suma de cuotas puede diferir del total en centavos (p. ej. 100000/3 → 3×33333.33 = 99999.99). El caso con interés fija la última cuota al balance restante, pero el caso sin interés no absorbe el residuo. baja solo reportado — fix sugerido: calcular en Decimal y ajustar la última cuota con el residuo
4.3 backend/app/services/queries.py:104-125 debts_status opera en float para deuda/límite/porcentajes; solo presentación, sin persistencia. baja solo reportado (aceptable)

Consistencia de signos verificada en transaction_engine.py:906-996: expense/asset_purchase/liability_discharge restan del origen; income/asset_sale/liability_acquisition suman; los pasivos acumulan balance negativo y debts_status/summary los leen con abs(balance) cuando balance < 0. Coherente.

5. Verificaciones sin hallazgo

  • Schemas Pydantic: TransactionCreate/Update, splits, etc. usan Decimal con gt=0 y validación de suma de splits con tolerancia 0.01. Correcto.
  • profiles.py: list/activate/delete validan profile.user_id == current_user.id; FKs hijos con ondelete="CASCADE" y SQLite con PRAGMA foreign_keys=ON. Correcto.
  • Routers excluidos de esta auditoría (accounts.py, transactions.py, goals.py): revisados solo en lectura — todos validan profile_id != active_profile_id en cada db.get. Sin hallazgos.
  • Montaje de rutas (main.py:135-157): todos los routers bajo /api con auth; los endpoints públicos (ingest/whatsapp/billing webhook/support ticket) tienen auth propia documentada.
  • Fechas con epoch (new Date(tx.date * 1000) en frontend): correcto, son timestamps del store local, no fecha-de-hoy.
  • todayCO() (frontend/lib/utils.ts:15): usa toLocaleDateString("en-CA", { timeZone: "America/Bogota" }) → formato YYYY-MM-DD correcto.

6. Limitaciones de esta pasada

  • npx tsc --noEmit no es ejecutable en el worktree (no hay node_modules instalado); los fixes de frontend son sustituciones mecánicas de expresión (new Date().toISOString()...todayCO()) con import agregado, verificadas por inspección. Ejecutar tsc en el checkout principal tras el merge.
  • No se corrió la suite completa de backend (20 min); no se tocó código backend.