Plan de Pruebas — Módulo Cartera Inteligente (Sprints 0 → 1 → IA-1..IA-4 → 3A)

---

Conceptos clave antes de empezar

┌────────────────────────┬──────────────────────────────────────────────────────────────────────────────────┐
│ Término                │ Qué es en la práctica                                                            │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Fuente                 │ De dónde lee la cartera: `erp-novasis` (facturas del ERP) o `cartera-importada`  │
│                        │ (tabla propia poblada por Excel/CSV o API)                                       │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Modo operativo         │ operacional = fuente de verdad; co-pilot = solo recomienda; híbrido = mix        │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Worklist               │ Bandeja priorizada por mora (facturas o docs importados con saldo > 0)           │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Mi día                 │ Agenda: seguimientos programados para hoy + cuentas críticas + KPIs             │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Recomendación IA       │ Canal + mensaje sugerido por cliente (short, cacheado 24h)                       │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Informe ejecutivo IA   │ Análisis largo estructurado por cliente para gerencia (cacheado 24h)             │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Resumen del día IA     │ Vista portfolio-level: prioridades del día + alertas + salud (cacheado 24h)      │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Campaña                │ Envío masivo WhatsApp/SMS con templates de placeholders                          │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ Webhook saliente       │ POST a URLs suscritas cuando ocurren eventos de dominio                          │
├────────────────────────┼──────────────────────────────────────────────────────────────────────────────────┤
│ API key                │ Credencial `sk_carc_*` para que sistemas externos llamen a la API REST           │
└────────────────────────┴──────────────────────────────────────────────────────────────────────────────────┘

---

Pre-requisitos antes de probar

1. Empresa con el submódulo `COB_CARTERA_INTELIGENTE` habilitado en su suscripción.
2. Usuario admin con permisos `COB_CART_VER` y `COB_CART_CONFIGURAR` asignados a su perfil.
3. Migraciones aplicadas:
   - `20260629_cartera_inteligente_setup`
   - `20260629_cartera_config_simplificar`
   - `20260629_cartera_importada`
   - `20260630_cartera_gestiones`
   - `20260630_cartera_webhook_deliveries`
   - `20260630_ai_dashboard_recalculo_auto`
   - `20260701_cartera_recomendaciones_auto`
   - `20260701_cartera_campanas`
   - `20260701_cartera_onboarding`
   - `20260701_cartera_whitelabel`
   - `20260701_cartera_api_keys`
4. Para pruebas con IA: Dashboard IA → Configuración con Anthropic/OpenAI/Ollama configurado + toggle "IA activa" ON.
5. Para pruebas con webhooks: cuenta en https://webhook.site (URL única para probar payloads).
6. Para pruebas con campañas: `TWILIO_ACCOUNT_SID`, `TWILIO_AUTH_TOKEN`, `TWILIO_PHONE_NUMBER` configurados en el `.env` del backend.
7. Archivos de prueba disponibles en `smartfactvoice-backend/samples/`:
   - `cartera-importada-ejemplo.csv`
   - `cartera-importada-ejemplo.xlsx`

═══════════════════════════════════════════════════════════════════
SECCIÓN A — Onboarding y Configuración inicial (Sprint 0 + 2N)
═══════════════════════════════════════════════════════════════════

PRUEBA A.1 — Primer ingreso al módulo (wizard automático)

Dónde: sidebar Cobranzas → Cartera Inteligente

Pasos:
1. Ingresar por primera vez al módulo con un usuario que sí tiene permisos.
2. El wizard de onboarding debe abrirse automáticamente (`onboarding_completado_en IS NULL`).
3. Paso 1 (Bienvenida): revisar el grid de 6 features del módulo.
4. Paso 2 (Modo operativo): seleccionar "Operacional".
5. Paso 3 (Fuente de datos): seleccionar "ERP Novasis".
6. Paso 4 (IA automática): dejar en OFF.
7. Paso 5 (Confirmación): revisar chips con las decisiones tomadas.
8. Click "Empezar a usar".

Resultado esperado:
- Wizard se cierra sin errores.
- Toast verde "¡Configuración inicial completa!".
- El Hub se carga sin volver a mostrar el wizard.
- En la BD: `tenant_cartera_config.onboarding_completado_en` debe estar seteado.

Resultado negativo:
- Si se cierra con la X sin completar, próxima vez que entre debe volver a mostrarlo (a menos que haya hecho click en "Saltar").


PRUEBA A.2 — Saltar el onboarding

Dónde: wizard onboarding

Pasos:
1. Ingresar por primera vez.
2. En cualquier paso, click "Saltar".

Resultado esperado:
- Wizard cierra inmediatamente.
- `onboarding_completado_en` queda seteado con la fecha actual.
- Al recargar el Hub, no vuelve a aparecer.


PRUEBA A.3 — Cambiar modo operativo

Dónde: Hub → sección "Configuración del tenant"

Pasos:
1. Cambiar "Modo operativo" a "Co-pilot".
2. Click "Guardar configuración".

Resultado esperado:
- Toast "Configuración guardada".
- Badge en el header cambia a "Co-pilot" (chip azul con ícono robot).
- En Mi día y Worklist aparece alert amarilla informativa "Modo Co-pilot".
- Botón "Registrar cobro" DESAPARECE del worklist.
- Título del modal de gestión cambia a "Registrar acción realizada".


