import { randomUUID } from "crypto"; import mongoose from "mongoose"; import { NextRequest } from "next/server"; import { Business } from "@/lib/server/models/Business"; import { Branch } from "@/lib/server/models/Branch"; import { User, UserRole } from "@/lib/server/models/User"; import { Product } from "@/lib/server/models/Product"; import { signAccessToken } from "@/lib/server/jwt"; import { ACCESS_COOKIE } from "@/lib/server/auth"; import { crossTenant } from "@/lib/server/tenantGuard"; let phoneSeq = 0; function nextPhone(): string { phoneSeq += 1; return "2547" + String(10_000_000 + phoneSeq).slice(-8); } export interface Actor { id: string; businessId: string; role: UserRole; branchId: string | null; email: string; } export interface Tenant { businessId: string; owner: Actor; branchA: string; branchB: string; managerA: Actor; cashierA: Actor; cashierB: Actor; water20: string; water5: string; } async function makeUser( businessId: string, role: UserRole, branchId: string | null, label: string ): Promise { const email = `${label}-${randomUUID().slice(0, 8)}@aquaflow.test`; const user = await User.create({ businessId, name: label, role, branchId, phoneNumber: nextPhone(), email, passwordHash: "$2a$12$abcdefghijklmnopqrstuuJ0dBz7m6Y8yCjvC7vQ7hZqXbY8yCjvC", // never used to log in }); return { id: user._id.toString(), businessId, role, branchId, email }; } /** A complete tenant: owner, two branches, a manager, a cashier per branch, two products. */ export async function makeTenant(name = "Tenant"): Promise { const business = await Business.create({ name, ownerId: new mongoose.Types.ObjectId(), tier: "ENTERPRISE", subscriptionStatus: "ACTIVE", }); const businessId = business._id.toString(); const owner = await makeUser(businessId, "SUPER_ADMIN", null, name + "-owner"); business.ownerId = new mongoose.Types.ObjectId(owner.id); await business.save(); const a = await Branch.create({ businessId, branchName: name + " A", locationCity: "Nairobi" }); const b = await Branch.create({ businessId, branchName: name + " B", locationCity: "Thika" }); const managerA = await makeUser(businessId, "BRANCH_MANAGER", a._id.toString(), name + "-mgrA"); const cashierA = await makeUser(businessId, "DELIVERY", a._id.toString(), name + "-cashA"); const cashierB = await makeUser(businessId, "DELIVERY", b._id.toString(), name + "-cashB"); const [p20, p5] = await Product.insertMany([ { businessId, label: "20L Jerrycan", sizeLiters: 20, unitPrice: 200, sortOrder: 1 }, { businessId, label: "5L Jerrycan", sizeLiters: 5, unitPrice: 60, sortOrder: 2 }, ]); return { businessId, owner, branchA: a._id.toString(), branchB: b._id.toString(), managerA, cashierA, cashierB, water20: p20._id.toString(), water5: p5._id.toString(), }; } export function tokenFor(actor: Actor, deviceId = "00000000-0000-4000-8000-000000000001"): string { return signAccessToken({ sub: actor.id, businessId: actor.businessId, role: actor.role, branchId: actor.branchId, deviceId, }); } export function req( method: string, path: string, opts: { as?: Actor; body?: unknown; cookies?: Record } = {} ): NextRequest { const cookies: Record = { ...(opts.cookies ?? {}) }; if (opts.as) cookies[ACCESS_COOKIE] = tokenFor(opts.as); const cookieHeader = Object.entries(cookies) .map(([k, v]) => `${k}=${v}`) .join("; "); return new NextRequest(new URL(path, "http://localhost"), { method, headers: { "content-type": "application/json", ...(cookieHeader ? { cookie: cookieHeader } : {}), }, body: opts.body === undefined ? undefined : JSON.stringify(opts.body), }); } export function params>(p: T) { return { params: p }; } export async function json(res: Response): Promise<{ status: number; body: T }> { return { status: res.status, body: (await res.json()) as T }; } export function uuid(): string { return randomUUID(); } /** Tests may inspect and arrange any business's data directly. */ export function db(fn: () => PromiseLike): Promise { return crossTenant("test inspects the database directly", fn); }