# CRM 360 — CLAUDE.md

> **Estado**: ❌ Fase 0 pendiente (módulo nuevo)
> **Spec funcional**: `docs/spec-crm-360.md` · **Plan de tickets**: `docs/plan-crm-360.md`
> **Backend**: `src/crm/` (prefijo de tablas `crm_`) · **Frontend**: repo `novasispy-erp`, rama `crm`
> **Rama**: `crm` (backend desde `pos`, frontend desde `main`)

## Qué es

Add-on de CRM de ventas y atención para las PyMEs que usan NOVASIS. Capta leads (WhatsApp,
email, IG/FB, web, Shop, TURNE), gestiona oportunidades en pipeline, atiende con tickets y SLA,
y al ganar una venta genera los datos de facturación **única, en cuotas o recurrente** usando
los servicios existentes del ERP.

Niveles: `CRM_ESENCIAL` (ventas + bandeja WhatsApp de 1 número) y `CRM_PRO`
(= `CRM_OMNICANAL` + `CRM_ATENCION` + `CRM_RECURRENTE` + `CRM_IA`).

---

## ⚠️ Correcciones verificadas contra el código

**La spec y el plan fueron escritos sin leer el código.** Estos nombres **no existen**. Cada
fila está verificada contra la rama `crm` (backend `b6ca79c`). Usá siempre la columna derecha.

| La spec / el plan dice | Realidad en el código | Verificado en |
|---|---|---|
| `ListaPreciosService.getPrecio()` | `obtenerPrecioProducto({...})` y `obtenerPreciosProductosBatch({...})` | `src/lista-precios/lista-precios.service.ts:39,294` |
| `INTEGRACION_IA`, `src/ia/*` | `ai_empresa_config` (`proveedor`, `api_key_encrypted`) y `ayuda_ia_config`. Módulos: `src/ai-usage`, `src/ayuda-ia`, `src/ai-dashboard` | `prisma/schema.prisma` |
| `suscripciones.limites_personalizados` | `suscripciones.limite_override` (Json), + `override_expira_at`, `override_nota` | `prisma/schema.prisma` |
| `PedidosService.convertirAFactura()` | **No existe.** Es exactamente el prerrequisito PRE-01 | `src/pedidos/pedidos.service.ts` |
| tabla `ordenes_venta` | No existe. Las órdenes de venta son `pedidos` con `estado_orden`, `tipo_facturacion`, `stock_reservado`, `origen_tipo` | `prisma/schema.prisma` |
| `ALTER TABLE presupuestos` | **`presupuesto_cab`** | `prisma/schema.prisma` |
| `ALTER TABLE nota_credito` | **`nota_credito_cab`** | `prisma/schema.prisma` |
| `CONFIG_NUMERACION` | `numeraciones_documento` (`punto_expedicion_id`, `timbrado_id`, `numero_actual`, `es_default`) | `prisma/schema.prisma` |
| `navigationConfig.ts` (frontend) | `src/components/organismos/sidebar/Sidebar.jsx` + `src/hooks/useMenuAcceso.js` + `src/routers/routes.jsx` | repo `novasispy-erp` |
| `src/stores/crmStore.js` (frontend) | El patrón es `src/store/*.jsx` (Zustand), p. ej. `AuthStore.jsx` | repo `novasispy-erp` |
| `@GetEmpresa()` | **No existe.** Es `@GetUser() user: LoginUserInfo` → `user.empresa_id` (1.285 usos) | `src/auth/get-user.decorator.ts` |
| Permisos en `seed.service.ts` | `src/seguridad/seeds/seguridad.seed-data.ts`, auto-aplicado en `onApplicationBootstrap` | `seguridad-seed.bootstrap.ts` |
| `gen_random_uuid()` en migraciones | `uuid_generate_v4()` | migraciones 2026-09 |
| Crear un `CrmModuloGuard` | Ya existen `ModuleGuard`, `SubmoduloGuard` y `PermissionGuard`; este último **ya valida la suscripción** | `src/auth/guards/` |

**Sí existen y se usan tal cual**: `FacturasService`, `PedidosService`, `ClientesService`,
`MonedasService.getUltimaCotizacion()`, `verificarCredito()`, `planes_cuotas`, `config_mora`,
`autorizaciones_caja`, `vendedores_cobradores`, `plan_modulos`, `suscripcion_modulos`,
`ContabilidadIntegracionService`, `AuditInterceptor`, `empresas_correo`, Bancard, PosGateway.