PRUEBA A.4 — Cambiar fuente de datos

Dónde: Hub → Configuración → "Fuente de datos (default)"

Pasos:
1. Cambiar a "Cartera importada (Excel)".
2. Guardar.

Resultado esperado:
- Toast confirma.
- Al abrir Worklist, la tabla queda vacía (aún no importaste nada) → EmptyState "No hay cuentas pendientes".
- El badge de fuente arriba del worklist muestra "cartera-importada".

═══════════════════════════════════════════════════════════════════
SECCIÓN B — Importación de cartera externa (Slice 2B)
═══════════════════════════════════════════════════════════════════

PRUEBA B.1 — Importar CSV de ejemplo (fuente cartera-importada)

Dónde: Hub → tile "Importar cartera"

Pre-req: fuente = `cartera-importada` (Prueba A.4).

Pasos:
1. Click "Seleccionar archivo".
2. Elegir `samples/cartera-importada-ejemplo.csv`.
3. Click "Subir y procesar".
4. En el paso 2 (Mapear columnas): revisar que el mapeo automático detectó:
   - `Cod Cliente` → cliente_externo_id
   - `Razon Social` → cliente_nombre
   - `RUC` → cliente_documento
   - `Telefono` → cliente_telefono
   - `Email` → cliente_email
   - `Nro Factura` → documento_numero
   - `Total` → monto_total
   - `Saldo` → saldo_pendiente
   - `Emision` → fecha_emision
   - `Vencimiento` → fecha_vencimiento
5. Click "Confirmar e importar".

Resultado esperado:
- Preview muestra 20 primeras filas.
- Al confirmar: pantalla de resultado con **30 válidas / 0 inválidas**.
- CTA "Ver worklist" navega y muestra las 30 filas.

Resultado negativo:
- Si el mapeo automático falla en alguna columna, el paso 2 marca en rojo los campos requeridos sin mapeo → botón "Confirmar" queda deshabilitado.


PRUEBA B.2 — Importar el mismo archivo dos veces (idempotencia)

Pasos:
1. Repetir Prueba B.1 con el mismo archivo.

Resultado esperado:
- Resultado final: **30 actualizadas / 0 creadas / 0 inválidas**.
- La constraint `(empresa_id, cliente_externo_id, documento_numero)` evita duplicados.


PRUEBA B.3 — Importar Excel

Pasos:
1. Seleccionar `samples/cartera-importada-ejemplo.xlsx`.
2. Confirmar el mapeo (debería ser el mismo automático).

Resultado esperado:
- Procesa las 30 filas exactamente igual que el CSV.


PRUEBA B.4 — Archivo con fila inválida

Pasos:
1. Editar una copia del CSV: en una fila poner `Total = ""` (vacío).
2. Importar.

Resultado esperado:
- Paso final: **29 válidas / 1 inválida**.
- Tabla de errores muestra la fila con motivo "monto_total requerido".


PRUEBA B.5 — Cambiar fuente de vuelta a ERP

Dónde: Hub → Configuración

Pasos:
1. Cambiar fuente a "ERP Novasis" y guardar.
2. Abrir Worklist.

Resultado esperado:
- Worklist ahora muestra facturas del ERP (no las importadas).
- Los datos importados NO se pierden — al volver a `cartera-importada` reaparecen.

═══════════════════════════════════════════════════════════════════
SECCIÓN C — Worklist y gestiones (Slices 2A, 2C, 2C-bis, 2H, 2I, 2J)
═══════════════════════════════════════════════════════════════════

PRUEBA C.1 — Ver Worklist con datos

Dónde: Hub → "Abrir worklist" o `/cartera-inteligente/worklist`

Pasos:
1. Abrir el worklist.

Resultado esperado:
- Header con avatar cuadrado + chips (cuentas, fuente, modo).
- Filtros: search, mora mínima, toggle "Solo mis clientes".
- Tabla con avatar de iniciales por cliente + accent vertical según mora (rojo>90d, ámbar>30d, gris otros).
- Chip de mora con label "N d" o "Al día" (verde).
- 4 acciones por fila: Nueva gestión / Historial / Ver cliente / Registrar cobro (esta última solo si erp-novasis + operacional/híbrido).


PRUEBA C.2 — Búsqueda con debounce

Pasos:
1. Escribir "Distribuidora" en el buscador.
2. Observar la pestaña Network.

Resultado esperado:
- Después de 400ms de no tipear, dispara una sola request.
- La tabla se filtra a los clientes que matcheen.
- No hay una request por tecla.


PRUEBA C.3 — Filtro "Solo mis clientes" (user sin cobrador vinculado)

Pre-req: usuario NO está vinculado a `vendedores_cobradores.usuario_id`.

Pasos:
1. Activar toggle "Solo mis clientes".

Resultado esperado:
- Tabla queda vacía.
- Alert warning: "No estás asignado como cobrador en este sistema. Pedile al admin que te vincule en Contactos → Vendedores y Cobradores."


PRUEBA C.4 — Filtro "Solo mis clientes" (user vinculado)

Pre-req: usuario vinculado a un cobrador X + clientes tienen `cobrador_id = X`.

Pasos:
1. Activar toggle.

