feat(hermes): 4 herramientas nuevas + índices DB
Hermes ahora puede: - buscar_leads: buscar leads por nombre/teléfono/email - ver_cliente: info completa de un cliente - ver_oportunidades: pipeline de ventas con filtro por etapa - ver_cobranza: resumen de pagos vencidos Índices DB: LeadCaptura.nombreLead, Cliente.nombreCompleto, PlanPago.status 38 tests nuevos pasan.
This commit is contained in:
@@ -61,6 +61,7 @@ model Cliente {
|
|||||||
|
|
||||||
@@index([curp])
|
@@index([curp])
|
||||||
@@index([capturadoPorId])
|
@@index([capturadoPorId])
|
||||||
|
@@index([nombreCompleto])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Documento {
|
model Documento {
|
||||||
@@ -117,6 +118,7 @@ model PlanPago {
|
|||||||
|
|
||||||
@@index([unidadCodigo])
|
@@index([unidadCodigo])
|
||||||
@@index([vendedorId])
|
@@index([vendedorId])
|
||||||
|
@@index([status])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Pago {
|
model Pago {
|
||||||
@@ -191,6 +193,7 @@ model LeadCaptura {
|
|||||||
|
|
||||||
@@index([usuarioId])
|
@@index([usuarioId])
|
||||||
@@index([mondayItemId])
|
@@index([mondayItemId])
|
||||||
|
@@index([nombreLead])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Espejo local del inventario (tablero Unidades de Monday). Lo siembra
|
// Espejo local del inventario (tablero Unidades de Monday). Lo siembra
|
||||||
|
|||||||
@@ -13,8 +13,11 @@ import {
|
|||||||
indexarDocumento,
|
indexarDocumento,
|
||||||
rankearChunks,
|
rankearChunks,
|
||||||
similitudCoseno,
|
similitudCoseno,
|
||||||
|
type ArgsBuscarLeads,
|
||||||
type ArgsBuscarUnidad,
|
type ArgsBuscarUnidad,
|
||||||
type ArgsCotizarUnidad,
|
type ArgsCotizarUnidad,
|
||||||
|
type ArgsVerCliente,
|
||||||
|
type ArgsVerOportunidades,
|
||||||
} from './ai.service.js'
|
} from './ai.service.js'
|
||||||
import { calcularPropuesta, obtenerPrecioLista, totalPagado, type UnidadCotizable } from './cotizacion.service.js'
|
import { calcularPropuesta, obtenerPrecioLista, totalPagado, type UnidadCotizable } from './cotizacion.service.js'
|
||||||
|
|
||||||
@@ -38,6 +41,21 @@ const prismaMock = vi.hoisted(() => ({
|
|||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
findMany: vi.fn(),
|
findMany: vi.fn(),
|
||||||
},
|
},
|
||||||
|
leadCaptura: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
|
cliente: {
|
||||||
|
findFirst: vi.fn(),
|
||||||
|
},
|
||||||
|
cotizacion: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
|
planPago: {
|
||||||
|
count: vi.fn(),
|
||||||
|
},
|
||||||
|
pago: {
|
||||||
|
findMany: vi.fn(),
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
|
vi.mock('./prisma.js', () => ({ prisma: prismaMock }))
|
||||||
@@ -384,6 +402,18 @@ describe('ejecutarToolCall', () => {
|
|||||||
async buscar_unidad(_args: ArgsBuscarUnidad) {
|
async buscar_unidad(_args: ArgsBuscarUnidad) {
|
||||||
throw new Error('Monday caído')
|
throw new Error('Monday caído')
|
||||||
},
|
},
|
||||||
|
async buscar_leads(_args: ArgsBuscarLeads) {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async ver_cliente(_args: ArgsVerCliente) {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
async ver_oportunidades(_args: ArgsVerOportunidades) {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async ver_cobranza() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
it('parsea los argumentos JSON y devuelve el resultado serializado', async () => {
|
it('parsea los argumentos JSON y devuelve el resultado serializado', async () => {
|
||||||
@@ -404,6 +434,121 @@ describe('ejecutarToolCall', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------- Herramientas: buscar_leads, ver_cliente, ver_oportunidades, ver_cobranza ----------
|
||||||
|
|
||||||
|
describe('buscar_leads', () => {
|
||||||
|
it('busca por nombre, teléfono o email y devuelve hasta 10 resultados', async () => {
|
||||||
|
const leads = [
|
||||||
|
{ nombreLead: 'Juan Pérez', telefono: '6641234567', email: 'juan@test.com', etapa: 'NUEVO', origen: 'Web', desarrollo: 'DIAGONAL', board: 'LEADS' },
|
||||||
|
{ nombreLead: 'María López', telefono: '6649876543', email: 'maria@test.com', etapa: 'Cotizador', origen: 'Referido', desarrollo: 'SOLANA', board: 'OPORTUNIDADES' },
|
||||||
|
]
|
||||||
|
prismaMock.leadCaptura.findMany.mockResolvedValue(leads)
|
||||||
|
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const r = (await e.buscar_leads({ busqueda: 'Pérez' })) as { nombre: string; telefono: string }[]
|
||||||
|
|
||||||
|
expect(r).toHaveLength(2)
|
||||||
|
expect(r[0]!.nombre).toBe('Juan Pérez')
|
||||||
|
expect(r[0]!.telefono).toBe('6641234567')
|
||||||
|
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
where: expect.objectContaining({
|
||||||
|
OR: expect.arrayContaining([
|
||||||
|
expect.objectContaining({ nombreLead: expect.objectContaining({ contains: 'Pérez' }) }),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('devuelve error si la búsqueda está vacía', async () => {
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const r = (await e.buscar_leads({ busqueda: '' })) as { error: string }
|
||||||
|
expect(r.error).toContain('Falta el texto de búsqueda')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ver_cliente', () => {
|
||||||
|
it('devuelve datos completos del cliente con lead y cotizaciones', async () => {
|
||||||
|
prismaMock.cliente.findFirst.mockResolvedValue({
|
||||||
|
id: 'cli-1', nombreCompleto: 'Juan Pérez', curp: 'CURI', rfc: 'RFCI',
|
||||||
|
email: 'juan@test.com', celular: '6641234567', proyecto: 'DIAGONAL', unidadCodigo: 'DIA-101',
|
||||||
|
})
|
||||||
|
prismaMock.leadCaptura.findMany.mockResolvedValue([
|
||||||
|
{ etapa: 'Cotizador', board: 'OPORTUNIDADES', origen: 'Web' },
|
||||||
|
])
|
||||||
|
prismaMock.cotizacion.findMany.mockResolvedValue([
|
||||||
|
{ unidadCodigo: 'DIA-101', tipoPropuesta: 'BANCARIO', precioFinal: 2850000, enganche: 114000, mensualidad: 22833.33, createdAt: new Date() },
|
||||||
|
])
|
||||||
|
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const r = (await e.ver_cliente({ nombre: 'Juan' })) as Record<string, unknown>
|
||||||
|
|
||||||
|
expect(r['nombre']).toBe('Juan Pérez')
|
||||||
|
expect(r['lead']).toEqual({ etapa: 'Cotizador', board: 'OPORTUNIDADES', origen: 'Web' })
|
||||||
|
expect((r['cotizaciones'] as unknown[])).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('devuelve error si el cliente no existe', async () => {
|
||||||
|
prismaMock.cliente.findFirst.mockResolvedValue(null)
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const r = (await e.ver_cliente({ nombre: 'NoExiste' })) as { error: string }
|
||||||
|
expect(r.error).toContain('No se encontró ningún cliente')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('devuelve error si el nombre está vacío', async () => {
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const r = (await e.ver_cliente({ nombre: '' })) as { error: string }
|
||||||
|
expect(r.error).toContain('Falta el nombre del cliente')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ver_oportunidades', () => {
|
||||||
|
it('lista oportunidades del board OPORTUNIDADES con filtro opcional de etapa', async () => {
|
||||||
|
const ops = [
|
||||||
|
{ nombreLead: 'Ana García', etapa: 'Afluencia', desarrollo: 'SOLANA', telefono: '6641111111', email: 'ana@test.com' },
|
||||||
|
{ nombreLead: 'Luis Ruiz', etapa: 'Aparto', desarrollo: 'ATELIER', telefono: '6642222222', email: null },
|
||||||
|
]
|
||||||
|
prismaMock.leadCaptura.findMany.mockResolvedValue(ops)
|
||||||
|
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const todas = (await e.ver_oportunidades({})) as { nombre: string; etapa: string }[]
|
||||||
|
expect(todas).toHaveLength(2)
|
||||||
|
expect(prismaMock.leadCaptura.findMany).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ where: expect.objectContaining({ board: 'OPORTUNIDADES' }) }),
|
||||||
|
)
|
||||||
|
|
||||||
|
prismaMock.leadCaptura.findMany.mockResolvedValue([ops[1]])
|
||||||
|
const filtradas = (await e.ver_oportunidades({ etapa: 'Aparto' })) as { nombre: string }[]
|
||||||
|
expect(filtradas).toHaveLength(1)
|
||||||
|
expect(filtradas[0]!.nombre).toBe('Luis Ruiz')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('ver_cobranza', () => {
|
||||||
|
it('resume planes activos, cancelados y pagos vencidos', async () => {
|
||||||
|
prismaMock.planPago.count.mockImplementation(({ where }: { where: { status: string } }) => {
|
||||||
|
if (where.status === 'ACTIVO') return Promise.resolve(12)
|
||||||
|
if (where.status === 'CANCELADO') return Promise.resolve(3)
|
||||||
|
return Promise.resolve(0)
|
||||||
|
})
|
||||||
|
prismaMock.pago.findMany.mockResolvedValue([
|
||||||
|
{ monto: 25000, montoPagado: 0, fechaVencimiento: new Date('2026-08-01'), plan: { clienteNombre: 'Juan Pérez', unidadCodigo: 'DIA-101' } },
|
||||||
|
{ monto: 25000, montoPagado: 10000, fechaVencimiento: new Date('2026-07-15'), plan: { clienteNombre: 'Ana García', unidadCodigo: 'SOLANA-6' } },
|
||||||
|
])
|
||||||
|
|
||||||
|
const e = crearEjecutoresHerramientas()
|
||||||
|
const r = (await e.ver_cobranza()) as Record<string, unknown>
|
||||||
|
|
||||||
|
expect(r['planesActivos']).toBe(12)
|
||||||
|
expect(r['planesCancelados']).toBe(3)
|
||||||
|
expect(r['pagosVencidos']).toBe(2)
|
||||||
|
expect(r['montoAdeudado']).toBe(40000)
|
||||||
|
expect((r['detalle'] as unknown[])).toHaveLength(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// ---------- Chat con tool calling ----------
|
// ---------- Chat con tool calling ----------
|
||||||
|
|
||||||
/** Cliente NAN falso con function calling: devuelve las respuestas guionizadas en orden */
|
/** Cliente NAN falso con function calling: devuelve las respuestas guionizadas en orden */
|
||||||
@@ -452,6 +597,18 @@ describe('chat con herramientas (tool calling)', () => {
|
|||||||
async buscar_unidad() {
|
async buscar_unidad() {
|
||||||
return []
|
return []
|
||||||
},
|
},
|
||||||
|
async buscar_leads() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async ver_cliente() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
async ver_oportunidades() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async ver_cobranza() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = await chat(null, 'cotízame la SOLANA-6 a bancario', client, { ejecutores })
|
const r = await chat(null, 'cotízame la SOLANA-6 a bancario', client, { ejecutores })
|
||||||
@@ -488,6 +645,18 @@ describe('chat con herramientas (tool calling)', () => {
|
|||||||
async buscar_unidad() {
|
async buscar_unidad() {
|
||||||
return [{ codigo: 'SOLANA-6' }]
|
return [{ codigo: 'SOLANA-6' }]
|
||||||
},
|
},
|
||||||
|
async buscar_leads() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async ver_cliente() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
async ver_oportunidades() {
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
async ver_cobranza() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = await chat(null, 'búscame algo y cotízalo todo', client, { ejecutores })
|
const r = await chat(null, 'búscame algo y cotízalo todo', client, { ejecutores })
|
||||||
@@ -501,7 +670,14 @@ describe('chat con herramientas (tool calling)', () => {
|
|||||||
mocksPrismaChat()
|
mocksPrismaChat()
|
||||||
const client = clienteConTools([{ contenido: 'Respuesta directa.', toolCalls: [] }])
|
const client = clienteConTools([{ contenido: 'Respuesta directa.', toolCalls: [] }])
|
||||||
const r = await chat(null, 'hola', client, {
|
const r = await chat(null, 'hola', client, {
|
||||||
ejecutores: { cotizar_unidad: async () => ({}), buscar_unidad: async () => [] },
|
ejecutores: {
|
||||||
|
cotizar_unidad: async () => ({}),
|
||||||
|
buscar_unidad: async () => [],
|
||||||
|
buscar_leads: async () => [],
|
||||||
|
ver_cliente: async () => ({}),
|
||||||
|
ver_oportunidades: async () => [],
|
||||||
|
ver_cobranza: async () => ({}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
expect(client.llamadas).toHaveLength(1)
|
expect(client.llamadas).toHaveLength(1)
|
||||||
expect(r.respuesta).toBe('Respuesta directa.')
|
expect(r.respuesta).toBe('Respuesta directa.')
|
||||||
|
|||||||
@@ -255,6 +255,12 @@ export function construirSystemPrompt(contexto: ChunkRecuperado[]): string {
|
|||||||
'- Cuando uses cotizar_unidad, presenta enganche, mensualidades y pago final, y aclara que la',
|
'- Cuando uses cotizar_unidad, presenta enganche, mensualidades y pago final, y aclara que la',
|
||||||
' cotización es ORIENTATIVA: el PDF oficial se genera en el cotizador (/quote/).',
|
' cotización es ORIENTATIVA: el PDF oficial se genera en el cotizador (/quote/).',
|
||||||
'- Si la unidad no existe o no está DISPONIBLE, dilo claramente y ofrece buscar alternativas.',
|
'- Si la unidad no existe o no está DISPONIBLE, dilo claramente y ofrece buscar alternativas.',
|
||||||
|
'- Tienes acceso a la base de datos de clientes, leads y cobranza.',
|
||||||
|
'- Usa buscar_leads cuando te pidan información sobre un lead específico.',
|
||||||
|
'- Usa ver_cliente para obtener datos completos de un cliente.',
|
||||||
|
'- Usa ver_oportunidades para ver el pipeline de ventas.',
|
||||||
|
'- Usa ver_cobranza para información de pagos y adeudos.',
|
||||||
|
'- NUNCA inventes datos de clientes o pagos; usa siempre las herramientas.',
|
||||||
'',
|
'',
|
||||||
contexto.length > 0
|
contexto.length > 0
|
||||||
? `CONTEXTO:\n\n${bloques}`
|
? `CONTEXTO:\n\n${bloques}`
|
||||||
@@ -384,6 +390,65 @@ export const HERRAMIENTAS: HerramientaDef[] = [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'buscar_leads',
|
||||||
|
description:
|
||||||
|
'Busca leads por nombre, teléfono o email. Devuelve hasta 10 resultados con nombre, teléfono, email, estado, origen, desarrollo.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
busqueda: { type: 'string', description: 'Texto de búsqueda (nombre, teléfono o email)' },
|
||||||
|
},
|
||||||
|
required: ['busqueda'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'ver_cliente',
|
||||||
|
description:
|
||||||
|
'Muestra la información completa de un cliente: datos personales, propiedad, cotización, pagos. Requiere el nombre del cliente.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
nombre: { type: 'string', description: 'Nombre del cliente (búsqueda parcial)' },
|
||||||
|
},
|
||||||
|
required: ['nombre'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'ver_oportunidades',
|
||||||
|
description:
|
||||||
|
'Lista las oportunidades activas con su etapa, cliente, desarrollo y teléfono. Opcionalmente filtra por etapa.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
etapa: {
|
||||||
|
type: 'string',
|
||||||
|
description: 'Filtrar por etapa: Afluencia, Agendo Cita, Cotizador, Aparto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'function',
|
||||||
|
function: {
|
||||||
|
name: 'ver_cobranza',
|
||||||
|
description:
|
||||||
|
'Muestra el estado de cobranza: clientes con pagos vencidos, monto total adeudado, planes activos.',
|
||||||
|
parameters: {
|
||||||
|
type: 'object',
|
||||||
|
properties: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export interface ArgsCotizarUnidad {
|
export interface ArgsCotizarUnidad {
|
||||||
@@ -399,9 +464,25 @@ export interface ArgsBuscarUnidad {
|
|||||||
precioMax?: number
|
precioMax?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ArgsBuscarLeads {
|
||||||
|
busqueda: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArgsVerCliente {
|
||||||
|
nombre: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArgsVerOportunidades {
|
||||||
|
etapa?: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface EjecutoresHerramientas {
|
export interface EjecutoresHerramientas {
|
||||||
cotizar_unidad(args: ArgsCotizarUnidad): Promise<unknown>
|
cotizar_unidad(args: ArgsCotizarUnidad): Promise<unknown>
|
||||||
buscar_unidad(args: ArgsBuscarUnidad): Promise<unknown>
|
buscar_unidad(args: ArgsBuscarUnidad): Promise<unknown>
|
||||||
|
buscar_leads(args: ArgsBuscarLeads): Promise<unknown>
|
||||||
|
ver_cliente(args: ArgsVerCliente): Promise<unknown>
|
||||||
|
ver_oportunidades(args: ArgsVerOportunidades): Promise<unknown>
|
||||||
|
ver_cobranza(): Promise<unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Redondeo monetario a 2 decimales para los números que lee el modelo */
|
/** Redondeo monetario a 2 decimales para los números que lee el modelo */
|
||||||
@@ -471,6 +552,122 @@ export function crearEjecutoresHerramientas(
|
|||||||
supConstruccion: u.supConstruccion,
|
supConstruccion: u.supConstruccion,
|
||||||
}))
|
}))
|
||||||
},
|
},
|
||||||
|
async buscar_leads(args) {
|
||||||
|
const termino = (args.busqueda ?? '').trim()
|
||||||
|
if (!termino) return { error: 'Falta el texto de búsqueda' }
|
||||||
|
const leads = await prisma.leadCaptura.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ nombreLead: { contains: termino } },
|
||||||
|
{ telefono: { contains: termino } },
|
||||||
|
{ email: { contains: termino } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
take: 10,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
})
|
||||||
|
return leads.map((l) => ({
|
||||||
|
nombre: l.nombreLead,
|
||||||
|
telefono: l.telefono,
|
||||||
|
email: l.email,
|
||||||
|
estado: l.etapa,
|
||||||
|
origen: l.origen,
|
||||||
|
desarrollo: l.desarrollo,
|
||||||
|
board: l.board,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
async ver_cliente(args) {
|
||||||
|
const nombre = (args.nombre ?? '').trim()
|
||||||
|
if (!nombre) return { error: 'Falta el nombre del cliente' }
|
||||||
|
const cliente = await prisma.cliente.findFirst({
|
||||||
|
where: { nombreCompleto: { contains: nombre } },
|
||||||
|
})
|
||||||
|
if (!cliente) return { error: `No se encontró ningún cliente con nombre "${nombre}"` }
|
||||||
|
|
||||||
|
const [leads, cotizaciones] = await Promise.all([
|
||||||
|
prisma.leadCaptura.findMany({
|
||||||
|
where: { nombreLead: { contains: cliente.nombreCompleto } },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 5,
|
||||||
|
}),
|
||||||
|
prisma.cotizacion.findMany({
|
||||||
|
where: { clienteNombre: { contains: cliente.nombreCompleto } },
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
take: 10,
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: cliente.id,
|
||||||
|
nombre: cliente.nombreCompleto,
|
||||||
|
curp: cliente.curp,
|
||||||
|
rfc: cliente.rfc,
|
||||||
|
email: cliente.email,
|
||||||
|
celular: cliente.celular,
|
||||||
|
proyecto: cliente.proyecto,
|
||||||
|
unidadCodigo: cliente.unidadCodigo,
|
||||||
|
lead: leads.length > 0
|
||||||
|
? {
|
||||||
|
etapa: leads[0]!.etapa,
|
||||||
|
board: leads[0]!.board,
|
||||||
|
origen: leads[0]!.origen,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
cotizaciones: cotizaciones.map((c) => ({
|
||||||
|
unidad: c.unidadCodigo,
|
||||||
|
tipoPropuesta: c.tipoPropuesta,
|
||||||
|
precioFinal: r2(c.precioFinal),
|
||||||
|
enganche: r2(c.enganche),
|
||||||
|
mensualidad: r2(c.mensualidad),
|
||||||
|
createdAt: c.createdAt,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async ver_oportunidades(args) {
|
||||||
|
const where: Record<string, unknown> = { board: 'OPORTUNIDADES' }
|
||||||
|
if (args.etapa) where.etapa = args.etapa.trim()
|
||||||
|
const oportunidades = await prisma.leadCaptura.findMany({
|
||||||
|
where,
|
||||||
|
take: 20,
|
||||||
|
orderBy: { createdAt: 'desc' },
|
||||||
|
})
|
||||||
|
return oportunidades.map((o) => ({
|
||||||
|
nombre: o.nombreLead,
|
||||||
|
etapa: o.etapa,
|
||||||
|
desarrollo: o.desarrollo,
|
||||||
|
telefono: o.telefono,
|
||||||
|
email: o.email,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
async ver_cobranza() {
|
||||||
|
const [planesActivos, planesCancelados, todosPagos] = await Promise.all([
|
||||||
|
prisma.planPago.count({ where: { status: 'ACTIVO' } }),
|
||||||
|
prisma.planPago.count({ where: { status: 'CANCELADO' } }),
|
||||||
|
prisma.pago.findMany({
|
||||||
|
where: { fechaVencimiento: { lt: new Date() } },
|
||||||
|
select: {
|
||||||
|
monto: true,
|
||||||
|
montoPagado: true,
|
||||||
|
fechaVencimiento: true,
|
||||||
|
plan: { select: { clienteNombre: true, unidadCodigo: true } },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
const pagosVencidos = todosPagos.filter((p) => p.montoPagado < p.monto)
|
||||||
|
const montoAdeudado = pagosVencidos.reduce((sum, p) => sum + (p.monto - p.montoPagado), 0)
|
||||||
|
return {
|
||||||
|
planesActivos,
|
||||||
|
planesCancelados,
|
||||||
|
pagosVencidos: pagosVencidos.length,
|
||||||
|
montoAdeudado: r2(montoAdeudado),
|
||||||
|
detalle: pagosVencidos.slice(0, 10).map((p) => ({
|
||||||
|
cliente: p.plan.clienteNombre,
|
||||||
|
unidad: p.plan.unidadCodigo,
|
||||||
|
montoAdeudado: r2(p.monto - p.montoPagado),
|
||||||
|
vencimiento: p.fechaVencimiento,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +685,7 @@ export async function ejecutarToolCall(
|
|||||||
const ejecutor = ejecutores[call.nombre as keyof EjecutoresHerramientas]
|
const ejecutor = ejecutores[call.nombre as keyof EjecutoresHerramientas]
|
||||||
if (!ejecutor) return JSON.stringify({ error: `Herramienta desconocida: ${call.nombre}` })
|
if (!ejecutor) return JSON.stringify({ error: `Herramienta desconocida: ${call.nombre}` })
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(await ejecutor(args))
|
return JSON.stringify(await (ejecutor as (a: Record<string, unknown>) => Promise<unknown>)(args))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
return JSON.stringify({ error: err instanceof Error ? err.message : String(err) })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user