Statusmaschine (transitionWorkOrder inkl. eventData), Zuweisung, Completion-Guards, Materialvorgabe, Checklisten/Pflichtfotos, Liste/Dashboard-Presets, Suche, Sync-Konflikte, Einstellungen (Auftragsarten, Vorlagen, Nummernkreise). Tests: Übergangsmatrix je Rolle, Kernlogik, Scope/Mandantentrennung, Nummernkreis-Parallelität. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
7.7 KiB
TypeScript
173 lines
7.7 KiB
TypeScript
// 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<unknown>, code: string, msg: string): Promise<unknown> {
|
|
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<string>(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<Fixture> {
|
|
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<Record<RoleKey, string>> = {};
|
|
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<string, unknown> = {}) {
|
|
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<void>) {
|
|
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);
|
|
};
|
|
}
|