Resultado esperado:
- Tabla filtra solo los clientes con `cobrador_id = X`.
- Alert warning NO aparece.


PRUEBA C.5 — Registrar gestión simple (resultado NO_ATIENDE)

Dónde: Worklist → ícono teléfono en una fila

Pasos:
1. Click "Nueva gestión".
2. Tipo: LLAMADA. Resultado: NO_ATIENDE. Observación: "No contesta".
3. Próxima fecha: mañana 10:00 AM.
4. Click "Registrar gestión".

Resultado esperado:
- Toast "Gestión registrada".
- El drawer del cliente muestra la gestión en el historial.
- Mañana esa gestión aparece en Mi día → Seguimientos.


PRUEBA C.6 — Registrar gestión + promesa formal (resultado PROMESA_PAGO)

Pre-req: fuente = `erp-novasis`.

Pasos:
1. Click "Nueva gestión" en una factura del ERP.
2. Resultado: PROMESA_PAGO.
3. Debe aparecer bloque adicional "PROMESA FORMAL":
   - Fecha prometida (default = hoy + 7 días).
   - Monto prometido (MonedaInput con la moneda de la cuenta).
   - Evidencia: Verbal.
   - Pagador: Cliente.
   - Notas.
4. Completar los campos.
5. Botón cambia a "Registrar gestión + promesa".
6. Click.

Resultado esperado:
- Toast "Gestión + promesa formal registradas".
- En Mesa de Gestión → Promesas o en Cartera Inteligente → Promesas aparece la nueva promesa.
- La gestión en `cob_gestion` tiene `promesa_creada_id` linkeado.
- Twilio queda con recordatorio D-1 agendado.


PRUEBA C.7 — Registrar cobro desde Worklist

Pre-req: fuente = `erp-novasis`, modo = `operacional` u `hibrido`.

Pasos:
1. Click ícono dólar verde ("Registrar cobro") en una fila.
2. El wizard `NuevoReciboMultiWizard` se abre con el cliente preseleccionado.
3. Seleccionar la factura.
4. Elegir medio de pago (EFECTIVO / TRANSFERENCIA / etc.).
5. Confirmar.

Resultado esperado:
- Toast "Recibo creado correctamente".
- Wizard cierra automáticamente.
- Worklist se refresca (React Query invalida queries).
- Si la factura quedó pagada, desaparece del worklist.
- Si fue pago parcial, muestra el nuevo saldo.
- Webhook `cobro.registrado` se dispara (verificar en Webhooks → Deliveries).


PRUEBA C.8 — Botón cobro OCULTO en modo co-pilot

Pre-req: modo = `co-pilot`.

Pasos:
1. Abrir Worklist.

Resultado esperado:
- Ícono dólar NO aparece en ninguna fila.
- Alert informativa arriba explica el modo Co-pilot.

═══════════════════════════════════════════════════════════════════
SECCIÓN D — Cliente drawer + IA (Slices 2D, IA-1, IA-2)
═══════════════════════════════════════════════════════════════════

PRUEBA D.1 — Abrir drawer del cliente

Dónde: Worklist → click nombre del cliente O ícono user-circle

Pasos:
1. Click sobre un cliente.

Resultado esperado:
- Drawer right (560px) abre.
- Header: nombre + chip de fuente + info de contacto (documento/tel/email si están cargados).
- 3 KPIs: Cuentas / Saldo total (rojo) / Gestiones total.
- Tabs: Cuentas / Gestiones / Informe IA.


PRUEBA D.2 — Generar recomendación IA

Pre-req: AI Dashboard configurado + IA activa.

Pasos:
1. En el drawer del cliente, click "Generar recomendación" en la card IA.
2. Esperar ~3-5s.

Resultado esperado:
- Chip canal sugerido (WhatsApp/Llamada/Visita/Email/SMS) con ícono + color.
- Chip prioridad (alta rojo / media ámbar / baja verde).
- Mensaje sugerido en quote `«...»` con botón copiar al portapapeles.
- Alert con razonamiento (por qué es prioritario y por qué ese canal).
- Chip "cacheado" NO aparece en primera generación.

Resultado negativo:
- Sin config IA en Dashboard → error 503 con toast "IA no configurada".
- Sin API key → error 503 con "Cargá la API key en Dashboard IA".
- Con `activo=false` → toast "IA desactivada para esta empresa".


PRUEBA D.3 — Cache de recomendación (segunda apertura)

Pasos:
1. Cerrar drawer y volver a abrir el MISMO cliente dentro de 24h.
2. Click "Generar recomendación".

Resultado esperado:
- Resultado INSTANTÁNEO (sin loading).
- Chip "cacheado" visible.
- Modelo usado visible al lado del chip cacheado.


PRUEBA D.4 — Regenerar recomendación

Pasos:
1. Con recomendación cacheada visible, click ícono refresh.

Resultado esperado:
- Nueva llamada IA (loading ~3-5s).
- Puede devolver mensaje distinto (temperatura 0.3).
- Chip "cacheado" desaparece.


PRUEBA D.5 — Informe ejecutivo IA

Pasos:
1. En drawer, click tab "Informe IA".
2. Click "Generar informe".
3. Esperar ~5-10s (respuesta más larga).

