// 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 | FRASA_2026 (esquema único) 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 // Esquema único FRASA 2026 (apartado + firma 2.5% + enganche 7.5% + // escrituración 5.5%). Las filas legacy tienen esquema='FRASA_2026' por el // DEFAULT pero saldoFirma/engancheDiferible/escrituracion null. esquema String @default("FRASA_2026") saldoFirma Float? // lo que se paga a la firma por el enganche de firma (>= 0) pctEngancheFirma Float? // 0.025 (constante del esquema; se persiste por si cambia) engancheDiferible Float? // enganche del 7.5%, neto del excedente del apartado pctEscrituracion Float? // 0.055 escrituracionDiferida Boolean @default(false) escrituracion Float? // gastos de escrituración (5.5% del precio) // Opciones de credito del saldo (84.5%): JSON {elegida, ajustes} serializado; // el cotizador manda el ajuste del vendedor y se persiste crudo. opcionesCredito String? 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 // Usuario que capturó el abono (sub del JWT); null en abonos históricos o // hechos sin auth (AUTH_ENABLED=false) capturadoPorId String? @@index([pagoId]) @@index([capturadoPorId]) } // 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? // Teléfono normalizado (solo dígitos, últimos 10) para el dedup de // capturas: la búsqueda por `telefono contains` fallaba con formatos // con espacios. Se mantiene en cada escritura vía normalizaTelefono(). telefonoNorm 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]) @@index([telefonoNorm]) } // 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("") nivel String @default("") // piso (desarrollos verticales); '' si no aplica 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? clienteClave 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]) } // ── Módulo Soporte: solicitudes de los departamentos (Sistemas, Admin, ...) ── model SolicitudSoporte { id String @id @default(uuid()) usuarioId String titulo String descripcion String departamento String prioridad String responsable String? fechaSolicitud DateTime @default(now()) fechaLimite DateTime? notas String? archivos String? // JSON array de rutas // Quién pide la solicitud (nombre y email; el front los manda o el API usa // el usuario del token) solicitante String @default("") solicitanteEmail String? estado String @default("ABIERTA") // ABIERTA, EN_PROCESO, CERRADA createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@index([usuarioId]) @@index([departamento]) } // ── Módulo Desarrollos: catálogo de desarrollos nuevos ──────────────── // La BD es la fuente de verdad: la ficha del desarrollo, sus modelos y las // reglas del cotizador (antes constantes fijas en el código de las apps). model Desarrollo { id String @id @default(uuid()) clave String @unique // SENDA, DIAGONAL… (forma los códigos de unidad) nombre String // nombre comercial estado String @default("ACTIVO") // ACTIVO | PREVENTA | CERRADO tipo String @default("VERTICAL") // VERTICAL | HORIZONTAL constructora String? direccion String? zona String? descripcion String? amenidades String? // texto libre o lista separada por comas fechaInicioObra DateTime? fechaEntregaProgramada DateTime? fechaEntregaReprogramada DateTime? activoEnCotizador Boolean @default(true) // aparece en el cotizador activoEnAlta Boolean @default(true) // aparece en altas de lead/afluencia/cliente prefijoUnidadPdf String? // p. ej. CASA (lo usa el PDF del cotizador) imagenUrl String? logoUrl String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt modelos ModeloDesarrollo[] reglas ReglaCotizadorDesarrollo? @@index([estado]) } model ModeloDesarrollo { id String @id @default(uuid()) desarrolloId String desarrollo Desarrollo @relation(fields: [desarrolloId], references: [id], onDelete: Cascade) nombre String recamaras Int? banos Float? estacionamientos Int? supConstruccion Float? activo Boolean @default(true) @@unique([desarrolloId, nombre]) @@index([desarrolloId]) } model ReglaCotizadorDesarrollo { // `desarrolloId` guarda el uuid de `Desarrollo.id` (es lo que escriben el seed // y PUT /desarrollos/:id/reglas). El endpoint que consume el cotizador // (`/desarrollos/reglas-cotizador`) traduce uuid → clave antes de responder. desarrolloId String @id desarrollo Desarrollo @relation(fields: [desarrolloId], references: [id], onDelete: Cascade) parametrosJson String // JSON con la forma ParametrosDesarrollo updatedAt DateTime @updatedAt updatedPorId String? }