Kernlogik, Mandantentrennung (Mandant B liest/ändert nichts von A) und Rollen/Scope (Monteur ohne Zuweisung → not_found/forbidden). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
96 lines
6.1 KiB
TypeScript
96 lines
6.1 KiB
TypeScript
// L1 Stammdaten — Teams (Spec §11.1):
|
|
// (1) Anlage mit Teamleiter/Mitgliedern (gültig ab/bis), Validierung, Audit
|
|
// (2) Mitgliedschaft wirkt auf die Auftragssichtbarkeit (activeTeamIds)
|
|
// (3) Rollen: Monteur liest, verwaltet aber nicht; Mandantentrennung
|
|
//
|
|
// Lauf: npx tsx scripts/test-stammdaten-teams.ts
|
|
|
|
import "dotenv/config"; // must run before any module that constructs the Prisma client
|
|
|
|
import { createTeam, deleteTeam, getTeam, listTeams, updateTeam } from "../src/server/services/teams/teams";
|
|
import { activeTeamIds } from "../src/server/services/work-orders/visibility";
|
|
import { checker, cleanupTenants, createTenant, createUser, createWorkOrder, ctxFor, disconnect, prisma } from "./lib-stammdaten-fixtures";
|
|
|
|
const SLUG_A = "zz-l1-team-a";
|
|
const SLUG_B = "zz-l1-team-b";
|
|
const DOMAIN = "zz-l1-team.test";
|
|
const c = checker("Teams");
|
|
|
|
async function main() {
|
|
await cleanupTenants([SLUG_A, SLUG_B], DOMAIN);
|
|
const tA = await createTenant(SLUG_A, "L1 Teams A");
|
|
const tB = await createTenant(SLUG_B, "L1 Teams B");
|
|
const boA = await createUser(tA.id, `bo@${DOMAIN}`, "Backoffice A");
|
|
const lead = await createUser(tA.id, `lead@${DOMAIN}`, "Tina Teamleiter");
|
|
const tech = await createUser(tA.id, `tech@${DOMAIN}`, "Max Monteur");
|
|
const inactive = await createUser(tA.id, `inactive@${DOMAIN}`, "Ina Inaktiv");
|
|
await prisma.user.update({ where: { id: inactive.id }, data: { status: "DEACTIVATED" } });
|
|
const boB = await createUser(tB.id, `bo-b@${DOMAIN}`, "Backoffice B");
|
|
const foreign = await createUser(tB.id, `foreign@${DOMAIN}`, "Fremd B");
|
|
const ctxA = ctxFor(tA.id, boA.id, "backoffice");
|
|
const ctxTech = ctxFor(tA.id, tech.id, "technician");
|
|
const ctxB = ctxFor(tB.id, boB.id, "backoffice");
|
|
|
|
console.log("— (1) Anlage —");
|
|
const team = await createTeam(ctxA, {
|
|
name: "Team Nord",
|
|
leaderUserId: lead.id,
|
|
phone: "040 555",
|
|
vehicle: "HH-CV 101",
|
|
area: "Hamburg Nord",
|
|
notes: "Schlüssel im Fahrzeug",
|
|
members: [{ userId: tech.id, validFrom: "2026-01-01", validTo: "" }],
|
|
});
|
|
c.ok(team.leader?.id === lead.id && team.members.length === 1 && team.vehicle === "HH-CV 101", "Team mit Teamleiter, Mitglied, Fahrzeug angelegt");
|
|
c.ok(team.members[0].validTo === null && team.members[0].validFrom.toISOString().startsWith("2026-01-01"), "gültig ab/bis übernommen");
|
|
c.ok(!!(await prisma.auditLog.findFirst({ where: { tenantId: tA.id, entity: "team", entityId: team.id, action: "create" } })), "Audit create");
|
|
await c.expectServiceError(() => createTeam(ctxA, { name: "Team Nord", members: [] }), "conflict", "doppelter Teamname → conflict", "name_taken");
|
|
await c.expectServiceError(() => createTeam(ctxA, { name: "Team Fremd", members: [{ userId: foreign.id }] }), "invalid", "Mitglied aus fremdem Mandanten → invalid", "inactive_user");
|
|
await c.expectServiceError(() => createTeam(ctxA, { name: "Team Inaktiv", leaderUserId: inactive.id, members: [] }), "invalid", "deaktivierter Nutzer als Teamleiter → invalid", "inactive_user");
|
|
await c.expectServiceError(() => createTeam(ctxA, { name: "Team Doppelt", members: [{ userId: tech.id }, { userId: tech.id }] }), "invalid", "Person doppelt → invalid", "duplicate_member");
|
|
await c.expectErrorName(() => createTeam(ctxA, { name: "Team Zeit", members: [{ userId: tech.id, validFrom: "2026-05-01", validTo: "2026-04-01" }] }), "ZodError", "gültig bis vor gültig ab → Validierungsfehler");
|
|
|
|
console.log("\n— (2) Mitgliedschaft & Sichtbarkeit —");
|
|
c.ok((await activeTeamIds(ctxTech)).includes(team.id), "aktives Mitglied → Team zählt für Auftragssichtbarkeit");
|
|
const customer = await prisma.customer.create({ data: { tenantId: tA.id, customerNumber: "K-1", companyName: "Kunde" } });
|
|
const order = await createWorkOrder(tA.id, { customerId: customer.id, assignedTeamId: team.id });
|
|
const ended = await updateTeam(ctxA, team.id, {
|
|
name: "Team Nord",
|
|
leaderUserId: lead.id,
|
|
members: [{ userId: tech.id, validFrom: "2025-01-01", validTo: "2025-12-31" }],
|
|
});
|
|
c.ok(ended.members.length === 1 && !!ended.members[0].validTo, "Mitgliedschaft beendet (gültig bis gesetzt)");
|
|
c.ok(!(await activeTeamIds(ctxTech)).includes(team.id), "abgelaufene Mitgliedschaft → Team zählt nicht mehr");
|
|
const upAudit = await prisma.auditLog.findFirst({ where: { tenantId: tA.id, entity: "team", entityId: team.id, action: "update" } });
|
|
c.ok(!!upAudit?.before && !!upAudit?.after, "Audit update mit before/after");
|
|
|
|
console.log("\n— (3) Rollen & Mandantentrennung —");
|
|
c.ok((await listTeams(ctxTech)).some((x) => x.id === team.id), "Monteur darf Teams lesen (team:read)");
|
|
await c.expectServiceError(() => createTeam(ctxTech, { name: "Monteurteam", members: [] }), "forbidden", "Monteur darf keine Teams anlegen");
|
|
await c.expectServiceError(() => updateTeam(ctxTech, team.id, { name: "x", members: [] }), "forbidden", "Monteur darf Teams nicht ändern");
|
|
await c.expectServiceError(() => getTeam(ctxB, team.id), "not_found", "Mandant B liest Team von A → not_found");
|
|
await c.expectServiceError(() => updateTeam(ctxB, team.id, { name: "gehackt", members: [] }), "not_found", "Mandant B ändert Team von A → not_found");
|
|
await c.expectServiceError(() => deleteTeam(ctxB, team.id), "not_found", "Mandant B löscht Team von A → not_found");
|
|
c.ok((await listTeams(ctxB)).length === 0, "Teamliste von B leer");
|
|
|
|
console.log("\n— Soft Delete —");
|
|
await c.expectServiceError(() => deleteTeam(ctxA, team.id), "blocked", "Löschen mit offenem Auftrag → blocked", "open_work_orders");
|
|
await prisma.workOrder.update({ where: { id: order.id }, data: { status: "billed" } });
|
|
const del = await deleteTeam(ctxA, team.id);
|
|
c.ok(!!del.deletedAt && del.status === "inactive", "Team soft-gelöscht und inaktiv");
|
|
const again = await createTeam(ctxA, { name: "Team Nord", members: [] });
|
|
c.ok(again.name === "Team Nord", "Name nach Löschen wieder verwendbar");
|
|
}
|
|
|
|
main()
|
|
.catch((err) => {
|
|
console.error(err);
|
|
c.ok(false, "unerwarteter Fehler");
|
|
})
|
|
.finally(async () => {
|
|
await cleanupTenants([SLUG_A, SLUG_B], DOMAIN).catch((e) => console.error("cleanup", e));
|
|
const failures = c.finish();
|
|
await disconnect();
|
|
process.exit(failures === 0 ? 0 : 1);
|
|
});
|