// FRASA API Gateway — persistencia local // Desarrollo: SQLite. Producción: Postgres 16 (cambiar provider y DATABASE_URL). generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" // prod: "postgresql" url = env("DATABASE_URL") } model Cotizacion { id String @id @default(uuid()) unidadCodigo String // Desarrollo de la unidad al momento de cotizar (DIAGONAL | ATELIER | ...) desarrollo String @default("DIAGONAL") // Usuario del ecosistema que generó la cotización (sub del JWT); null en // cotizaciones históricas o creadas sin auth (AUTH_ENABLED=false) vendedorId String? clienteNombre String clienteEmail String? clienteTelefono String? tipoPropuesta String // BANCARIO | INFONAVIT | CONTADO | SEMI_CONTADO acabados Boolean apartado Float // TODO prod: Decimal en Postgres descuento Float // fracción (0.02 = 2%) precioLista Float // TODO prod: Decimal en Postgres precioFinal Float enganche Float numMensualidades Int mensualidad Float? // null => N/A (Contado) pagoFinal Float mondayItemId String? createdAt DateTime @default(now()) @@index([unidadCodigo]) @@index([vendedorId]) } model Cliente { id String @id @default(uuid()) nombreCompleto String curp String rfc String email String? celular String? proyecto String unidadCodigo String? // Payload completo del alta como JSON serializado (SQLite no tiene Json nativo; // en Postgres puede migrarse a la columna Json sin cambiar el contrato). payloadJson String firmaPng String? // base64 opcional mondayContactoId String? mondayApartadoId String? // Usuario.id de quien inició el alta (sub del JWT); null en altas históricas // o creadas sin auth (AUTH_ENABLED=false). Alimenta "Mis clientes" de Mi día. capturadoPorId String? createdAt DateTime @default(now()) documentos Documento[] @@index([curp]) @@index([capturadoPorId]) } model Documento { id String @id @default(uuid()) clienteId String cliente Cliente @relation(fields: [clienteId], references: [id], onDelete: Cascade) tipo String // INE | CONSTANCIA_RFC | CURP | COMPROBANTE_DOMICILIO | ACTA_NACIMIENTO | OTRO nombreArchivo String ruta String mime String tamano Int createdAt DateTime @default(now()) @@index([clienteId]) } // Borrador del wizard de alta de clientes: el asesor guarda el avance y lo // retoma después. `datosJson` es el estado serializado del wizard (sin los // File de documentos; solo se persiste lo que cabe en JSON). model BorradorAlta { id String @id @default(uuid()) usuarioId String // sub del JWT del asesor dueño del borrador nombre String // etiqueta visible: nombre del cliente o "Sin nombre" datosJson String paso Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([usuarioId]) } model PlanPago { id String @id @default(uuid()) ventaRef String? clienteNombre String clienteTelefono String? unidadCodigo String proyecto String fechaInicio DateTime // se normaliza a medianoche local engancheTotal Float // TODO prod: Decimal en Postgres numPagos Int mensualidad Float tasaMoraMensual Float @default(0) status String @default("ACTIVO") // ACTIVO | CANCELADO (LIQUIDADO se deriva) // Item del tablero Apartado (Monday) ligado a este plan; al liquidarse el // plan, ese item pasa a 'X Escriturar' (puente cobranza → postventa). mondayItemId String? // Vendedor dueño del plan (sub del JWT al crearlo); null en planes históricos. // Alimenta el filtro "mis cobranzas" y la ruta de recordatorios al vendedor. vendedorId String? createdAt DateTime @default(now()) notas NotaPlan[] pagos Pago[] @@index([unidadCodigo]) @@index([vendedorId]) } model Pago { id String @id @default(uuid()) planId String plan PlanPago @relation(fields: [planId], references: [id], onDelete: Cascade) numPago Int fechaVencimiento DateTime monto Float montoPagado Float @default(0) aplicaciones AplicacionPago[] @@index([planId]) @@index([fechaVencimiento]) } model AplicacionPago { id String @id @default(uuid()) pagoId String pago Pago @relation(fields: [pagoId], references: [id], onDelete: Cascade) fechaPago DateTime monto Float referencia String @@index([pagoId]) } // Vinculación lead ↔ oportunidad del funnel: cada lead capturado entra al // tablero Oportunidades en etapa 'Afluencia'; este mapping evita duplicar la // oportunidad y permite moverla de etapa cuando el lead avanza. model LeadOportunidad { id String @id @default(uuid()) leadId String @unique // item del tablero Leads (Monday) oportunidadId String @unique // item del tablero Oportunidades (Monday) createdAt DateTime @default(now()) } // Registro local de qué usuario del ecosistema capturó cada lead (alta de // afluencia). Monday no guarda al capturista (el creador del item siempre es // el token del API); esta tabla alimenta "Mis capturas" del panel Mi día. // Además es la entidad local del embudo en modo local (Monday caído): guarda // el board/etapa actuales y el perfil de la captura hasta que el sync worker // la replique en Monday. model LeadCaptura { id String @id @default(uuid()) leadId String @unique // item del tablero Leads (Monday); en modo local: id local loc_ oportunidadId String? // item del tablero Oportunidades ligado al capturar usuarioId String // Usuario.id del capturista nombreLead String telefono String? // Board del embudo donde vive el registro: LEADS | OPORTUNIDADES | APARTADO board String @default("LEADS") // Etapa/estado local actual (NUEVO al capturar) etapa String @default("NUEVO") origen String? desarrollo String? modelo String? email String? // Motivo / submotivo del descarte motivo String? submotivo String? // Perfilamiento completo de afluencia / payload extra (JSON serializado) datosJson String? // Item de Monday una vez sincronizado (rows viejas: = leadId) mondayItemId String? // true = falta replicar a Monday (lo drena el sync worker) // Fecha límite del apartado (para "Apartados por vencer" en Mi día) fechaLimite DateTime? pendienteSync Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @default(now()) @@index([usuarioId]) @@index([mondayItemId]) } // Espejo local del inventario (tablero Unidades de Monday). Lo siembra // scripts/seed-unidades-locales.js desde el INVENTARIO_SEED del cotizador y lo // mantiene fresco el write-through de monday.service.getUnidades(); es el // fallback de lectura del cotizador en modo local. model Unidad { id String @id @default(uuid()) codigo String @unique // 'U-101', 'ATELIER-10', 'SENDA-411-1-7', ... desarrollo String // DIAGONAL | ATELIER | ESTANZA | SENDA | SOLANA modelo String @default("") manzana String @default("") lote String @default("") supConstruccion Float @default(0) supExcedente Float @default(0) supEstacionamiento Float @default(0) precioLista Float @default(0) precioAcabados Float @default(0) status String @default("DISPONIBLE") // DISPONIBLE | APARTADO | VENDIDO | ESCRITURADO | BLOQUEADO | ... etapa String @default("") // etapa del pipeline de venta ('' si no aplica) updatedAt DateTime @updatedAt @default(now()) @@index([desarrollo]) @@index([status]) } // Cola de sincronización a Monday (modo local). Cada operación hecha contra la // base local mientras Monday está caído se encola aquí; sync-monday.service la // drena FIFO (createdAt asc) cuando Monday vuelve. Estados: PENDIENTE | // PROCESANDO | HECHO | ERROR. Idempotencia vía AutomatizacionState 'sync:'. model SyncPendiente { id String @id @default(uuid()) // CREAR_LEAD | CREAR_AFLUENCIA | ACTUALIZAR_AFLUENCIA | CAMBIO_ETAPA | // DESCARTE | CONVERSION | CREAR_CONTACTO | ACTUALIZAR_CONTACTO | // CREAR_APARTADO | COTIZACION_OPORTUNIDAD | CHECKBOX_DOC | // APARTO_OPORTUNIDAD | LIQUIDADO_ESCRITURAR | ASIGNAR_PERSONA | // DISPONIBILIDAD_UNIDAD tipo String payloadJson String // Referencia local (leadId / clienteId / cotizacionId) para actualizar ids referenciaLocal String? estado String @default("PENDIENTE") intentos Int @default(0) ultimoError String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([estado, createdAt]) } // Estado persistente de las automatizaciones de Monday (round-robin, recordatorios). // Clave-valor genérico: 'rr:indice', 'rr:vendedores', 'recordatorio::', etc. model AutomatizacionState { clave String @id valor String updatedAt DateTime @updatedAt } // Bitácora de recordatorios de pago enviados por el motor de cobranza // (recordatorios-cobranza.service). Sirve a la vez de dedup (no repetir el // mismo recordatorio el mismo día / respetar la ventana de re-recordatorio) y // de historial consultable por GET /api/cobranza/recordatorios. model RecordatorioLog { id String @id @default(uuid()) pagoId String tipo String // PROXIMO_VENCER | VENCIDO canal String // monday-actividad | whatsapp | log destinatario String // mondayUserId del responsable o 'sin-destinatario' mensaje String createdAt DateTime @default(now()) @@index([pagoId]) @@index([createdAt]) } model NotaPlan { id String @id @default(uuid()) planId String plan PlanPago @relation(fields: [planId], references: [id], onDelete: Cascade) fecha DateTime @default(now()) texto String @@index([planId]) } // ---------- Autenticación ---------- // Usuario del ecosistema (vendedores, coordinadores, corporativo y admin). // Sembrado por scripts/seed-usuarios.ts desde reference/vendedores.json. model Usuario { id String @id @default(uuid()) username String @unique // nombre completo normalizado (minúsculas, sin acentos) nombre String // nombre completo para mostrar passwordHash String // bcrypt (cost 12) rol String // vendedor | coordinador | corporativo | admin asignacion String? // ATELIER | RESIDENCIAL | MONTEBELLO | BROKER | ACORPORATIVO empleadoId Int? // User ID de Monday (people). null = el usuario aún no tiene cuenta Monday; // los filtros "míos" de /api/mi-dia responden vacíos con sinMapeoMonday: true mondayUserId Int? activo Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } // ---------- Agente IA interno (RAG) ---------- // Documento de la base de conocimiento; `hash` (sha256 del contenido) permite // re-indexar solo cuando cambió. model AIDocumento { id String @id @default(uuid()) titulo String @unique contenido String hash String createdAt DateTime @default(now()) chunks AIChunk[] } model ConfiguracionSistema { clave String @id valor String updatedAt DateTime @updatedAt @default(now()) } model RolConfiguracion { rol String @id modulos String // JSON array acciones String // JSON object updatedAt DateTime @updatedAt @default(now()) } model AIChunk { id String @id @default(uuid()) documentoId String documento AIDocumento @relation(fields: [documentoId], references: [id], onDelete: Cascade) texto String embedding String // JSON array de floats (SQLite no tiene vector nativo) orden Int @@index([documentoId]) } model AIConversacion { id String @id @default(uuid()) // Dueño de la conversación (sub del JWT); null en anónimas/históricas. // GET /api/ai/conversations devuelve solo las del usuario (admin: todas). usuarioId String? createdAt DateTime @default(now()) mensajes AIMensaje[] @@index([usuarioId]) } model AIMensaje { id String @id @default(uuid()) conversacionId String conversacion AIConversacion @relation(fields: [conversacionId], references: [id], onDelete: Cascade) rol String // user | assistant contenido String fuentes String? // JSON [{titulo, score}] solo en mensajes del asistente createdAt DateTime @default(now()) @@index([conversacionId]) } // ---------- Actividades locales (modo sin Monday) ---------- model ActividadLocal { id String @id @default(uuid()) titulo String descripcion String? usuarioId String leadCapturaId String? fechaVencimiento DateTime? fechaCierre DateTime? estado String @default("ABIERTA") // ABIERTA | CERRADA tipo String @default("SEGUIMIENTO") // SEGUIMIENTO | RECORDATORIO | REUNION createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([usuarioId]) @@index([estado]) @@index([fechaVencimiento]) } // Asignaciones locales de leads (reemplaza round-robin de Monday) model AsignacionLocal { id String @id @default(uuid()) leadCapturaId String usuarioId String asignadoPor String? // round-robin | capturista | manual createdAt DateTime @default(now()) @@unique([leadCapturaId]) @@index([usuarioId]) }