### Dependencias a instalar

- **Drag & drop** para el Kanban (CRM-17): **no hay ninguna librería**. Elegir e instalar.
- `nock` para los tests de los proveedores de WhatsApp (CRM-22).
- Cliente IMAP para CRM-25 (ya está `nodemailer` para el envío).
- Ya instalado y en uso: `bullmq`, `@nestjs/bullmq`, `@nestjs/schedule`, `@nestjs/websockets`,
  `socket.io`, `ioredis`, `nodemailer`, `pgvector` (migración `20260526_ayuda_ia_init`).
  Frontend: `zustand`, `dexie`, `socket.io-client`, `recharts`, MUI 7.

---

## Decisión de arquitectura: SIN bus de eventos

La spec (§6.11) y el plan asumen `@nestjs/event-emitter` con 11 eventos de dominio.
**No se va a usar.** Razones verificadas:

- `@nestjs/event-emitter` **no está instalado** y hay **cero** usos de `EventEmitter2` o
  `@OnEvent` en los 100+ módulos del backend. Todo el ERP resuelve por llamada directa.
- Ya existe el mecanismo de desacople que el bus daría: **5 colas BullMQ en producción**
  (`email`, `sifen`, `sifen-nc`, `sifen-sync`, `middleware-empresa`).
- Introducir un tercer patrón en un equipo de 2 devs genera una discusión por PR sobre cuál usar.

**La regla:**

| Situación | Patrón |
|---|---|
| Tiene que pasar ahora y el resultado depende de ello | Llamada directa al servicio |
| Puede pasar después y no debe frenar al usuario | Job en una cola BullMQ |

Consecuencias concretas:

- **CRM-03** no instala el event emitter. Queda numeración + E.164 + colas.
- **CRM-14** (cierre) llama directo a lo que necesite. En F1 son dos cosas.
- **`presupuesto.aceptado`**: en vez de refactorizar `presupuestos.service.ts` a eventos, el
  CRM expone un método y `presupuestos` lo llama. Una línea agregada a código de producción,
  en lugar de cambiarle el patrón a un servicio que ya funciona.
- **CRM-31** (automatizaciones, F2): acá sí va una cola `crm-automatizaciones`. Para entonces
  se sabrá qué disparadores hacen falta de verdad, en vez de adivinar 11 ahora.

Si más adelante se decide meter el bus, que sea **solo** para eventos `crm.*` entre módulos
del CRM, nunca para hablar con el resto del ERP.

---

## Decisiones de diseño (no reabrir sin consultar)

| # | Decisión |
|---|---|
| 1 | El CRM **no duplica** clientes, productos, precios ni documentos. Referencia por FK |
| 2 | Facturar = construir `PayloadFacturacion` (`cierre/payload-facturacion.ts`) y delegar en `FacturasService` / `PedidosService`. **Nunca** XML SIFEN, stock ni asientos directos |
| 3 | WhatsApp vía `IWhatsAppProvider`: `meta_cloud` (default producción), `evolution_cloud`, `evolution_baileys` (solo demo). Twilio solo SMS |
| 4 | Con `evolution_baileys`, todo envío **automático** lanza `BaileysAutomaticoBloqueadoError` |
| 5 | Facturas recurrentes: el job solo crea ciclos `pendiente_aprobacion`. **Solo** el endpoint de aprobación emite. Aprueba el **Supervisor** (`CRM_CONTRATOS_APROBAR`), fallback Administrador |
| 6 | Fecha de emisión recurrente = fecha de aprobación. USD → cotización del día de aprobación |
| 7 | Débito Bancard solo después de DE aprobado por SIFEN |
| 8 | IA siempre con API key del cliente (BYOK, `ai_empresa_config`). Agente con herramientas read-only |
| 9 | Período de prueba del add-on: días configurables, override en `suscripciones.limite_override`. Vencido → solo lectura 30 días → bloqueado |
| 10 | "Contrato recurrente" ≠ `suscripciones` (esa tabla es la facturación SaaS de NOVASIS) |
| 11 | Ganar una oportunidad solo vía asistente de cierre (`PATCH /etapa` a etapa ganada → 409) |
| 12 | **Sin bus de eventos** (ver sección anterior) |

---

## Estado por fase

