Files
FRASA-CAS/api-gateway/prisma/schema.prisma
Consult AS Technology b1e408f435 fix(notas): notas persisten entre etapas via clienteClave
- Campo clienteClave en RecordatorioVendedor (hash nombre+telefono)
- Al crear nota: se calcula clienteClave del lead/cliente
- Al buscar notas: busca por leadId O clienteClave (no por nombre suelto)
- Mantiene compatibilidad: notas viejas sin clienteClave siguen por leadId
- Migración SQL aplicada
2026-09-10 22:55:11 +00:00

503 lines
17 KiB
Plaintext

// 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_<uuid>
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:<id>'.
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:<itemId>:<fecha>', 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])
}