feat(cotizador): historial, compartir WhatsApp, folio+QR, comparador de unidades, alertas de inventario

- Mis cotizaciones (guardar con cliente, retomar, nueva) asociadas al vendedor del token
- Compartir: resumen copiable + deep link wa.me; PDF con folio, vigencia y QR
- GET /api/cotizaciones/:id/publico (público, datos mínimos sin PII del cliente)
- Comparador lado a lado de 2 unidades (mismo o distinto desarrollo)
- POST /api/cotizaciones/alertas-inventario: avisa al vendedor si una unidad
  cotizada cambia de estatus (dedup; wire con syncs documentado)
- Fix: labels de propuesta mapeados a los reales del board Oportunidades
- Tests: api 201, cotizador 82
This commit is contained in:
2026-08-28 06:58:10 +00:00
parent bc0f9da8fe
commit 4f31d1c171
24 changed files with 1938 additions and 20 deletions

View File

@@ -29,8 +29,11 @@ npm test # vitest: fórmulas del cotizador (fixture U-101), cobranza, monday.s
| 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 |
| 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) |
@@ -57,6 +60,21 @@ npm test # vitest: fórmulas del cotizador (fixture U-101), cobranza, monday.s
- **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):
```bash
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.