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:
+29
-3
@@ -13,6 +13,19 @@ function check(name, cond, extra = "") {
|
||||
else { fail++; console.log(` ✗ ${name} ${extra}`); }
|
||||
}
|
||||
|
||||
// Fetch the first available slot (iso) for a service over the next `maxDays` days.
|
||||
// Slots already filter out non-working days and double-booked specialists, so any
|
||||
// returned iso is safe to use as start_at for either autoAssign or self-assign paths.
|
||||
async function findSlot(slug, serviceId, maxDays = 30) {
|
||||
for (let i = 1; i <= maxDays; i++) {
|
||||
const d = new Date(Date.now() + i * 86400000);
|
||||
const date = d.toISOString().slice(0, 10);
|
||||
const { json } = await req("GET", `/public/${slug}/slots?service_id=${serviceId}&date=${date}`);
|
||||
if (Array.isArray(json.slots) && json.slots.length > 0) return json.slots[0].iso;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// /me now works (the bug we fixed) — login first to get a real token
|
||||
const { json: login } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
|
||||
check("login owner", !!login.token && login.user?.role === "owner", JSON.stringify(login).slice(0, 100));
|
||||
@@ -20,6 +33,11 @@ const t = login.token;
|
||||
const { json: me } = await req("GET", "/auth/me", null, t);
|
||||
check("/api/auth/me returns user", me.user?.email === "[email protected]", JSON.stringify(me).slice(0, 100));
|
||||
|
||||
// Slug del negocio (para consultar slots públicos)
|
||||
const { json: settings } = await req("GET", "/settings", null, t);
|
||||
const slug = settings.settings?.slug;
|
||||
check("tiene slug", !!slug, String(slug));
|
||||
|
||||
const checks = [
|
||||
["business", "/business", (j) => j.business?.name === "Lumière Estética & Spa"],
|
||||
["employees+stats", "/employees", (j) => j.employees?.length === 6 && !!j.employees[0].stats],
|
||||
@@ -42,13 +60,21 @@ for (const [name, path, cond] of checks) {
|
||||
}
|
||||
|
||||
// Lifecycle
|
||||
const { json: created } = await req("POST", "/appointments", { service_id: 1, client_id: 1, start_at: "2026-09-15T11:00:00Z" }, t);
|
||||
const slotOwner = await findSlot(slug, 1);
|
||||
check("found slot for service 1", !!slotOwner);
|
||||
const { json: created } = await req("POST", "/appointments", { service_id: 1, client_id: 1, start_at: slotOwner }, t);
|
||||
check("create appointment", !!created.appointment?.id);
|
||||
const { json: completed } = await req("POST", `/appointments/${created.appointment.id}/complete`, { tip: 50, payment_method: "cash" }, t);
|
||||
check("complete -> ticket", completed.appointment?.status === "completed");
|
||||
const { json: empLogin } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
|
||||
const { json: empAppt } = await req("POST", "/appointments", { service_id: 5, client_id: 2, start_at: "2026-09-20T10:00:00Z" }, empLogin.token);
|
||||
check("employee auto-assign", empAppt.appointment?.employee_id === empLogin.user?.employee_id);
|
||||
// Mateo offers services "Corte caballero" (5) and "Corte + arreglo de barba" (6).
|
||||
// Try 5 first (keeps the original assertion intent); fall back to 6 if 5 has no slots.
|
||||
let empSvcId = 5;
|
||||
let slotEmp = await findSlot(slug, empSvcId);
|
||||
if (!slotEmp) { empSvcId = 6; slotEmp = await findSlot(slug, empSvcId); }
|
||||
check("found slot for employee service", !!slotEmp);
|
||||
const { json: empAppt } = await req("POST", "/appointments", { service_id: empSvcId, client_id: 2, start_at: slotEmp }, empLogin.token);
|
||||
check("employee auto-assign", empAppt.appointment?.employee_id === empLogin.user?.employee_id, JSON.stringify(empAppt).slice(0, 150));
|
||||
const { status: badLogin } = await req("POST", "/auth/login", { email: "[email protected]", password: "wrong" });
|
||||
check("bad login 401", badLogin === 401);
|
||||
const { status: noAuth } = await req("GET", "/employees");
|
||||
|
||||
Reference in New Issue
Block a user