Resultado esperado:
- Card con accent secondary.
- Chip de nivel de riesgo (alto/medio/bajo con ícono + color).
- Alert "Esta semana:" con resumen ejecutivo.
- Sección "Perfil de cumplimiento" (2-3 oraciones).
- Sección "Evolución de la mora".
- 3 listas con íconos semánticos: Factores de riesgo (rojo) / Acciones correctivas (ámbar) / Oportunidades (verde).
- Botón "Copiar informe completo" formatea todo a texto plano y lo manda al portapapeles.


PRUEBA D.6 — JSON truncado (fix del bug)

Pre-req: usar un modelo con `max_tokens` bajo, ej. Ollama con `llama3.2:1b`.

Pasos:
1. Generar recomendación.

Resultado esperado:
- Si la respuesta se corta, el parser intenta reparar y devolver algo válido.
- Log del backend: "Resumen del día: JSON truncado reparado heurísticamente".
- Si no puede repararlo: toast rojo "La IA devolvió un JSON truncado e irreparable. Probá con un modelo más grande".

═══════════════════════════════════════════════════════════════════
SECCIÓN E — Mi día + Resumen IA del día (Slices 2F, IA-3)
═══════════════════════════════════════════════════════════════════

PRUEBA E.1 — Abrir Mi día (sin datos programados)

Dónde: Hub → "Abrir Mi día"

Pre-req: sin gestiones con `proxima_fecha` seteada.

Pasos:
1. Abrir Mi día.

Resultado esperado:
- Header con avatar + toggle "Solo mis clientes" + refresh.
- 3 KPIs: Para hoy=0 / Atrasados=0 / Gestiones hoy=0.
- Sección "Seguimientos pendientes" con EmptyState.
- Sección "Top 10 cuentas críticas" con las cuentas del worklist con mora>0.


PRUEBA E.2 — Ver seguimientos programados para hoy

Pre-req: al menos una gestión con `proxima_fecha` dentro del día actual.

Pasos:
1. Registrar una gestión con próxima_fecha = hoy + 2h (Prueba C.5).
2. Abrir Mi día.

Resultado esperado:
- KPI "Para hoy" = 1.
- Fila en "Seguimientos pendientes" con chip verde "Hoy".
- Muestra info del cliente + documento + próxima acción.
- Botón "Hacer ahora" abre GestionDialog con esa cuenta.


PRUEBA E.3 — Seguimiento atrasado

Pasos:
1. Modificar en DB una gestión con `proxima_fecha` ayer.
2. Abrir Mi día.

Resultado esperado:
- KPI "Atrasados" >= 1.
- Fila muestra chip rojo "Atrasado 1d".


PRUEBA E.4 — Generar resumen IA del día

Dónde: Mi día → card "Resumen IA del día" (arriba de los KPIs)

Pre-req: AI Dashboard configurado + al menos 5 cuentas con mora.

Pasos:
1. Click "Generar resumen del día".
2. Esperar 5-10s.

Resultado esperado:
- Chip de salud de cartera (sana/tensionada/crítica con ícono + color).
- Contador "N cuentas analizadas".
- Resumen general en itálica (2-3 oraciones).
- Sección "Prioridades del día" con 3-5 clientes numerados.
- Cada prioridad clickeable → abre drawer del cliente.
- Sección "Alertas" con warnings apilados.

Caso especial: sin cuentas con mora
- Devuelve resultado directo sin llamar al LLM (modelo: "sin-llm", salud="sana").
- No consume tokens.

Resultado negativo:
- Sin AI configurado → toast "IA no disponible" (503).


PRUEBA E.5 — Cache del resumen (misma empresa dentro de 24h)

Pasos:
1. Generar resumen (E.4).
2. Cerrar la página y volver a abrir Mi día.

Resultado esperado:
- Al hacer click en "Generar resumen del día" de nuevo: resultado INSTANTÁNEO.
- Chip "cacheado" visible.


PRUEBA E.6 — Toggle "Solo mis clientes" en Mi día

Pre-req: usuario vinculado a cobrador X con clientes asignados.

Pasos:
1. Activar toggle.

Resultado esperado:
- KPIs y listas filtran solo cuentas del cobrador X.
- Los seguimientos también filtran (solo gestiones cuya `cliente.cobrador_id = X`).

═══════════════════════════════════════════════════════════════════
SECCIÓN F — Dashboard Supervisor (Slice 2L + 2O Excel)
═══════════════════════════════════════════════════════════════════

PRUEBA F.1 — Abrir Dashboard

Dónde: Hub → "Abrir dashboard"

Pasos:
1. Abrir el dashboard con rango default (30 días).

Resultado esperado:
- Header con avatar violeta + selector de rango (7/14/30/60/90 d) + botón "Excel".
- 4 BigKpi cards: Saldo total / Saldo vencido / Tasa de mora / Cumplimiento promesas.
- 2 paneles side-by-side: Promesas (barras) + Gestiones por canal (barras).
- Sparkline con tendencia de gestiones diarias.
- Panel "Top cobradores" con avatars rankeados.
- Tabla "Top 10 clientes en mora".


PRUEBA F.2 — Cambiar rango

Pasos:
1. Cambiar selector a "7 días" y esperar refresh.
2. Cambiar a "90 días".

