# Núcleo de cobranzas

El evaluador de cartera implementa reglas de dominio puras y no depende de Express, WhatsApp, base de datos ni de los contratos legacy de Medusa.

> Manual operativo del módulo: [Manual de uso](./MANUAL_DE_USO.md)

## Política obligatoria para pruebas con datos reales

Mientras se utilice la API productiva del cliente, todas las pruebas son estrictamente de solo lectura respecto de sistemas externos.

Está prohibido durante pruebas:

- enviar WhatsApp, SMS, correos o iniciar cualquier otro contacto con clientes reales;
- crear o modificar tickets, promesas de pago, pizarras, facturas o datos de clientes en Phantom;
- ejecutar cualquier operación externa con efectos laterales, aunque exista un endpoint documentado;
- conectar un cambio de estado local con un envío real.

Las escrituras funcionales se limitan a las tablas locales `cobranza_*` de Medusa; Phantom permanece en modo lectura. El transporte de WhatsApp inicia `DESHABILITADO`, admite una lista cerrada en `PRUEBA` y requiere habilitación explícita mediante `PRODUCCION` para contactar destinos elegibles.

## Orden de evaluación

1. Con la consulta masiva se valida estado de servicio, balance negativo y cantidad de comprobantes adeudados.
2. Solo para los preseleccionados se consultan los comprobantes y sus primeros vencimientos.
3. Para clientes activos se usa `MedioPago` entregado por la consulta masiva, incluso cuando su valor está vacío.
4. La consulta avanzada queda únicamente como fallback si Phantom omite completamente ese campo.
5. Los suspendidos que califican no necesitan consultar el medio de pago.

Este orden evita ejecutar una consulta avanzada para toda la cartera.

## Casos

- `ACTIVO_SIN_DEBITO`: activo, balance negativo y al menos un comprobante adeudado, vencido o no; `MedioPago` no es `SDC` ni `SDT`.
- `ACTIVO_CON_DEBITO`: las mismas condiciones, con `MedioPago` igual a `SDC` o `SDT`.
- `SUSPENDIDO`: suspendido, balance negativo, al menos dos comprobantes adeudados y al menos dos impagos vencidos.

Una factura se considera vencida cuando su primer vencimiento es igual o anterior a la fecha efectiva de evaluación. Para activos, el vencimiento es información del reporte y no una condición de exclusión. Para suspendidos siguen siendo obligatorios al menos dos comprobantes impagos vencidos. Se usan los criterios numéricos explícitos del documento (`> 0` y `> 1`), no una igualdad exacta.

`medioPagoDisponible` diferencia un valor vacío confirmado por Phantom de una respuesta masiva que omitió el campo. Esto evita la consulta avanzada en el flujo normal sin perder el fallback de compatibilidad.

## Endpoint de diagnóstico

El servidor de API de Medusa (puerto 8081) expone:

```text
GET /cobranzas/clientes/:ida/evaluacion?fecha=YYYY-MM-DD
```

`fecha` es opcional y fija la fecha de corte usada para comparar vencimientos sobre los datos actuales de Phantom. La respuesta incluye `consultasRealizadas` para verificar que el detalle avanzado solo se solicite cuando sea necesario.

### Vista previa paginada

El endpoint siguiente ejecuta el generador sin persistir una campaña:

```text
POST /cobranzas/cartera/preview
Content-Type: application/json

{
  "fecha": "2026-08-06",
  "maxClientes": 100,
  "estados": ["Suspendido", "Activo"],
  "incluirExcluidos": false
}
```

La vista previa procesa primero `Suspendido`, limita la concurrencia contra Phantom a tres clientes y admite entre 1 y 200 clientes. Es una herramienta de validación; la generación completa se implementará como trabajo persistente para no mantener una petición HTTP abierta durante todo el recorrido.

## Ejecuciones persistentes

La migración aditiva está en `backend/database/cobranzas.sql`. Crea `cobranza_ejecuciones` y `cobranza_ejecucion_clientes` sin modificar tablas existentes.

Crear una ejecución en segundo plano:

