// 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]) @@index([nombreCompleto]) } 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]) @@index([status]) } 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]) @@index([nombreLead]) } // 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]) } model NotaApartado { id String @id @default(uuid()) apartadoId String usuarioId String contenido String createdAt DateTime @default(now()) @@index([apartadoId]) } model AuditoriaLog { id String @id @default(uuid()) usuarioId String accion String entidad String entidadId String detalle String? createdAt DateTime @default(now()) @@index([usuarioId]) @@index([entidad, entidadId]) @@index([createdAt]) } // Recordatorios personales del vendedor (ver Mi día / panel de recordatorios). model RecordatorioVendedor { id String @id @default(uuid()) usuarioId String leadId String? titulo String descripcion String? fechaRecordar DateTime completado Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([usuarioId]) @@index([fechaRecordar]) } // Captura por departamentos: cada campo de cada unidad se registra como fila. // Unique constraint: desarrollo + unidadCodigo + departamento + campo. model CapturaDepartamento { id String @id @default(uuid()) desarrollo String unidadCodigo String departamento String campo String valor String? tipo String @default("text") usuarioId String capturadoEn DateTime @default(now()) actualizadoEn DateTime @updatedAt @@unique([desarrollo, unidadCodigo, departamento, campo]) @@index([desarrollo]) @@index([departamento]) } // Suscripciones push de los navegadores (Web Push API) model PushSubscription { id String @id @default(uuid()) usuarioId String endpoint String p256dh String auth String createdAt DateTime @default(now()) @@unique([usuarioId]) @@index([usuarioId]) } // ── Clientes Maestro (importado desde Excel) ────────────────────────── model ClienteMaestro { id String @id @default(uuid()) numCliente String @unique nombre String apellidoPaterno String apellidoMaterno String? sexo String? telefono String? email String? estadoCivil String? rangoEdad String? ocupacion String? rangoIngresos String? credito String? institucion String? medioContacto String? canal String? desarrollo String? manzana String? lote String? interior String? tipoCasa String? etapa String? valorTotal Float? estatus String? fechaLiberacion DateTime? vendedor String? vendedorId String? importadoEn DateTime @default(now()) @@index([desarrollo]) @@index([estatus]) @@index([vendedor]) }