Files
FRASA-CAS/api-gateway/prisma/schema.prisma
Consult AS Technology c2a8f4e929 feat(captura): módulo de captura por departamentos
Backend: tabla CapturaDepartamento, 5 departamentos, 7 desarrollos,
service CRUD + importador Excel + 6 endpoints.
Frontend: tabla editable inline, tabs por dept, resumen avance,
paginación, filtro estatus, permisos por rol, dark mode.
746 API tests + 42 landing tests.
2026-09-07 07:59:14 +00:00

446 lines
15 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)
// 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:<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])
}
// 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])
}