package database import ( "context" "fmt" ) // migrateDSR creates DSGVO Data Subject Request tables (Phase 10) // and EduSearch seed management tables (Phase 11). func migrateDSR(db *DB) error { ctx := context.Background() migrations := []string{ // ============================================= // Phase 10: DSGVO Betroffenenanfragen (DSR) // Data Subject Request Management // ============================================= // Sequence for request numbers `CREATE SEQUENCE IF NOT EXISTS dsr_request_number_seq START 1`, // Main table: Data Subject Requests `CREATE TABLE IF NOT EXISTS data_subject_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id) ON DELETE SET NULL, request_number VARCHAR(50) UNIQUE NOT NULL, request_type VARCHAR(30) NOT NULL, status VARCHAR(30) NOT NULL DEFAULT 'intake', priority VARCHAR(20) DEFAULT 'normal', source VARCHAR(30) NOT NULL DEFAULT 'api', requester_email VARCHAR(255) NOT NULL, requester_name VARCHAR(255), requester_phone VARCHAR(50), identity_verified BOOLEAN DEFAULT FALSE, identity_verified_at TIMESTAMPTZ, identity_verified_by UUID REFERENCES users(id), identity_verification_method VARCHAR(50), request_details JSONB DEFAULT '{}', deadline_at TIMESTAMPTZ NOT NULL, legal_deadline_days INT NOT NULL, extended_deadline_at TIMESTAMPTZ, extension_reason TEXT, assigned_to UUID REFERENCES users(id), processing_notes TEXT, completed_at TIMESTAMPTZ, completed_by UUID REFERENCES users(id), result_summary TEXT, result_data JSONB, rejected_at TIMESTAMPTZ, rejected_by UUID REFERENCES users(id), rejection_reason TEXT, rejection_legal_basis TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id) )`, // DSR Status History for audit trail `CREATE TABLE IF NOT EXISTS dsr_status_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), request_id UUID NOT NULL REFERENCES data_subject_requests(id) ON DELETE CASCADE, from_status VARCHAR(30), to_status VARCHAR(30) NOT NULL, changed_by UUID REFERENCES users(id), comment TEXT, metadata JSONB, created_at TIMESTAMPTZ DEFAULT NOW() )`, // DSR Communications log `CREATE TABLE IF NOT EXISTS dsr_communications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), request_id UUID NOT NULL REFERENCES data_subject_requests(id) ON DELETE CASCADE, direction VARCHAR(10) NOT NULL, channel VARCHAR(20) NOT NULL, communication_type VARCHAR(50) NOT NULL, template_version_id UUID, subject VARCHAR(500), body_html TEXT, body_text TEXT, recipient_email VARCHAR(255), sent_at TIMESTAMPTZ, error_message TEXT, attachments JSONB DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id) )`, // DSR Templates `CREATE TABLE IF NOT EXISTS dsr_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_type VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, request_types JSONB DEFAULT '["access","rectification","erasure","restriction","portability"]', is_active BOOLEAN DEFAULT TRUE, sort_order INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() )`, // DSR Template Versions `CREATE TABLE IF NOT EXISTS dsr_template_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_id UUID REFERENCES dsr_templates(id) ON DELETE CASCADE, version VARCHAR(20) NOT NULL, language VARCHAR(5) DEFAULT 'de', subject VARCHAR(500) NOT NULL, body_html TEXT NOT NULL, body_text TEXT NOT NULL, status VARCHAR(20) DEFAULT 'draft', published_at TIMESTAMPTZ, created_by UUID REFERENCES users(id), approved_by UUID REFERENCES users(id), approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(template_id, version, language) )`, // DSR Exception Checks (for Art. 17(3) erasure exceptions) `CREATE TABLE IF NOT EXISTS dsr_exception_checks ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), request_id UUID NOT NULL REFERENCES data_subject_requests(id) ON DELETE CASCADE, exception_type VARCHAR(50) NOT NULL, description TEXT NOT NULL, applies BOOLEAN, checked_by UUID REFERENCES users(id), checked_at TIMESTAMPTZ, notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW() )`, // Phase 10 Indexes `CREATE INDEX IF NOT EXISTS idx_dsr_user ON data_subject_requests(user_id)`, `CREATE INDEX IF NOT EXISTS idx_dsr_status ON data_subject_requests(status)`, `CREATE INDEX IF NOT EXISTS idx_dsr_type ON data_subject_requests(request_type)`, `CREATE INDEX IF NOT EXISTS idx_dsr_deadline ON data_subject_requests(deadline_at)`, `CREATE INDEX IF NOT EXISTS idx_dsr_assigned ON data_subject_requests(assigned_to)`, `CREATE INDEX IF NOT EXISTS idx_dsr_request_number ON data_subject_requests(request_number)`, `CREATE INDEX IF NOT EXISTS idx_dsr_created ON data_subject_requests(created_at)`, `CREATE INDEX IF NOT EXISTS idx_dsr_status_history_request ON dsr_status_history(request_id)`, `CREATE INDEX IF NOT EXISTS idx_dsr_communications_request ON dsr_communications(request_id)`, `CREATE INDEX IF NOT EXISTS idx_dsr_exception_checks_request ON dsr_exception_checks(request_id)`, `CREATE INDEX IF NOT EXISTS idx_dsr_templates_type ON dsr_templates(template_type)`, `CREATE INDEX IF NOT EXISTS idx_dsr_template_versions_template ON dsr_template_versions(template_id)`, `CREATE INDEX IF NOT EXISTS idx_dsr_template_versions_status ON dsr_template_versions(status)`, // Insert default DSR templates `INSERT INTO dsr_templates (template_type, name, description, request_types, sort_order) VALUES ('dsr_receipt_access', 'Eingangsbestätigung Auskunft', 'Bestätigung des Eingangs einer Auskunftsanfrage nach Art. 15 DSGVO', '["access"]', 1), ('dsr_receipt_rectification', 'Eingangsbestätigung Berichtigung', 'Bestätigung des Eingangs einer Berichtigungsanfrage nach Art. 16 DSGVO', '["rectification"]', 2), ('dsr_receipt_erasure', 'Eingangsbestätigung Löschung', 'Bestätigung des Eingangs einer Löschanfrage nach Art. 17 DSGVO', '["erasure"]', 3), ('dsr_receipt_restriction', 'Eingangsbestätigung Einschränkung', 'Bestätigung des Eingangs einer Einschränkungsanfrage nach Art. 18 DSGVO', '["restriction"]', 4), ('dsr_receipt_portability', 'Eingangsbestätigung Datenübertragung', 'Bestätigung des Eingangs einer Datenübertragungsanfrage nach Art. 20 DSGVO', '["portability"]', 5), ('dsr_identity_request', 'Anfrage Identitätsnachweis', 'Aufforderung zur Identitätsverifizierung', '["access","rectification","erasure","restriction","portability"]', 6), ('dsr_processing_started', 'Bearbeitungsbestätigung', 'Bestätigung, dass die Bearbeitung begonnen hat', '["access","rectification","erasure","restriction","portability"]', 7), ('dsr_processing_update', 'Zwischenbericht', 'Zwischenstand zur Bearbeitung', '["access","rectification","erasure","restriction","portability"]', 8), ('dsr_clarification_request', 'Rückfragen', 'Anfrage zur Klärung des Begehrens', '["access","rectification","erasure","restriction","portability"]', 9), ('dsr_completed_access', 'Auskunft erteilt', 'Abschließende Mitteilung mit Datenauskunft', '["access"]', 10), ('dsr_completed_access_negative', 'Negativauskunft', 'Mitteilung dass keine Daten vorhanden sind', '["access"]', 11), ('dsr_completed_rectification', 'Berichtigung durchgeführt', 'Bestätigung der Datenberichtigung', '["rectification"]', 12), ('dsr_completed_erasure', 'Löschung durchgeführt', 'Bestätigung der Datenlöschung', '["erasure"]', 13), ('dsr_completed_restriction', 'Einschränkung aktiviert', 'Bestätigung der Verarbeitungseinschränkung', '["restriction"]', 14), ('dsr_completed_portability', 'Daten bereitgestellt', 'Mitteilung zur Datenübermittlung', '["portability"]', 15), ('dsr_restriction_lifted', 'Einschränkung aufgehoben', 'Vorabbenachrichtigung vor Aufhebung der Einschränkung', '["restriction"]', 16), ('dsr_rejected_identity', 'Ablehnung - Identität nicht verifizierbar', 'Ablehnung mangels Identitätsnachweis', '["access","rectification","erasure","restriction","portability"]', 17), ('dsr_rejected_exception', 'Ablehnung - Ausnahme', 'Ablehnung aufgrund gesetzlicher Ausnahmen (z.B. Art. 17 Abs. 3)', '["erasure","restriction"]', 18), ('dsr_rejected_unfounded', 'Ablehnung - Offensichtlich unbegründet', 'Ablehnung nach Art. 12 Abs. 5 DSGVO', '["access","rectification","erasure","restriction","portability"]', 19) ON CONFLICT (template_type) DO NOTHING`, // ============================================= // Phase 11: EduSearch Seeds Management // Seed URLs for the education search crawler // ============================================= // EduSearch Seed Categories `CREATE TABLE IF NOT EXISTS edu_search_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(50) UNIQUE NOT NULL, display_name VARCHAR(100) NOT NULL, description TEXT, icon VARCHAR(10), sort_order INT DEFAULT 0, is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() )`, // EduSearch Seeds (crawler seed URLs) `CREATE TABLE IF NOT EXISTS edu_search_seeds ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), url VARCHAR(500) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, description TEXT, category_id UUID REFERENCES edu_search_categories(id) ON DELETE SET NULL, source_type VARCHAR(20) DEFAULT 'GOV', scope VARCHAR(20) DEFAULT 'FEDERAL', state VARCHAR(5), trust_boost DECIMAL(3,2) DEFAULT 0.50, enabled BOOLEAN DEFAULT TRUE, crawl_depth INT DEFAULT 2, crawl_frequency VARCHAR(20) DEFAULT 'weekly', last_crawled_at TIMESTAMPTZ, last_crawl_status VARCHAR(20), last_crawl_docs INT DEFAULT 0, total_documents INT DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id) )`, // EduSearch Crawl Runs (history of crawl executions) `CREATE TABLE IF NOT EXISTS edu_search_crawl_runs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), seed_id UUID REFERENCES edu_search_seeds(id) ON DELETE CASCADE, status VARCHAR(20) DEFAULT 'running', started_at TIMESTAMPTZ DEFAULT NOW(), completed_at TIMESTAMPTZ, pages_crawled INT DEFAULT 0, documents_indexed INT DEFAULT 0, errors_count INT DEFAULT 0, error_details JSONB, triggered_by UUID REFERENCES users(id) )`, // EduSearch Denylist (URLs/domains to never crawl) `CREATE TABLE IF NOT EXISTS edu_search_denylist ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), pattern VARCHAR(500) UNIQUE NOT NULL, pattern_type VARCHAR(20) DEFAULT 'domain', reason TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES users(id) )`, // Phase 11 Indexes `CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_category ON edu_search_seeds(category_id)`, `CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_enabled ON edu_search_seeds(enabled)`, `CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_state ON edu_search_seeds(state)`, `CREATE INDEX IF NOT EXISTS idx_edu_search_seeds_scope ON edu_search_seeds(scope)`, `CREATE INDEX IF NOT EXISTS idx_edu_search_crawl_runs_seed ON edu_search_crawl_runs(seed_id)`, `CREATE INDEX IF NOT EXISTS idx_edu_search_crawl_runs_status ON edu_search_crawl_runs(status)`, // Insert default EduSearch categories `INSERT INTO edu_search_categories (name, display_name, description, icon, sort_order) VALUES ('federal', 'Bundesebene', 'KMK, BMBF, Bildungsserver', '🏛️', 1), ('states', 'Bundesländer', 'Ministerien, Landesbildungsserver', '🗺️', 2), ('science', 'Wissenschaft', 'Bertelsmann, PISA, IGLU, TIMSS', '🔬', 3), ('universities', 'Universitäten', 'Deutsche Hochschulen', '🎓', 4), ('schools', 'Schulen', 'Schulwebsites', '🏫', 5), ('portals', 'Bildungsportale', 'Lehrer-Online, 4teachers, ZUM', '📚', 6), ('eu', 'EU/International', 'Europäische Bildungsberichte', '🇪🇺', 7), ('authorities', 'Schulbehörden', 'Regierungspräsidien, Schulämter', '📋', 8) ON CONFLICT (name) DO NOTHING`, } for _, migration := range migrations { if _, err := db.Pool.Exec(ctx, migration); err != nil { return fmt.Errorf("migrateDSR: %w", err) } } return nil }