```text
POST /cobranzas/cartera/ejecuciones
Content-Type: application/json

{
  "fecha": "2026-08-06",
  "estados": ["Suspendido", "Activo"]
}
```

`maxClientes` es opcional y permite limitar una corrida de prueba. La creación responde `202 Accepted` con un `idEjecucion`.

Consultar estado y progreso:

```text
GET /cobranzas/cartera/ejecuciones/:idEjecucion
```

Consultar resultados persistidos:

```text
GET /cobranzas/cartera/ejecuciones/:idEjecucion/clientes
```

Filtros opcionales: `resultado=INCLUIDO|EXCLUIDO|ERROR`, `caso=ACTIVO_SIN_DEBITO|ACTIVO_CON_DEBITO|SUSPENDIDO`, `limit` y `offset`.

Las ejecuciones se procesan de a una dentro de una cola del proceso 8081.

### Snapshot operativo y diagnósticos

Las ejecuciones poseen tres tipos:

- `INICIAL`: crea la primera cartera operativa completa.
- `SINCRONIZACION`: compara una lectura completa contra el snapshot vigente.
- `DIAGNOSTICO`: permite limitar clientes o estados y nunca reemplaza la cartera operativa.

Las ejecuciones `INICIAL` y `SINCRONIZACION` siempre recorren Activos y Suspendidos, sin límite. La interfaz continúa leyendo el snapshot anterior durante todo el procesamiento. El nuevo snapshot se activa de manera atómica únicamente cuando la ejecución termina sin errores.

Al activar una sincronización se calculan nuevos deudores, regularizados, deuda aumentada, deuda reducida y casos sin cambios. Las gestiones abiertas conservan operador, estado, notas e historial. Un balance confirmado igual o mayor que cero finaliza la gestión automáticamente; si luego reaparece deuda gestionable, se reactiva como `PENDIENTE`. Tanto el cierre como la reactivación generan eventos locales del sistema.

La ausencia de un cliente o una ejecución con errores no se interpreta como pago. Una corrida operativa con errores queda disponible para diagnóstico, pero no reemplaza la cartera vigente.

Consultar el snapshot vigente y una sincronización en curso:

```text
GET /cobranzas/cartera/estado
```

## Ciclos auditables de cobranza

Los snapshots operativos pertenecen a un ciclo. Solo puede existir un ciclo abierto y las gestiones se identifican por la combinación `(idCiclo, phantom_cliente_id)`, por lo que una cuenta Phantom conserva una gestión independiente en cada período sin perder su identidad global.

Flujo del ciclo:

1. `POST /cobranzas/ciclos` crea el ciclo y encola su snapshot inicial completo.
2. `POST /cobranzas/ciclos/:id/sincronizaciones` compara contra el último snapshot del mismo ciclo.
3. `POST /cobranzas/ciclos/:id/cerrar` ejecuta una sincronización final y, si termina sin errores, congela el ciclo.
4. `GET /cobranzas/ciclos` permite consultar ciclos activos y cerrados.

Estados: `PREPARANDO`, `ACTIVO`, `CERRANDO`, `CERRADO` y `FALLIDO`. Una cancelación o falla durante el cierre devuelve el ciclo a `ACTIVO`; una creación inicial fallida queda auditada como `FALLIDO` y permite abrir otro ciclo.

`cobranza_ciclo_clientes` conserva a todos los clientes que ingresaron con deuda. Cuando Phantom confirma balance cero o positivo, el cliente pasa a `REGULARIZADO`, permanece visible en el ciclo y su gestión termina como `FINALIZADA_PAGO`. Si la deuda reaparece dentro del mismo ciclo, se reactiva la misma gestión. Al cerrar el ciclo, las gestiones todavía abiertas pasan a `FINALIZADA_CICLO`; el ciclo y sus notas quedan en modo de solo lectura.

Un nuevo ciclo genera gestiones nuevas para los deudores de ese período. Las gestiones de ciclos anteriores no se transfieren ni se sobrescriben y pueden consultarse desde el historial.

Cancelar una ejecución pendiente o en proceso:

```text
POST /cobranzas/cartera/ejecuciones/:idEjecucion/cancelar
```

