El bot rechazaba leads con presupuesto bajo y sonaba frío/repetitivo. Ahora Luisa nunca rechaza (el cierre siempre es positivo; la rentabilidad la valora el reformista en el panel, no el agente) y tiene el tono del agente de voz: simpática, variada y dispuesta a ayudar con referencias. - leads.service: getSiguienteEstado tras presupuesto siempre fin_viable; evaluarViabilidad ya no filtra (informa viable=true). - claude.service: relaja las reglas de la pasada de corrección (permite calidez, conectores, emoji suave, 2-3 líneas, variación) y quita el guard que tomaba "en qué puedo ayudarte" como frase de IA. - prompts: reescribe el guion de Luisa (mensajes como ejemplos variables, ayuda con referencias de tamaño/materiales, sin rechazo) y elimina el bloque <DATOS_EXTRAIDOS>, que era código muerto contradictorio con el generador. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
408 lines
19 KiB
TypeScript
408 lines
19 KiB
TypeScript
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import axios from 'axios';
|
|
import { LeadsService } from '../leads/leads.service';
|
|
|
|
const DEFAULT_SYSTEM_PROMPT =
|
|
'Eres Luisa, asesora de Reformix en Madrid. Cualificas leads de reforma por WhatsApp. ' +
|
|
'Hablas espanol de Espana, calida, simpatica y siempre dispuesta a ayudar; adapta el registro al usuario. ' +
|
|
'Un mensaje por turno, breve (2-3 lineas). Puedes usar algun emoji suave de vez en cuando. ' +
|
|
'Varia como lo dices, no suenes a plantilla y no vuelvas a preguntar lo que ya te han contado. ' +
|
|
'NUNCA digas que eres IA, ChatGPT, OpenAI ni un asistente virtual.';
|
|
|
|
const FRASES_IA_PROHIBIDAS = [
|
|
/soy un (modelo|asistente)/i,
|
|
/desarrollado por openai/i,
|
|
/\bopenai\b/i,
|
|
/\bchatgpt\b/i,
|
|
/inteligencia artificial/i,
|
|
/no tengo un nombre propio/i,
|
|
];
|
|
|
|
export interface ClasificacionResultado {
|
|
responde_pregunta: boolean;
|
|
valor_extraido: string | null;
|
|
es_desvio: boolean;
|
|
intencion: 'respuesta' | 'desvio' | 'despedida' | 'insulto' | 'pregunta';
|
|
}
|
|
|
|
export interface ValidacionResultado {
|
|
valido: boolean;
|
|
valorNormalizado: string | null;
|
|
viable?: boolean;
|
|
}
|
|
|
|
export interface LeadBasico {
|
|
id: string;
|
|
telefono: string;
|
|
nombre: string;
|
|
estado_actual: string;
|
|
espacio: string | null;
|
|
rango_m2: string | null;
|
|
estilo: string | null;
|
|
urgencia: string | null;
|
|
presupuesto_declarado: string | null;
|
|
viable: boolean | null;
|
|
email: string | null;
|
|
}
|
|
|
|
export interface ClaudeResponse {
|
|
respuesta: string;
|
|
entidad?: Partial<LeadBasico>;
|
|
viable?: boolean;
|
|
nuevoEstado?: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class ClaudeService implements OnModuleInit {
|
|
private readonly logger = new Logger(ClaudeService.name);
|
|
private readonly promptsDir = path.join(process.cwd(), 'prompts');
|
|
private systemPromptCache = '';
|
|
private reglasPromptCache = '';
|
|
private readonly reintentosPorLead = new Map<string, { estado: string; count: number }>();
|
|
|
|
constructor(private readonly leadsService: LeadsService) {}
|
|
|
|
onModuleInit() {
|
|
this.systemPromptCache = this.cargarPrompts(['luisa_core.md', 'luisa_flujo.md', 'luisa_casos.md']);
|
|
this.reglasPromptCache = this.cargarPrompts(['luisa_core.md', 'luisa_casos.md']);
|
|
this.logger.log(`Prompts cargados: system=${this.systemPromptCache.length} chars, reglas=${this.reglasPromptCache.length} chars`);
|
|
}
|
|
|
|
private cargarPrompts(archivos: string[]): string {
|
|
const partes: string[] = [];
|
|
for (const archivo of archivos) {
|
|
const rutaCompleta = path.join(this.promptsDir, archivo);
|
|
try {
|
|
if (!fs.existsSync(rutaCompleta)) { this.logger.warn(`Prompt no encontrado: ${archivo}`); continue; }
|
|
const contenido = fs.readFileSync(rutaCompleta, 'utf-8');
|
|
if (contenido.trim()) partes.push(`\n\n## ${archivo}\n${contenido}`);
|
|
} catch { this.logger.warn(`No se pudo leer el prompt: ${archivo}`); }
|
|
}
|
|
return partes.join('\n').trim() || DEFAULT_SYSTEM_PROMPT;
|
|
}
|
|
|
|
private getModelo(clave: 'clasificador' | 'generador' | 'reglas'): string {
|
|
const envMap: Record<string, string | undefined> = {
|
|
clasificador: process.env.MODEL_CLASIFICADOR,
|
|
generador: process.env.MODEL_GENERADOR || process.env.MODEL,
|
|
reglas: process.env.MODEL_REGLAS || process.env.MODEL_CLASIFICADOR,
|
|
};
|
|
return envMap[clave] || (clave === 'generador' ? 'anthropic/claude-sonnet-4-5' : 'anthropic/claude-haiku-4-5');
|
|
}
|
|
|
|
private serializarLead(lead: LeadBasico): string {
|
|
return [
|
|
`- ID: ${lead.id}`,
|
|
`- Telefono: ${lead.telefono}`,
|
|
`- Estado actual: ${lead.estado_actual}`,
|
|
`- Nombre: ${lead.nombre || 'no capturado'}`,
|
|
`- Email: ${lead.email || 'no capturado'}`,
|
|
`- Espacio: ${lead.espacio || 'no capturado'}`,
|
|
`- Rango m2: ${lead.rango_m2 || 'no capturado'}`,
|
|
`- Estilo: ${lead.estilo || 'no capturado'}`,
|
|
`- Urgencia: ${lead.urgencia || 'no capturado'}`,
|
|
`- Presupuesto declarado: ${lead.presupuesto_declarado || 'no capturado'}`,
|
|
`- Viable: ${lead.viable !== null && lead.viable !== undefined ? lead.viable : 'pendiente'}`,
|
|
].join('\n');
|
|
}
|
|
|
|
private async llamarOpenRouter(
|
|
model: string,
|
|
system: string,
|
|
messages: Array<{ role: string; content: string }>,
|
|
options: { temperature?: number; jsonMode?: boolean } = {},
|
|
): Promise<string> {
|
|
const { temperature = 0.7, jsonMode = false } = options;
|
|
const payload: Record<string, unknown> = {
|
|
model,
|
|
messages: [{ role: 'system', content: system }, ...messages],
|
|
max_tokens: 1024,
|
|
temperature,
|
|
};
|
|
if (jsonMode) payload.response_format = { type: 'json_object' };
|
|
|
|
const response = await axios.post('https://openrouter.ai/api/v1/chat/completions', payload, {
|
|
headers: {
|
|
Authorization: `Bearer ${process.env.OPENROUTER_API_KEY}`,
|
|
'Content-Type': 'application/json',
|
|
'HTTP-Referer': 'https://reformix.es',
|
|
'X-Title': 'Reformix Luisa Bot',
|
|
},
|
|
});
|
|
return response.data.choices?.[0]?.message?.content || '';
|
|
}
|
|
|
|
private parsearJson<T>(texto: string): T | null {
|
|
const limpio = texto.replace(/```json\s*/gi, '').replace(/```\s*/g, '').trim();
|
|
const inicio = limpio.indexOf('{');
|
|
const fin = limpio.lastIndexOf('}');
|
|
if (inicio === -1 || fin === -1) return null;
|
|
try { return JSON.parse(limpio.slice(inicio, fin + 1)) as T; } catch { return null; }
|
|
}
|
|
|
|
private normalizarClasificacion(raw: Partial<ClasificacionResultado>): ClasificacionResultado | null {
|
|
const intenciones = ['respuesta', 'desvio', 'despedida', 'insulto', 'pregunta'] as const;
|
|
if (!raw || typeof raw.responde_pregunta !== 'boolean') return null;
|
|
const intencion = intenciones.includes(raw.intencion as typeof intenciones[number])
|
|
? (raw.intencion as ClasificacionResultado['intencion']) : 'respuesta';
|
|
return {
|
|
responde_pregunta: raw.responde_pregunta,
|
|
valor_extraido: raw.valor_extraido === null || raw.valor_extraido === undefined ? null : String(raw.valor_extraido),
|
|
es_desvio: Boolean(raw.es_desvio),
|
|
intencion,
|
|
};
|
|
}
|
|
|
|
private async clasificar(mensaje: string, estadoActual: string): Promise<ClasificacionResultado> {
|
|
const valoresPermitidos = this.leadsService.getValoresPermitidos(estadoActual);
|
|
const system = `Eres un clasificador de mensajes para un bot de cualificacion de leads de reformas.
|
|
Responde UNICAMENTE con un objeto JSON valido. Sin markdown, sin texto antes ni despues.
|
|
|
|
Estado actual del lead: ${estadoActual}
|
|
Valores permitidos para este estado (si aplica): ${valoresPermitidos.length ? valoresPermitidos.join(', ') : 'ninguno (estado sin valor concreto)'}
|
|
|
|
Formato exacto:
|
|
{
|
|
"responde_pregunta": true,
|
|
"valor_extraido": null,
|
|
"es_desvio": false,
|
|
"intencion": "respuesta"
|
|
}
|
|
|
|
Valores validos de intencion: respuesta, desvio, despedida, insulto, pregunta
|
|
|
|
Reglas para valor_extraido:
|
|
- espacio: cocina, bano, salon, comedor, integral, otro
|
|
- tamano: menos10, 10a20, 20a40, mas40
|
|
- estilo: funcional, cuidado, exclusivo
|
|
- urgencia: alta, media, baja
|
|
- presupuesto: numero o rango en euros tal como lo dijo el usuario
|
|
- apertura: null si solo confirma disponibilidad; extrae nombre si lo menciona
|
|
- Si el usuario pregunta algo (nombre, precios, etc.) usa intencion "pregunta" y responde_pregunta false
|
|
- Saludos casuales sin confirmar disponibilidad: intencion "desvio", es_desvio true
|
|
- Usuarios de Madrid/Espana: interpreta coloquialismos, jerga y dialecto peninsular (vale, tio, mola, guay, etc.) como respuesta valida si aportan el dato del estado
|
|
- Extrae el valor semantico aunque venga en lenguaje coloquial`;
|
|
|
|
const intentos = [{ jsonMode: true, temperature: 0.1 }, { jsonMode: true, temperature: 0 }];
|
|
for (const opts of intentos) {
|
|
const contenido = await this.llamarOpenRouter(this.getModelo('clasificador'), system, [{ role: 'user', content: mensaje }], opts);
|
|
const parsed = this.normalizarClasificacion(this.parsearJson<Partial<ClasificacionResultado>>(contenido) ?? {});
|
|
if (parsed) return parsed;
|
|
this.logger.warn(`Clasificador JSON invalido (intento): ${contenido.slice(0, 200)}`);
|
|
}
|
|
return { responde_pregunta: false, valor_extraido: null, es_desvio: true, intencion: 'desvio' };
|
|
}
|
|
|
|
private validar(clasificacion: ClasificacionResultado, estadoActual: string): ValidacionResultado {
|
|
const estado = this.leadsService.normalizarEstadoFlujo(estadoActual);
|
|
if (clasificacion.es_desvio || clasificacion.intencion === 'desvio' || clasificacion.intencion === 'pregunta' ||
|
|
clasificacion.intencion === 'insulto' || clasificacion.intencion === 'despedida') {
|
|
return { valido: false, valorNormalizado: null };
|
|
}
|
|
if (estado === 'nuevo') return { valido: false, valorNormalizado: null };
|
|
if (estado === 'apertura') {
|
|
return { valido: clasificacion.responde_pregunta && clasificacion.intencion === 'respuesta' && !clasificacion.es_desvio, valorNormalizado: clasificacion.valor_extraido };
|
|
}
|
|
if (estado === 'presupuesto') {
|
|
const valor = clasificacion.valor_extraido?.trim();
|
|
if (!valor || !this.leadsService.esPresupuestoValido(valor)) return { valido: false, valorNormalizado: null };
|
|
return { valido: true, valorNormalizado: valor, viable: this.leadsService.evaluarViabilidad(valor) };
|
|
}
|
|
const valoresPermitidos = this.leadsService.getValoresPermitidos(estado);
|
|
const valor = this.normalizarTexto(clasificacion.valor_extraido ?? '');
|
|
if (!valor) return { valido: false, valorNormalizado: null };
|
|
const coincide = valoresPermitidos.some((v) => v === valor || valor.includes(v) || v.includes(valor));
|
|
if (!coincide) return { valido: false, valorNormalizado: null };
|
|
const valorNormalizado = valoresPermitidos.find((v) => v === valor || valor.includes(v) || v.includes(valor)) ?? valor;
|
|
return { valido: true, valorNormalizado };
|
|
}
|
|
|
|
private normalizarTexto(valor: string): string {
|
|
return valor.trim().toLowerCase().normalize('NFD').replace(/\p{Diacritic}/gu, '');
|
|
}
|
|
|
|
private claveReintento(leadId: string, estado: string): string { return `${leadId}:${estado}`; }
|
|
|
|
private obtenerReintentos(leadId: string, estado: string): number {
|
|
const entry = this.reintentosPorLead.get(this.claveReintento(leadId, estado));
|
|
return entry?.estado === estado ? entry.count : 0;
|
|
}
|
|
|
|
private incrementarReintentos(leadId: string, estado: string): number {
|
|
const clave = this.claveReintento(leadId, estado);
|
|
const actual = this.obtenerReintentos(leadId, estado);
|
|
const count = actual + 1;
|
|
this.reintentosPorLead.set(clave, { estado, count });
|
|
return count;
|
|
}
|
|
|
|
private resetearReintentos(leadId: string, estado: string): void {
|
|
this.reintentosPorLead.delete(this.claveReintento(leadId, estado));
|
|
}
|
|
|
|
private async generar(
|
|
lead: LeadBasico,
|
|
historial: Array<{ role: string; content: string }>,
|
|
mensajeActual: string,
|
|
clasificacion: ClasificacionResultado,
|
|
validacion: ValidacionResultado,
|
|
reintentos: number,
|
|
avanzarEstado: boolean,
|
|
siguienteEstado: string | null,
|
|
forzarApertura = false,
|
|
): Promise<string> {
|
|
const estadoFlujo = this.leadsService.normalizarEstadoFlujo(lead.estado_actual);
|
|
const contextoGeneracion = `
|
|
## Contexto del lead
|
|
${this.serializarLead(lead)}
|
|
|
|
## Estado del turno
|
|
- Estado de flujo: ${estadoFlujo}
|
|
- Clasificacion: ${JSON.stringify(clasificacion)}
|
|
- Validacion valida: ${validacion.valido}
|
|
- Valor validado: ${validacion.valorNormalizado ?? 'ninguno'}
|
|
- Reintentos en este estado: ${reintentos}
|
|
- Avanzar estado: ${avanzarEstado}
|
|
- Siguiente estado (si avanza): ${siguienteEstado ?? 'sin cambio'}
|
|
- Forzar mensaje de apertura: ${forzarApertura}
|
|
|
|
## Instrucciones de respuesta
|
|
Eres Luisa de Reformix en Madrid. Sigue el system prompt al pie de la letra.
|
|
Habla espanol de Espana; calida, simpatica y siempre dispuesta a ayudar, como una asesora de confianza.
|
|
Varia como lo dices en cada turno (no repitas frases calcadas) y no vuelvas a preguntar un dato que el
|
|
usuario ya te haya dado en este mensaje o en el historial; reconocelo con naturalidad y sigue con lo que falte.
|
|
Devuelve SOLO el texto del mensaje a enviar por WhatsApp.
|
|
NO incluyas JSON, etiquetas XML ni bloques de datos extraidos.
|
|
NUNCA digas que eres IA, OpenAI, ChatGPT ni un asistente virtual.
|
|
|
|
Si forzar mensaje de apertura es true, envia el mensaje de APERTURA del flujo.
|
|
Si validacion valida es false y reintentos < 2, ayudale con calidez dando ejemplos o referencias para que se aclare.
|
|
Si validacion valida es false y reintentos >= 2, vuelve a la pregunta del estado de otra forma, sin sonar borde.
|
|
Si es_desvio es true o intencion es pregunta, atiende su duda con simpatia (concede algo util) y retoma el flujo sin avanzar.
|
|
Si avanzar estado es true, haz la pregunta correspondiente al siguiente estado.
|
|
Si el siguiente estado es fin_viable, cierra con calidez anunciando que ya le preparas el presupuesto.`;
|
|
|
|
const contenido = await this.llamarOpenRouter(this.getModelo('generador'),
|
|
`${this.systemPromptCache || DEFAULT_SYSTEM_PROMPT}\n${contextoGeneracion}`,
|
|
[...historial, { role: 'user', content: mensajeActual }],
|
|
{ temperature: 0.7 },
|
|
);
|
|
return contenido.trim();
|
|
}
|
|
|
|
private async aplicarReglas(borrador: string, lead: LeadBasico, estadoFlujo: string, clasificacion: ClasificacionResultado): Promise<string> {
|
|
const reglas = this.reglasPromptCache || DEFAULT_SYSTEM_PROMPT;
|
|
const system = `${reglas}
|
|
|
|
## Tu tarea
|
|
Recibes un borrador de respuesta para WhatsApp. Debes corregirlo para que cumpla TODAS las reglas de Luisa.
|
|
Devuelve SOLO el mensaje final listo para enviar, sin explicaciones ni JSON.
|
|
|
|
## Contexto
|
|
- Estado del lead: ${estadoFlujo}
|
|
- Nombre lead: ${lead.nombre || 'desconocido'}
|
|
- Intencion del usuario: ${clasificacion.intencion}
|
|
|
|
## Reglas de correccion obligatorias
|
|
- Debe sonar como Luisa de Reformix (Madrid): calida, simpatica y servicial, nunca un asistente generico ni un teleoperador
|
|
- Espanol de Espana, natural; usa coloquialismos y conectores suaves (vale, mira, oye, genial, tranquila, perfecto) cuando encajen
|
|
- Un mensaje por turno, breve (2-3 lineas como mucho); puede llevar algun emoji suave ocasional, sin abusar
|
|
- Varia la redaccion; no dejes frases calcadas ni que repitan literalmente lo que ya se dijo en turnos anteriores
|
|
- No reescribas para quitar la cercania: si el borrador suena frio o robotico, dale calidez en vez de recortarla
|
|
- NUNCA mencionar IA, OpenAI, ChatGPT, modelos de lenguaje ni asistentes virtuales
|
|
- Si preguntan el nombre: "Soy Luisa de Reformix"
|
|
- Quita cualquier JSON o etiqueta tecnica que se haya colado; deja solo el mensaje natural
|
|
- Si el borrador viola alguna regla, reescribelo completamente manteniendo la intencion`;
|
|
|
|
const contenido = await this.llamarOpenRouter(this.getModelo('reglas'), system,
|
|
[{ role: 'user', content: `Borrador a corregir:\n${borrador}` }],
|
|
{ temperature: 0.3 },
|
|
);
|
|
return contenido.trim() || borrador;
|
|
}
|
|
|
|
private contieneFraseProhibida(texto: string): boolean {
|
|
return FRASES_IA_PROHIBIDAS.some((regex) => regex.test(texto));
|
|
}
|
|
|
|
private mensajeFallback(estadoFlujo: string, lead: LeadBasico): string {
|
|
const nombre = lead.nombre ? lead.nombre : '';
|
|
const fallbacks: Record<string, string> = {
|
|
nuevo: `Hola${nombre ? ' ' + nombre : ''}, soy Luisa de Reformix; vi que dejaste tus datos en nuestra web y queria ayudarte a preparar tu presupuesto. Tienes unos minutos ahora?`,
|
|
apertura: `Hola${nombre ? ' ' + nombre : ''}, soy Luisa de Reformix; vi que dejaste tus datos en nuestra web y queria ayudarte a preparar tu presupuesto. Tienes unos minutos ahora?`,
|
|
espacio: 'Que espacio tienes en mente, cocina, bano, salon, o algo mas completo?',
|
|
tamano: 'Tienes idea del tamano aproximado, menos de 10m2, entre 10 y 20, entre 20 y 40, o mas de 40?',
|
|
estilo: 'Como te imaginas el resultado; algo funcional y limpio, un acabado mas cuidado con buenos materiales, o algo mas exclusivo donde cada detalle cuenta?',
|
|
urgencia: 'Y cuando tienes pensado arrancar, es algo proximo o todavia estas explorando?',
|
|
presupuesto: 'Ultima pregunta; tienes en mente un presupuesto aproximado para la reforma?',
|
|
};
|
|
return fallbacks[estadoFlujo] ?? 'Soy Luisa de Reformix; sigamos con tu presupuesto de reforma.';
|
|
}
|
|
|
|
async llamarClaude(
|
|
lead: LeadBasico,
|
|
historial: Array<{ role: string; content: string }>,
|
|
mensajeActual: string,
|
|
): Promise<ClaudeResponse> {
|
|
const estadoFlujo = this.leadsService.normalizarEstadoFlujo(lead.estado_actual);
|
|
|
|
if (estadoFlujo === 'nuevo') {
|
|
const clasificacion: ClasificacionResultado = { responde_pregunta: false, valor_extraido: null, es_desvio: false, intencion: 'respuesta' };
|
|
const borrador = await this.generar(lead, historial, mensajeActual, clasificacion,
|
|
{ valido: false, valorNormalizado: null }, 0, false, null, true);
|
|
const respuesta = await this.aplicarReglas(borrador, lead, 'nuevo', clasificacion);
|
|
return {
|
|
respuesta: this.contieneFraseProhibida(respuesta) ? this.mensajeFallback('nuevo', lead) : respuesta,
|
|
nuevoEstado: 'apertura',
|
|
};
|
|
}
|
|
|
|
const clasificacion = await this.clasificar(mensajeActual, estadoFlujo);
|
|
const validacion = this.validar(clasificacion, estadoFlujo);
|
|
|
|
let reintentos = this.obtenerReintentos(lead.id, estadoFlujo);
|
|
let avanzarEstado = false;
|
|
let siguienteEstado: string | null = null;
|
|
let entidad: Partial<LeadBasico> = {};
|
|
let viable: boolean | undefined;
|
|
|
|
const puedeAvanzar = validacion.valido && !clasificacion.es_desvio && clasificacion.intencion === 'respuesta';
|
|
|
|
if (puedeAvanzar) {
|
|
avanzarEstado = true;
|
|
this.resetearReintentos(lead.id, estadoFlujo);
|
|
if (validacion.valorNormalizado) {
|
|
const campo = this.leadsService.getCampoParaEstado(estadoFlujo);
|
|
if (campo) {
|
|
(entidad as any)[campo] = validacion.valorNormalizado;
|
|
} else if (estadoFlujo === 'apertura' && clasificacion.valor_extraido?.trim()) {
|
|
entidad.nombre = clasificacion.valor_extraido.trim();
|
|
}
|
|
}
|
|
siguienteEstado = this.leadsService.getSiguienteEstado(estadoFlujo);
|
|
// `viable` es solo informativo (siempre true): no cambia la ruta. Luisa nunca rechaza.
|
|
if (estadoFlujo === 'presupuesto') viable = validacion.viable;
|
|
} else if (!validacion.valido && clasificacion.responde_pregunta && !clasificacion.es_desvio) {
|
|
reintentos = this.incrementarReintentos(lead.id, estadoFlujo);
|
|
if (reintentos > 2) reintentos = 2;
|
|
}
|
|
|
|
const borrador = await this.generar(lead, historial, mensajeActual, clasificacion, validacion, reintentos, avanzarEstado, siguienteEstado);
|
|
let respuesta = await this.aplicarReglas(borrador, lead, estadoFlujo, clasificacion);
|
|
|
|
if (this.contieneFraseProhibida(respuesta)) {
|
|
this.logger.warn(`Respuesta final viola reglas, usando fallback para estado=${estadoFlujo}`);
|
|
respuesta = this.mensajeFallback(estadoFlujo, lead);
|
|
}
|
|
|
|
return {
|
|
respuesta,
|
|
entidad: Object.keys(entidad).length > 0 ? entidad : undefined,
|
|
viable,
|
|
nuevoEstado: avanzarEstado ? siguienteEstado ?? undefined : undefined,
|
|
};
|
|
}
|
|
}
|