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 | corregido → todayCO() |
| 2.2 | frontend/app/agent/page.tsx:373 | Fallback de fecha del borrador de transacción usaba fecha UTC | media | corregido → todayCO() |
| 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 | corregido → todayCO() |
| 2.4 | frontend/app/goals/page.tsx:41,113,127 | Mismo patrón (new Date().toISOString().slice(0,10)) para formStartDate | media | corregido → todayCO() (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. usanDecimalcongt=0y validación de suma de splits con tolerancia0.01. Correcto. - profiles.py: list/activate/delete validan
profile.user_id == current_user.id; FKs hijos conondelete="CASCADE"y SQLite conPRAGMA foreign_keys=ON. Correcto. - Routers excluidos de esta auditoría (
accounts.py,transactions.py,goals.py): revisados solo en lectura — todos validanprofile_id != active_profile_iden cadadb.get. Sin hallazgos. - Montaje de rutas (
main.py:135-157): todos los routers bajo/apicon 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): usatoLocaleDateString("en-CA", { timeZone: "America/Bogota" })→ formato YYYY-MM-DD correcto.
6. Limitaciones de esta pasada
npx tsc --noEmitno es ejecutable en el worktree (no haynode_modulesinstalado); los fixes de frontend son sustituciones mecánicas de expresión (new Date().toISOString()...→todayCO()) con import agregado, verificadas por inspección. Ejecutartscen el checkout principal tras el merge.- No se corrió la suite completa de backend (20 min); no se tocó código backend.