La cancelación es cooperativa: deja terminar las consultas que ya están en vuelo, evita comenzar nuevos clientes y conserva los resultados procesados. Al iniciar 8081, las ejecuciones que quedaron `PENDIENTE` o `PROCESANDO` se reinician desde el comienzo de forma idempotente; primero se limpian sus resultados parciales. Una ejecución que quedó con `CANCELACION_SOLICITADA` se cierra como `CANCELADA`.

## Estados de gestión

Cada cliente incluido crea o actualiza una gestión independiente de los snapshots de ejecución. Una nueva generación no pisa una gestión abierta; si la gestión anterior estaba `FINALIZADA_PAGO`, una deuda nueva la reactiva como `PENDIENTE`. `NO_GESTIONAR` permanece bloqueada hasta una decisión explícita futura.

Estados disponibles:

- `PENDIENTE`
- `NOTIFICACION_PENDIENTE`
- `NOTIFICACION_ENVIADA`
- `LLAMADA_PROGRAMADA`
- `EN_GESTION`
- `SIN_RESPUESTA_NOTIFICACION`
- `SIN_RESPUESTA_LLAMADA`
- `PROMESA_PAGO`
- `COMPROBANTE_RECIBIDO`
- `FINALIZADA_PAGO`
- `NO_GESTIONAR`

Consultar bandeja y detalle:

```text
GET /cobranzas/gestiones
GET /cobranzas/gestiones/:idGestion
```

La bandeja acepta `estado`, `caso`, `operadorId`, `limit` y `offset`.

Cambiar estado:

```text
PATCH /cobranzas/gestiones/:idGestion/estado
Content-Type: application/json

{
  "estado": "EN_GESTION",
  "version": 1,
  "operadorId": 7,
  "actorId": 7,
  "notas": "Cliente contactado"
}
```

`version` implementa control de concurrencia. `EN_GESTION` exige `operadorId`; `LLAMADA_PROGRAMADA` y `PROMESA_PAGO` exigen `proximaAccionAt`. Cada cambio se registra en `cobranza_gestion_eventos`.

## Bandeja consolidada

```text
GET /cobranzas/bandeja
```

Combina la gestión actual, el último snapshot del cliente y sus comprobantes. Devuelve saldo deudor, medio de pago, períodos, primer vencimiento, cantidades impagas/vencidas, operador, próxima acción y una explicación de disponibilidad para contactar.

Filtros opcionales:

- `estado`
- `caso`
- `operadorId`
- `estadoServicio=Activo|Suspendido`
- `tipoDebito=CON_DEBITO|SIN_DEBITO`
- `conVencidas=true|false`
- `habilitado=true|false`
- `ciudad`
- `limit` y `offset`
- `fecha=YYYY-MM-DD`, útil para diagnósticos reproducibles

La bandeja prioriza suspendidos. Una gestión notificada queda bloqueada durante siete días; llamadas y promesas se habilitan al alcanzar `proximaAccionAt`; `FINALIZADA_PAGO` y `NO_GESTIONAR` permanecen excluidas.

## Vinculación con contactos Medusa

Phantom es la fuente de identidad y deuda; `contactos` de Medusa es la única fuente autorizada para teléfonos. La relación se calcula normalizando y comparando el documento de Phantom con `contactos.dni`. El teléfono informado por Phantom nunca se usa como destino.

Varios contactos pueden compartir DNI: representan distintos teléfonos de una misma cuenta y no son un error. Los estados son `VINCULADO`, `SIN_CONTACTO`, `SIN_TELEFONO_VALIDO` y `DOCUMENTO_PHANTOM_AUSENTE`. Si hay varios teléfonos válidos, el destino queda en `REQUIERE_SELECCION` hasta guardar una preferencia local.

```text
GET /cobranzas/contactos/conciliacion
PUT /cobranzas/clientes/:idCliente/contacto-preferido
DELETE /cobranzas/clientes/:idCliente/contacto-preferido
```

La conciliación admite los mismos filtros de la bandeja y `estadoVinculacion`. La preferencia guarda un `idContacto`, nunca copia el teléfono, y se acepta solo si el DNI coincide y el identificador termina en `@c.us` con formato individual válido.

