Primera fase del plugin de propuesta consultiva (docs/superpowers/specs/ 2026-07-28-propuesta-consultiva-ia-fase0-fase1-design.md). Recupera el contexto humano que hoy se captura y se descarta, y cierra los bloqueadores que el analisis previo destapo. Seguridad (lo mas urgente): - POST /mcp no tenia NINGUNA autenticacion: cero Depends() en main.py, mientras el servicio recibe dominio publico en produccion (SERVICE_FQDN_API_8000). Cualquiera en internet podia leer y escribir cotizaciones. Ahora exige require_auth, que ya existia en app/auth.py y no se estaba usando ahi. - _obtener_cotizacion hacia SELECT c.* y devolvia dict(cot) al agente, asi que cualquier columna nueva se publicaba sola. Ahora usa lista blanca espejo de CotizacionResponse, excluyendo observacionesInternas. Totales: - Nueva calcularTotalesCotizacion() en calculators.ts como fuente de verdad unica. El calculo estaba duplicado a mano en siete consumidores. - conIva() sustituye a los `* 1.16` hardcodeados de pdf-generator y excel-builder, que ignoraban IVA_RATE y el flag incluirIva. Contexto humano: - Campo nuevo Cotizacion.observacionesInternas. La migracion es PURAMENTE ADITIVA: no mueve ni una fila. El movimiento de datos no hace falta porque la unica fila de produccion con observaciones ya contiene texto dirigido al cliente, y separarlo en dos despliegues mantiene el rollback limpio. - Dos textareas visualmente inconfundibles en el formulario. - observaciones se imprime por primera vez en el PDF y el Excel; el dato ya viajaba hasta las rutas de borrador y se tiraba. - observacionesInternas solo se ve en la app. La garantia es estructural: el campo no existe en CotizacionPDFData ni en ExcelData, asi que el generador no puede filtrarlo aunque alguien lo intente. Bugs vecinos: - orderBy explicito en las cuatro rutas de export: el PDF asume las partidas agrupadas por fase y sin orderBy podia diferir del Excel del mismo envio. - El PDF de borrador leia solo el branding, no los datos bancarios, e imprimia los hardcodeados del generador. - detalleModelo local en PDF y Excel omitia la rama "demanda": esa partida salia en $0 y sin explicacion. Ahora delegan en la version canonica. - Los bonos salen de la tabla Bono; la lista hardcodeada queda de respaldo y su texto ya no coincidia con el seed. - La palomita de los bonos mide 0pt en las fuentes base de PDFKit (verificado), o sea que salia como dos espacios. Sustituida por una vineta. Ademas: zod pasa a ser dependencia declarada. Se importaba en schemas.ts y resolvia transitivamente, asi que un npm ci --omit=dev reventaba. Verificado con build, lint y 22 comprobaciones funcionales sobre PDF y Excel reales (texto del cliente presente, texto interno ausente incluso inyectandolo a la fuerza, texto largo multipagina, y totales con y sin IVA). Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
246 lines
7.5 KiB
Plaintext
246 lines
7.5 KiB
Plaintext
generator client {
|
|
provider = "prisma-client"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
password String
|
|
name String
|
|
role String @default("asesor")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
cotizaciones Cotizacion[] @relation("AsesorCotizaciones")
|
|
}
|
|
|
|
model Cliente {
|
|
id String @id @default(cuid())
|
|
nombre String
|
|
empresa String?
|
|
email String?
|
|
telefono String?
|
|
rfc String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
cotizaciones Cotizacion[]
|
|
|
|
@@index([nombre, empresa])
|
|
}
|
|
|
|
model Cotizacion {
|
|
id String @id @default(cuid())
|
|
numero String @unique
|
|
fecha DateTime @default(now())
|
|
vigencia DateTime
|
|
moneda String @default("MXN")
|
|
tipoCambio String @default("NA")
|
|
proyecto String @default("MKT Digital")
|
|
esquemaPago String @default("Pago Unico/Mensual")
|
|
estado String @default("borrador")
|
|
incluirBonos Boolean @default(false)
|
|
incluirFinanciamiento Boolean @default(false)
|
|
incluirIva Boolean @default(true)
|
|
esDoble Boolean @default(false)
|
|
opcionesMetadata Json?
|
|
// Texto que SI ve el cliente: se imprime en el PDF y en el Excel.
|
|
observaciones String?
|
|
// Contexto de discovery, notas del asesor, riesgos. NO sale de la app: ningun
|
|
// exportador declara este campo en su interfaz de datos (CotizacionPDFData /
|
|
// ExcelData) y la herramienta MCP obtener_cotizacion usa lista blanca de
|
|
// columnas. La garantia es estructural, no depende de recordar filtrarlo.
|
|
observacionesInternas String?
|
|
clienteId String
|
|
asesorId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
cliente Cliente @relation(fields: [clienteId], references: [id])
|
|
asesor User @relation("AsesorCotizaciones", fields: [asesorId], references: [id])
|
|
servicios ServicioCotizado[]
|
|
planBucefalo PlanBucefaloCotizacion?
|
|
registrosHoras RegistroHoras[]
|
|
|
|
@@index([clienteId])
|
|
@@index([asesorId])
|
|
@@index([estado])
|
|
@@index([createdAt])
|
|
}
|
|
|
|
model Categoria {
|
|
id String @id @default(cuid())
|
|
nombre String @unique
|
|
descripcion String?
|
|
color String @default("#6b7280")
|
|
activo Boolean @default(true)
|
|
orden Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
servicios ServicioCatalogo[]
|
|
}
|
|
|
|
model Paquete {
|
|
id String @id @default(cuid())
|
|
nombre String
|
|
descripcion String?
|
|
activo Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
fases FasePaquete[]
|
|
}
|
|
|
|
model FasePaquete {
|
|
id String @id @default(cuid())
|
|
paqueteId String
|
|
nombre String
|
|
orden Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
paquete Paquete @relation(fields: [paqueteId], references: [id], onDelete: Cascade)
|
|
servicios ServicioPaquete[]
|
|
|
|
@@index([paqueteId])
|
|
}
|
|
|
|
model ServicioCatalogo {
|
|
id String @id @default(cuid())
|
|
nombre String
|
|
descripcion String?
|
|
fase Int @default(1)
|
|
tipoPago String @default("unico")
|
|
precioBase Float
|
|
tiempoEntrega String @default("4 - 10 dias")
|
|
entregablesDefault Json @default("[]")
|
|
categoriaId String?
|
|
variante String?
|
|
nivel String?
|
|
activo Boolean @default(true)
|
|
orden Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
servicios ServicioCotizado[]
|
|
categoriaRel Categoria? @relation(fields: [categoriaId], references: [id])
|
|
paquetes ServicioPaquete[]
|
|
|
|
@@index([categoriaId])
|
|
@@index([activo, fase, orden])
|
|
}
|
|
|
|
model ServicioPaquete {
|
|
id String @id @default(cuid())
|
|
servicioCatalogoId String
|
|
fasePaqueteId String
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
servicio ServicioCatalogo @relation(fields: [servicioCatalogoId], references: [id], onDelete: Cascade)
|
|
fasePaquete FasePaquete @relation(fields: [fasePaqueteId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([servicioCatalogoId, fasePaqueteId])
|
|
@@index([fasePaqueteId])
|
|
}
|
|
|
|
model ServicioCotizado {
|
|
id String @id @default(cuid())
|
|
cotizacionId String
|
|
servicioCatalogoId String?
|
|
nombre String?
|
|
esPersonalizado Boolean @default(false)
|
|
horas Float?
|
|
tarifaHora Float?
|
|
modeloCobro String @default("fijo")
|
|
montoMinimo Float?
|
|
horasIncluidas Float?
|
|
opcion String?
|
|
fase Int
|
|
tipoPago String
|
|
precio Float
|
|
tiempoEntrega String
|
|
entregables Json @default("[]")
|
|
beneficios Json @default("[]")
|
|
notas String?
|
|
seleccionado Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
cotizacion Cotizacion @relation(fields: [cotizacionId], references: [id], onDelete: Cascade)
|
|
servicioCatalogo ServicioCatalogo? @relation(fields: [servicioCatalogoId], references: [id])
|
|
|
|
@@index([cotizacionId])
|
|
@@index([servicioCatalogoId])
|
|
}
|
|
|
|
model PlanBucefaloCotizacion {
|
|
id String @id @default(cuid())
|
|
cotizacionId String @unique
|
|
nivel String @default("basico")
|
|
precio Float
|
|
seleccionado Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
cotizacion Cotizacion @relation(fields: [cotizacionId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
// Registro de horas trabajadas contra una cotizacion aprobada con cobro por tiempo
|
|
// (modeloCobro horas/retainer/demanda). Se usa para emitir notas de pago al cliente:
|
|
// cada entrada es un dia con un rango horario, una descripcion breve y una tarifa
|
|
// (snapshot). `horas` se calcula del rango (calcularHorasRango) y se persiste.
|
|
model RegistroHoras {
|
|
id String @id @default(cuid())
|
|
cotizacionId String
|
|
fecha DateTime
|
|
horaInicio String
|
|
horaFin String
|
|
horas Float
|
|
tarifaHora Float @default(0)
|
|
descripcion String
|
|
// Estado de cobro de la partida de horas: "por_pagar" (default) o "pagada".
|
|
// Permite separar lo pendiente (nota por pagar al cliente) de lo ya cobrado
|
|
// (recibo/historial) sin que los totales se mezclen. fechaPago = cuando se marco pagada.
|
|
estadoPago String @default("por_pagar")
|
|
fechaPago DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
cotizacion Cotizacion @relation(fields: [cotizacionId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([cotizacionId])
|
|
@@index([fecha])
|
|
@@index([estadoPago])
|
|
}
|
|
|
|
model Configuracion {
|
|
id String @id @default(cuid())
|
|
clave String @unique
|
|
valor String
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model Bono {
|
|
id String @id @default(cuid())
|
|
numero Int
|
|
titulo String
|
|
descripcion String
|
|
activo Boolean @default(true)
|
|
}
|
|
|
|
model FinanciamientoPlan {
|
|
id String @id @default(cuid())
|
|
meses Int @unique
|
|
tasa Float
|
|
comision Float
|
|
montoMinimo Float
|
|
iva Float @default(0.16)
|
|
}
|