Files
AgendaPro/server/scripts/booking-e2e.mjs
AgendaPro DevandClaude Opus 5 521c206ba7 fix: el paso de especialista solo ofrece a quien da ese servicio
Elegir "Corte + arreglo de barba" y luego cualquier especialista que no fuera el
barbero dejaba el asistente atascado: /slots responde 400 "El especialista no
ofrece este servicio" y la UI lo pintaba como "No pudimos cargar los horarios.
Intenta otra fecha" — un mensaje que manda a probar días al azar cuando ninguna
fecha lo resuelve.

La raíz es que el payload público listaba todos los empleados activos sin decir
qué servicios ofrece cada uno, así que el cliente no tenía con qué filtrar.

- GET /api/public/:slug devuelve ahora `service_ids` por empleado. El JOIN contra
  employees filtra por negocio: employee_services no tiene business_id propio.
- El paso 2 lista solo a los elegibles, y si nadie ofrece el servicio en concreto
  lo dice en vez de mostrar una lista vacía.
- Cambiar de servicio suelta un especialista que ya no aplique, en lugar de
  arrastrar una combinación que el servidor va a rechazar.
- El estado de error muestra el motivo real del servidor.

booking-e2e.mjs cubre el contrato: cada empleado publica service_ids, quien
ofrece el servicio responde 200 y quien no responde 400. 15/15.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-28 15:24:10 -06:00

97 lines
5.2 KiB
JavaScript

// server/scripts/booking-e2e.mjs — requiere el server dev corriendo (npm run dev)
const BASE = "http://localhost:5173/api";
let pass = 0, fail = 0;
async function req(method, path, body, token) {
const headers = { "Content-Type": "application/json" };
if (token) headers.authorization = `Bearer ${token}`;
const res = await fetch(`${BASE}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
const text = await res.text();
let json; try { json = JSON.parse(text); } catch { json = text; }
return { status: res.status, json };
}
function check(name, cond, extra = "") {
if (cond) { pass++; console.log(` \u2713 ${name}`); }
else { fail++; console.log(` \u2717 ${name} ${extra}`); }
}
const { json: login } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
const t = login.token;
check("login owner", !!t);
// 1) Slug del negocio
const { json: settings } = await req("GET", "/settings", null, t);
const slug = settings.settings?.slug;
check("tiene slug", !!slug, String(slug));
// 2) Datos públicos + servicios
const { json: pub } = await req("GET", `/public/${slug}`);
check("public business", !!pub.business && Array.isArray(pub.services) && pub.services.length > 0);
const svc = pub.services[0];
// El paso "Elige un especialista" solo puede ofrecer a quien realmente da el servicio:
// si ofrece a otro, /slots responde 400 y el cliente queda atascado. Estas dos
// comprobaciones fijan el contrato que hace posible ese filtro en el cliente.
check(
"cada especialista publica sus service_ids",
pub.employees.every((e) => Array.isArray(e.service_ids)),
JSON.stringify(pub.employees.map((e) => e.service_ids)).slice(0, 120)
);
{
const target = pub.services.find((x) => pub.employees.some((e) => e.service_ids.includes(x.id)));
const offers = pub.employees.filter((e) => e.service_ids.includes(target.id));
const denies = pub.employees.filter((e) => !e.service_ids.includes(target.id));
const probe = new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10);
const okRes = await req("GET", `/public/${slug}/slots?service_id=${target.id}&employee_id=${offers[0].id}&date=${probe}`);
check("especialista que SÍ ofrece el servicio → 200", okRes.status === 200, `status=${okRes.status}`);
if (denies.length) {
const badRes = await req("GET", `/public/${slug}/slots?service_id=${target.id}&employee_id=${denies[0].id}&date=${probe}`);
check("especialista que NO lo ofrece → 400 (por eso se filtra en la UI)", badRes.status === 400, `status=${badRes.status}`);
}
}
// 3) Slots disponibles.
// Se busca el próximo día ABIERTO en vez de asumir "mañana": el negocio demo cierra
// sábado y domingo, así que fijar mañana hacía que la suite fallara cada viernes y
// sábado por calendario, no por un defecto del producto.
let date = null;
let slotsRes = null;
for (let i = 1; i <= 8; i++) {
const d = new Date(Date.now() + i * 86400000).toISOString().slice(0, 10);
const r = await req("GET", `/public/${slug}/slots?service_id=${svc.id}&date=${d}`);
if (i === 1) check("slots devuelve lista", Array.isArray(r.json.slots));
if (Array.isArray(r.json.slots) && r.json.slots.length > 0) { date = d; slotsRes = r.json; break; }
}
check("hay slots en algún día abierto de la próxima semana", !!slotsRes, date ? `fecha ${date}` : "ningún día con slots");
if (!slotsRes) { console.error("Sin slots en 8 días: revisa working_hours del negocio demo."); process.exit(1); }
// 4) Reservar (auto-asignación, sin employee_id)
const slot = slotsRes.slots[0];
const { status: bookStatus, json: booked } = await req("POST", `/public/${slug}/book`, {
service_id: svc.id,
start_at: slot.iso,
client: { name: "Cliente Prueba Auto", phone: "+525500000001" },
});
check("reserva creada (auto-assign)", bookStatus === 201 && !!booked.appointment?.employee_id, `status=${bookStatus}`);
check("response trae reasons", Array.isArray(booked.appointment?.reasons) && booked.appointment.reasons.length > 0);
check("response trae no_show_count", typeof booked.no_show_count === "number", `got ${typeof booked.no_show_count}`);
check("response trae risk_flag", typeof booked.risk_flag === "boolean", `got ${typeof booked.risk_flag}`);
check("cliente nuevo → sin riesgo", booked.no_show_count === 0 && booked.risk_flag === false, `ns=${booked.no_show_count} risk=${booked.risk_flag}`);
// 5) Mismo slot otra vez → 409 (doble reserva del mismo especialista)
const { status: conflict } = await req("POST", `/public/${slug}/book`, {
service_id: svc.id,
employee_id: booked.appointment.employee_id,
start_at: slot.iso,
client: { name: "Cliente Conflictivo", phone: "+525500000002" },
});
check("doble reserva → 409", conflict === 409, `status=${conflict}`);
// 6) Activar auto_assign_specialist y verificar que la respuesta pública lo expone
await req("PATCH", "/settings", { auto_assign_specialist: 1 }, t);
const { json: pub2 } = await req("GET", `/public/${slug}`);
check("auto_assign_specialist expuesto en público", pub2.business?.auto_assign_specialist === 1);
await req("PATCH", "/settings", { auto_assign_specialist: 0 }, t); // cleanup
console.log(`\n${pass}/${pass + fail} passed${fail ? `, ${fail} FAILED` : ""}`);
process.exit(fail ? 1 : 0);