// AP5 — Dokumentenlenkung: Prüfzyklus/-termin, Neuversion + Historie, Lesebestätigung // je Version. Prüft die reine Zyklus-Logik und die Daten-Invarianten (Wegwerf-Mandant). // // Lauf: npx tsx scripts/test-doc-control.ts import "dotenv/config"; import { prisma } from "../src/server/db"; import { computeNextReview, isReviewDue, bumpMinorVersion } from "../src/lib/review-cycle"; let failures = 0; const ok = (c: boolean, m: string) => { console.log(`${c ? "✓" : "✗ FEHLER"} ${m}`); if (!c) failures++; }; const SLUG = "ap5-test-doc"; async function cleanup() { const t = await prisma.tenant.findUnique({ where: { slug: SLUG }, select: { id: true } }); if (!t) return; await prisma.policyAcknowledgement.deleteMany({ where: { tenantId: t.id } }); await prisma.policyDocumentVersion.deleteMany({ where: { tenantId: t.id } }); await prisma.policyDocument.deleteMany({ where: { tenantId: t.id } }); await prisma.tenant.delete({ where: { id: t.id } }); } async function main() { await cleanup(); // ── Reine Zyklus-Logik ────────────────────────────────────────────────────── const from = new Date("2026-01-15T00:00:00Z"); const next = computeNextReview(from, "jährlich"); ok(next?.getUTCFullYear() === 2027 && next?.getUTCMonth() === 0, "computeNextReview: jährlich → +12 Monate"); ok(computeNextReview(from, null) === null, "kein Zyklus → kein Termin"); ok(isReviewDue(new Date("2020-01-01")) === true && isReviewDue(new Date("2999-01-01")) === false, "isReviewDue erkennt Überfälligkeit"); ok(bumpMinorVersion("1.0") === "1.1" && bumpMinorVersion("2.9") === "2.10" && bumpMinorVersion("3") === "3.1", "bumpMinorVersion korrekt"); // ── Daten-Invarianten ─────────────────────────────────────────────────────── const tenant = await prisma.tenant.create({ data: { name: "AP5", slug: SLUG } }); const t = tenant.id; const doc = await prisma.policyDocument.create({ data: { tenantId: t, code: "R08", type: "RICHTLINIE", title: "Zugriffskontrolle", version: "1.0", rawMarkdown: "# Zugriffskontrolle" }, select: { id: true, version: true }, }); // Lesebestätigung v1.0 durch zwei Nutzer; Duplikat (gleicher Nutzer/Version) wird verhindert. await prisma.policyAcknowledgement.create({ data: { tenantId: t, policyDocumentId: doc.id, userId: "user-1", version: "1.0" } }); await prisma.policyAcknowledgement.create({ data: { tenantId: t, policyDocumentId: doc.id, userId: "user-2", version: "1.0" } }); let dupBlocked = false; try { await prisma.policyAcknowledgement.create({ data: { tenantId: t, policyDocumentId: doc.id, userId: "user-1", version: "1.0" } }); } catch { dupBlocked = true; } ok(dupBlocked, "Lesebestätigung je (Dokument, Nutzer, Version) nur einmal (Unique)"); ok((await prisma.policyAcknowledgement.count({ where: { policyDocumentId: doc.id, version: "1.0" } })) === 2, "2 Lesebestätigungen für v1.0"); // „Als geprüft markieren": Momentaufnahme + Minor-Bump. await prisma.policyDocumentVersion.create({ data: { tenantId: t, policyDocumentId: doc.id, version: "1.0", title: "Zugriffskontrolle", changeNote: "Turnusmäßige Überprüfung", createdBy: "user-1" } }); const newVersion = bumpMinorVersion(doc.version); await prisma.policyDocument.update({ where: { id: doc.id }, data: { version: newVersion, reviewCycle: "jährlich", nextReviewAt: computeNextReview(new Date(), "jährlich") } }); const after = await prisma.policyDocument.findUnique({ where: { id: doc.id }, include: { versionHistory: true } }); ok(after?.version === "1.1", "Neuversion nach Prüfung (v1.0 → v1.1)"); ok((after?.versionHistory.length ?? 0) >= 1, "Versionshistorie enthält die frühere Version (≥2 Versionen sichtbar: Historie + aktuell)"); // Version-Scoping: alte Bestätigungen (v1.0) zählen NICHT für v1.1. ok((await prisma.policyAcknowledgement.count({ where: { policyDocumentId: doc.id, version: "1.1" } })) === 0, "Lesebestätigungen sind versions-scoped (v1.1 startet bei 0)"); // Prüftermin gesetzt. ok(after?.reviewCycle === "jährlich" && !!after?.nextReviewAt, "Prüfzyklus + nächster Prüftermin gesetzt"); await cleanup(); console.log("\n✓ aufgeräumt (Wegwerf-Mandant entfernt)"); } main() .then(() => { console.log(failures === 0 ? "\nAP5-Dokumentenlenkung grün." : `\n${failures} Prüfung(en) fehlgeschlagen.`); process.exit(failures === 0 ? 0 : 1); }) .catch(async (e) => { console.error(e); await cleanup().catch(() => {}); process.exit(1); });