feat(backend): auto-assign + transactional double-booking guard + working-hours slots
POST /book and POST /appointments now resolve the specialist via autoAssign (or isAvailable-guard a chosen one) inside BEGIN IMMEDIATE...COMMIT, returning 409 on conflict. Slots endpoint derives the day window from real working_hours and ranks the best specialist per slot. settings/employees expose + persist the new fields. Adds booking-e2e.mjs and adapts e2e-test.mjs fixtures to in-hours slots.
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
// 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];
|
||||
const tomorrow = new Date(Date.now() + 86400000);
|
||||
const date = tomorrow.toISOString().slice(0, 10);
|
||||
|
||||
// 3) Slots disponibles
|
||||
const { json: slotsRes } = await req("GET", `/public/${slug}/slots?service_id=${svc.id}&date=${date}`);
|
||||
check("slots devuelve lista", Array.isArray(slotsRes.slots));
|
||||
check("hay slots disponibles hoy/mañana", slotsRes.slots.length > 0, `got ${slotsRes.slots.length}`);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
Reference in New Issue
Block a user