Files
FRASA-CAS/api-gateway/prisma/schema.prisma
Consult AS Technology 895a831ff8 feat(mi-dia): seccion 'Mis capturas de afluencia' en el panel del vendedor
Nueva tabla LeadCaptura (quien capturo cada lead, algo que Monday no
guarda porque el creador del item siempre es el token del API). El
POST /api/leads la registra con el usuario del token y el resumen de
Mi dia la enriquece con el estado/origen actual del lead en Monday.
La captura rapida refresca el panel al registrar.
2026-09-02 02:13:13 +00:00

269 lines
9.1 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?
createdAt DateTime @default(now())
documentos Documento[]
@@index([curp])
}
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.
model LeadCaptura {
id String @id @default(uuid())
leadId String @unique // item del tablero Leads (Monday)
oportunidadId String? // item del tablero Oportunidades ligado al capturar
usuarioId String // Usuario.id del capturista
nombreLead String
telefono String?
createdAt DateTime @default(now())
@@index([usuarioId])
}
// 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 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])
}