L2 Aufträge & Backoffice: Service-Schicht Aufträge + Tests
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>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
// 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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user