// Shared fixtures for scripts/test-auftraege-*.ts (lane L2). Not a test itself (no "test-" prefix). // // Creates isolated zz-test tenants with the four standard roles, a team with lead + technician, // an outside technician, a customer and a site; builds ServiceCtx objects exactly like // ctxFromGuard does (tenant db + DB-authoritative permission set). import "dotenv/config"; import { prisma, dbForTenant } from "../src/server/db"; import { ROLE_DEFS, type RoleKey } from "../src/server/rbac"; import type { ServiceCtx } from "../src/server/services/context"; export let failures = 0; export const ok = (cond: boolean, msg: string) => { console.log(`${cond ? "✓" : "✗ FEHLER"} ${msg}`); if (!cond) failures++; }; /** Expect a ServiceError with the given code (e.g. "not_found"). Returns the error. */ export async function expectCode(fn: () => Promise, code: string, msg: string): Promise { try { await fn(); ok(false, `${msg} — kein Fehler (erwartet ${code})`); return null; } catch (e) { const c = (e as { code?: string }).code; ok(c === code, `${msg}${c === code ? "" : ` — erhalten ${c ?? (e as Error).message}`}`); return e; } } export type Fixture = { tenantId: string; users: Record<"admin" | "backoffice" | "lead" | "tech" | "outsider", { id: string }>; ctx: Record<"admin" | "backoffice" | "lead" | "tech" | "outsider", ServiceCtx>; teamId: string; otherTeamId: string; customerId: string; siteId: string; contactId: string; }; const ROLE_OF = { admin: "tenant-admin", backoffice: "backoffice", lead: "team-lead", tech: "technician", outsider: "technician" } as const; export function ctxFor(tenantId: string, userId: string, role: RoleKey): ServiceCtx { return { db: dbForTenant(tenantId), tenantId, userId, permissions: new Set(ROLE_DEFS[role].permissions) }; } export async function cleanupTenants(slugs: string[]) { const tenants = await prisma.tenant.findMany({ where: { slug: { in: slugs } }, select: { id: true } }); const ids = tenants.map((t) => t.id); if (!ids.length) return; const w = { where: { tenantId: { in: ids } } }; await prisma.syncOperation.deleteMany(w); await prisma.notification.deleteMany(w); await prisma.signature.deleteMany(w); await prisma.report.deleteMany(w); await prisma.photo.deleteMany(w); await prisma.activityNote.deleteMany(w); await prisma.voiceNote.deleteMany(w); await prisma.timeEntry.deleteMany(w); await prisma.materialUsage.deleteMany(w); await prisma.workSession.deleteMany(w); await prisma.materialPlan.deleteMany(w); await prisma.photoRequirement.deleteMany(w); await prisma.checklistItem.deleteMany(w); await prisma.workOrderStatusChange.deleteMany(w); await prisma.workOrderAssignee.deleteMany(w); await prisma.document.deleteMany(w); await prisma.workOrder.deleteMany(w); await prisma.teamMember.deleteMany(w); await prisma.team.deleteMany(w); await prisma.site.deleteMany(w); await prisma.contact.deleteMany(w); await prisma.customer.deleteMany(w); await prisma.checklistTemplate.deleteMany(w); await prisma.orderType.deleteMany(w); await prisma.numberSequence.deleteMany(w); await prisma.tenantSettings.deleteMany(w); await prisma.auditLog.deleteMany(w); await prisma.user.deleteMany(w); await prisma.role.deleteMany(w); await prisma.tenant.deleteMany({ where: { id: { in: ids } } }); await prisma.identity.deleteMany({ where: { email: { endsWith: "@zz-auftraege.test" }, memberships: { none: {} } } }); } async function ensurePermissionIds(keys: readonly string[]) { const rows = await Promise.all(keys.map((key) => prisma.permission.upsert({ where: { key }, update: {}, create: { key } }))); return rows.map((r) => r.id); } export async function createFixture(slug: string): Promise { const tenant = await prisma.tenant.create({ data: { name: `L2 Test ${slug}`, slug } }); await prisma.tenantSettings.create({ data: { tenantId: tenant.id, orgName: `L2 Test ${slug}`, timezone: "Europe/Berlin" } }); const roleIds: Partial> = {}; for (const key of Object.keys(ROLE_DEFS) as RoleKey[]) { const permIds = await ensurePermissionIds(ROLE_DEFS[key].permissions); const role = await prisma.role.create({ data: { tenantId: tenant.id, key, name: ROLE_DEFS[key].name, rolePermissions: { create: permIds.map((permissionId) => ({ permissionId })) } }, }); roleIds[key] = role.id; } const users = {} as Fixture["users"]; const ctx = {} as Fixture["ctx"]; for (const name of Object.keys(ROLE_OF) as (keyof typeof ROLE_OF)[]) { const email = `${name}-${slug}@zz-auftraege.test`; const identity = await prisma.identity.create({ data: { email, passwordHash: "x" } }); const user = await prisma.user.create({ data: { tenantId: tenant.id, identityId: identity.id, email, name: `${name} ${slug}`, userRoles: { create: [{ roleId: roleIds[ROLE_OF[name]]! }] } }, }); users[name] = { id: user.id }; ctx[name] = ctxFor(tenant.id, user.id, ROLE_OF[name]); } const team = await prisma.team.create({ data: { tenantId: tenant.id, name: "Team Nord", leaderUserId: users.lead.id } }); await prisma.teamMember.create({ data: { tenantId: tenant.id, teamId: team.id, userId: users.tech.id, validFrom: new Date(Date.now() - 86400000) } }); const otherTeam = await prisma.team.create({ data: { tenantId: tenant.id, name: "Team Süd" } }); await prisma.teamMember.create({ data: { tenantId: tenant.id, teamId: otherTeam.id, userId: users.outsider.id, validFrom: new Date(Date.now() - 86400000) } }); const customer = await prisma.customer.create({ data: { tenantId: tenant.id, companyName: `Kunde ${slug} GmbH`, customerNumber: `K-${slug}`, city: "Hamburg", street: "Hafenweg" } }); const site = await prisma.site.create({ data: { tenantId: tenant.id, customerId: customer.id, name: `Objekt ${slug}`, city: "Hamburg", street: "Kaistraße", houseNumber: "7" } }); const contact = await prisma.contact.create({ data: { tenantId: tenant.id, customerId: customer.id, name: `Frau Kontakt ${slug}` } }); return { tenantId: tenant.id, users, ctx, teamId: team.id, otherTeamId: otherTeam.id, customerId: customer.id, siteId: site.id, contactId: contact.id }; } /** Raw work order in a given status, assigned to the fixture team (setup only — bypasses the state machine). */ export async function rawWorkOrder(f: Fixture, status: string, extra: Record = {}) { const n = Math.random().toString(36).slice(2, 10); return prisma.workOrder.create({ data: { tenantId: f.tenantId, number: `T-${n}`, title: `Testauftrag ${n}`, customerId: f.customerId, siteId: f.siteId, status: status as never, assignedTeamId: f.teamId, teamLeadUserId: f.users.lead.id, signatureRequired: true, ...extra, }, }); } export async function approvedCompletionReport(f: Fixture, workOrderId: string, opts: { status?: string; signature?: boolean } = {}) { const report = await prisma.report.create({ data: { tenantId: f.tenantId, workOrderId, type: "completion", reportDate: new Date(), lineageId: `lin-${workOrderId}-${Math.random().toString(36).slice(2, 8)}`, status: (opts.status ?? "approved") as never, content: {}, }, }); if (opts.signature) { await prisma.signature.create({ data: { tenantId: f.tenantId, reportId: report.id, outcome: "signed", signerName: "Kunde", signedAt: new Date() } }); } return report; } export function finish(cleanup: () => Promise) { return async () => { await cleanup(); await prisma.$disconnect(); if (failures === 0) console.log("\nOK — alle Nachweise erfüllt."); else console.log(`\n${failures} FEHLER.`); process.exit(failures === 0 ? 0 : 1); }; }