Link Search Menu Expand Document

Fixes — 2026-07-02

Registro de los arreglos hechos en esta sesión (rama main).

1. Metas híbridas: sync automático corregido + modo manual

Problema reportado: al asociar una cuenta a una meta de “pagar deuda”, el progreso se calculaba “por derecha” con el saldo completo, sin que el usuario eligiera monto. Además la meta de deuda nacía completada.

Causas:

  • pay_off_debt calculaba current = target - balance, pero los pasivos guardan saldo negativo → progreso instantáneo ≥ 100%.
  • El selector de cuentas del frontend no filtraba por tipo (podías atar la cuenta de ahorros a una meta de deuda).
  • No existía forma de destinar un monto parcial: atar cuenta = usar todo el saldo.

Solución:

  • Nuevos campos en goals: tracking_mode (auto/manual) y baseline_balance (snapshot de la deuda al atar la cuenta). Migración e5f7a9b1c3d2.
  • Modo auto deuda: progreso = |deuda al atar| - |deuda actual|, acotado a [0, target]. Metas viejas sin snapshot usan target como deuda inicial.
  • Modo manual: la cuenta queda como referencia y el progreso avanza solo con aportes explícitos (POST /goals/{id}/contribute).
  • Validación 400 si el tipo de cuenta no corresponde al tipo de meta.
  • Editar el nombre de una meta ya no re-toma el snapshot (no resetea progreso).
  • Frontend: selector filtrado por tipo, toggle ⚡Automático/✋Manual, botón “Aportar Monto” para metas manuales, badge “Sincronizado”/”Referencia”.

Tests: backend/tests/test_goals.py (5 casos nuevos, 12/12 pasan).

2. Corregir saldo de apertura no guardaba

Problema reportado: editar el saldo de apertura desde el listado de transacciones y guardar dejaba el valor anterior.

Causa: las filas “Saldo inicial” son transacciones virtuales (id = initial_{account_id}, generadas al vuelo en GET /transactions con include_initial=true; no existen en la tabla transactions). El PUT /transactions/initial_xxx iba al motor de transacciones, no encontraba la fila y fallaba; el frontend refrescaba mostrando el valor viejo.

Solución (backend/app/routers/transactions.py):

  • PUT /transactions/initial_{account_id}: actualiza Account.initial_balance y desplaza Account.balance por el delta (preserva el efecto del historial de transacciones). En pasivos se conserva el signo (el form manda magnitud).
  • DELETE /transactions/initial_{account_id}: deja la apertura en 0 con el mismo ajuste de balance.
  • Validación de perfil activo y 404 si la cuenta no existe.
  • Frontend (transactions/page.tsx): el modal muestra “Corregir Saldo Inicial” con aviso de que solo aplica el monto.

Tests: backend/tests/test_initial_balance.py (5 casos, 5/5 pasan).

3. Bot de WhatsApp: responde “Moneybot” viejo pero no registra en la app

Diagnóstico final (2026-07-02, corrige la primera hipótesis de abajo):

El webhook de WhatsApp de Meta apunta al pipeline VIEJO de MoneyPro, no al backend nuevo. Evidencia:

  1. Los textos que envía el bot (“Pregunta:”, “📋 Detalles a registrar:”, “¿Procedo?”) no existen en este repo ni en toda su historia git (git log --all -S sin resultados) → responde otro codebase.
  2. CloudWatch muestra que la cola SQS vieja moneypro-commands.fifo (cuenta AWS 811710375370) recibió mensajes hoy exactamente cuando el usuario escribió → Meta entrega los mensajes al flujo viejo (webhook → AWS → SQS → bot viejo).
  3. El bot viejo escribe en la base de datos vieja de MoneyPro, por eso “hace como que registra” pero nada aparece en finanzasai.me.
  4. El backend nuevo en api.finanzasai.me está vivo y con verify_token/app_secret configurados (verificado con curl), pero Meta nunca le envía nada.

Hipótesis inicial descartada: el WHATSAPP_ACCESS_TOKEN del .env LOCAL está vencido desde 01-abr-26 (error 190), pero ese es solo el token de desarrollo; el pipeline viejo responde con su propio token válido.

MIGRACIÓN COMPLETADA (2026-07-02, misma tarde) — pasos ejecutados:

  1. El usuario revocó el token viejo y generó uno nuevo de la app MoneyProBot (id 942101211562111, la única app con WhatsApp del negocio).
  2. Token nuevo verificado contra Graph API (200) y guardado en backend/.env; App Secret confirmado contra producción con un POST firmado de payload vacío (200 → la firma valida).
  3. WHATSAPP_ACCESS_TOKEN actualizado en DigitalOcean vía doctl apps update (app 34921df6, deployment de spec quedó ACTIVE).
  4. Webhook re-apuntado por API (POST /{app_id}/subscriptions con app token app_id|app_secret):
    • Antes: https://8u8c5ws3xb.execute-api.us-east-1.amazonaws.com/webhook (API Gateway del pipeline MoneyPro viejo — por eso no aparecía fácil en el dashboard: estaba configurado por API).
    • Ahora: https://api.finanzasai.me/api/whatsapp/webhook (verificado por Meta con el verify_token, subscription active:true).
  5. El número (Test Number de Meta, +1 555-171-9269) no tiene override de webhook a nivel de teléfono: hereda la URL nueva.

Pendientes:

  • Actualizar WHATSAPP_ACCESS_TOKEN en Azure App Service (no afecta al bot: el webhook apunta a DO, pero el env quedó con el token revocado).
  • Apagar el pipeline viejo de AWS (API Gateway 8u8c5ws3xb + Lambda + cola SQS moneypro-commands.fifo, cuenta 811710375370) cuando el bot nuevo quede confirmado en uso.
  • Migrar del Test Number a un número real de WhatsApp (el test tiene límite de 5 destinatarios).
  • Prueba end-to-end del usuario: CONFIRMADA (transcript 13:19-14:08). El bot nuevo responde y registra.

