- SENDA-410-1-G102, SENDA-410-1-G201, SENDA-401-1-L41 agregadas a Monday, seeds y conocimiento IA (inventario 288 unidades; solo existían en el reporte de estatus de venta, todas VENDIDO) - PDFs del alta verificados exhaustivamente (5 documentos × 4 variantes, contenido extraído con pdfplumber + renders visuales): - firma huérfana en página propia (PLD) corregida - encabezados de contacto huérfanos (checklist) corregidos - modelo duplicado y enums crudos en perfil corregidos - Tests pdf 6→16; suites: alta 76, cotizador 71, api 168
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 |
| GET | /api/cotizaciones · /api/cotizaciones/:id |
Consulta |
| 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.
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