// L2 Aufträge: Sichtbarkeit (Rollen/Scope) und Mandantentrennung. // // (1) Monteur eines fremden Teams: Liste leer, Detail/Übergang/Blocker → not_found // (2) Team-Mitglied, Teamleiter und einzeln zugewiesener Monteur sehen den Auftrag // (3) Monteur ohne Rechte: Anlage/Änderung/Zuweisung/Material → forbidden // (4) Mandant B kann Aufträge von A weder lesen (Liste, Detail, Suche, Dashboard) noch ändern // (Update, Übergang, Zuweisung, Material, Konflikt) — auch nicht über den Tenant-Guard direkt // (5) Suche respektiert Scope (Monteur fremdes Team findet nichts) und Mandant // // Lauf: npx tsx scripts/test-auftraege-scope.ts import "dotenv/config"; import { prisma } from "../src/server/db"; import { parseListParams } from "../src/lib/work-orders/filters"; import { createWorkOrder } from "../src/server/services/work-orders/create"; import { updateWorkOrder } from "../src/server/services/work-orders/update"; import { assignWorkOrder } from "../src/server/services/work-orders/assign"; import { transitionWorkOrder } from "../src/server/services/work-orders/transition"; import { getCompletionBlockers } from "../src/server/services/work-orders/completion"; import { addMaterialPlan } from "../src/server/services/work-orders/materials"; import { listWorkOrders } from "../src/server/services/work-orders/list"; import { getWorkOrderDetail } from "../src/server/services/work-orders/detail"; import { getDashboardTiles } from "../src/server/services/work-orders/dashboard"; import { searchAll } from "../src/server/services/work-orders/search"; import { discardSyncConflict } from "../src/server/services/work-orders/conflicts"; import { cleanupTenants, createFixture, expectCode, finish, ok } from "./lib-auftraege-fixtures"; const SLUG_A = "zz-l2-scope-a"; const SLUG_B = "zz-l2-scope-b"; async function main() { await cleanupTenants([SLUG_A, SLUG_B]); const A = await createFixture(SLUG_A); const B = await createFixture(SLUG_B); const wo = await createWorkOrder(A.ctx.backoffice, { title: "Wärmepumpe Zauberwort", customerId: A.customerId, siteId: A.siteId }); await assignWorkOrder(A.ctx.backoffice, { workOrderId: wo.id, teamId: A.teamId }); await prisma.activityNote.create({ data: { tenantId: A.tenantId, workOrderId: wo.id, text: "Zauberwort im Keller", authorId: A.users.tech.id } }); const solo = await createWorkOrder(A.ctx.backoffice, { title: "Einzelzuweisung", customerId: A.customerId }); await assignWorkOrder(A.ctx.backoffice, { workOrderId: solo.id, teamId: A.otherTeamId, userIds: [A.users.tech.id] }); console.log("\n— (1) Monteur fremdes Team —"); const outsiderList = await listWorkOrders(A.ctx.outsider, parseListParams({})); ok(outsiderList.items.every((i) => i.id !== wo.id), "Liste des fremden Monteurs enthält den Auftrag nicht"); await expectCode(() => getWorkOrderDetail(A.ctx.outsider, wo.id), "not_found", "Detail → not_found"); await expectCode(() => transitionWorkOrder(A.ctx.outsider, { workOrderId: wo.id, to: "accepted" }), "not_found", "Übergang → not_found (Existenz nicht preisgegeben)"); await expectCode(() => getCompletionBlockers(A.ctx.outsider, wo.id), "not_found", "Blocker → not_found"); console.log("\n— (2) Berechtigte Feldrollen —"); const techList = await listWorkOrders(A.ctx.tech, parseListParams({})); ok(techList.items.some((i) => i.id === wo.id), "Team-Mitglied sieht den Team-Auftrag"); ok(techList.items.some((i) => i.id === solo.id), "Einzeln zugewiesener Monteur sieht Auftrag eines anderen Teams"); ok((await getWorkOrderDetail(A.ctx.lead, wo.id)).id === wo.id, "Teamleiter sieht den Auftrag"); const accepted = await transitionWorkOrder(A.ctx.tech, { workOrderId: wo.id, to: "accepted" }); ok(accepted.status === "accepted", "Team-Mitglied darf annehmen"); const outsiderSolo = await listWorkOrders(A.ctx.outsider, parseListParams({})); ok(outsiderSolo.items.some((i) => i.id === solo.id), "Mitglied des zugewiesenen Teams sieht den Einzelzuweisungs-Auftrag"); console.log("\n— (3) Monteur ohne Backoffice-Rechte —"); await expectCode(() => createWorkOrder(A.ctx.tech, { title: "X", customerId: A.customerId }), "forbidden", "Anlage → forbidden"); await expectCode(() => updateWorkOrder(A.ctx.tech, wo.id, { title: "X" }), "forbidden", "Änderung → forbidden"); await expectCode(() => assignWorkOrder(A.ctx.tech, { workOrderId: wo.id, teamId: A.teamId }), "forbidden", "Zuweisung → forbidden"); await expectCode(() => addMaterialPlan(A.ctx.tech, wo.id, { name: "X", plannedQuantity: 1, unit: "m" }), "forbidden", "Materialvorgabe → forbidden"); await expectCode(() => transitionWorkOrder(A.ctx.tech, { workOrderId: wo.id, to: "cancelled", reason: "x" }), "forbidden", "Storno → forbidden"); console.log("\n— (4) Mandantentrennung —"); const bList = await listWorkOrders(B.ctx.admin, parseListParams({})); ok(bList.total === 0, "Mandant B: Liste leer"); await expectCode(() => getWorkOrderDetail(B.ctx.admin, wo.id), "not_found", "Mandant B: Detail → not_found"); await expectCode(() => updateWorkOrder(B.ctx.admin, wo.id, { title: "gekapert" }), "not_found", "Mandant B: Update → not_found"); await expectCode(() => transitionWorkOrder(B.ctx.admin, { workOrderId: wo.id, to: "cancelled", reason: "x" }), "not_found", "Mandant B: Übergang → not_found"); await expectCode(() => assignWorkOrder(B.ctx.admin, { workOrderId: wo.id, teamId: B.teamId }), "not_found", "Mandant B: Zuweisung → not_found"); await expectCode(() => addMaterialPlan(B.ctx.admin, wo.id, { name: "X", plannedQuantity: 1, unit: "m" }), "not_found", "Mandant B: Materialvorgabe → not_found"); await expectCode(() => createWorkOrder(B.ctx.admin, { title: "X", customerId: A.customerId }), "invalid", "Mandant B: Anlage mit Kunde von A → invalid"); const tilesB = await getDashboardTiles(B.ctx.admin, {}); ok(Object.values(tilesB).every((v) => v === 0), "Mandant B: alle Dashboard-Kacheln 0"); const direct = await B.ctx.admin.db.workOrder.findUnique({ where: { id: wo.id } }); ok(direct === null, "Tenant-Guard: findUnique über Mandant B → null"); const updMany = await B.ctx.admin.db.workOrder.updateMany({ where: { id: wo.id }, data: { title: "gekapert" } }); ok(updMany.count === 0, "Tenant-Guard: updateMany über Mandant B ändert 0 Zeilen"); const opA = await prisma.syncOperation.create({ data: { tenantId: A.tenantId, userId: A.users.tech.id, clientOpId: crypto.randomUUID(), opType: "note.create", payload: {}, status: "conflict", clientCreatedAt: new Date() } }); await expectCode(() => discardSyncConflict(B.ctx.admin, opA.id), "not_found", "Mandant B: Konflikt von A → not_found"); const title = (await prisma.workOrder.findUnique({ where: { id: wo.id } }))?.title; ok(title === "Wärmepumpe Zauberwort", "Auftrag von A unverändert"); console.log("\n— (5) Suche —"); const sA = await searchAll(A.ctx.backoffice, { q: "zauberwort" }); ok(sA.workOrders.some((w) => w.id === wo.id) && sA.notes.length === 1, "Backoffice A findet Auftrag + Notiz"); const sCust = await searchAll(A.ctx.backoffice, { q: "Kaistraße" }); ok(sCust.sites.length === 1 && sCust.workOrders.some((w) => w.id === wo.id), "Adresssuche findet Objekt und Auftrag"); const sB = await searchAll(B.ctx.admin, { q: "zauberwort" }); ok(sB.workOrders.length === 0 && sB.notes.length === 0, "Mandant B findet nichts von A"); const sB2 = await searchAll(B.ctx.admin, { q: SLUG_A }); ok(sB2.customers.length === 0 && sB2.sites.length === 0 && sB2.contacts.length === 0, "Mandant B findet keine Kunden/Objekte/Kontakte von A"); const sOut = await searchAll(A.ctx.outsider, { q: "zauberwort" }); ok(sOut.workOrders.length === 0 && sOut.notes.length === 0, "Monteur fremdes Team findet Auftrag/Notiz nicht"); const sTech = await searchAll(A.ctx.tech, { q: "zauberwort", statuses: ["accepted"] }); ok(sTech.workOrders.length === 1, "Team-Mitglied findet Auftrag (mit Statusfilter)"); const sFiltered = await searchAll(A.ctx.backoffice, { q: "zauberwort", statuses: ["billed"] }); ok(sFiltered.workOrders.length === 0, "Statusfilter der Suche greift"); } main() .catch((e) => { console.error(e); ok(false, "unerwarteter Fehler"); }) .finally(finish(() => cleanupTenants([SLUG_A, SLUG_B])));