4. Fluidez del bot nuevo (hallazgos del transcript de prueba)

Del transcript de la primera prueba real salieron 4 bugs, corregidos:

  1. ** literales en WhatsApp: los mensajes usaban negrita Markdown (**texto**) pero WhatsApp usa UN asterisco → se veían asteriscos sueltos. Fix: format_draft_confirmation_message convierte *** al final; mensajes de éxito y bienvenida reescritos con *.
  2. Confirmación disparada por corrección: “Cambia a categoría Alimentación si existe…” registraba la transacción porque is_confirmation_message aceptaba CUALQUIER palabra del set en el mensaje (“si” ∈ mensaje → confirmar). Fix: solo frases exactas o mensajes ≤3 palabras que EMPIEZAN con palabra de confirmación.
  3. Error técnico crudo al usuario: ACCOUNT_AMBIGUOUS:Cartera:... llegaba tal cual al chat. Fix: se traduce a pregunta conversacional (“Tienes varias cuentas que coinciden con Cartera: … ¿con cuál?”) y el draft se conserva para reintentar.
  4. “¿Cuánto tengo en Nequi?” devolvía TODOS los saldos: la ruta regex llegaba a handle_query sin params.account. Fix: se detecta la cuenta mencionada en el texto contra los nombres del perfil (si varias coinciden, muestra solo esas).

Causa de fondo de la falta de fluidez: producción NO tenía LLM_PROVIDER configurado → el “cerebro” Bedrock estaba apagado y todo caía a las heurísticas regex (por eso “el restaurante y pagué” terminó como categoría). Fix operativo: LLM_PROVIDER=bedrock + BEDROCK_MODEL_ID agregados al spec de DigitalOcean (las credenciales AWS ya estaban). Para acercarse a la fluidez del bot MoneyPro original, ver el plan de mejoras en bot-moneypro-original-informe.md §4 (prompt restrictivo, campo faltante, validación post-LLM).

Mejora implementada: nuevo endpoint de diagnóstico GET /api/whatsapp/status que valida el token contra Meta y reporta token_valid, token_error y webhook_subscribed, para que este tipo de fallo silencioso sea visible sin acceso a logs.

5. Fluidez fase 2: virtudes del bot MoneyPro original portadas

Implementación del plan de bot-moneypro-original-informe.md §4 sobre el bot nuevo (LLM Bedrock + fallback regex). Cambios:

  1. Prompt restrictivo con Reglas de Oro (nlp_llm._build_context): 11 reglas imperativas numeradas (“NUNCA inventes categorías”, “NUNCA inventes montos”, “payee solo si es un nombre propio”) + 10 ejemplos con casos edge (consulta vs transacción, multi, cuotas, corrección de borrador). Categorías en formato jerárquico Tronco: subcategorías con instrucción de preferir la subcategoría (virtud 5).
  2. _normalize_draft ya no inventa defaults: amount puede quedar None (el usuario no lo dijo), account_name/payee_name quedan None en vez de “Cartera”/”Establecimiento” duros. En multi los drafts sin monto se descartan (se registran directo, sin confirmación).
  3. Confirmación en dos fases con campo faltante (virtud 2): nueva agent.validate_draft(db, draft)(draft, pregunta|None). Si falta el monto (o la cuenta es ambigua/inexistente, o falta el destino de una transferencia) el bot pregunta SOLO ese campo, con action clarify_draft (sin botones Sí/Cancelar) y el borrador parcial persistido en whatsapp_drafts (status=”incomplete”) → sobrevive redeploys y turnos. La respuesta del usuario (“45 mil”, “con Nequi Personal”) pasa por apply_feedback_to_draft + re-validación.
  4. Validación post-LLM antes de confirmar (virtud 4):
    • Cuentas resueltas contra la BD en fase de BORRADOR: un ACCOUNT_AMBIGUOUS ahora sale como pregunta con opciones ANTES de confirmar, no como error al registrar; los nombres quedan exactos.
    • Payees genéricos rechazados (_GENERIC_PAYEES, portado del original): “el restaurante”, “la tienda” → sin payee. Tampoco se registra más el payee basura “Establecimiento” por defecto.
    • Cuenta ausente → autocompleta con la cuenta por defecto de la categoría o la primera del perfil (misma regla del engine), y la confirmación muestra la cuenta REAL.
    • “sí” sobre un borrador incompleto NO registra: re-pregunta.
  5. Compatibilidad ingest: /api/ingest/notification acepta también clarify_draft (sobreescribe cuenta/payee con datos de la notificación y re-valida al confirmar). El camino regex ya no fuerza “Cartera”.

Tests: 5 nuevos en test_agent_llm.py (campo faltante en dos turnos, “sí” sobre incompleto, payee genérico, cuenta ambigua en dos turnos, cuenta inexistente). Suites verdes: agent_llm 11, whatsapp, ingest, new_features, transaction_engine, api, payees, balance_invariants.

Compatibilidad Azure (migración en unas semanas): todo es código backend puro + env vars — nada atado a DigitalOcean. Pendientes en Azure App Service: WHATSAPP_ACCESS_TOKEN nuevo (el viejo está revocado), LLM_PROVIDER=bedrock, BEDROCK_MODEL_ID y credenciales AWS. El webhook de Meta apunta al dominio api.finanzasai.me: al transferir el dominio de Namecheap solo se cambia el DNS hacia Azure (+ certificado del custom domain); no hay que tocar Meta.