feat(whatsapp): máquina de conversación del bot de reservas (F2) + test
- lib/turnos.ts: crearTurno() compartido (valida servicio/jefe/slot); cliente/turnos POST lo reutiliza - lib/telefono.ts: normalización de teléfono (E.164 sin '+') para matchear el wa_id - lib/whatsapp-bot.ts: procesarMensaje(telefono, texto), state machine completa: identidad por teléfono + verificación por email registrado / alta nueva, elegir mecánico (botones) -> trabajo (letras) -> horario (letras, con "más") -> confirmar, crea turno sin auto/km; expiración 30min, "cancelar" global, manejo de inválidos y 409. Devuelve objetos-respuesta (texto/botones) -> independiente de Meta (lo traduce F0). - scripts/test-whatsapp-bot.ts + npm run test:bot: corre la máquina de estados con un Prisma en memoria (sin DB ni Meta), ejercitando scheduling y crearTurno reales. 27/27 OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
import { prisma } from "./prisma";
|
||||
import { slotEstaDisponible } from "./scheduling";
|
||||
|
||||
export interface CrearTurnoInput {
|
||||
clienteId: string;
|
||||
jefeMecanicoId: string;
|
||||
tipoServicioId: string;
|
||||
fechaInicio: Date;
|
||||
autoId?: string | null;
|
||||
kilometros?: number | null;
|
||||
}
|
||||
|
||||
export type CrearTurnoResultado =
|
||||
| { ok: true; turno: Awaited<ReturnType<typeof prisma.turno.create>> }
|
||||
| { ok: false; error: string; status: number };
|
||||
|
||||
/**
|
||||
* Crea un turno validando servicio/jefe activos y disponibilidad del slot.
|
||||
* Lo usan tanto el flujo web (con auto+km) como el bot de WhatsApp (sin auto+km).
|
||||
* La duración del turno la define el TipoServicio.
|
||||
*/
|
||||
export async function crearTurno(input: CrearTurnoInput): Promise<CrearTurnoResultado> {
|
||||
const { clienteId, jefeMecanicoId, tipoServicioId, fechaInicio } = input;
|
||||
|
||||
const servicio = await prisma.tipoServicio.findUnique({ where: { id: tipoServicioId } });
|
||||
if (!servicio || !servicio.activo)
|
||||
return { ok: false, error: "Servicio no disponible", status: 404 };
|
||||
|
||||
const jefe = await prisma.jefeMecanico.findUnique({ where: { id: jefeMecanicoId } });
|
||||
if (!jefe || !jefe.activo)
|
||||
return { ok: false, error: "Jefe mecánico no disponible", status: 404 };
|
||||
|
||||
const fin = new Date(fechaInicio.getTime() + servicio.duracionHoras * 60 * 60 * 1000);
|
||||
|
||||
const disponible = await slotEstaDisponible(fechaInicio, fin, jefeMecanicoId);
|
||||
if (!disponible) return { ok: false, error: "El horario ya no está disponible", status: 409 };
|
||||
|
||||
const turno = await prisma.turno.create({
|
||||
data: {
|
||||
clienteId,
|
||||
jefeMecanicoId,
|
||||
tipoServicioId,
|
||||
fechaInicio,
|
||||
fechaFin: fin,
|
||||
autoId: input.autoId ?? null,
|
||||
kilometros: input.kilometros ?? null,
|
||||
},
|
||||
include: {
|
||||
auto: true,
|
||||
tipoServicio: true,
|
||||
jefeMecanico: { select: { id: true, nombre: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return { ok: true, turno };
|
||||
}
|
||||
Reference in New Issue
Block a user