Resultado esperado:
- Sparkline redimensiona (7 barras / 90 barras).
- Contadores de KPIs y barras se actualizan.
- Refetch automático cada 60s se mantiene.


PRUEBA F.3 — Exportar Excel

Pasos:
1. Click "Excel".

Resultado esperado:
- Descarga automática de `cartera-dashboard-30d.xlsx` (o el rango elegido).
- Archivo abre con Excel/LibreOffice y tiene 6 hojas:
  - Resumen (indicadores generales)
  - Promesas
  - Gestiones por canal
  - Gestiones por cobrador
  - Tendencia diaria
  - Top morosos
- Toast verde "Excel descargado".

═══════════════════════════════════════════════════════════════════
SECCIÓN G — Campañas masivas WhatsApp/SMS (Slice 2K)
═══════════════════════════════════════════════════════════════════

PRUEBA G.1 — Preview de campaña

Dónde: Hub → "Abrir campañas" → botón verde "Nueva campaña"

Pasos:
1. Wizard paso 1: Nombre "Test campaña", canal "WhatsApp", mora mínima 30.
2. Paso 2: usar template sugerido "Recordatorio cordial" (con placeholders).
3. Paso 3: click "Ver preview".

Resultado esperado:
- 3 KPIs: Elegibles / Con teléfono / Sin teléfono.
- Tabla con muestra de 5 clientes: nombre + teléfono enmascarado (`0981••4567`) + saldo + mora.

Resultado negativo:
- Si ningún cliente tiene teléfono cargado → Alert rojo "Ningún cliente elegible tiene teléfono".


PRUEBA G.2 — Crear y enviar campaña

Pre-req: Twilio configurado + algunos clientes con teléfono válido.

Pasos:
1. Con preview OK, click "Enviar a N clientes".

Resultado esperado:
- Toast "Campaña enviada — los mensajes se están procesando".
- Vuelve a la lista de campañas.
- La campaña aparece con estado "enviando" + LinearProgress avanzando.
- Cada 10s el poll refresca el estado.
- Cuando finaliza: estado = "completada", enviados = N, fallidos = 0 (idealmente).
- Se dispara webhook `campana.enviada` (verificar en Webhooks).


PRUEBA G.3 — Cancelar campaña en curso

Pasos:
1. Crear otra campaña con muchos envíos.
2. Antes de que termine, click ícono X en la fila.
3. Confirmar cancelación.

Resultado esperado:
- Toast "Campaña cancelada".
- Estado = "cancelada".
- Envíos con status "pendiente" se marcan como "cancelado".
- Envíos ya enviados quedan intactos.


PRUEBA G.4 — Placeholders del template

Pasos:
1. Nueva campaña con template: `Hola {{nombre}}, saldo {{saldo}}, doc {{documento}}, mora {{dias_mora}}d, venc {{vencimiento}}`
2. Enviar.

Resultado esperado:
- El mensaje llega al teléfono con todos los placeholders reemplazados por los datos del cliente.
- En Twilio Console debe verse el mensaje completo.

═══════════════════════════════════════════════════════════════════
SECCIÓN H — Webhooks salientes (Slice 2E + 2G)
═══════════════════════════════════════════════════════════════════

PRUEBA H.1 — Crear endpoint webhook

Dónde: Hub → "Configurar webhooks" → "Nuevo endpoint"

Pre-req: URL única de webhook.site.

Pasos:
1. Evento: "Gestión registrada".
2. URL: la de webhook.site.
3. Secreto HMAC: `mi-secreto-super-seguro-123456`.
4. Crear.

Resultado esperado:
- Toast "Endpoint creado".
- El endpoint aparece en la lista con toggle activo=ON.
- Chip "Secreto" muestra shield-check verde.


PRUEBA H.2 — Verificar delivery al registrar una gestión

Pasos:
1. Ir al Worklist, registrar una gestión cualquiera.
2. Volver a Webhooks → sección "Deliveries recientes".

Resultado esperado:
- Aparece una fila con:
  - Evento: `gestion.registrada`
  - Estado: `entregado` (verde)
  - HTTP: 200
- En webhook.site: el POST llegó con:
  - Header `X-Cartera-Event: gestion.registrada`
  - Header `X-Cartera-Delivery: <uuid>`
  - Header `X-Cartera-Signature: sha256=<hmac>`
  - Body JSON: `{ evento, payload, timestamp, empresa_id }`


PRUEBA H.3 — Endpoint que falla (retry exponencial)

Pasos:
1. Crear endpoint con URL inválida `https://esta-url-no-existe-jamas.example.com/webhook`.
2. Registrar una gestión.

Resultado esperado:
- Primera entrega falla → estado `falla_temporal`.
- Retry a los 30s → falla → `falla_temporal`.
- Retry a los 60s → falla → `falla_temporal`.
- Retry a los 120s → falla → estado final `falla_definitiva`.
- 3 intentos totales visibles en la tabla.


PRUEBA H.4 — Deshabilitar endpoint

Pasos:
1. Toggle activo=OFF en un endpoint.
2. Registrar gestiones.

Resultado esperado:
- No se crean deliveries nuevas para ese endpoint.
- Los otros endpoints activos SÍ reciben.


PRUEBA H.5 — Webhook de cobro (integración con CobrosService)