La disponibilidad final exige simultáneamente que la gestión permita contacto y que exista un destino Medusa único o preferido. Ninguno de estos endpoints envía mensajes.

## Diseño diferido: colaboración en tiempo real

La interfaz de cobranzas podrá reutilizar la infraestructura existente de Socket.IO cuando las reglas de negocio, acciones y permisos del módulo estén confirmados por el cliente. Esta integración queda deliberadamente postergada para evitar modificar el contrato de sockets varias veces durante la etapa de definición.

La propuesta es crear una room exclusiva, inicialmente `cobranzas`, a la que se unan los operadores y administradores mientras tengan abierto el módulo. No debe acoplarse con el transporte ni con los procesos de WhatsApp.

Eventos previstos:

- `cobranzas:gestion-actualizada`
- `cobranzas:nota-agregada`
- `cobranzas:operador-asignado`
- `cobranzas:contacto-actualizado`
- `cobranzas:cartera-progreso`
- `cobranzas:cartera-finalizada`
- `cobranzas:presencia-actualizada`

Cada evento de datos deberá emitirse únicamente después de confirmar la transacción en MySQL. El payload mínimo debería incluir un identificador de evento, `idCliente`, `idGestion` cuando corresponda, nueva `version`, actor, fecha y los campos modificados. El receptor actualizará solo la fila, panel o contador afectado; ante pérdida de conexión deberá resincronizar mediante los endpoints HTTP vigentes.

El tiempo real complementará, pero no reemplazará, el control optimista por `version`. Si dos operadores escriben simultáneamente, MySQL y el error HTTP `409` seguirán definiendo qué operación es válida; el socket distribuirá el resultado confirmado para mantener alineadas las demás pantallas.

La presencia será efímera y no implicará por sí sola un bloqueo de escritura. Podrá informar:

- operadores conectados al módulo;
- quién tiene abierto un cliente;
- quién está editando o gestionando un caso;
- actividad visible para supervisores y administradores.

Antes de implementar este diseño deben quedar definidos:

1. permisos por rango para observar, tomar, reasignar y modificar gestiones;
2. acciones y estados definitivos del flujo de cobranza;
3. eventos que requieren auditoría persistente frente a presencia meramente temporal;
4. comportamiento de reconexión, expiración de presencia y conflictos;
5. datos que puede ver un administrador en el tablero en tiempo real.

## Acceso temporal al módulo

El primer despliegue utiliza listas blancas por `users.idUser`, independientes del rango general de Medusa:

```text
COBRANZAS_ACCESO_ADMIN_IDS=1,7
COBRANZAS_ACCESO_COBRANZA_IDS=12,18,24
```

Los IDs de la primera lista reciben la vista administrativa; los de la segunda, la vista operativa.
Si un ID aparece en ambas, prevalece `admin`. Las listas aceptan comas, punto y coma o espacios.
Un usuario ausente no ve el acceso en la navegación y recibe `403` si intenta utilizar la API; el
acceso directo a `cobranzas.html` lo devuelve a la pantalla principal. Con ambas listas vacías, el
módulo queda cerrado para todos.

## Instalación nueva de base de datos

Para una base que nunca tuvo el módulo, ejecutar únicamente y en este orden:

1. `backend/database/cobranzas.sql`;
2. `backend/database/cobranzas_comunicaciones.sql`;
3. `backend/database/cobranzas_instalacion_nueva_completar.sql`.

Los archivos `cobranzas_snapshot_operativo.sql`, `cobranzas_ciclos.sql`,
`cobranzas_plantillas_segmentadas.sql` y `cobranzas_chat_operativo.sql` son migraciones históricas
para instalaciones anteriores. No deben aplicarse después de los scripts consolidados porque sus
columnas ya existen. El script de finalización es idempotente: completa plantillas, área y cola sin
eliminar datos y puede ejecutarse nuevamente.

## Modos de envío

Antes de incorporar transporte de WhatsApp existe una política cerrada por defecto:

