Files
FRASA-CAS/api-gateway
Consult AS Technology 8a2584438d fix(auditoria general): 25 errores encontrados en la revision modulo por modulo
SEGURIDAD (agente IA):
- Conversaciones: GET /ai/conversations/:id y el reanudar chat verifican
  propiedad (antes cualquier usuario leia/continuaba la conversacion de otro).
- /ai/documents pasa a solo-admin (antes cualquien podia inyectar o borrar
  documentos de la base de conocimiento).

DATOS / CORRUPCION:
- Cotizador: elegir un cliente del maestro en el buscador ya no crea un lead
  fantasma "Registro previo (CRM anterior)" en el embudo.
- Afluencia con oportunidadId inexistente: 404 en vez de crear sombra fantasma.
- Cobranza: los 103 planes importados quedaron con vendedor asignado (7 con su
  asesor real segun el Excel, el resto al admin) y el motor de recordatorios
  cae al admin cuando un plan no tiene vendedor: antes los 411 recordatorios
  se registraban 'sin-destinatario' y no los veia nadie.
- Backups: script reescrito para el host real (sqlite .backup + uploads + env
  en /root/backups) y cron instalado (antes: script que asumia docker y sin
  cron programado = cero respaldos).

REPORTES / METRICAS:
- Mi tablero: el embudo ya excluye cancelaciones reales (Cancelo/Cancelacion
  Apartado) y cuenta como vendidos Firma Cliente/Escriturado/Titulado (antes
  41 "apartados" donde habia 13 activos y vendidos buscaba una etapa que no
  existe); tasa de descarte ya compara el mismo periodo.
- Scoring: etapa real 'Agendo Cita' (antes 'AGENDO' nunca aplicaba) y los
  descartados/perdidos ya no reciben score.
- ver_cobranza (IA): ignora planes cancelados y no cuenta como vencido el pago
  que vence hoy.

UI / FLUJOS:
- mi-dia: typo CANCO -> CANCELO (los cancelados reaparecen en Escrituracion);
  confirmar cita ya no retrocede la etapa de Cotizador/Aparto (boton y kanban).
- alta-clientes: la firma ya no se borra al navegar fuera del paso de firmas;
  toast de exito solo cuando el API confirma; prefill de apartado trae
  direccion, estado civil y fecha de nacimiento desde el alta.
- Auditoria: el filtro por usuario funciona (el front enviaba 'usuario' y el
  API esperaba 'usuarioId') y 'hasta' incluye todo el dia.
- Contactos: paginacion aplicada despues de fusionar fuentes (antes saltaban o
  se repetian contactos entre paginas y 'Siguiente' se deshabilitaba antes de
  tiempo).
- Buscador global: leads -> /hoy/, contactos -> /; el cotizador entiende
  ?cotizacion=<id> para retomar desde la liga.
- Export mi-dia: columnas Desarrollo/Origen rellenas y sin duplicados.
- Recibo de cobranza: "Recibio" es FRASA (el cliente paga, no recibe).
- Alta de plan de cobranza: proyecto libre con sugerencias del catalogo real
  (antes limitado a 3 proyectos que no cubrian la cartera).

Verificado en vivo: autorizacion IA (3x 403), filtro de auditoria, 404 de
afluencia. Suites: 717 API + 124 alta-clientes + 115 cotizador + 92 cobranza +
89 landing + 32 mi-dia en verde. Backup de hoy generado en /root/backups.
2026-09-24 10:54:09 +00:00
..

FRASA API Gateway

Backend Node.js que conecta las 3 apps web del ecosistema FRASA (cotizador :3001, alta-clientes :3002, cobranza :3003) con Monday.com y da persistencia real (Prisma + SQLite en dev, Postgres 16 en prod).

Stack

Express 5 · TypeScript estricto · zod · Prisma (SQLite dev) · multer 2 · vitest

Cómo correr en dev

npm install
npx prisma migrate dev   # crea/actualiza prisma/dev.db
npm run dev              # tsx watch en :3000