Pre-req: endpoint suscrito a `cobro.registrado`.

Pasos:
1. Registrar un cobro cualquiera desde POS o desde el worklist.

Resultado esperado:
- Webhook `cobro.registrado` llega al endpoint con: `recibo_id`, `numero_recibo`, `cliente_id`, `monto_total`, `fecha`.


PRUEBA H.6 — Webhook de promesa cumplida

Pre-req: endpoint suscrito a `promesa.cumplida`.

Pasos:
1. Cobranzas → Mesa de Gestión → Promesas → marcar una promesa como "Cumplida".

Resultado esperado:
- Webhook `promesa.cumplida` llega con el detalle de la promesa.

═══════════════════════════════════════════════════════════════════
SECCIÓN I — API REST entrante (Slice 3A)
═══════════════════════════════════════════════════════════════════

PRUEBA I.1 — Crear API key

Dónde: Hub → "API REST" → "Nueva API key"

Pasos:
1. Nombre: "Test integración".
2. Scopes: sync + cobros + gestiones (todos).
3. Expira en: 30 días.
4. Crear.

Resultado esperado:
- Modal con la key plaintext (`sk_carc_xxxxxxxx...`).
- Alert warning: "Esta es la única vez que vas a ver la key".
- Botón copiar al portapapeles funciona.
- Al cerrar el modal, en la tabla se ve solo el prefijo (`sk_carc_xxxx…`).


PRUEBA I.2 — Llamada a /sync con la key

Pasos:
1. Copiar la key.
2. Ejecutar en terminal (reemplazar `sk_carc_XXXX`):
```bash
curl -X POST http://localhost:3000/api/v1/cartera/api/sync \
  -H "X-Api-Key: sk_carc_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "modo": "delta",
    "documentos": [{
      "cliente_externo_id": "TEST-001",
      "cliente_nombre": "Test SA",
      "cliente_telefono": "0981234567",
      "documento_numero": "FAC-TEST-001",
      "monto_total": 1000000,
      "saldo_pendiente": 1000000,
      "fecha_emision": "2026-06-01"
    }]
  }'
```

Resultado esperado:
- HTTP 201 con JSON: `{ modo: "delta", creados: 1, actualizados: 0, ... }`.
- En Cartera Inteligente (fuente=cartera-importada) → Worklist muestra la nueva fila.


PRUEBA I.3 — Key inválida

Pasos:
1. Mismo curl pero con `X-Api-Key: sk_carc_INVENTADA123`.

Resultado esperado:
- HTTP 401 "API key inválida, expirada o revocada.".


PRUEBA I.4 — Sin header

Pasos:
1. Mismo curl sin el header X-Api-Key.

Resultado esperado:
- HTTP 401 "Falta header X-Api-Key. Generá una en Cartera Inteligente → API → Claves.".


PRUEBA I.5 — Scope insuficiente

Pasos:
1. Crear otra API key con solo scope `sync`.
2. Llamar a `/api/v1/cartera/api/cobros` con esa key.

Resultado esperado:
- HTTP 403 "La API key no tiene scope \"cobros\". Scopes actuales: sync".


PRUEBA I.6 — Notificar cobro (integración con webhook)

Pre-req: endpoint suscrito a `cobro.registrado`.

Pasos:
1. Con documento importado del test I.2:
```bash
curl -X POST http://localhost:3000/api/v1/cartera/api/cobros \
  -H "X-Api-Key: sk_carc_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "cliente_externo_id": "TEST-001",
    "documento_numero": "FAC-TEST-001",
    "monto": 500000,
    "moneda_codigo": "PYG",
    "fecha": "2026-06-30T15:00:00Z",
    "medio_pago": "TRANSFERENCIA"
  }'
```

Resultado esperado:
- HTTP 201 con `{ ok: true, documento_encontrado: true, saldo_actualizado: 500000 }`.
- Saldo del documento en BD: 500.000 (1.000.000 - 500.000).
- Webhook `cobro.registrado` con `origen: "api-externa"` llega al endpoint suscrito.


PRUEBA I.7 — Registrar gestión externa

Pasos:
1. Con doc importado, llamar:
```bash
curl -X POST http://localhost:3000/api/v1/cartera/api/gestiones \
  -H "X-Api-Key: sk_carc_XXXX" \
  -H "Content-Type: application/json" \
  -d '{
    "cliente_externo_id": "TEST-001",
    "documento_numero": "FAC-TEST-001",
    "tipo_gestion": "LLAMADA",
    "resultado": "PROMESA_PAGO",
    "observacion": "Cliente promete pagar el 5/07"
  }'
```

Resultado esperado:
- HTTP 201 con `{ ok: true, gestion_id: <uuid> }`.
- En el drawer del cliente → tab Gestiones: aparece la gestión.
- Webhook `gestion.registrada` se dispara con `origen: "api-externa"`.


PRUEBA I.8 — Revocar key

Pasos:
1. En la UI → click ícono trash rojo en la fila.
2. Confirmar.

Resultado esperado:
- Toast "API key revocada".
- Estado cambia a "revocada" (chip rojo).
- Cualquier request con esa key devuelve HTTP 401.


PRUEBA I.9 — Verificar auditoría

Pasos:
1. Ir a la tab "Auditoría".

