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 { leads, conversacionWhatsapp, leadPipelineEventos } from '@/db/schema';
|
||||
import { chatJSON, openrouterConfigurado } from '@/lib/ai/openrouter';
|
||||
import { calcularPresupuestoLead } from '@/lib/funnel/calcular-presupuesto';
|
||||
|
||||
export interface AnalisisResultado {
|
||||
ok: boolean;
|
||||
perfil?: Record<string, unknown>;
|
||||
turnos?: number;
|
||||
presupuesto?: number;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
@@ -84,7 +86,11 @@ export async function analizarTranscripcion(
|
||||
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.
|
||||
|
||||
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 { resolveTheme } from '@/lib/funnel/themes';
|
||||
import { normalizarTelefonoEs } from '@/lib/voice/retell';
|
||||
import { calcularPresupuestoLead } from '@/lib/funnel/calcular-presupuesto';
|
||||
|
||||
export type ResultadoFinalizar = {
|
||||
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
|
||||
// intermedio del funnel). Best-effort en email/WhatsApp: el lead avanza igualmente.
|
||||
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);
|
||||
if (!pdf) return { ok: false, emailEnviado: false, whatsappSenal: false };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user