// 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("owner@agendapro.demo"); check("owner login", !!owner.token && owner.user.role === "owner"); const empLogin = await login("valentina.cruz@lumiere.mx"); check("employee login", empLogin.user.role === "employee" && empLogin.user.employee_id); const adminLogin = await login("admin@agendapro.demo"); 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 === "owner@agendapro.demo"); 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_month > 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: "audit@barberia.mx" }), }); check("admin creates new biz + template seed", newBiz.status === 201 && newBiz.json.seeded?.services === 8); const newOwner = await login("audit@barberia.mx"); 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: "audit@barberia.mx", 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: "x@x.com", 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);