Files
craftvia/scripts/test-review.ts
T
msolarczekandClaude Opus 5 c8e6f30a27
CI / build-and-check (push) Canceled after 0s
CI / audit (push) Canceled after 0s
CI / sbom (push) Canceled after 0s
Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation
und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 11:05:39 +02:00

79 lines
5.7 KiB
TypeScript

// AP4 — Managementklauseln (9.1 Kennzahlen, 9.3 Managementbewertung, 10.2 CAPA).
// Prüft Datenmodell, Relationen und Kern-Invarianten der DoD gegen die lokale DB
// (Wegwerf-Mandant, dbForTenant → RLS-Pfad).
//
// Lauf: npx tsx scripts/test-review.ts
import "dotenv/config";
import { prisma, dbForTenant } from "../src/server/db";
let failures = 0;
const ok = (c: boolean, m: string) => { console.log(`${c ? "✓" : "✗ FEHLER"} ${m}`); if (!c) failures++; };
const SLUG = "ap4-test-review";
async function cleanup() {
const t = await prisma.tenant.findUnique({ where: { slug: SLUG }, select: { id: true } });
if (!t) return;
const id = t.id;
await prisma.correctiveAction.deleteMany({ where: { tenantId: id } });
await prisma.nonconformity.deleteMany({ where: { tenantId: id } });
await prisma.managementReviewDecision.deleteMany({ where: { tenantId: id } });
await prisma.managementReview.deleteMany({ where: { tenantId: id } });
await prisma.kpiValue.deleteMany({ where: { tenantId: id } });
await prisma.kpi.deleteMany({ where: { tenantId: id } });
await prisma.tenant.delete({ where: { id } });
}
async function main() {
await cleanup();
const tenant = await prisma.tenant.create({ data: { name: "AP4", slug: SLUG } });
const t = tenant.id;
const db = dbForTenant(t);
// ── 9.1 Kennzahl über zwei Perioden auswertbar ──────────────────────────────
const kpi = await db.kpi.create({ data: { tenantId: t, name: "Überfällige Maßnahmen", target: "< 5%", unit: "%", cadence: "quartalsweise" }, select: { id: true } });
for (const [period, value] of [["2026-Q1", "8%"], ["2026-Q2", "4%"]]) {
await db.kpiValue.upsert({
where: { tenantId_kpiId_period: { tenantId: t, kpiId: kpi.id, period } },
update: { value }, create: { tenantId: t, kpiId: kpi.id, period, value },
});
}
// Upsert derselben Periode → Update statt Duplikat (Unique).
await db.kpiValue.upsert({ where: { tenantId_kpiId_period: { tenantId: t, kpiId: kpi.id, period: "2026-Q2" } }, update: { value: "3%" }, create: { tenantId: t, kpiId: kpi.id, period: "2026-Q2", value: "3%" } });
const vals = await db.kpiValue.findMany({ where: { kpiId: kpi.id }, orderBy: { period: "asc" } });
ok(vals.length === 2, `Kennzahl über zwei Perioden auswertbar (${vals.length} Werte, keine Dubletten)`);
ok(vals[1].value === "3%", "Messwert je Periode ist aktualisierbar (Upsert)");
// ── 9.3 Managementbewertung entlang 9.3.2 mit Beschlüssen ───────────────────
const review = await db.managementReview.create({ data: { tenantId: t, reviewDate: new Date("2026-06-01"), inputs: "Kennzahlen, Auditergebnisse, Vorjahresmaßnahmen …", results: "Ressourcen genehmigt" }, select: { id: true } });
await db.managementReviewDecision.create({ data: { tenantId: t, reviewId: review.id, decision: "Zusätzliche Awareness-Schulung", dueDate: new Date("2026-09-30") } });
const d2 = await db.managementReviewDecision.create({ data: { tenantId: t, reviewId: review.id, decision: "Backup-Konzept prüfen" }, select: { id: true } });
await db.managementReviewDecision.update({ where: { id: d2.id }, data: { status: "erledigt" } });
const withDec = await db.managementReview.findUnique({ where: { id: review.id }, include: { decisions: true } });
ok(withDec?.decisions.length === 2, "Managementbewertung mit Beschlüssen (Verantwortlicher/Termin) protokollierbar");
ok(withDec?.decisions.filter((d) => d.status === "erledigt").length === 1, "Beschluss-Status nachverfolgbar (1 erledigt)");
await db.managementReview.update({ where: { id: review.id }, data: { status: "abgeschlossen" } });
ok((await db.managementReview.findUnique({ where: { id: review.id } }))?.status === "abgeschlossen", "Managementbewertung abschließbar");
// ── 10.2 Nichtkonformität + Korrekturmaßnahme inkl. Wirksamkeit ──────────────
const nc = await db.nonconformity.create({ data: { tenantId: t, refNo: "NC-2026-0001", source: "Internes Audit", description: "Zugriffsrechte nicht rezertifiziert", immediateCorrection: "Sofort-Review" }, select: { id: true } });
const nc2 = await db.nonconformity.create({ data: { tenantId: t, refNo: "NC-2026-0002", source: "Vorfall", description: "Test" }, select: { refNo: true } });
ok(nc2.refNo === "NC-2026-0002", "fortlaufende NC-Kennung (NC-2026-0002)");
const ca = await db.correctiveAction.create({ data: { tenantId: t, nonconformityId: nc.id, action: "Rezertifizierungs-Prozess etablieren", rootCause: "Kein definierter Turnus" }, select: { id: true } });
// Wirksamkeit dokumentieren + Maßnahme umgesetzt.
await db.correctiveAction.update({ where: { id: ca.id }, data: { status: "umgesetzt", effectivenessCheck: "Rezertifizierung im Folgequartal vollständig", effectivenessConfirmedAt: new Date() } });
const caAfter = await db.correctiveAction.findUnique({ where: { id: ca.id } });
ok(!!caAfter?.effectivenessConfirmedAt && !!caAfter?.effectivenessCheck, "Korrekturmaßnahme mit dokumentierter Wirksamkeitsprüfung");
// Nichtkonformität schließen.
await db.nonconformity.update({ where: { id: nc.id }, data: { status: "abgeschlossen", closedAt: new Date() } });
const ncAfter = await db.nonconformity.findUnique({ where: { id: nc.id } });
ok(ncAfter?.status === "abgeschlossen" && !!ncAfter?.closedAt, "Maßnahmenfall inkl. Wirksamkeitsprüfung abschließbar");
await cleanup();
console.log("\n✓ aufgeräumt (Wegwerf-Mandant entfernt)");
}
main()
.then(() => { console.log(failures === 0 ? "\nAP4-Managementklauseln 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); });