Files
FRASA-CAS/api-gateway/README.md
Consult AS Technology f0f923b2ef feat(monday): alinear Frasa-CAS con CRM real del cliente (7 mejoras)
- Vocabulario funnel: labels Intento de contacto/Abandono en Leads,
  etapas completas en Oportunidades (Cotizador, Ganado, Perdido...)
- Apartados: pipeline Enkontrol (8 estados) + checkboxes CURP/RFC/Comprobante
  que la app de alta prende al subir documentos
- Motivos No Compra: board catálogo + dropdown + endpoint /oportunidades/:id/perder
- Perfil demográfico en Contactos mapeado desde el alta (estado civil,
  vivienda, propósito, fecha nac., rango de edad calculado)
- Desarrollos: catálogo maestro sincronizado (precios, m2, tipo, entrega)
- Actividades: board nuevo + creación automática en round-robin e inactividad
- Cobranza: plan liquidado mueve el apartado a 'X Escriturar' en Monday
- Scripts: setup:crm y sync:desarrollos (idempotentes)
- Tests 119/119 verdes
2026-08-22 07:18:48 +00:00

80 lines
5.7 KiB
Markdown

# 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
```bash
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
```bash
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'|'down'}` |
| 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 `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.
- **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`.
## 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
```bash
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
```