| Fase | Alcance | Estado |
|---|---|---|
| PRE | Facturar desde Orden de Venta, control de crédito, Cobrar Pedido en POS, Meta verificado, Bancard real, incidencias DOBASA | ❌ |
| F0 | Config, módulos, permisos, guard, prueba, numeración, E.164, seed, menú y pantalla | **✅ completa** (CRM-01..05) |
| F1 | Leads, oportunidades, actividades, cierre → factura única/cuotas, timeline 360, fechas especiales, reportes básicos | ❌ |
| F2 | WhatsApp/Email, bandeja, plantillas, tickets/SLA, NC desde ticket, CSAT, automatizaciones | ❌ |
| F3 | Contratos, ciclos, aprobación, débito, dunning, MRR, portal OTP, comisiones, PWA | ❌ |
| F4 | IA (copiloto, agente, scoring, churn), IG/FB, web chat, KB, secuencias, calendario | ❌ |

---

## Tablas (prefijo `crm_`)

Definición completa de columnas en `docs/spec-crm-360.md` §8.1 y §8.2.

| Tabla | Fase | Notas |
|---|---|---|
| `crm_pipelines`, `crm_etapas` | F0 | etapa: `orden`, `probabilidad`, `campos_obligatorios` JSONB, `dias_estancamiento`, `es_ganada`, `es_perdida` |
| `crm_origenes`, `crm_motivos` | F0 | `motivo.tipo`: `perdida \| descarte \| churn` |
| `crm_parametros` | F0 | 1 por empresa: `rec_dias_anticipacion`(3), `rec_dia_emision`(1), `rec_dias_gracia`(10), `envios_habilitados` |
| `crm_sla_politicas`, `crm_calendarios` | F0 | |
| `crm_numeracion` | F0 | OPP- / CTR- / TCK- por empresa |
| `crm_leads` | F1 | único parcial `(empresa_id, telefono_e164)` si no descartado |
| `crm_oportunidades`, `crm_oportunidad_items` | F1 | `item.tipo_cobro`: `unico \| recurrente` |
| `crm_actividades` | F1 | `geo_lat`/`geo_lng` para visitas |
| `crm_fechas_especiales` | F1 | cumpleaños/aniversario/comercial |
| `crm_canales` | F2 | `proveedor`: `meta_cloud \| evolution_cloud \| evolution_baileys`; credenciales AES |
| `crm_conversaciones`, `crm_mensajes`, `crm_plantillas`, `crm_tarifas_whatsapp` | F2 | `ventana_24h_hasta` |
| `crm_tickets`, `crm_ticket_eventos`, `crm_encuestas` | F2 | |
| `crm_automatizaciones`, `crm_automatizacion_log` | F2 | |
| `crm_contratos`, `crm_contrato_lineas`, `crm_contrato_consumos` | F3 | |
| `crm_contrato_ciclos` | F3 | **`UNIQUE(contrato_id, periodo_desde)`**; estado: `pendiente_aprobacion \| aprobado \| omitido \| emitido \| rechazado_sifen \| cobrado \| cobro_fallido` |
| `crm_tokens_cobro` | F3 | solo token de pasarela, nunca PAN |
| `crm_kb_articulos` | F4 | embedding pgvector |

**Columnas a agregar a tablas existentes** (nombres corregidos — ver tabla de correcciones):

```sql
ALTER TABLE factura_cab      ADD COLUMN IF NOT EXISTS oportunidad_id UUID,
                             ADD COLUMN IF NOT EXISTS contrato_id UUID,
                             ADD COLUMN IF NOT EXISTS contrato_ciclo_id UUID;
ALTER TABLE presupuesto_cab  ADD COLUMN IF NOT EXISTS oportunidad_id UUID;
ALTER TABLE pedidos          ADD COLUMN IF NOT EXISTS oportunidad_id UUID;
ALTER TABLE clientes         ADD COLUMN IF NOT EXISTS lead_origen_id UUID,
                             ADD COLUMN IF NOT EXISTS telefono_e164 VARCHAR(20),
                             ADD COLUMN IF NOT EXISTS etiquetas TEXT[],
                             ADD COLUMN IF NOT EXISTS segmento VARCHAR(50);
ALTER TABLE nota_credito_cab ADD COLUMN IF NOT EXISTS ticket_id UUID;
```

Más `suscripcion_modulos.fecha_fin_prueba` y `planes.crm_dias_prueba`.

---

## Permisos — modelo de 3 niveles

El repo **no** usa una lista plana de permisos. Usa `modulos` → `submodulos` → `privilegios`,
y el gating comercial vive en tablas aparte. La spec §10 está escrita como lista plana: **no la
sigas**, seguí esto.

