import type { PrismaClient, Framework } from "@prisma/client"; import type { ParsedPackage, ParsedDoc } from "./import-policies"; import { parsePackageFiles, readPackageVersion, mappingFileFor } from "./import-policies"; /** * Globale DB-Vorlagenablage (Plattform-Ebene). Master-Quelle für den Mandanten-Import: * eine `PolicyTemplateVersion` je Paketversion mit Inhalten je Sprache (`locale`). * * - `syncTemplatesFromParsed`: schreibt ein `ParsedPackage` (aus den Dateien geparst) * für eine Sprache in eine Version. Für eine bestehende Version werden die Inhalte * dieser Sprache vollständig ersetzt (idempotent). Sicher, weil die Vorlagen-Tabellen * keine Mandanten-/Nutzerdaten enthalten. * - `loadPublishedPackage`: liest die aktuell veröffentlichte Version in der gewünschten * Sprache als `ParsedPackage` — dieselbe Struktur wie der Datei-Parser, sodass der * non-destruktive Mandanten-Reconcile identisch damit arbeitet. Liefert null, wenn es * keine veröffentlichte Version bzw. keine Inhalte in der Sprache gibt (→ Datei-Fallback). */ type DocType = ParsedDoc["type"]; /** Ein Datei-Paket (eine Sprache) in eine Vorlagen-Version schreiben; Inhalte der Sprache ersetzen. */ export async function syncTemplatesFromParsed( prisma: PrismaClient, pkg: ParsedPackage, locale: string, framework: Framework = "TISAX", options: { publish?: boolean; publishedBy?: string | null } = {}, ): Promise<{ versionId: string; version: string }> { const publish = options.publish ?? true; // Eindeutigkeit je (Framework, Version): ISO 2.1 und TISAX 2.1 sind verschiedene Zeilen. const existing = await prisma.policyTemplateVersion.findUnique({ where: { framework_version: { framework, version: pkg.version } }, }); const version = existing ?? (await prisma.policyTemplateVersion.create({ data: { framework, version: pkg.version, status: publish ? "PUBLISHED" : "DRAFT", publishedBy: publish ? (options.publishedBy ?? null) : null, publishedAt: publish ? new Date() : null, }, })); if (existing && publish && existing.status !== "PUBLISHED") { await prisma.policyTemplateVersion.update({ where: { id: existing.id }, data: { status: "PUBLISHED", publishedAt: new Date(), publishedBy: options.publishedBy ?? null }, }); } const versionId = version.id; // Inhalte dieser Sprache vollständig ersetzen (idempotenter Sync). await prisma.$transaction([ prisma.policyTemplateDoc.deleteMany({ where: { versionId, locale } }), prisma.policyTemplateRequirement.deleteMany({ where: { versionId, locale } }), prisma.policyTemplateVariable.deleteMany({ where: { versionId, locale } }), prisma.policyTemplateBaselineParam.deleteMany({ where: { versionId, locale } }), prisma.policyTemplateEvidence.deleteMany({ where: { versionId, locale } }), prisma.policyTemplateDoc.createMany({ data: pkg.documents.map((d) => ({ versionId, locale, code: d.code, type: d.type, title: d.title, policyCode: d.policyCode, fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx, })), }), prisma.policyTemplateRequirement.createMany({ data: pkg.requirements.map((r) => ({ versionId, locale, reqId: r.reqId, policyCode: r.policyCode, control: r.control, obligation: r.obligation, condition: r.condition, requirement: r.requirement, implementation: r.implementation, vaCodes: r.vaCodes, nachweisLink: r.nachweisLink, })), }), prisma.policyTemplateVariable.createMany({ data: pkg.variables.map((v) => ({ versionId, locale, key: v.key, title: v.title, kind: v.kind, groupName: v.groupName, value: v.value, required: v.required, orderIdx: v.orderIdx, })), }), prisma.policyTemplateBaselineParam.createMany({ data: pkg.baseline.map((b) => ({ versionId, locale, blId: b.blId, section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx, })), }), prisma.policyTemplateEvidence.createMany({ data: pkg.evidence.map((e) => ({ versionId, locale, nr: e.nr, policyCode: e.policyCode, nachweis: e.nachweis, quelle: e.quelle, verantwortlich: e.verantwortlich, turnus: e.turnus, })), }), ]); return { versionId, version: version.version }; } /** * Aktuell veröffentlichte Vorlagen-Version in der gewünschten Sprache als `ParsedPackage`. * Liefert null, wenn keine veröffentlichte Version existiert oder die Sprache keine * Dokumente enthält (→ Aufrufer nutzt den Datei-Fallback). */ export async function loadPublishedPackage( prisma: PrismaClient, locale: string, framework: Framework = "TISAX", ): Promise { const version = await prisma.policyTemplateVersion.findFirst({ where: { status: "PUBLISHED", framework }, orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }], }); if (!version) return null; const [docs, reqs, vars, baseline, evidence] = await Promise.all([ prisma.policyTemplateDoc.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }), prisma.policyTemplateRequirement.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }), prisma.policyTemplateVariable.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }), prisma.policyTemplateBaselineParam.findMany({ where: { versionId: version.id, locale }, orderBy: { orderIdx: "asc" } }), prisma.policyTemplateEvidence.findMany({ where: { versionId: version.id, locale }, orderBy: { nr: "asc" } }), ]); if (docs.length === 0) return null; // Sprache (noch) nicht befüllt → Fallback return { version: version.version, documents: docs.map((d) => ({ code: d.code, type: d.type as DocType, title: d.title, policyCode: d.policyCode, fulfills: d.fulfills, rawMarkdown: d.rawMarkdown, orderIdx: d.orderIdx, })), requirements: reqs.map((r) => ({ reqId: r.reqId, policyCode: r.policyCode, control: r.control, obligation: r.obligation, condition: r.condition, requirement: r.requirement, implementation: r.implementation, vaCodes: r.vaCodes, nachweisLink: r.nachweisLink, })), variables: vars.map((v) => ({ key: v.key, title: v.title, kind: v.kind, groupName: v.groupName, value: v.value, required: v.required, orderIdx: v.orderIdx, })), baseline: baseline.map((b) => ({ blId: b.blId, section: b.section, name: b.name, vorgabe: b.vorgabe, orderIdx: b.orderIdx, })), evidence: evidence.map((e) => ({ nr: e.nr, policyCode: e.policyCode, nachweis: e.nachweis, quelle: e.quelle, verantwortlich: e.verantwortlich, turnus: e.turnus, })), }; } /** Sprachpräferenz eines Mandanten (Default "de"). Owner-Client (RLS-Bypass) — für Plattform- UND Mandanten-Aufrufer nutzbar. */ export async function getTenantLocale(prisma: PrismaClient, tenantId: string): Promise { const s = await prisma.tenantSettings.findUnique({ where: { tenantId }, select: { locale: true } }); return s?.locale ?? "de"; } /** * Auflösen des Vorlagenpakets für einen Mandanten: veröffentlichte DB-Version in der * Sprache des Mandanten → DE-Fallback → Datei-Fallback. So funktioniert der Import * identisch, egal ob die DB-Ablage schon befüllt ist. */ export async function resolvePackageForTenant( prisma: PrismaClient, tenantId: string, seedDir: string, framework: Framework = "TISAX", ): Promise<{ pkg: ParsedPackage; source: "db" | "file"; locale: string }> { const locale = await getTenantLocale(prisma, tenantId); const inLocale = await loadPublishedPackage(prisma, locale, framework); if (inLocale) return { pkg: inLocale, source: "db", locale }; if (locale !== "de") { const de = await loadPublishedPackage(prisma, "de", framework); if (de) return { pkg: de, source: "db", locale: "de" }; } // Datei-Fallback: das framework-passende Mapping (mapping.json / mapping-iso.json). return { pkg: parsePackageFiles(seedDir, mappingFileFor(framework)), source: "file", locale }; } /** Verfügbare Paketversion für den Update-Vergleich: veröffentlichte DB-Version, sonst Datei-Version. */ export async function getAvailableVersion( prisma: PrismaClient, seedDir: string, framework: Framework = "TISAX", ): Promise { const v = await prisma.policyTemplateVersion.findFirst({ where: { status: "PUBLISHED", framework }, orderBy: [{ publishedAt: "desc" }, { createdAt: "desc" }], select: { version: true }, }); return v?.version ?? readPackageVersion(seedDir, mappingFileFor(framework)); } /** * Frameworks eines Mandanten (AP1), Primär-Framework zuerst. Fällt auf `["TISAX"]` * zurück, falls (noch) keine `TenantFramework`-Zeile existiert — so bleibt jeder * Bestands-/Alt-Mandant ohne Zeile als TISAX lauffähig (Fail-safe zu Invariante 3). * Owner-Client (RLS-Bypass) — für Plattform- UND Mandanten-Aufrufer nutzbar. */ export async function getTenantFrameworks(prisma: PrismaClient, tenantId: string): Promise { const rows = await prisma.tenantFramework.findMany({ where: { tenantId }, orderBy: [{ isPrimary: "desc" }, { createdAt: "asc" }], select: { framework: true }, }); return rows.length ? rows.map((r) => r.framework) : ["TISAX"]; } /** Primär-Framework eines Mandanten (erstes aus {@link getTenantFrameworks}). */ export async function getTenantPrimaryFramework(prisma: PrismaClient, tenantId: string): Promise { return (await getTenantFrameworks(prisma, tenantId))[0]; }