Agrega al formulario ligero y a POST /api/leads: tipo de propiedad, medio de contacto, edad, estado civil, tipo de empleo, ocupacion, rango salarial, credito, tipo de economia, califica y lo quiere para. Selects obligatorios (estado civil, tipo de empleo, tipo de economia, califica) con las etiquetas sembradas como labels status en el tablero Leads de Monday.
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