```
Módulo        CRM                        ← lo que gatea la suscripción
 └ Submódulo  CRM_CONFIG                 ← capacidad; puede ser addon cobrable
    └ Privilegio CRM_CFG_PIPELINE_CREAR  (recurso: PIPELINE, acción: CREAR)
```

**Los niveles comerciales NO son módulos.** Esencial y Pro son filas en `plan_submodulos`;
`CRM_IA` es un submódulo con `es_addon = true`. Y lo que la spec llama "Esencial = 1 número"
son **límites** (`plan-limits` + `suscripciones.limite_override`), no gating de submódulo.

Submódulos previstos, uno por módulo funcional de la spec. Cada ticket agrega el suyo al seed
junto con sus privilegios; hoy sólo existe `CRM_CONFIG`.

| Submódulo | Spec | Ticket que lo crea | Esencial | Pro |
|---|---|---|---|---|
| `CRM_CONFIG` ✅ | M90 | CRM-01 | ✓ | ✓ |
| `CRM_LEADS` | M91 | CRM-10 | ✓ | ✓ |
| `CRM_CLIENTES_360` | M92 | CRM-16 | ✓ | ✓ |
| `CRM_OPORTUNIDADES` | M93 | CRM-12 | ✓ | ✓ |
| `CRM_ACTIVIDADES` | M94 | CRM-13 | ✓ | ✓ |
| `CRM_CIERRE` | M98 | CRM-14 | ✓ | ✓ |
| `CRM_BANDEJA` | M95 | CRM-23 | ✓ | ✓ |
| `CRM_AUTOMATIZACIONES` | M100 | CRM-31 | ✓ | ✓ |
| `CRM_REPORTES` | M102 | CRM-19 | ✓ | ✓ |
| `CRM_TICKETS` | M96 | CRM-28 | — | ✓ |
| `CRM_PORTAL` | M97 | CRM-40 | — | ✓ |
| `CRM_CONTRATOS` | M99 | CRM-33 | — | ✓ |
| `CRM_IA` (`es_addon`) | M101 | CRM-44 | — | addon |

Convención de privilegio: `CRM_<SUB3>_<RECURSO>_<ACCION>`, con acciones del vocabulario que ya
existe (`VER`, `CREAR`, `EDITAR`, `ELIMINAR`, `PROCESAR`, `APROBAR`, `EXPORTAR`, `IMPORTAR`,
`CONVERTIR`, `ASIGNAR`, `CANCELAR`, `VER_AJENAS`). No inventes acciones nuevas.

`CRM_OPORTUNIDADES_TODAS` de la spec se escribe `CRM_OPO_OPORTUNIDAD_VER_AJENAS`.

En el controller: `@UseGuards(AuthGuard('jwt'), SubmoduloGuard)` + `@RequireSubmodulo('<SUB>')`
a nivel de clase, y `@UseGuards(PermissionGuard)` + `@RequirePermission('CRM', '<PRIVILEGIO>')`
por endpoint.

## Infraestructura (CRM-03)

- **`NumeracionCrmService.siguiente(empresaId, tipo, tx?)`** → `OPP-0000001`, `CTR-…`, `TCK-…`.
  Nada que ver con `numeraciones_documento`, que es de SIFEN y lleva timbrado; esto es un
  código interno legible. **Pedilo siempre dentro de la transacción que crea la entidad**
  pasando `tx`: si la creación falla, el número vuelve al pool en vez de dejar un hueco.
  Usa `SELECT … FOR UPDATE`, igual que presupuestos, porque sin eso dos vendedores creando
  una oportunidad a la vez se llevan el mismo número.
- **`aE164()`** en `common/telefono.util.ts`: base de la deduplicación de leads. El mismo
  cliente escribe su número de cinco formas según por dónde entre. Devuelve `null` cuando no
  puede interpretarlo, a propósito: inventar un prefijo crearía un choque en el índice único
  de `crm_leads` contra el teléfono de otro cliente real.

### Lo que CRM-03 NO hizo, y por qué

El plan pedía también registrar las colas `crm-automatizaciones`, `crm-recurrente` y
`crm-mensajeria`, y un lock Redis por empresa. Las dos cosas quedan para el ticket que las
necesite de verdad, por el mismo criterio que con el bus de eventos:

