Calcula el presupuesto en el flujo: el PDF ya incluye el importe
El PDF llegaba con render+fotos pero sin importe porque nadie ejecutaba el motor. Nuevo helper calcularPresupuestoLead (reutiliza computeBudget + catálogo del orquestador). Se llama tras el post-análisis (WhatsApp/llamada) y, como red de seguridad, al inicio de finalizarYEntregar antes de construir el PDF. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,11 +2,13 @@ import { asc, eq } from 'drizzle-orm';
|
|||||||
import { db } from '@/db';
|
import { db } from '@/db';
|
||||||
import { leads, conversacionWhatsapp, leadPipelineEventos } from '@/db/schema';
|
import { leads, conversacionWhatsapp, leadPipelineEventos } from '@/db/schema';
|
||||||
import { chatJSON, openrouterConfigurado } from '@/lib/ai/openrouter';
|
import { chatJSON, openrouterConfigurado } from '@/lib/ai/openrouter';
|
||||||
|
import { calcularPresupuestoLead } from '@/lib/funnel/calcular-presupuesto';
|
||||||
|
|
||||||
export interface AnalisisResultado {
|
export interface AnalisisResultado {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
perfil?: Record<string, unknown>;
|
perfil?: Record<string, unknown>;
|
||||||
turnos?: number;
|
turnos?: number;
|
||||||
|
presupuesto?: number;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +86,11 @@ export async function analizarTranscripcion(
|
|||||||
metadata: { origen: `analisis_${origen}`, campos: Object.keys(set) },
|
metadata: { origen: `analisis_${origen}`, campos: Object.keys(set) },
|
||||||
});
|
});
|
||||||
|
|
||||||
return { ok: true, perfil: set };
|
// Con los datos capturados, calcula ya el presupuesto orientativo (para que el PDF y el panel lo
|
||||||
|
// muestren). Best-effort: si faltan tipoReforma/m2 no se calcula, pero el resto sí queda guardado.
|
||||||
|
const presupuesto = await calcularPresupuestoLead(leadId);
|
||||||
|
|
||||||
|
return { ok: true, perfil: set, presupuesto: presupuesto.ok ? presupuesto.total : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entrada para WhatsApp: arma la transcripción desde conversacion_whatsapp y delega en el núcleo.
|
// Entrada para WhatsApp: arma la transcripción desde conversacion_whatsapp y delega en el núcleo.
|
||||||
|
|||||||
62
mvp/b2c/src/lib/funnel/calcular-presupuesto.ts
Normal file
62
mvp/b2c/src/lib/funnel/calcular-presupuesto.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { db } from '@/db';
|
||||||
|
import { leads } from '@/db/schema';
|
||||||
|
import { getPricingConfigFor, getCatalogFor } from '@/db/pricing-queries';
|
||||||
|
import { computeBudget } from '@/budget';
|
||||||
|
import { deterministicExtractor } from '@/lib/voice/extractor';
|
||||||
|
import { mergeIntoBudgetInputs, applyPreferences } from '@/lib/voice/apply';
|
||||||
|
import type { RawCallData } from '@/lib/voice/preferences';
|
||||||
|
|
||||||
|
export interface CalculoResultado {
|
||||||
|
ok: boolean;
|
||||||
|
total?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calcula el presupuesto orientativo del lead con su catálogo (mismo motor que el orquestador) y lo
|
||||||
|
// persiste en presupuestoEstimado + desgloseSnapshot. Reutilizable desde el post-análisis (WhatsApp/
|
||||||
|
// llamada) y desde finalizar (antes de construir el PDF). Requiere al menos tipoReforma + m2Suelo.
|
||||||
|
export async function calcularPresupuestoLead(leadId: string): Promise<CalculoResultado> {
|
||||||
|
const [lead] = await db.select().from(leads).where(eq(leads.id, leadId)).limit(1);
|
||||||
|
if (!lead) return { ok: false, error: 'Lead no encontrado.' };
|
||||||
|
if (!lead.tipoReforma || !lead.m2Suelo) {
|
||||||
|
return { ok: false, error: 'Faltan tipoReforma o m2Suelo para calcular el presupuesto.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const [config, catalog] = await Promise.all([
|
||||||
|
getPricingConfigFor(lead.tenantId),
|
||||||
|
getCatalogFor(lead.tenantId),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const raw: RawCallData = {
|
||||||
|
tipoReforma: lead.tipoReforma,
|
||||||
|
m2Suelo: lead.m2Suelo ?? null,
|
||||||
|
calidad: lead.calidadGlobal ?? null,
|
||||||
|
estructural: lead.estructural,
|
||||||
|
urgencia: lead.urgencia ?? null,
|
||||||
|
presupuestoTarget: lead.presupuestoTarget ?? null,
|
||||||
|
tasteText: lead.tasteText ?? '',
|
||||||
|
};
|
||||||
|
const prefs = deterministicExtractor.extract(raw, catalog);
|
||||||
|
const inputs = mergeIntoBudgetInputs(prefs, {
|
||||||
|
tipoReforma: lead.tipoReforma,
|
||||||
|
m2Suelo: lead.m2Suelo ?? null,
|
||||||
|
alturaTecho: lead.alturaTecho ?? null,
|
||||||
|
provincia: lead.provincia ?? null,
|
||||||
|
anteriorA2000: lead.anteriorA2000,
|
||||||
|
cambioDistribucion: lead.cambioDistribucion,
|
||||||
|
});
|
||||||
|
const result = applyPreferences(computeBudget(inputs, config, catalog), prefs);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(leads)
|
||||||
|
.set({
|
||||||
|
presupuestoEstimado: result.total,
|
||||||
|
desgloseSnapshot: { stage: 'presupuesto_generado', result },
|
||||||
|
preferencesSnapshot: prefs,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(leads.id, leadId));
|
||||||
|
|
||||||
|
return { ok: true, total: result.total };
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { enviarPresupuestoEmail } from '@/lib/email/mailer';
|
|||||||
import { notificarFlujoWhatsapp } from '@/lib/webhooks';
|
import { notificarFlujoWhatsapp } from '@/lib/webhooks';
|
||||||
import { resolveTheme } from '@/lib/funnel/themes';
|
import { resolveTheme } from '@/lib/funnel/themes';
|
||||||
import { normalizarTelefonoEs } from '@/lib/voice/retell';
|
import { normalizarTelefonoEs } from '@/lib/voice/retell';
|
||||||
|
import { calcularPresupuestoLead } from '@/lib/funnel/calcular-presupuesto';
|
||||||
|
|
||||||
export type ResultadoFinalizar = {
|
export type ResultadoFinalizar = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
@@ -18,6 +19,10 @@ export type ResultadoFinalizar = {
|
|||||||
// flujo externo. Entrega real (la rama simulada de orchestrator.ts:Paso 7 es solo el estado
|
// flujo externo. Entrega real (la rama simulada de orchestrator.ts:Paso 7 es solo el estado
|
||||||
// intermedio del funnel). Best-effort en email/WhatsApp: el lead avanza igualmente.
|
// intermedio del funnel). Best-effort en email/WhatsApp: el lead avanza igualmente.
|
||||||
export async function finalizarYEntregar(leadId: string): Promise<ResultadoFinalizar> {
|
export async function finalizarYEntregar(leadId: string): Promise<ResultadoFinalizar> {
|
||||||
|
// Asegura el presupuesto orientativo ANTES de construir el PDF (si los datos lo permiten), para
|
||||||
|
// que el documento incluya la cifra. Best-effort: sin tipoReforma/m2 el PDF sale sin importe.
|
||||||
|
await calcularPresupuestoLead(leadId);
|
||||||
|
|
||||||
const pdf = await construirPresupuestoPdf(leadId);
|
const pdf = await construirPresupuestoPdf(leadId);
|
||||||
if (!pdf) return { ok: false, emailEnviado: false, whatsappSenal: false };
|
if (!pdf) return { ok: false, emailEnviado: false, whatsappSenal: false };
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user