- Tablas ConfiguracionSistema (7 configs) + RolConfiguracion (6 roles) - Endpoints: config/sistema GET/PATCH, config/roles GET/PATCH, config/usuarios GET/PATCH, config/reset-roles POST, config/acciones GET - modulosDeRol() async con fallback a MATRIZ estatica - auth/me devuelve acciones[] segun rol del usuario - saludMonday.estaDisponible() lee modo_local_forzado de DB (cache 30s) - Landing: vista Configuraciones con 4 tabs (Modo, Roles, Usuarios, Sistema) - Mi-dia: boton Descartar se deshabilita si !descartar_lead - 630 API tests + 24 landing tests + 20 mi-dia tests verdes Seed: npm run seed:config
356 lines
13 KiB
Plaintext
356 lines
13 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])
|
|
}
|
|
|
|
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_<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)
|
|
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:<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])
|
|
}
|