- **Colas sin consumidor** acumulan jobs que nadie procesa. Cada una entra con su processor:
  `crm-mensajeria` en CRM-23, `crm-automatizaciones` en CRM-31, `crm-recurrente` en CRM-34.
  El patrón del repo es registrarlas en `src/queues/queues.module.ts`, junto a las otras cinco.
- **El lock Redis** protegería contra doble ejecución con varias instancias PM2. Hoy prod
  corre en modo `fork`, una sola instancia. Y el job que realmente podría duplicar algo
  —`crm.facturacion-recurrente`— ya está protegido por `UNIQUE(contrato_id, periodo_desde)`,
  que es una garantía de la base y no depende de que Redis esté vivo. El cron de avisos de
  CRM-02 ni siquiera lo necesita: `AlertasEmpresaService.registrar` deduplica por clave, así
  que una corrida doble no produce nada. Entra en CRM-34 si ahí se decide escalar a `cluster`.

## Configuración por defecto (CRM-04)

`CrmSeedService.asegurarConfiguracion(empresaId)` deja la empresa lista para usar: el pipeline
"Ventas" con las 6 etapas de la spec §5.3, 11 orígenes, 13 motivos, el calendario comercial
(L–V 08–18, Sáb 08–12), las 3 políticas de SLA y la fila de `crm_parametros`.

**Se dispara solo** la primera vez que alguien abre el CRM: `findAllPipelines` siembra si la
empresa no tiene ningún pipeline. Elegido así en vez de un barrido al arrancar el backend
porque no siembra empresas que no usan el CRM y funciona igual para las que lo contraten
mañana, sin cron ni paso manual post-deploy.

Es **idempotente**: todos los `createMany` van con `skipDuplicates` contra los índices únicos,
así que agrega lo que falta y nunca pisa lo que el cliente cambió. El endpoint
`POST /v1/crm/config/seed` (`CRM_CFG_CONFIG_SEMBRAR`) lo vuelve a correr — sirve para
re-sembrar cuando una versión agregue catálogos nuevos, mismo criterio que `seedMapeoFaltantes`
de contabilidad.

Los valores viven en `seeds/crm-config-default.seed.ts` y hay tests que los comparan contra la
spec: si alguien cambia una probabilidad o un minuto de SLA, el test lo marca.

## Período de prueba de add-ons

Implementado en CRM-02, pero **el mecanismo no es del CRM**: las columnas viven en
`submodulos.dias_prueba` y `suscripcion_submodulos.fecha_fin_prueba`, no en tablas `crm_`.
Cualquier add-on futuro lo usa sin migrar nada. Cuando un segundo add-on lo consuma, mover
`CrmPruebaService` a `src/suscripciones` y dejar un re-export.

**El estado no se persiste: se deriva** de `fecha_fin_prueba` contra hoy
(`src/common/prueba-submodulo.ts`). Guardarlo obligaría a un job que mueva estados y que, el
día que no corra, deja empresas en un estado que no es el real.

```
|<------- prueba ------->|<--- solo_lectura (30 d) --->|  bloqueado
                         ^ fecha_fin_prueba
```

`fecha_fin_prueba IS NULL` → `activo`, que es el caso de **todas** las empresas hoy. Hasta que
alguien arranque una prueba, el comportamiento del ERP no cambia para nadie.

El corte lo aplica `SubmoduloGuard`, no un guard nuevo: en `solo_lectura` pasan sólo
GET/HEAD/OPTIONS, en `bloqueado` no pasa nada. `GET /v1/crm/prueba/estado` **no** lleva
`@RequireSubmodulo` a propósito: tiene que seguir respondiendo con la prueba vencida para que
el panel muestre el cartel de contratación en vez de un 403 mudo.

Diferencias con la spec §12, deliberadas:

| Spec | Implementado | Por qué |
|---|---|---|
| `planes.crm_dias_prueba` | `submodulos.dias_prueba` | El submódulo es el nivel al que el repo ya gatea y cobra addons |
| Columnas en `suscripcion_modulos` | En `suscripcion_submodulos` | Mismo motivo |
| `limites_personalizados` | `suscripciones.limite_override.dias_prueba[<CODIGO>]` | Es la columna que existe |
| Job que transiciona estados | Job que **sólo avisa** | El estado se deriva; no hay nada que transicionar |

## Jobs

Todos con lock Redis por empresa (`SET NX`), porque prod corre varias instancias PM2.