```text
COBRANZAS_ENVIO_MODO=DESHABILITADO
COBRANZAS_CONTACTO_PRUEBA_ID=
COBRANZAS_CONTACTOS_PRUEBA=<idContacto>:<telefono>@c.us[,<idContacto>:<telefono>@c.us]
COBRANZAS_TELEFONO_PRUEBA=
COBRANZAS_MAX_ENVIOS_POR_OPERACION=1
COBRANZAS_MAX_ENVIOS_DIARIOS=60
COBRANZAS_HORA_INICIO=9
COBRANZAS_HORA_FIN=20
COBRANZAS_INTERVALO_MINIMO=8
COBRANZAS_INTERVALO_MAXIMO=11
```

En una instalación local puede cambiarse el modo a `PRUEBA`. La política exige simultáneamente el `idContacto`, el teléfono actual, una confirmación literal y exactamente un mensaje. Cualquier diferencia bloquea la operación. Los valores reales permanecen únicamente en `.env`, fuera de Git.

Para habilitar la operación real debe configurarse explícitamente
`COBRANZAS_ENVIO_MODO=PRODUCCION` tanto en 8081 como en 8082. Este modo elimina solamente la
lista cerrada de destinatarios de prueba: continúa exigiendo un contacto individual válido de
Medusa, deuda actual, regla operativa, plantilla automática, gestión notificable, ausencia de un
envío anterior en el ciclo, confirmación explícita y un único mensaje por operación. La cola
mantiene además el cupo diario y la separación temporal configurada.

## Comunicaciones del MVP

- Los administradores configuran el plazo de respuesta y mantienen plantillas versionadas.
- El plazo predeterminado es de 48 horas corridas; cada comunicación conserva el valor aplicado al enviarse.
- La previsualización resuelve variables con el snapshot vigente y utiliza exclusivamente el contacto vinculado en Medusa.
- La gestión cambia a `NOTIFICACION_ENVIADA` únicamente cuando el transporte confirma el envío.
- Un reloj local en 8081 pasa las esperas vencidas a `SIN_RESPUESTA_NOTIFICACION`.
- El resultado `SIN_RESPUESTA_LLAMADA` lo registra manualmente un operador luego de intentar una llamada.
- Las plantillas pueden asignarse automáticamente a `ACTIVO_SIN_DEBITO`, `ACTIVO_CON_DEBITO` o `SUSPENDIDO`; las demás quedan para selección manual.
- `{{links_facturas}}` inserta los enlaces ya disponibles en los comprobantes del snapshot, sin consultar nuevamente a Phantom.
- WhatsApp conserva en memoria solo los teléfonos que esperan respuesta. Los demás mensajes no consultan tablas de cobranzas.
- Una respuesta vincula el `idMensaje` del chat, cambia la gestión a `EN_GESTION` y registra los archivos como pendientes de revisión sin duplicar los binarios.

El transporte permanece cerrado en `DESHABILITADO`, limitado a la lista configurada en `PRUEBA`
y habilitado para contactos elegibles en `PRODUCCION`.

La cola masiva requiere aplicar también la migración aditiva
`backend/database/cobranzas_cola_envios.sql`. Los lotes confirmados se distribuyen dentro de la
ventana configurada, respetan el cupo diario y cambian temporalmente la gestión a
`NOTIFICACION_PENDIENTE`. El proceso 8081 revisa una posición por minuto; el intervalo efectivo
de cada mensaje continúa determinado por su fecha `programado_at`.

### Permisos temporales

Hasta la reestructuración general de autenticación, la API contrasta el `idUser` y el `rango` informados por la interfaz contra `users`. Solo `admin` puede crear, sincronizar o cerrar ciclos, reintentar ejecuciones y modificar configuración o plantillas. Los operadores pueden consultar, gestionar, previsualizar y enviar dentro de la política activa.

La migración base es `backend/database/cobranzas_comunicaciones.sql`. Para una instalación que ya tenía comunicaciones, aplicar después `backend/database/cobranzas_plantillas_segmentadas.sql`; conserva las versiones existentes, separa los dos resultados sin respuesta y agrega el criterio automático de las plantillas.