Resultado esperado:
- Tabla con las últimas llamadas (autorefresh 15s).
- Cada fila: fecha, method, endpoint, key nombre, status code (verde/ámbar/rojo), latency, error si aplica.


PRUEBA I.10 — Sync modo snapshot (desactiva no-incluidos)

Pasos:
1. Con la key activa, llamar sync con `modo=snapshot` y solo 1 documento.

Resultado esperado:
- HTTP 201 con `{ desactivados: N }` donde N = docs previos que no estaban en el payload.
- En BD: `cartera_importada_documentos.active = false` para los desactivados.
- Worklist muestra solo el documento del snapshot.

═══════════════════════════════════════════════════════════════════
SECCIÓN J — Cron IA recomendaciones automáticas (Slice IA-4)
═══════════════════════════════════════════════════════════════════

PRUEBA J.1 — Activar cron

Dónde: Hub → Configuración → sección "Recomendaciones IA automáticas"

Pasos:
1. Activar toggle "Generar resumen IA del día automáticamente".
2. Seleccionar hora: la hora ACTUAL + 1 (para probar sin esperar).
3. Guardar.

Resultado esperado:
- Toast "Configuración guardada".
- En BD: `recomendaciones_auto_activas=true`, `recomendaciones_auto_hora=<H>`.


PRUEBA J.2 — Esperar el tick del cron

Pre-req: endpoint suscrito a `recomendacion.alta_prioridad`.

Pasos:
1. Esperar a la hora configurada (o modificar directamente `tenant_cartera_config.recomendaciones_auto_hora = <hora_actual>` en DB para forzar).
2. El cron corre cada hora al minuto 5.

Resultado esperado:
- En logs del backend: "Tick recomendaciones IA — hora=X, empresas elegibles=1".
- Log siguiente: "Empresa <id>: N recomendaciones emitidas (salud=X)".
- En webhook.site: N POSTs con `recomendacion.alta_prioridad` (uno por prioridad, con `rank: 1..N`).
- En BD: `tenant_cartera_config.ultimo_recomendaciones_auto_at` actualizado.


PRUEBA J.3 — No re-ejecutar en la misma hora

Pasos:
1. Después de J.2, esperar el próximo tick (misma hora).

Resultado esperado:
- El cron entra pero salta la empresa porque `ya corrió hoy`.
- Log: "Empresa <id>: ya corrió hoy, skip".


PRUEBA J.4 — Desactivar cron

Pasos:
1. Toggle a OFF.
2. Esperar próximo tick.

Resultado esperado:
- El cron NO procesa esa empresa (WHERE `recomendaciones_auto_activas=true`).
- Ningún webhook se dispara.

═══════════════════════════════════════════════════════════════════
SECCIÓN K — White-label (Slice 2P)
═══════════════════════════════════════════════════════════════════

PRUEBA K.1 — Personalizar nombre y logo

Dónde: Hub → Configuración → "Marca (white-label)"

Pasos:
1. Nombre: "NovaCobranza".
2. Logo URL: `https://placehold.co/128x128/6366f1/ffffff?text=NC`.
3. Color primario: `#22c55e`.
4. Guardar.

Resultado esperado:
- Al recargar el Hub:
  - Header muestra "NovaCobranza" en vez de "Cartera Inteligente".
  - Ícono cuadrado reemplazado por la imagen del logo.
  - Descripción cambia a "Plataforma de cobranza inteligente".


PRUEBA K.2 — Logo con URL inválida

Pasos:
1. Setear `marca_logo_url = "https://url-invalida-que-no-existe.example.com/logo.png"`.

Resultado esperado:
- Al recargar, el `img.onError` esconde la imagen y NO rompe el layout.
- El nombre custom sigue mostrándose.


PRUEBA K.3 — Volver al default

Pasos:
1. Borrar los 3 campos de marca.
2. Guardar.

Resultado esperado:
- Header vuelve a "Cartera Inteligente" con el ícono cerebro.

═══════════════════════════════════════════════════════════════════
SECCIÓN L — Permisos y multi-tenant
═══════════════════════════════════════════════════════════════════

PRUEBA L.1 — Usuario sin permiso COB_CART_VER

Pre-req: usuario con perfil que NO tiene el permiso.

Pasos:
1. Login con ese usuario.
2. Intentar acceder a `/cartera-inteligente`.

Resultado esperado:
- Redirección a Access Denied (o pantalla con mensaje "No tenés permisos").
- Item "Cartera Inteligente" NO aparece en el sidebar.


PRUEBA L.2 — Usuario con COB_CART_VER pero sin COB_CART_CONFIGURAR

Pasos:
1. Login.
2. Abrir el Hub.

Resultado esperado:
- Puede ver todas las tiles y navegar a Worklist, Mi día, Dashboard, etc.
- En Configuración operativa: los inputs están disabled.
- Botón "Guardar configuración" disabled.
- Botón "Importar Excel" disabled.
- Botón "Nueva campaña" disabled.
- Botón "Nueva API key" disabled.


PRUEBA L.3 — Aislamiento entre empresas

Pre-req: 2 empresas A y B con Cartera Inteligente activada.

Pasos:
1. Login en empresa A → crear campaña, endpoint webhook, API key.
2. Cambiar a empresa B.