| Job | Frecuencia |
|---|---|
| `crm.prueba-avisos` | diario |
| `crm.estancadas` | diario |
| `crm.fechas-especiales` | diario 08:00 |
| `crm.sla` | cada 5 min |
| `crm.facturacion-recurrente` (solo borradores) | diario 05:00 |
| `crm.recordatorio-aprobacion` | diario 08:00 |
| `crm.cobro-debito` | diario 07:00 |
| `crm.dunning` | diario 09:00 |
| `crm.renovaciones` | diario |
| `crm.metricas` | diario 02:00 |
| `crm.ia-scoring` | nocturno |

---

## Reglas críticas — NO hacer

1. No emitir facturas recurrentes sin `aprobado_por` (test automatizado lo verifica).
2. No llamar a `FacturasService` desde el job recurrente.
3. No enviar mensajes automáticos por canales `evolution_baileys`.
4. No tomar precios del body sin validarlos contra `lista-precios`.
5. No crear clientes insertando directo: usar `ClientesService`.
6. No mover una oportunidad a ganada fuera del asistente de cierre.
7. No guardar números de tarjeta: solo tokens de Bancard.
8. No exponer al agente IA herramientas que escriban datos.
9. No usar la palabra/tabla `suscripciones` para contratos del CRM.
10. No instalar `@nestjs/event-emitter` (ver decisión de arquitectura).

---

## Pilotos

- **AGOGO** — CRM Esencial. Tienda de regalos, vende por WhatsApp y en el local, 2 usuarios,
  **sin envíos** (`envios_habilitados = false`, flujo "listo para retirar"). Requiere PRE-04
  (Cobrar Pedido en POS). WhatsApp por Meta Cloud API, **nunca Baileys**: es su canal principal.
- **DOBASA** — CRM Pro. Mayorista a supermercados: pipeline mayorista, bandeja, reclamos → NC.
  Requiere PRE-07 (cerrar incidencias del ERP abiertas).

---

## Frontend (CRM-05)

Repo `novasispy-erp`, rama `crm`, commit `969bb2c`. Archivos:

| | |
|---|---|
| Servicio | `src/api/crm-config.service.js` (plano, no `src/api/crm/`) |
| Store | `src/store/crmStore.jsx` — sólo estado de UI; los datos del servidor van en react-query |
| Pantalla | `src/pages/crm/CrmConfig.jsx` + `components/templates/crm/` + `components/organismos/crm/` |
| Menú | `utils/dataEstatica.jsx` (la entrada) + `hooks/useMenuAcceso.js` (`MODULO_RAIZ_META.CRM`) + `routers/routes.jsx` (la ruta) |

Gating de la entrada: `modulo: "CRM"` + `submodulo: "CRM_CONFIG"` + `permission:
"CRM_CFG_PIPELINE_VER"`. Es el mismo trío que usa el backend, así que el menú no ofrece lo
que el guard después va a rechazar.

**Falta elegir librería de drag & drop.** El reordenamiento de etapas va con flechas porque el
repo no tiene ninguna instalada. El kanban de CRM-17 va a necesitarla sí o sí; cuando entre,
esta pantalla puede adoptarla.

El frontend tiene estándares obligatorios en su `CLAUDE.md` y `docs/ui-standards.md`: toda
pantalla arranca con `ScreenGuia`, las listas vacías usan `EmptyState`, los montos van con
`MonedaInput` y nunca `Gs.` hardcodeado. Ojo con dos componentes que se usan distinto de lo
que el nombre sugiere: `FieldHint` **reemplaza** al `TextField` y recibe `hint`, y
`InlineValidationBanner` recibe `errors` (no `errores`).

## Instrucciones para Claude

- Antes de implementar un ticket, leé su sección en `docs/plan-crm-360.md` y la sección
  correspondiente de `docs/spec-crm-360.md`. Mostrá el plan de archivos antes de escribir código.
- **Leé primero la tabla de correcciones de este archivo.** Si un prompt del plan nombra
  `getPrecio`, `INTEGRACION_IA`, `limites_personalizados`, `presupuestos` o `nota_credito`,
  está mal: usá el nombre real.
- Reutilizá servicios existentes (Clientes, ListaPrecios, Pedidos, Facturas, Monedas, Cobros,
  Bancard) — leé sus DTOs antes de mapear.
- DTOs: `@IsUUID()` en todo id; nunca aceptar `empresa_id` desde el body.
- Al terminar un ticket, actualizá la tabla "Estado por fase" de este archivo en el mismo commit.
