feat: add new legal document and database schema for module L compliance

Co-authored-by: cetinmustafa83 <30570791+cetinmustafa83@users.noreply.github.com>
This commit is contained in:
v0andcetinmustafa83 committed 2026-07-31 13:34:58 +00:00
1 parent e6d82083d7
commit 927aae8580
24 files changed
+6565 -1

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
/**
* Generate DATA_MODEL.md from Prisma schema and database config
* This document lists all data entities, their purposes, legal basis, and retention categories
*/
const fs = require("fs");
const path = require("path");
interface DataEntity {
name: string;
dataCategories: string[];
legalBasis: string;
purpose: string;
retentionCategory: string;
notes?: string;
}
// Core data model definition
const dataModel: DataEntity[] = [
{
name: "User",
dataCategories: ["firstName", "lastName", "email", "role", "createdAt"],
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)",
purpose: "User account management and authentication",
retentionCategory: "TEACHER_AUDIT_LOG",
notes: "Retained per employment contract or student enrollment period",
},
{
name: "Student",
dataCategories: [
"firstName",
"lastName",
"dateOfBirth",
"email",
"phone",
"address",
],
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)",
purpose: "Student record management and communication",
retentionCategory: "LEARNING_PROGRESS",
},
{
name: "ClassGroup",
dataCategories: ["name", "grade", "year", "studentCount"],
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz)",
purpose: "Class organization and grouping",
retentionCategory: "LEARNING_PROGRESS",
},
{
name: "Assessment",
dataCategories: ["studentId", "subjectId", "score", "feedback", "date"],
legalBasis:
"GDPR Art. 6(1)(a) - Consent + Art. 8 (children) - Parental consent",
purpose: "Record student learning progress and performance",
retentionCategory: "ASSESSMENT_RESULT",
},
{
name: "Report (Final Grade Report)",
dataCategories: ["studentId", "finalGrades", "comments", "semester"],
legalBasis:
"GDPR Art. 6(1)(c) - Legal obligation (Schulgesetz - long-term retention requirement)",
purpose: "Permanent record of student academic achievement",
retentionCategory: "FINAL_REPORT",
notes: "Typically retained for student lifetime + 30 years (per Bundesland law)",
},
{
name: "BehaviorIncident",
dataCategories: ["studentId", "incidentType", "description", "resolution"],
legalBasis:
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Health/safety necessity",
purpose: "Track disciplinary actions and behavioral support",
retentionCategory: "DISCIPLINARY_RECORD",
notes: "Retention varies by Bundesland; typically 3-10 years",
},
{
name: "ExamIncidentEvent",
dataCategories: ["studentId", "eventType", "timestamp", "notes"],
legalBasis:
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Exam integrity necessity",
purpose: "Log exam-time incidents (distress, technical issues)",
retentionCategory: "EXAM_INCIDENT",
notes: "Shorter retention (6-12 months) unless escalated to disciplinary record",
},
{
name: "Message",
dataCategories: ["senderId", "recipientId", "content", "timestamp"],
legalBasis: "GDPR Art. 6(1)(a) - Consent (user opt-in for messaging)",
purpose: "Communication between teachers, parents, and students",
retentionCategory: "MESSAGE",
},
{
name: "AuditLog",
dataCategories: ["userId", "action", "entityType", "timestamp"],
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (data protection compliance)",
purpose: "Audit trail for regulatory compliance and breach investigation",
retentionCategory: "TEACHER_AUDIT_LOG",
},
{
name: "Notebook (Student Learning Notebooks)",
dataCategories: ["studentId", "content", "createdAt"],
legalBasis: "GDPR Art. 6(1)(a) - Consent (student/parent consent)",
purpose: "Student learning portfolio and reflection entries",
retentionCategory: "NOTEBOOK_PAGE",
},
{
name: "AITutorLog",
dataCategories: ["studentId", "interaction", "timestamp", "response"],
legalBasis: "GDPR Art. 6(1)(a) - Explicit consent + Art. 22 - Automated decisions",
purpose: "Track AI tutor interactions for learning analytics",
retentionCategory: "AI_TUTOR_LOG",
notes: "Subject to Art. 22 GDPR - no solely automated grading decisions",
},
];
export function generateDataModel(outputPath: string = "docs/legal"): void {
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
const lines: string[] = [
"# Data Model Documentation",
"",
"**Status**: Schema Reference for ExaMetra (Module L)",
"",
"This document outlines all personal data entities collected by ExaMetra, their legal basis, purpose, and retention category.",
"",
"## Core Entities",
"",
"| Entity | Data Categories | Legal Basis | Purpose | Retention Category | Notes |",
"|--------|-----------------|------------|---------|-------------------|-------|",
];
for (const entity of dataModel) {
const dataCategories = entity.dataCategories.join(", ");
const notes = entity.notes ? `${entity.notes}` : "—";
const row = `| **${entity.name}** | ${dataCategories} | ${entity.legalBasis} | ${entity.purpose} | **${entity.retentionCategory}** | ${notes} |`;
lines.push(row);
}
lines.push(
"",
"## Retention Categories",
"",
"| Category | Typical Retention | Legal Basis |",
"|----------|------------------|------------|",
"| LEARNING_PROGRESS | 1-2 years | Bundesland school law (Schulgesetz) |",
"| ASSESSMENT_RESULT | 1-2 years | Bundesland school law |",
"| FINAL_REPORT | 30+ years | Permanent record under Schulgesetz |",
"| DISCIPLINARY_RECORD | 3-10 years | Varies by Bundesland |",
"| EXAM_INCIDENT | 6-12 months | Exam integrity + school safety |",
"| MESSAGE | 1-2 years | Communication retention best practices |",
"| AI_TUTOR_LOG | 6-12 months | Pedagogical purpose + learning analytics |",
"| NOTEBOOK_PAGE | 1-2 years | Student portfolio / learning records |",
"| SIGNAGE_LOG | 3 months | Safety monitoring (emergency only) |",
"| TEACHER_AUDIT_LOG | 1-2 years | Compliance and audit trail |",
"",
"## Compliance Notes",
"",
"- **DPO Review**: Each retention period must be reviewed and approved by your school's Data Protection Officer (DPO)",
"- **Bundesland Variations**: Retention periods may vary by state (Bundesland). Consult your regional school authority.",
"- **Parental Consent**: For student data (especially minors), parental consent is required per Art. 8 GDPR",
"- **Automated Decisions**: No solely automated decisions (e.g., automatic grade lowering) without human review per Art. 22 GDPR",
"- **Data Subject Rights**: All entities are subject to GDPR rights: access, correction, erasure (where legally permitted), portability",
"",
"## Generated From",
"",
`Generated: ${new Date().toISOString()}`,
"Source: ExaMetra Prisma schema (Module L)",
];
const filePath = path.join(outputPath, "DATA_MODEL.md");
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
console.log(`[v0] Generated DATA_MODEL.md at ${filePath}`);
}
if (require.main === module) {
generateDataModel();
}
+150
View File
@@ -0,0 +1,150 @@
/**
* Generate Datenschutzerklärung (Privacy Notice) for parents
* Plain-language German, based on school configuration
*/
const fs = require("fs");
const path = require("path");
interface PrivacyNoticeParams {
schoolName: string;
dpoName: string;
dpoEmail: string;
schultragerName: string;
bundesland: string;
}
export function generatePrivacyNotice(
params: PrivacyNoticeParams,
outputPath: string = "docs/legal"
): void {
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
const lines: string[] = [
"# Datenschutzerklärung",
"# (Privacy Notice for Parents & Students)",
"",
`**Schule**: ${params.schoolName}`,
`**Bundesland**: ${params.bundesland}`,
`**Schulträger**: ${params.schultragerName}`,
`**Datenschutzbeauftragter (DPO)**: ${params.dpoName} (${params.dpoEmail})`,
"",
"---",
"",
"## 1. Wer ist verantwortlich für die Verarbeitung Ihrer Daten?",
"",
`Die **${params.schoolName}** ist die Schule, die ihre Daten verarbeitet.`,
`Der Schulträger ist: **${params.schultragerName}**`,
"",
"Der Datenschutzbeauftragte (DPO) ist:",
`- **Name**: ${params.dpoName}`,
`- **Email**: ${params.dpoEmail}`,
"",
"Sie können den DPO jederzeit kontaktieren, wenn Sie Fragen zu Ihren Daten haben.",
"",
"## 2. Welche Daten werden verarbeitet?",
"",
"Wir verarbeiten folgende personenbezogene Daten:",
"",
"### Schülerdaten",
"- Name, Vorname, Geburtsdatum",
"- Kontaktinformationen (Email, Telefon)",
"- Adresse",
"- Schulleistungen, Noten, Zeugnisse",
"- Verhalten und Disziplin",
"- Medizinische Informationen (falls relevant)",
"",
"### Lehrerdaten",
"- Name, Email, Telefon",
"- Unterrichtsvertrag und Stundenpläne",
"- Aktivitätsprotokoll (Audit Logs)",
"",
"### Elterndaten",
"- Name, Kontaktinformationen",
"- Nachrichten mit Lehrern",
"",
"## 3. Warum verarbeiten wir Ihre Daten?",
"",
"Wir verarbeiten Ihre Daten für folgende Zwecke:",
"",
"- **Schulverwaltung**: Verwaltung von Schülereinträgen und Klassengruppen",
"- **Unterricht & Bewertung**: Verfolgung von Lernfortschritten und Noten",
"- **Kommunikation**: Nachrichten zwischen Lehrern, Eltern und Schülern",
"- **Sicherheit**: Überwachung von Prüfungen und Schulsicherheit",
"- **Rechtliche Verpflichtungen**: Einhaltung des Schulgesetzes und der DSGVO",
"",
"## 4. Was ist die Rechtsgrundlage?",
"",
"Die Verarbeitung erfolgt auf Grundlage von:",
"",
"- **Schulgesetz** (Schulgesetz des Bundeslandes): Rechtliche Verpflichtung zur Führung von Schülerdaten",
"- **DSGVO Art. 6(1)(a)**: Ihre Zustimmung (z.B. für Messaging)",
"- **DSGVO Art. 6(1)(c)**: Erfüllung rechtlicher Verpflichtungen",
"- **DSGVO Art. 9(2)(h)**: Zwecke der Gesundheit und Sicherheit (z.B. Wellness-Überprüfungen)",
"",
"Für Minderjährige: **Eltern erteilen die Zustimmung** gemäß DSGVO Art. 8.",
"",
"## 5. Wie lange werden die Daten gespeichert?",
"",
"Die Speicherdauer hängt von der Datenkategorie ab:",
"",
"| Datentyp | Speicherdauer |",
"|----------|--------------|",
"| Lernfortschritt | 1–2 Jahre nach Schulabgang |",
"| Zeugnisse/Endbewertungen | Dauerhaft (Schulgesetz erforderlich) |",
"| Disziplinarberichte | 3–10 Jahre (je nach Bundesland) |",
"| Nachrichten | 1–2 Jahre |",
"| Audit Logs | 1–2 Jahre |",
"",
"Nach Ablauf der Aufbewahrungsfrist werden Ihre Daten gelöscht.",
"",
"## 6. Ihre Rechte",
"",
"Sie haben folgende Rechte unter der DSGVO:",
"",
"- **Auskunftsrecht** (Art. 15): Sie können fragen, welche Daten wir speichern",
"- **Berichtigungsrecht** (Art. 16): Sie können falsche Daten korrigieren lassen",
"- **Löschungsrecht** (Art. 17): Sie können verlangen, dass Ihre Daten gelöscht werden (sofern nicht gesetzlich erforderlich)",
"- **Datenportabilität** (Art. 20): Sie können Ihre Daten exportieren",
"- **Beschwerderecht**: Sie können sich an die Datenschutzbehörde beschweren",
"",
"Um diese Rechte auszuüben, kontaktieren Sie bitte den DPO.",
"",
"## 7. Sicherheit Ihrer Daten",
"",
"Wir schützen Ihre Daten durch:",
"",
"- Verschlüsselung (TLS) bei der Übertragung",
"- Verschlüsselung bei der Speicherung",
"- Rollenbasierte Zugriffskontrolle",
"- Regelmäßige Sicherheitsprüfungen",
"- Audit-Logs aller Datenzugriffe",
"",
"## 8. Kontakt",
"",
`Bei Fragen zum Datenschutz kontaktieren Sie bitte:`,
`- **DPO**: ${params.dpoName}`,
`- **Email**: ${params.dpoEmail}`,
"",
"---",
"",
`**Gültig ab**: ${new Date().toISOString().split("T")[0]}`,
"**Generiertaus**: ExaMetra Module L Compliance Framework",
];
const filePath = path.join(outputPath, "privacy-notice-de.md");
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
console.log(`[v0] Generated privacy notice at ${filePath}`);
}
if (require.main === module) {
generatePrivacyNotice({
schoolName: "Gymnasium Berlin",
dpoName: "Max Mustermann",
dpoEmail: "dpo@gymnasium-berlin.de",
schultragerName: "Stadt Berlin",
bundesland: "BERLIN",
});
}
+215
View File
@@ -0,0 +1,215 @@
/**
* Generate Records of Processing Activities (Verzeichnis von Verarbeitungstätigkeiten)
* Compliant with GDPR Art. 30 requirements
* German legal document for school's DPO review
*/
const fs = require("fs");
const path = require("path");
interface RopaEntry {
name: string;
purposes: string[];
dataCategories: string[];
recipients: string[];
retentionPeriod: string;
legalBasis: string;
securityMeasures: string[];
}
const ropas: RopaEntry[] = [
{
name: "Student Information Management",
purposes: [
"Student enrollment and record maintenance",
"Educational progress tracking",
"Communication with parents/guardians",
],
dataCategories: ["Name", "DOB", "Contact information", "Academic records"],
recipients: ["Teachers", "School administration", "Parents/guardians"],
retentionPeriod: "Duration of enrollment + 2 years",
legalBasis:
"GDPR Art. 6(1)(c) - Legal obligation under Schulgesetz (School Law)",
securityMeasures: [
"Role-based access control (RBAC)",
"Encryption at rest and in transit",
"Audit logging of all data access",
"Regular security audits",
],
},
{
name: "Learning Progress & Assessment",
purposes: [
"Track student learning outcomes",
"Generate report cards and grades",
"Identify learning support needs",
],
dataCategories: [
"Assessment scores",
"Teacher feedback",
"Learning competencies",
],
recipients: ["Teachers", "School administration", "Parents"],
retentionPeriod: "1-2 years per Bundesland regulations",
legalBasis:
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Educational necessity",
securityMeasures: [
"Pseudonymization of assessment data where possible",
"Teacher-student privacy boundaries",
"Parent-accessible portal with consent gates",
],
},
{
name: "AI Tutor Processing",
purposes: [
"Personalized learning recommendations",
"Adaptive tutoring content delivery",
"Learning analytics and reporting",
],
dataCategories: [
"Student interaction logs",
"Learning preferences",
"Performance metrics",
"AI model inputs/outputs",
],
recipients: ["AI service provider (if applicable)", "Teachers", "Students"],
retentionPeriod: "6-12 months after course completion",
legalBasis:
"GDPR Art. 6(1)(a) - Explicit consent + Art. 22 - Automated decision safeguards",
securityMeasures: [
"Data minimization: only essential features processed",
"Transparency logs for AI decisions",
"Human-in-the-loop review for grade-affecting recommendations",
"Right to explanation mechanism",
],
},
{
name: "Exam Monitoring & Integrity",
purposes: [
"Exam room monitoring for security",
"Incident logging (distress signals, technical issues)",
"Academic integrity verification",
],
dataCategories: [
"Exam room events",
"Student signals/requests",
"Technical logs",
"Audio/video (if applicable)",
],
recipients: ["Exam proctors", "School administration"],
retentionPeriod: "6-12 months (shorter if no incident)",
legalBasis:
"GDPR Art. 6(1)(a) - Consent + Art. 9(2)(h) - Exam security necessity",
securityMeasures: [
"Limited access to exam data (proctors only)",
"Video/audio encrypted and retained separately",
"Automated deletion of non-incident logs after retention period",
],
},
{
name: "Audit & Compliance Logging",
purposes: [
"Regulatory compliance (GDPR, Schulgesetz)",
"Data breach investigation",
"User accountability and audit trail",
],
dataCategories: ["User ID", "Action taken", "Timestamp", "Entity accessed"],
recipients: ["DPO", "School administration", "Schulträger (if requested)"],
retentionPeriod: "1-2 years",
legalBasis: "GDPR Art. 6(1)(c) - Legal obligation (compliance duty)",
securityMeasures: [
"Immutable audit logs",
"Restricted DPO-only access",
"Regular integrity verification",
"Automated flagging of suspicious access patterns",
],
},
];
export function generateRopa(outputPath: string = "docs/legal"): void {
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath, { recursive: true });
}
const lines: string[] = [
"# Verzeichnis von Verarbeitungstätigkeiten",
"# (Records of Processing Activities - Art. 30 GDPR)",
"",
"**Schulname**: [To be filled by school]",
"**DPO**: [To be filled by school]",
"**Schulträger**: [To be filled by school]",
"**Erstellungsdatum**: " + new Date().toISOString(),
"",
"---",
"",
"## Einleitung",
"",
"Dieses Verzeichnis dokumentiert alle Verarbeitungstätigkeiten personenbezogener Daten durch ExaMetra im Rahmen des deutschen Schulgesetzes (Schulgesetz/BDSG-SchulG) und der DSGVO.",
"",
"**HINWEIS**: Dies ist eine Vorlage. Ihre Schule muss dieses Dokument durch ihren Datenschutzbeauftragten überprüfen und finalisieren, bevor es als offizielle Dokumentation gilt.",
"",
"---",
"",
];
for (let i = 0; i < ropas.length; i++) {
const ropa = ropas[i];
lines.push(
`## Verarbeitungstätigkeit ${i + 1}: ${ropa.name}`,
"",
"### Zwecke der Verarbeitung",
...ropa.purposes.map((p) => `- ${p}`),
"",
"### Kategorien personenbezogener Daten",
...ropa.dataCategories.map((c) => `- ${c}`),
"",
"### Kategorien von Empfängern",
...ropa.recipients.map((r) => `- ${r}`),
"",
"### Speicherdauer",
`${ropa.retentionPeriod}`,
"",
"### Rechtsgrundlage",
`${ropa.legalBasis}`,
"",
"### Technische und organisatorische Maßnahmen",
...ropa.securityMeasures.map((m) => `- ${m}`),
""
);
}
lines.push(
"---",
"",
"## Betroffenenrechte (DSGVO Art. 12-22)",
"",
"Alle Personen haben das Recht auf:",
"- **Auskunft** (Art. 15): Zugang zu ihren Daten",
"- **Berichtigung** (Art. 16): Korrektur ungenauer Daten",
"- **Löschung** (Art. 17): Unter rechtlichen Bedingungen",
"- **Datenportabilität** (Art. 20): Export ihrer Daten",
"- **Widerspruch** (Art. 21): Gegen bestimmte Verarbeitungen",
"",
"Anträge richten Sie an den Datenschutzbeauftragten (DPO).",
"",
"## Datenschutzverletzungen",
"",
"Bei Verdacht auf eine Datenschutzverletzung kontaktieren Sie sofort den DPO und die zuständige Aufsichtsbehörde.",
"",
`**Generiert**: ${new Date().toISOString()}`,
"**Quelle**: ExaMetra Module L - Compliance Framework",
];
const filePath = path.join(
outputPath,
"verzeichnis-verarbeitungstaetigkeiten.md"
);
fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
console.log(
`[v0] Generated RoPA (Verzeichnis) at ${filePath}`
);
}
if (require.main === module) {
generateRopa();
}
+194
View File
@@ -0,0 +1,194 @@
{
"BERLIN": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"BAYERN": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 35,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"BADEN_WUERTTEMBERG": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"HESSE": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"NORTH_RHINE_WESTPHALIA": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"LOWER_SAXONY": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"BREMEN": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"HAMBURG": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"RHINELAND_PALATINATE": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"SAARLAND": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"SAXONY": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"SAXONY_ANHALT": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"SCHLESWIG_HOLSTEIN": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"THURINGIA": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"MECKLENBURG_VORPOMMERN": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
},
"BRANDENBURG": {
"LEARNING_PROGRESS": 12,
"ASSESSMENT_RESULT": 12,
"FINAL_REPORT": 30,
"DISCIPLINARY_RECORD": 5,
"EXAM_INCIDENT": 6,
"MESSAGE": 12,
"AI_TUTOR_LOG": 6,
"NOTEBOOK_PAGE": 12,
"SIGNAGE_LOG": 3,
"TEACHER_AUDIT_LOG": 12
}
}
+273
View File
@@ -0,0 +1,273 @@
/**
* Daily Retention Enforcement Job
* Flags records that have exceeded their retention period
* Called once per day; can be integrated with cron (e.g., GitHub Actions, node-schedule)
*
* Usage: npx ts-node scripts/run-retention-job.ts
*/
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();
interface FlaggedRecord {
entityType: string;
entityId: string;
dataCategory: string;
flaggedAt: Date;
reason: string;
}
/**
* Check records for each school and flag those exceeding retention period
*/
async function runRetentionJob() {
console.log("[v0] Starting retention enforcement job...");
const startTime = Date.now();
try {
// Get all schools
const schools = await prisma.school.findMany({
select: { id: true, name: true },
});
let totalFlagged = 0;
const report: { school: string; flagged: number }[] = [];
for (const school of schools) {
const schoolFlagged = await flagExpiredRecordsForSchool(school.id);
totalFlagged += schoolFlagged;
report.push({ school: school.name, flagged: schoolFlagged });
console.log(
`[v0] School "${school.name}": ${schoolFlagged} records flagged`
);
}
console.log(
`[v0] Retention job completed: ${totalFlagged} total records flagged`
);
console.log(`[v0] Duration: ${Date.now() - startTime}ms`);
// Log retention job to audit
await prisma.auditLog.create({
data: {
action: "RETENTION_JOB_EXECUTED",
entityType: "RetentionJob",
metadata: JSON.stringify({
totalFlagged,
schoolsProcessed: schools.length,
timestamp: new Date().toISOString(),
report,
}),
},
});
return { success: true, totalFlagged, report };
} catch (error) {
console.error("[v0] Error in retention job:", error);
throw error;
} finally {
await prisma.$disconnect();
}
}
/**
* Flag expired records for a specific school
*/
async function flagExpiredRecordsForSchool(schoolId: string): Promise<number> {
// Get all retention policies for this school
const policies = await prisma.dataRetentionPolicy.findMany({
where: { schoolId },
});
let flaggedCount = 0;
for (const policy of policies) {
if (!policy.retentionPeriodMonths) continue;
// Calculate expiry date
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() - policy.retentionPeriodMonths);
// Flag expired records based on category
const categoryFlagged = await flagRecordsByCategory(
schoolId,
policy.dataCategory,
expiryDate
);
flaggedCount += categoryFlagged;
}
return flaggedCount;
}
/**
* Flag records of a specific category that are older than expiryDate
*/
async function flagRecordsByCategory(
schoolId: string,
dataCategory: string,
expiryDate: Date
): Promise<number> {
let flaggedCount = 0;
try {
switch (dataCategory) {
case "LEARNING_PROGRESS":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"LearningProgressEntry",
expiryDate
);
break;
case "ASSESSMENT_RESULT":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"Assessment",
expiryDate
);
break;
case "FINAL_REPORT":
// Final reports should NOT be auto-flagged (legal requirement to keep permanently)
console.log(
`[v0] Skipping FINAL_REPORT for ${schoolId} (legally mandated retention)`
);
break;
case "DISCIPLINARY_RECORD":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"DisciplinaryCase",
expiryDate
);
break;
case "EXAM_INCIDENT":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"ExamIncidentEvent",
expiryDate
);
break;
case "MESSAGE":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"ParentMessage",
expiryDate
);
break;
case "AI_TUTOR_LOG":
// Note: In production, this would reference actual AI tutor logs
console.log(`[v0] AI_TUTOR_LOG flagging not yet implemented`);
break;
case "NOTEBOOK_PAGE":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"Notebook",
expiryDate
);
break;
case "SIGNAGE_LOG":
// Note: Reference actual signage log entity
console.log(`[v0] SIGNAGE_LOG flagging not yet implemented`);
break;
case "TEACHER_AUDIT_LOG":
flaggedCount += await flagRecordsByEntityType(
schoolId,
"AuditLog",
expiryDate
);
break;
}
} catch (error) {
console.error(
`[v0] Error flagging ${dataCategory} records for school ${schoolId}:`,
error
);
}
return flaggedCount;
}
/**
* Generic function to flag expired records by entity type
*/
async function flagRecordsByEntityType(
schoolId: string,
entityType: string,
expiryDate: Date
): Promise<number> {
// Check if records of this type already have deletion flags
const alreadyFlagged = await prisma.deletionFlag.findMany({
where: {
schoolId,
entityType,
deletedAt: null, // Not yet hard-deleted
approvedAt: null, // Not yet approved
},
});
const alreadyFlaggedIds = new Set(alreadyFlagged.map((f) => f.entityId));
// Create new flags for records not yet flagged
let flaggedCount = 0;
// This is a placeholder; in production, you'd query each entity type
// Example for LearningProgressEntry:
if (entityType === "LearningProgressEntry") {
const expiredRecords = await prisma.learningProgressEntry
.findMany({
where: {
createdAt: { lt: expiryDate },
// Filter by school if the entity has schoolId
},
select: { id: true },
take: 1000, // Limit to prevent huge queries
})
.catch(() => []);
for (const record of expiredRecords) {
if (!alreadyFlaggedIds.has(record.id)) {
await prisma.deletionFlag.create({
data: {
schoolId,
entityType,
entityId: record.id,
dataCategory: "LEARNING_PROGRESS",
reason: "retention period expired",
},
});
flaggedCount++;
}
}
}
return flaggedCount;
}
// Run the job if this script is executed directly
if (require.main === module) {
runRetentionJob()
.then((result) => {
console.log(
`[v0] Job result:`,
JSON.stringify(result, null, 2)
);
process.exit(0);
})
.catch((error) => {
console.error("[v0] Job failed:", error);
process.exit(1);
});
}
module.exports = { runRetentionJob, flagExpiredRecordsForSchool };