1. Re-captura: el dedup por telefono/email ya no considera etapas cerradas (Descartado, Perdido, Cancelo, Cancelacion Apartado). Los restos de la ultima limpieza (17 filas de APARTADO cancelados sin contraparte activa) bloqueaban a los vendedores al re-capturar; los registros activos siguen bloqueando con 409 como siempre. 2. Bitacora compartida: GET /recordatorios con leadId/clienteClave devuelve las notas de TODOS los usuarios de ese cliente (antes cada quien solo veia las suyas). Mi dia pide la bitacora con esos filtros; la lista personal sigue siendo propia. 3. Origen en oportunidades: el resumen de mi-dia ya devuelve origen y la card de cada oportunidad lo muestra (telefono · desarrollo · canal · modelo). 4. Privacidad del buscador: /leads/buscar y /buscar filtran por dueno cuando el rol es vendedor (leads por usuarioId, contactos por capturadoPorId, cotizaciones por vendedorId). Los roles de supervision siguen viendo todo. Ya no aparecen leads de otros asesores con nombre y telefono al teclear. Verificado en vivo contra el API: 10/10 comprobaciones (re-captura del telefono cancelado 201, duplicado activo 409, nota del admin visible para la vendedora, origen presente en 27/27 oportunidades, buscador del vendedor sin datos ajenos y del admin completo). 716 pruebas del API y 32 de mi-dia en verde. Los tests de roles ahora usan prisma mockeado (la BD viva cambia cuando el cliente edita permisos desde la UI).
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
Vendedorreciben el siguiente vendedor de la lista rotativa. La lista viene deMONDAY_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 tablaAutomatizacionState(sobrevive reinicios). - Recordatorio de inactividad: leads con estado no terminal (
DESCARTADOyABANDONOson terminales) y más deINACTIVIDAD_DIAS(default 10) sin actualizarse reciben un update en el item. Dedup por día: claverecordatorio:<itemId>:<fecha>enAutomatizacionState. - 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 statusEtapadel 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 maestroreference/maestro/maestro-unidades.json). Idempotente; al llegar a Vendido/Escriturado crea la actividad de traspaso de departamento (a Escrituración / a SAC) con deduptraspaso:<codigo>:<etapa>enAutomatizacionState. En la PRIMERA corrida (carga histórica) usar--semilla: registra el dedup sin crear actividades retroactivas. - Disparadores: webhook
POST /api/webhooks/monday(eventocreate_itemen el board Leads) y, como respaldo mientras no haya URL pública, crons con node-cron:POLL_CRON(default*/5 * * * *) para round-robin y0 9 * * *para inactividad. Los crons se apagan conMONDAY_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_valuessiempre viaJSON.stringifyen 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
Floatpor ahora — TODO prod:Decimalen 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