Multi-tenant scheduling SaaS (AgendaPro-equivalent): - Backend: Node + Express + node:sqlite, multi-tenant (admin/owner/employee), versioned migrations, 4 templates (estetica-spa, barberia, clinica, blank). - Frontend: React 18 + TS + Vite + Tailwind, FullCalendar drag-and-drop, Recharts dashboard, mobile-first (day/list views on mobile). - Features: calendar+services+employees+clients+tickets, dashboard with best employee/service, top tickets, top/frequent clients, commissions, cancellation policy + no-show tracking, public online booking (/b/:slug), cash register (cierre de caja), reminders/notifications center. - Verification: 65/65 e2e API, 58/58 visual (0 overflow, 0 console errors), typecheck clean, vite build OK.
84 lines
4.5 KiB
JavaScript
84 lines
4.5 KiB
JavaScript
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(` ✓ ${name}`); }
|
|
else { fail++; console.log(` ✗ ${name} ${extra}`); }
|
|
}
|
|
|
|
// Admin login
|
|
const { json: aLogin } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
|
|
check("admin login", aLogin.user?.role === "admin" && aLogin.token, JSON.stringify(aLogin).slice(0, 100));
|
|
const A = aLogin.token;
|
|
|
|
// Existing data preserved (resilient count — other test suites may add appointments)
|
|
const { json: list } = await req("GET", "/admin/businesses", null, A);
|
|
check("admin lists businesses", list.businesses?.length >= 1, `got ${list.businesses?.length}`);
|
|
check("biz1 data intact", list.businesses[0]?.stats?.appointments >= 100, `appts=${list.businesses[0]?.stats?.appointments}`);
|
|
|
|
// Platform overview
|
|
const { json: ov } = await req("GET", "/admin/overview", null, A);
|
|
check("platform overview", ov.businesses >= 1 && ov.total_appointments >= 100, JSON.stringify(ov));
|
|
|
|
// Templates
|
|
const { json: tpls } = await req("GET", "/admin/templates", null, A);
|
|
check("templates list", tpls.templates?.length === 4, `got ${tpls.templates?.length}`);
|
|
|
|
// Create a barberia business with template + owner
|
|
const { json: created, status: cStatus } = await req("POST", "/admin/businesses", {
|
|
name: "Barbería Test",
|
|
industry: "Barbería",
|
|
template: "barberia",
|
|
ownerName: "Test Owner",
|
|
ownerEmail: "[email protected]",
|
|
ownerPassword: "demo1234",
|
|
}, A);
|
|
check("create business + template seed", cStatus === 201 && !!created.business?.id, JSON.stringify(created).slice(0, 150));
|
|
check("barberia seeded services", created.seeded?.services === 8, `got ${created.seeded?.services}`);
|
|
const newBizId = created.business?.id;
|
|
|
|
// Owner of new business can log in and see only their data (tenant isolation)
|
|
const { json: oLogin } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
|
|
check("new owner login", oLogin.user?.role === "owner" && oLogin.user?.business_id === newBizId);
|
|
const OT = oLogin.token;
|
|
const { json: oEmps } = await req("GET", "/employees", null, OT);
|
|
check("tenant isolation: new owner sees only their employees", oEmps.employees?.length === 4, `got ${oEmps.employees?.length}`);
|
|
const { json: oBiz } = await req("GET", "/business", null, OT);
|
|
check("new owner business name", oBiz.business?.name === "Barbería Test");
|
|
|
|
// Owner1 still sees only Lumière (isolation the other way)
|
|
const { json: owner1 } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
|
|
const { json: o1Emps } = await req("GET", "/employees", null, owner1.token);
|
|
check("owner1 isolation: still 6 employees", o1Emps.employees?.length === 6, `got ${o1Emps.employees?.length}`);
|
|
|
|
// Admin cannot be blocked, but owner cannot access admin
|
|
const { status: forb } = await req("GET", "/admin/businesses", null, owner1.token);
|
|
check("owner blocked from admin (403)", forb === 403);
|
|
|
|
// Update business (plan/status)
|
|
const { json: upd } = await req("PATCH", `/admin/businesses/${newBizId}`, { plan: "pro", status: "suspended" }, A);
|
|
check("update business plan/status", upd.business?.plan === "pro" && upd.business?.status === "suspended");
|
|
|
|
// Reset demo
|
|
const { json: reset } = await req("POST", `/admin/businesses/${newBizId}/reset-demo`, { template: "blank" }, A);
|
|
check("reset to blank template", reset.business?.template === "blank" && reset.stats?.services === 0, JSON.stringify(reset.stats));
|
|
|
|
// Delete business
|
|
const { status: del } = await req("DELETE", `/admin/businesses/${newBizId}`, null, A);
|
|
check("delete business", del === 200);
|
|
const { json: list2 } = await req("GET", "/admin/businesses", null, A);
|
|
check("business removed", list2.businesses?.length === 1, `got ${list2.businesses?.length}`);
|
|
// Cascade: owner.test user should be gone
|
|
const { status: gone } = await req("POST", "/auth/login", { email: "[email protected]", password: "demo1234" });
|
|
check("deleted business users removed", gone === 401);
|
|
|
|
console.log(`\n${pass}/${pass + fail} passed${fail ? `, ${fail} FAILED` : ""}`);
|
|
process.exit(fail ? 1 : 0);
|