215 lines
15 KiB
JavaScript
215 lines
15 KiB
JavaScript
// Comprehensive e2e: ALL features (existing + new) must pass before declaring 100% functional.
|
|
const BASE = "http://localhost:5173";
|
|
let pass = 0, fail = 0;
|
|
const failures = [];
|
|
async function req(path, init = {}) {
|
|
const url = path.startsWith("http") ? path : `${BASE}${path}`;
|
|
const res = await fetch(url, init);
|
|
const text = await res.text();
|
|
let json; try { json = JSON.parse(text); } catch { json = text; }
|
|
return { status: res.status, json, headers: res.headers };
|
|
}
|
|
function check(name, cond, extra = "") {
|
|
if (cond) { pass++; console.log(` ✓ ${name}`); }
|
|
else { fail++; failures.push(name + (extra ? " :: " + extra : "")); console.log(` ✗ ${name} ${extra}`); }
|
|
}
|
|
async function login(email, password = "demo1234") {
|
|
const r = await req("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }) });
|
|
if (r.status !== 200) throw new Error(`login ${email} failed: ${r.status} ${JSON.stringify(r.json)}`);
|
|
return r.json;
|
|
}
|
|
const H = (token) => ({ authorization: `Bearer ${token}`, "Content-Type": "application/json" });
|
|
|
|
console.log("\n=== HEALTH & AUTH ===");
|
|
const h = await req("/api/health"); check("health", h.json?.ok === true);
|
|
const me = await req("/api/auth/demo-users"); check("demo-users >= 8", me.json.users.length >= 8);
|
|
|
|
const owner = await login("[email protected]"); check("owner login", !!owner.token && owner.user.role === "owner");
|
|
const empLogin = await login("[email protected]"); check("employee login", empLogin.user.role === "employee" && empLogin.user.employee_id);
|
|
const adminLogin = await login("[email protected]"); check("admin login", adminLogin.user.role === "admin");
|
|
const t = owner.token, eT = empLogin.token, aT = adminLogin.token;
|
|
|
|
// /me fix
|
|
const meR = await req("/api/auth/me", { headers: H(t) }); check("/me returns owner", meR.json.user.email === "[email protected]");
|
|
|
|
console.log("\n=== EXISTING: business / employees / services / clients / appointments / dashboard ===");
|
|
const biz = await req("/api/business", { headers: H(t) }); check("business loaded", biz.json.business?.name === "Lumière Estética & Spa");
|
|
const emps = await req("/api/employees", { headers: H(t) }); check("employees list", emps.json.employees.length === 6 && emps.json.employees[0].stats?.revenue_total >= 0);
|
|
const svcs = await req("/api/services", { headers: H(t) }); check("services list", svcs.json.services.length === 12 && svcs.json.services[0].employee_ids);
|
|
const cls = await req("/api/clients", { headers: H(t) }); check("clients list", cls.json.clients.length > 0 && cls.json.clients[0].stats?.visits >= 0);
|
|
const appts = await req("/api/appointments?limit=5", { headers: H(t) }); check("appointments joined", appts.json.appointments[0]?.service && appts.json.appointments[0]?.client);
|
|
const ov = await req("/api/dashboard/overview", { headers: H(t) }); check("overview KPIs", ov.json.revenue_range > 0 && ov.json.appts_upcoming > 0);
|
|
const be = await req("/api/dashboard/best-employees", { headers: H(t) }); check("best-employees ranked", be.json.employees.length === 6);
|
|
const bs = await req("/api/dashboard/best-services", { headers: H(t) }); check("best-services ranked", bs.json.services.length > 0);
|
|
const tt = await req("/api/dashboard/top-tickets", { headers: H(t) }); check("top-tickets", tt.json.tickets.length > 0);
|
|
const tc = await req("/api/dashboard/top-clients", { headers: H(t) }); check("top-clients", tc.json.clients.length > 0);
|
|
const fc = await req("/api/dashboard/frequent-clients", { headers: H(t) }); check("frequent-clients", fc.json.clients.length > 0);
|
|
const rt = await req("/api/dashboard/revenue-trend?days=30", { headers: H(t) }); check("revenue-trend 30 points", rt.json.points.length === 30);
|
|
const rb = await req("/api/dashboard/revenue-by-category", { headers: H(t) }); check("revenue-by-category", rb.json.slices.length > 0);
|
|
const tk = await req("/api/dashboard/tickets?limit=20", { headers: H(t) }); check("tickets list", tk.json.tickets.length > 0);
|
|
|
|
console.log("\n=== NEW: COMMISSIONS ===");
|
|
const com = await req("/api/dashboard/commissions?range=60", { headers: H(t) });
|
|
check("commissions has rows", com.json.rows?.length === 6);
|
|
check("commissions total > 0 (after backfill)", com.json.total > 0);
|
|
const topEmp = com.json.rows[0];
|
|
check("top employee has commission > 0", topEmp.commission > 0 && topEmp.sales > 0);
|
|
|
|
// Lifecycle creates commission on complete
|
|
const createR = await req("/api/appointments", {
|
|
method: "POST", headers: H(t),
|
|
body: JSON.stringify({ service_id: svcs.json.services[0].id, client_id: cls.json.clients[0].id, start_at: "2026-09-01T10:00:00Z" }),
|
|
});
|
|
check("create appointment", !!createR.json.appointment?.id);
|
|
const completed = await req(`/api/appointments/${createR.json.appointment.id}/complete`, {
|
|
method: "POST", headers: H(t), body: JSON.stringify({ tip: 50, payment_method: "card" }),
|
|
});
|
|
check("complete -> status completed", completed.json.appointment.status === "completed");
|
|
// ticket should have commission > 0
|
|
const tkAfter = await req(`/api/dashboard/tickets?limit=5`, { headers: H(t) });
|
|
const ourTicket = tkAfter.json.tickets.find((t2) => t2.service?.id === svcs.json.services[0].id && t2.amount > 0);
|
|
check("new ticket has commission > 0", ourTicket && ourTicket.commission > 0, `ticket=${JSON.stringify(ourTicket)}`);
|
|
|
|
console.log("\n=== NEW: CANCELLATION POLICY ===");
|
|
const settings = await req("/api/settings", { headers: H(t) });
|
|
check("settings loaded", settings.json.settings?.cancel_window_hours !== undefined);
|
|
check("booking slug set", settings.json.settings?.slug === "lumiere-estetica-spa");
|
|
// Update policy (24h, 50% penalty)
|
|
const upd = await req("/api/settings", {
|
|
method: "PATCH", headers: H(t),
|
|
body: JSON.stringify({ cancel_window_hours: 24, cancel_penalty_pct: 50 }),
|
|
});
|
|
check("update cancellation policy", upd.json.settings.cancel_penalty_pct === 50);
|
|
// Policy preview: appointment 1h from now should be within 24h window -> penalty
|
|
const nearAppt = await req("/api/appointments", {
|
|
method: "POST", headers: H(t),
|
|
body: JSON.stringify({ service_id: svcs.json.services[0].id, client_id: cls.json.clients[0].id, start_at: new Date(Date.now() + 60 * 60000).toISOString() }),
|
|
});
|
|
check("created near-future appt for policy test", !!nearAppt.json.appointment?.id);
|
|
const policy = await req(`/api/appointments/${nearAppt.json.appointment.id}/cancellation-policy`, { headers: H(t) });
|
|
check("policy preview within window", policy.json.within_window === true && policy.json.penalty_amount > 0);
|
|
// Far future appointment should be outside window
|
|
const farAppt = await req("/api/appointments", {
|
|
method: "POST", headers: H(t),
|
|
body: JSON.stringify({ service_id: svcs.json.services[0].id, client_id: cls.json.clients[0].id, start_at: new Date(Date.now() + 48 * 3600000).toISOString() }),
|
|
});
|
|
const policyFar = await req(`/api/appointments/${farAppt.json.appointment.id}/cancellation-policy`, { headers: H(t) });
|
|
check("policy preview outside window (no penalty)", policyFar.json.within_window === false && policyFar.json.penalty_amount === 0);
|
|
// Restore default 0% penalty for downstream tests
|
|
await req("/api/settings", { method: "PATCH", headers: H(t), body: JSON.stringify({ cancel_penalty_pct: 0 }) });
|
|
|
|
console.log("\n=== NEW: PUBLIC BOOKING ===");
|
|
const pubInfo = await req(`${BASE}/api/public/lumiere-estetica-spa`);
|
|
check("public business info", pubInfo.json.business?.name === "Lumière Estética & Spa" && pubInfo.json.services.length === 12 && pubInfo.json.employees.length === 6);
|
|
const pubSlots = await req(`${BASE}/api/public/lumiere-estetica-spa/slots?service_id=1&date=2026-08-12`);
|
|
check("public slots > 0", pubSlots.json.slots?.length > 0);
|
|
const slotIso = pubSlots.json.slots[0].iso;
|
|
// Unique phone per run so the test is idempotent across runs
|
|
const uniq = Date.now().toString().slice(-8);
|
|
// Public book
|
|
const book = await req(`${BASE}/api/public/lumiere-estetica-spa/book`, {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
service_id: 1, employee_id: pubSlots.json.slots[0].employee_id, start_at: slotIso,
|
|
client: { name: "Cliente Web Test " + uniq, phone: "+52 55 " + uniq + " 1111", email: `webtest${uniq}@demo.com`, notes: "Online booking test" },
|
|
}),
|
|
});
|
|
check("public booking 201 + client_created", book.status === 201 && book.json.client_created === true && !!book.json.appointment?.id, `status=${book.status} body=${JSON.stringify(book.json).slice(0,160)}`);
|
|
// Verify the booking shows up in owner appointments (via the new client's history)
|
|
const webClient = (await req("/api/clients?q=" + uniq, { headers: H(t) })).json.clients.find((c) => c.phone && c.phone.includes(uniq));
|
|
check("web client created & searchable", !!webClient);
|
|
// Booking-disabled gate
|
|
await req("/api/settings", { method: "PATCH", headers: H(t), body: JSON.stringify({ booking_enabled: 0 }) });
|
|
const pubDis = await req(`${BASE}/api/public/lumiere-estetica-spa`);
|
|
check("booking disabled -> 403", pubDis.status === 403);
|
|
await req("/api/settings", { method: "PATCH", headers: H(t), body: JSON.stringify({ booking_enabled: 1 }) });
|
|
// Invalid slug
|
|
const pubBad = await req(`${BASE}/api/public/no-such-slug`);
|
|
check("invalid slug -> 404", pubBad.status === 404);
|
|
|
|
console.log("\n=== NEW: CASH REGISTER ===");
|
|
// Defensive: close any leftover open session from previous runs
|
|
const sess0 = await req("/api/cash/session", { headers: H(t) });
|
|
if (sess0.json.session) {
|
|
await req("/api/cash/close", { method: "POST", headers: H(t), body: JSON.stringify({ closing_amount: 0 }) });
|
|
console.log(" (cleaned leftover open session)");
|
|
}
|
|
const sessCheck = await req("/api/cash/session", { headers: H(t) });
|
|
check("no open session initially", sessCheck.json.session === null);
|
|
const open = await req("/api/cash/open", { method: "POST", headers: H(t), body: JSON.stringify({ opening_amount: 500, note: "Apertura test" }) });
|
|
check("open session 201", open.status === 201 && open.json.session?.status === "open", `status=${open.status} body=${JSON.stringify(open.json).slice(0,160)}`);
|
|
const open2 = await req("/api/cash/open", { method: "POST", headers: H(t), body: JSON.stringify({ opening_amount: 100 }) });
|
|
check("cannot open second session (400)", open2.status === 400);
|
|
const entry1 = await req("/api/cash/entries", { method: "POST", headers: H(t), body: JSON.stringify({ type: "income", amount: 300, concept: "Venta producto", payment_method: "cash" }) });
|
|
check("add income entry", entry1.status === 201 && entry1.json.entry?.type === "income", `status=${entry1.status} body=${JSON.stringify(entry1.json).slice(0,160)}`);
|
|
const entry2 = await req("/api/cash/entries", { method: "POST", headers: H(t), body: JSON.stringify({ type: "expense", amount: 50, concept: "Insumos", payment_method: "cash" }) });
|
|
check("add expense entry", entry2.status === 201, `status=${entry2.status}`);
|
|
const sess = await req("/api/cash/session", { headers: H(t) });
|
|
check("session stats: income=300 expense=50 expected=750", sess.json.stats?.income === 300 && sess.json.stats?.expense === 50, `stats=${JSON.stringify(sess.json.stats)} session=${sess.json.session?.id}`);
|
|
const close = await req("/api/cash/close", { method: "POST", headers: H(t), body: JSON.stringify({ closing_amount: 750 }) });
|
|
check("close session (perfect cuadre)", close.status === 200 && close.json.expected === 750 && close.json.session.status === "closed", `status=${close.status}`);
|
|
const sessAfter = await req("/api/cash/session", { headers: H(t) });
|
|
check("no open session after close", sessAfter.json.session === null);
|
|
|
|
console.log("\n=== NEW: NOTIFICATIONS ===");
|
|
const nstats = await req("/api/notifications/stats", { headers: H(t) });
|
|
check("notifications stats", nstats.json.pending >= 0 && nstats.json.upcoming_reminders >= 0);
|
|
const regen = await req("/api/notifications/regenerate", { method: "POST", headers: H(t) });
|
|
check("regenerate notifications", regen.json.ok === true);
|
|
const list = await req("/api/notifications?status=pending", { headers: H(t) });
|
|
check("notifications list pending", list.json.notifications.length > 0);
|
|
const firstN = list.json.notifications[0];
|
|
const send = await req(`/api/notifications/${firstN.id}/send`, { method: "POST", headers: H(t) });
|
|
check("send notification (simulated)", send.json.ok === true);
|
|
const listSent = await req("/api/notifications?status=sent", { headers: H(t) });
|
|
check("notification now sent", listSent.json.notifications.some((n) => n.id === firstN.id));
|
|
|
|
console.log("\n=== MULTI-TENANT ISOLATION ===");
|
|
const newBiz = await req("/api/admin/businesses", {
|
|
method: "POST", headers: H(aT),
|
|
body: JSON.stringify({ name: "Audit Barbería", template: "barberia", ownerName: "Audit Owner", ownerEmail: "[email protected]" }),
|
|
});
|
|
check("admin creates new biz + template seed", newBiz.status === 201 && newBiz.json.seeded?.services === 8);
|
|
const newOwner = await login("[email protected]");
|
|
check("new owner login", newOwner.user.role === "owner" && newOwner.user.business_id === newBiz.json.business.id);
|
|
const isoEmps = await req("/api/employees", { headers: H(newOwner.token) });
|
|
check("isolation: new owner sees only 4 employees", isoEmps.json.employees.length === 4);
|
|
const isoBiz = await req("/api/business", { headers: H(newOwner.token) });
|
|
check("isolation: new owner sees only their business", isoBiz.json.business.name === "Audit Barbería");
|
|
// Owner1 (Lumière) still 6 employees
|
|
const o1Emps = await req("/api/employees", { headers: H(t) });
|
|
check("isolation: Lumière owner still 6 employees", o1Emps.json.employees.length === 6);
|
|
// Owner blocked from admin
|
|
const forb = await req("/api/admin/businesses", { headers: H(t) });
|
|
check("owner blocked from admin (403)", forb.status === 403);
|
|
// Employee auto-assign still works
|
|
const empAppt = await req("/api/appointments", {
|
|
method: "POST", headers: H(eT),
|
|
body: JSON.stringify({ service_id: 1, client_id: cls.json.clients[0].id, start_at: "2026-10-01T10:00:00Z" }),
|
|
});
|
|
check("employee auto-assign (matches emp_id)", empAppt.json.appointment?.employee_id === empLogin.user.employee_id);
|
|
// Cleanup test business
|
|
const del = await req(`/api/admin/businesses/${newBiz.json.business.id}`, { method: "DELETE", headers: H(aT) });
|
|
check("admin delete business (cascade)", del.status === 200);
|
|
const gone = await req("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "[email protected]", password: "demo1234" }) });
|
|
check("deleted business users removed", gone.status === 401);
|
|
|
|
console.log("\n=== AUTH GUARDS ===");
|
|
const badLogin = await req("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "[email protected]", password: "wrong" }) });
|
|
check("bad login 401", badLogin.status === 401);
|
|
const noAuth = await req("/api/employees"); check("no auth 401", noAuth.status === 401);
|
|
const empForb = await req("/api/dashboard/overview", { headers: H(eT) });
|
|
check("employee blocked from dashboard 403", empForb.status === 403);
|
|
const empDash = await req("/api/dashboard/commissions", { headers: H(eT) });
|
|
check("employee blocked from commissions 403", empDash.status === 403);
|
|
const empCash = await req("/api/cash/session", { headers: H(eT) });
|
|
check("employee blocked from cash 403", empCash.status === 403);
|
|
|
|
console.log(`\n${pass}/${pass + fail} passed`);
|
|
if (fail) {
|
|
console.log("\nFAILURES:");
|
|
failures.forEach((f) => console.log(" •", f));
|
|
process.exit(1);
|
|
}
|
|
process.exit(0);
|