Resultado esperado:
- En B: campañas vacías, endpoints vacíos, API keys vacías.
- Ningún dato de A visible en B (aislamiento por `empresa_id`).

═══════════════════════════════════════════════════════════════════
SECCIÓN M — Casos borde y regresiones
═══════════════════════════════════════════════════════════════════

PRUEBA M.1 — Módulo cartera importada sin datos

Pasos:
1. Fuente = `cartera-importada`, sin importar nada.
2. Abrir Worklist, Mi día, Dashboard.

Resultado esperado:
- Todas las pantallas cargan sin errores.
- EmptyStates apropiados.


PRUEBA M.2 — Cartera con miles de facturas (performance)

Pre-req: empresa con >1000 facturas pendientes.

Pasos:
1. Abrir Worklist.
2. Buscar y filtrar.

Resultado esperado:
- Paginación funciona (25/50/100 por página).
- Debounce evita saturar el backend.
- Query < 500ms.


PRUEBA M.3 — Multi-moneda

Pre-req: facturas en PYG, USD y BRL.

Pasos:
1. Abrir Worklist.

Resultado esperado:
- Cada fila muestra el saldo con `fmtMoneda(saldo, moneda_codigo)` — símbolo correcto por moneda.
- No hay conversión automática — se muestra la moneda de la factura.


PRUEBA M.4 — Módulo activo con IA desactivada

Pre-req: `ai_empresa_config.activo = false`.

Pasos:
1. Todas las features NO-IA funcionan (Worklist, Gestiones, Campañas, Cobros, Webhooks).
2. Botones "Generar recomendación" / "Generar informe" / "Generar resumen" devuelven toast "IA desactivada para esta empresa".

Resultado esperado:
- El módulo Cartera Inteligente sigue 100% operativo sin IA.

═══════════════════════════════════════════════════════════════════
Checklist final — Antes de dar por probado el módulo
═══════════════════════════════════════════════════════════════════

- [ ] Onboarding se muestra en primer ingreso y se persiste al completar
- [ ] Importación CSV/Excel funciona idempotente (constraint unique respetada)
- [ ] Worklist ordena por vencimiento más antiguo
- [ ] Filtros con debounce (400ms) no saturan backend
- [ ] Toggle "Solo mis clientes" respeta vinculación cobrador↔usuario
- [ ] Gestión + Promesa formal en una sola operación (transacción)
- [ ] Cobro desde worklist reusa `NuevoReciboMultiWizard` con cliente preseleccionado
- [ ] Modo co-pilot: bloquea escritura (botón cobro oculto)
- [ ] Recomendación IA cachea 24h por cliente y usa config `ai_empresa_config`
- [ ] Informe ejecutivo IA se genera con distinta versión de prompt
- [ ] Resumen IA del día es portfolio-level (1 llamada por empresa por día)
- [ ] Parser tolera JSON truncado por límite de tokens
- [ ] Mi día muestra correctamente "Hoy" vs "Atrasado N días"
- [ ] Dashboard KPIs se refrescan cada 60s
- [ ] Export Excel del Dashboard tiene 6 hojas
- [ ] Campañas: preview, envío, tracking, cancelación
- [ ] Placeholders en templates: `{{nombre}}`, `{{saldo}}`, `{{documento}}`, `{{dias_mora}}`, `{{vencimiento}}`
- [ ] Webhooks: HMAC-SHA256, retry exponencial 3x (30s/60s/120s)
- [ ] Webhook `cobro.registrado` desde CobrosService del ERP
- [ ] Webhook `promesa.cumplida/incumplida` desde PromesasService
- [ ] Webhook `campana.enviada` al finalizar campaña
- [ ] Webhook `recomendacion.alta_prioridad` desde cron IA-4
- [ ] API key con hash SHA-256 (nunca plaintext en DB)
- [ ] API key plaintext se muestra UNA sola vez
- [ ] Scopes de API key respetados (sync/cobros/gestiones)
- [ ] Auditoría de llamadas API visible con últimos 100 requests
- [ ] Cron IA-4 respeta `recomendaciones_auto_activas` per empresa
- [ ] Cron IA-4 evita doble-corrida en misma hora
- [ ] White-label: nombre/logo/color personalizables
- [ ] Permisos COB_CART_VER y COB_CART_CONFIGURAR correctamente aplicados
- [ ] Multi-empresa: aislamiento total entre tenants
- [ ] Multi-moneda: `fmtMoneda(value, code)` derivada de la entidad
- [ ] Fechas: `fmtFechaCalendario` para dates, `fmtFechaHora` para timestamps
- [ ] Todas las pantallas con `ScreenGuia` colapsable
- [ ] Iconografía 100% `lucide:*` (no mixing con `mdi:`)
- [ ] Skeletons durante loading (no spinners full-screen)
- [ ] Módulo completamente funcional aunque IA esté desactivada

---

Referencia de logs útiles para debug

- Backend: `pm2 logs smartfactvoice-backend | grep -i cartera`
- Cron IA-4: buscar "Tick recomendaciones IA"
- Webhook processor: buscar "Delivery ... OK/fallo"
- Campanas processor: buscar "SMS enviado a"
- API key guard: buscar "carteraApiContext"
- IA-3 JSON truncado: buscar "JSON truncado reparado heurísticamente"