Las credenciales de Monday y los board IDs se leen del .env raíz del ecosistema (../.env); el servicio también acepta un .env local (Prisma CLI lo requiere para DATABASE_URL). El token NUNCA se imprime en logs.

Tests

npm test   # vitest: fórmulas del cotizador (fixture U-101), cobranza, monday.service con fetch mockeado

Endpoints (prefijo /api)

Método Ruta Descripción
GET /api/health `{status, monday: 'up'
GET /api/unidades Inventario desde Monday (?disponibles=true filtra; ?desarrollo= filtra por DIAGONAL/ATELIER/ESTANZA/SENDA/SOLANA)
GET /api/unidades/:codigo Una unidad (404 si no existe)
POST /api/cotizaciones Calcula propuesta (server-side), persiste y crea Oportunidad en Monday. Asocia vendedorId = sub del JWT
GET /api/cotizaciones · /api/cotizaciones/:id Consulta
GET /api/cotizaciones/mias Últimas 50 del vendedor autenticado (más recientes primero)
GET /api/cotizaciones/:id/publico Público (sin auth), destino del QR del PDF: folio, desarrollo, unidad, propuesta, montos, vigencia y nombre del vendedor. NUNCA datos de contacto del cliente
POST /api/cotizaciones/alertas-inventario Alerta de cambio de estatus (ver abajo)
POST /api/clientes Alta de cliente: persiste payload completo, crea Contacto y Apartado en Monday
GET /api/clientes/:id Cliente con documentos
POST /api/clientes/:id/documentos Multipart (campo archivos, máx 10, pdf/jpg/png, 10 MB)
GET /api/clientes/:id/documentos Lista documentos
POST /api/planes Crea plan de pago con calendario generado
GET /api/planes · /api/planes/:id Con saldo/status calculados
GET /api/planes/cobranza-del-dia Urgencias de cobranza (?diasAnticipacion=7)
POST /api/planes/:id/abonos Aplica abono (estricto al centavo)
POST /api/planes/:id/notas Agrega nota a la bitácora
POST /api/planes/:id/cancelar Cancela el plan
PATCH /api/planes/:id/monday Liga/desliga el item del tablero Apartado ({mondayItemId}; al liquidarse el plan, ese item pasa a X Escriturar)
POST /api/oportunidades/:mondayItemId/perder Etapa Perdido + dropdown motivo; registra en el tablero Motivos No Compra ({motivo, comentario?})
POST /api/webhooks/monday Handshake challenge; create_item en Leads dispara round-robin
GET /api/automatizaciones/estado Índice RR actual, vendedores, último escaneo
POST /api/automatizaciones/round-robin/run Pasada manual de asignación round-robin
GET /api/automatizaciones/inactividad/preview Leads que serían recordados hoy (solo lectura)

Automatizaciones de Monday (tablero Leads)

  • Round-robin de vendedores: leads sin persona en la columna people Vendedor reciben el siguiente vendedor de la lista rotativa. La lista viene de MONDAY_RR_USER_IDS (coma-separados) o, si está vacía, de los usuarios no-guest del workspace (cacheados en BD). El índice se persiste en la tabla AutomatizacionState (sobrevive reinicios).
  • Recordatorio de inactividad: leads con estado no terminal (DESCARTADO y ABANDONO son terminales) y más de INACTIVIDAD_DIAS (default 10) sin actualizarse reciben un update en el item. Dedup por día: clave recordatorio:<itemId>:<fecha> en AutomatizacionState.
  • Actividades automáticas (tablero Actividades): al asignar vendedor se crea "Contactar a {lead}" (tipo Llamada, hoy+1, Abierto) y el recordatorio de inactividad crea "Retomar contacto con {lead}" al vendedor asignado. Se apagan con ACTIVIDADES_ENABLED=false; un fallo al crear la actividad no rompe la asignación.
  • Setup CRM (npm run setup:crm): crea/alinea tableros, columnas y labels del vocabulario CRM FRASA (idempotente). Sync de desarrollos (npm run sync:desarrollos): recalcula Precio Desde/Hasta, M2, entrega y modelos del tablero Desarrollos desde los inventarios reales.
  • Sync de etapas (npm run sync:etapas -- [--archivo <path>] [--dry-run] [--semilla]): sincroniza la columna status Etapa del tablero Unidades (pipeline de 10 etapas: Apartado → Reserva → Vendido → Integración → En Análisis → Autorizado → Notaría → Escriturado → Entrega → PostVenta) desde un JSON [{codigo, etapa}] (default: el maestro reference/maestro/maestro-unidades.json). Idempotente; al llegar a Vendido/Escriturado crea la actividad de traspaso de departamento (a Escrituración / a SAC) con dedup traspaso:<codigo>:<etapa> en AutomatizacionState. En la PRIMERA corrida (carga histórica) usar --semilla: registra el dedup sin crear actividades retroactivas.
  • Disparadores: webhook POST /api/webhooks/monday (evento create_item en el board Leads) y, como respaldo mientras no haya URL pública, crons con node-cron: POLL_CRON (default */5 * * * *) para round-robin y 0 9 * * * para inactividad. Los crons se apagan con MONDAY_AUTOMATIONS_ENABLED=false.

Alerta de inventario (cambio de estatus de unidad)

POST /api/cotizaciones/alertas-inventario con body {"cambios": [{"unidadCodigo": "U-101", "statusNuevo": "APARTADO"}]} (máx 100). Por cada unidad busca cotizaciones de los últimos 30 días y crea una Actividad en Monday al vendedor de cada una ("La unidad X que cotizaste pasó a APARTADO", tipo Tarea, Abierto), solo si el vendedor tiene mondayUserId mapeado. Dedup persistente por unidad+cotización+estatus (clave alerta-inv:<unidad>:<cotizacionId>:<status> en AutomatizacionState); si Monday falla, esa alerta NO queda deduplicada y el siguiente sync la reintenta. Responde {cambiosProcesados, alertasCreadas, omitidas, fallidas}.

Cómo invocarlo desde el sync: al final de sync-etapas.ts / sync-estatus-maestro.py, recolectar los cambios de estatus aplicados y hacer un POST autenticado (Bearer JWT):

curl -X POST "$API_URL/api/cotizaciones/alertas-inventario" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"cambios":[{"unidadCodigo":"U-101","statusNuevo":"APARTADO"}]}'

(Wire directo pendiente: los syncs actuales no reportan su lista de cambios; por eso queda como endpoint invocable.)

Notas de arquitectura

  • Monday service (src/services/monday.service.ts): resuelve column IDs por TÍTULO en runtime (los slugs autogenerados nunca se hardcodean), paginación por cursor, retry con backoff ante 429/complejidad, column_values siempre via JSON.stringify en variables, cache de unidades 5 min con invalidación tras escrituras.
  • Cálculos del cotizador (src/services/cotizacion.service.ts): port exacto del Excel de producción (quirks incluidos: Infonavit usa precio de lista en mensualidades; enganche bancario sobre precio lista). NO corregir.
  • Cobranza (src/services/cobranza.service.ts): espejo de la lógica del frontend; los controllers mapean Prisma → dominio, ejecutan las funciones puras y persisten.
  • Dinero como Float por ahora — TODO prod: Decimal en Postgres.
  • Webhooks de Monday no están firmados → el payload se trata como no confiable; el evento solo es un disparador y el round-robin re-consulta el tablero vía API antes de escribir.

TODO producción (pendientes deliberados)

  • Auth (JWT ya hay vars en .env), helmet, rate-limit — es red interna por ahora.
  • Postgres 16 (provider = "postgresql", Decimal), Redis para cache de unidades.
  • CORS restringido a los orígenes de las apps.

Docker

docker build -t frasa-api-gateway .
docker run -p 3000:3000 --env-file ../.env frasa-api-gateway
# El arranque corre `prisma migrate deploy` antes de levantar el server