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,138 @@
|
||||
// L2 Aufträge: vollständige Übergangsmatrix des Auftrags-Statusmodells (ARCHITEKTUR §3).
|
||||
//
|
||||
// (1) Reine Tabelle: jedes (von, nach)-Paar — erlaubte Paare exakt wie im Vertrag, alle anderen verboten.
|
||||
// (2) Service je Rolle (tenant-admin, backoffice, team-lead, technician) für JEDEN erlaubten Übergang:
|
||||
// Erwartung unabhängig aus der Rechte-Tabelle abgeleitet → Erfolg bzw. forbidden;
|
||||
// zusätzlich Version +1, WorkOrderStatusChange und Audit-Eintrag.
|
||||
// (3) Verbotene Paare über den Service → invalid (Stichprobe über alle Statuswerte).
|
||||
//
|
||||
// Lauf: npx tsx scripts/test-auftraege-status.ts
|
||||
|
||||
import "dotenv/config";
|
||||
import { prisma } from "../src/server/db";
|
||||
import { WORK_ORDER_STATUSES, canTransition, type WorkOrderStatus } from "../src/lib/work-orders/status";
|
||||
import { transitionWorkOrder } from "../src/server/services/work-orders/transition";
|
||||
import { approvedCompletionReport, cleanupTenants, createFixture, expectCode, finish, ok, rawWorkOrder } from "./lib-auftraege-fixtures";
|
||||
|
||||
const SLUG = "zz-l2-status";
|
||||
|
||||
const CONTRACT: Record<WorkOrderStatus, WorkOrderStatus[]> = {
|
||||
draft: ["review_required", "planned", "assigned", "cancelled"],
|
||||
review_required: ["planned", "assigned", "cancelled"],
|
||||
planned: ["assigned", "cancelled"],
|
||||
assigned: ["accepted", "en_route", "in_progress", "planned", "cancelled"],
|
||||
accepted: ["en_route", "in_progress", "assigned", "cancelled"],
|
||||
en_route: ["in_progress", "cancelled"],
|
||||
in_progress: ["paused", "waiting_material", "daily_report_created", "technically_completed", "cancelled"],
|
||||
paused: ["in_progress", "en_route", "cancelled"],
|
||||
waiting_material: ["in_progress", "en_route", "cancelled"],
|
||||
daily_report_created: ["en_route", "in_progress", "cancelled"],
|
||||
technically_completed: ["signature_pending", "in_review", "in_progress", "cancelled"],
|
||||
signature_pending: ["in_review", "cancelled"],
|
||||
in_review: ["released_for_billing", "in_progress", "cancelled"],
|
||||
released_for_billing: ["billed", "in_review"],
|
||||
billed: [],
|
||||
cancelled: [],
|
||||
};
|
||||
|
||||
type Role = "admin" | "backoffice" | "lead" | "tech";
|
||||
|
||||
/** Expected roles per transition — written from the ARCHITEKTUR §3 rights table, NOT from status.ts. */
|
||||
function expectedRoles(from: WorkOrderStatus, to: WorkOrderStatus): Role[] {
|
||||
if (to === "cancelled") return ["admin", "backoffice"];
|
||||
if (to === "released_for_billing" || to === "billed" || (from === "released_for_billing" && to === "in_review")) return ["admin", "backoffice"];
|
||||
if (from === "in_review" && to === "in_progress") return ["admin", "backoffice", "lead"];
|
||||
if (["draft", "review_required", "planned"].includes(from)) return ["admin", "backoffice"];
|
||||
if (from === "assigned" && to === "planned") return ["admin", "backoffice"];
|
||||
return ["admin", "lead", "tech"];
|
||||
}
|
||||
|
||||
const needsReason = (from: WorkOrderStatus, to: WorkOrderStatus) =>
|
||||
to === "cancelled" || (from === "in_review" && to === "in_progress") || (from === "released_for_billing" && to === "in_review");
|
||||
|
||||
async function main() {
|
||||
await cleanupTenants([SLUG]);
|
||||
const f = await createFixture(SLUG);
|
||||
|
||||
console.log("\n— (1) Übergangstabelle —");
|
||||
let tableOk = true;
|
||||
for (const from of WORK_ORDER_STATUSES) {
|
||||
for (const to of WORK_ORDER_STATUSES) {
|
||||
if (canTransition(from, to) !== CONTRACT[from].includes(to)) {
|
||||
tableOk = false;
|
||||
console.log(` Abweichung ${from} → ${to}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(tableOk, `Alle ${WORK_ORDER_STATUSES.length ** 2} Paare entsprechen dem Vertrag`);
|
||||
|
||||
console.log("\n— (2) Erlaubte Übergänge je Rolle —");
|
||||
let checked = 0;
|
||||
for (const from of WORK_ORDER_STATUSES) {
|
||||
for (const to of CONTRACT[from]) {
|
||||
for (const role of ["admin", "backoffice", "lead", "tech"] as Role[]) {
|
||||
const wo = await rawWorkOrder(f, from);
|
||||
if (to === "in_review" && (from === "technically_completed" || from === "signature_pending")) {
|
||||
await approvedCompletionReport(f, wo.id, { status: "submitted", signature: true });
|
||||
}
|
||||
if (to === "released_for_billing") await approvedCompletionReport(f, wo.id);
|
||||
const allowed = expectedRoles(from, to).includes(role);
|
||||
const input = { workOrderId: wo.id, to, reason: needsReason(from, to) ? "Testgrund" : undefined, baseVersion: wo.version };
|
||||
checked++;
|
||||
if (allowed) {
|
||||
try {
|
||||
const res = await transitionWorkOrder(f.ctx[role], input);
|
||||
const after = await prisma.workOrder.findUnique({ where: { id: wo.id } });
|
||||
const change = await prisma.workOrderStatusChange.count({ where: { workOrderId: wo.id, fromStatus: from, toStatus: to } });
|
||||
const audit = await prisma.auditLog.count({ where: { tenantId: f.tenantId, entity: "work_order", entityId: wo.id } });
|
||||
const good = res.status === to && after?.status === to && after.version === wo.version + 1 && change === 1 && audit >= 1;
|
||||
if (!good) ok(false, `${role}: ${from} → ${to} (Status/Version/Historie/Audit)`);
|
||||
} catch (e) {
|
||||
ok(false, `${role}: ${from} → ${to} sollte erlaubt sein — ${(e as { code?: string }).code ?? (e as Error).message}`);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await transitionWorkOrder(f.ctx[role], input);
|
||||
ok(false, `${role}: ${from} → ${to} sollte verboten sein`);
|
||||
} catch (e) {
|
||||
if ((e as { code?: string }).code !== "forbidden") ok(false, `${role}: ${from} → ${to} erwartet forbidden, erhalten ${(e as { code?: string }).code ?? (e as Error).message}`);
|
||||
const still = await prisma.workOrder.findUnique({ where: { id: wo.id } });
|
||||
if (still?.status !== from || still.version !== wo.version) ok(false, `${role}: ${from} → ${to} hat trotz Verbot geschrieben`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(true, `${checked} Rolle×Übergang-Kombinationen geprüft`);
|
||||
|
||||
console.log("\n— (3) Verbotene Paare über den Service —");
|
||||
let invalidOk = true;
|
||||
for (const from of WORK_ORDER_STATUSES) {
|
||||
const forbiddenTargets = WORK_ORDER_STATUSES.filter((t) => t !== from && !CONTRACT[from].includes(t));
|
||||
const to = forbiddenTargets[(from.length * 7) % forbiddenTargets.length];
|
||||
const wo = await rawWorkOrder(f, from);
|
||||
try {
|
||||
await transitionWorkOrder(f.ctx.admin, { workOrderId: wo.id, to, reason: "x" });
|
||||
invalidOk = false;
|
||||
console.log(` ${from} → ${to} wurde nicht abgewiesen`);
|
||||
} catch (e) {
|
||||
if ((e as { code?: string }).code !== "invalid") {
|
||||
invalidOk = false;
|
||||
console.log(` ${from} → ${to}: ${(e as { code?: string }).code}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
ok(invalidOk, "Nicht in der Tabelle enthaltene Übergänge → invalid (je Ausgangsstatus)");
|
||||
|
||||
console.log("\n— (4) Begründungspflicht, Versionskonflikt —");
|
||||
const c1 = await rawWorkOrder(f, "planned");
|
||||
await expectCode(() => transitionWorkOrder(f.ctx.backoffice, { workOrderId: c1.id, to: "cancelled" }), "invalid", "Stornierung ohne Grund → invalid");
|
||||
await expectCode(() => transitionWorkOrder(f.ctx.backoffice, { workOrderId: c1.id, to: "assigned", baseVersion: c1.version + 5 }), "conflict", "Veraltete baseVersion → conflict");
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
ok(false, "unerwarteter Fehler");
|
||||
})
|
||||
.finally(finish(() => cleanupTenants([SLUG])));
|
||||
Reference in New Issue
Block a user