-- ============================================================================ -- Migration 008: Academy (E-Learning / Compliance Academy) Schema -- Compliance training courses, enrollments, and certificate management -- ============================================================================ -- Academy courses table CREATE TABLE IF NOT EXISTS academy_courses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE, -- Course info title VARCHAR(255) NOT NULL, description TEXT, category VARCHAR(50) NOT NULL, -- 'dsgvo_basics', 'it_security', 'ai_literacy', 'whistleblower_protection', 'custom' duration_minutes INT DEFAULT 0, required_for_roles JSONB DEFAULT '[]', -- Array of role strings -- Status is_active BOOLEAN DEFAULT TRUE, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Academy lessons table CREATE TABLE IF NOT EXISTS academy_lessons ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), course_id UUID NOT NULL REFERENCES academy_courses(id) ON DELETE CASCADE, -- Lesson info title VARCHAR(255) NOT NULL, description TEXT, lesson_type VARCHAR(50) NOT NULL, -- 'video', 'text', 'quiz', 'interactive' content_url TEXT, duration_minutes INT DEFAULT 0, order_index INT DEFAULT 0, -- Quiz questions (only for lesson_type = 'quiz') quiz_questions JSONB DEFAULT '[]', -- Array of {question, options, correct_index, explanation} -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Academy enrollments table CREATE TABLE IF NOT EXISTS academy_enrollments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL REFERENCES compliance_tenants(id) ON DELETE CASCADE, course_id UUID NOT NULL REFERENCES academy_courses(id) ON DELETE CASCADE, user_id UUID NOT NULL, -- User info (denormalized for reporting) user_name VARCHAR(255) NOT NULL, user_email VARCHAR(255) NOT NULL, -- Progress tracking status VARCHAR(50) DEFAULT 'not_started', -- 'not_started', 'in_progress', 'completed', 'expired' progress_percent INT DEFAULT 0, -- 0-100 current_lesson_index INT DEFAULT 0, -- Timestamps started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, deadline TIMESTAMPTZ, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Academy certificates table CREATE TABLE IF NOT EXISTS academy_certificates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), enrollment_id UUID NOT NULL UNIQUE REFERENCES academy_enrollments(id) ON DELETE CASCADE, -- Certificate info user_name VARCHAR(255) NOT NULL, course_title VARCHAR(255) NOT NULL, issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), valid_until TIMESTAMPTZ, pdf_url TEXT, -- Audit created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- ============================================================================ -- Indexes -- ============================================================================ -- Course indexes CREATE INDEX IF NOT EXISTS idx_academy_courses_tenant ON academy_courses(tenant_id); CREATE INDEX IF NOT EXISTS idx_academy_courses_category ON academy_courses(tenant_id, category); CREATE INDEX IF NOT EXISTS idx_academy_courses_active ON academy_courses(tenant_id, is_active); -- Lesson indexes CREATE INDEX IF NOT EXISTS idx_academy_lessons_course ON academy_lessons(course_id); CREATE INDEX IF NOT EXISTS idx_academy_lessons_order ON academy_lessons(course_id, order_index); -- Enrollment indexes CREATE INDEX IF NOT EXISTS idx_academy_enrollments_tenant ON academy_enrollments(tenant_id); CREATE INDEX IF NOT EXISTS idx_academy_enrollments_course ON academy_enrollments(course_id); CREATE INDEX IF NOT EXISTS idx_academy_enrollments_user ON academy_enrollments(user_id); CREATE INDEX IF NOT EXISTS idx_academy_enrollments_status ON academy_enrollments(tenant_id, status); CREATE INDEX IF NOT EXISTS idx_academy_enrollments_deadline ON academy_enrollments(deadline) WHERE deadline IS NOT NULL AND status NOT IN ('completed', 'expired'); CREATE INDEX IF NOT EXISTS idx_academy_enrollments_tenant_course ON academy_enrollments(tenant_id, course_id); -- Certificate indexes CREATE INDEX IF NOT EXISTS idx_academy_certificates_enrollment ON academy_certificates(enrollment_id); CREATE INDEX IF NOT EXISTS idx_academy_certificates_valid_until ON academy_certificates(valid_until) WHERE valid_until IS NOT NULL; -- ============================================================================ -- Triggers -- ============================================================================ -- Reuse existing update_updated_at_column function -- Courses trigger DROP TRIGGER IF EXISTS update_academy_courses_updated_at ON academy_courses; CREATE TRIGGER update_academy_courses_updated_at BEFORE UPDATE ON academy_courses FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Lessons trigger DROP TRIGGER IF EXISTS update_academy_lessons_updated_at ON academy_lessons; CREATE TRIGGER update_academy_lessons_updated_at BEFORE UPDATE ON academy_lessons FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Enrollments trigger DROP TRIGGER IF EXISTS update_academy_enrollments_updated_at ON academy_enrollments; CREATE TRIGGER update_academy_enrollments_updated_at BEFORE UPDATE ON academy_enrollments FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Certificates trigger DROP TRIGGER IF EXISTS update_academy_certificates_updated_at ON academy_certificates; CREATE TRIGGER update_academy_certificates_updated_at BEFORE UPDATE ON academy_certificates FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================ -- Comments -- ============================================================================ COMMENT ON TABLE academy_courses IS 'Compliance training courses (DSGVO, IT-Security, AI Literacy, Whistleblower)'; COMMENT ON TABLE academy_lessons IS 'Individual lessons within a course (video, text, quiz, interactive)'; COMMENT ON TABLE academy_enrollments IS 'User enrollments in courses with progress tracking'; COMMENT ON TABLE academy_certificates IS 'Completion certificates issued for finished enrollments'; COMMENT ON COLUMN academy_courses.category IS 'Course category: dsgvo_basics, it_security, ai_literacy, whistleblower_protection, custom'; COMMENT ON COLUMN academy_courses.required_for_roles IS 'JSON array of role names that are required to complete this course'; COMMENT ON COLUMN academy_lessons.quiz_questions IS 'JSON array of quiz questions: [{question, options, correct_index, explanation}]'; COMMENT ON COLUMN academy_enrollments.status IS 'Enrollment status: not_started, in_progress, completed, expired'; COMMENT ON COLUMN academy_enrollments.progress_percent IS 'Course completion percentage (0-100)'; COMMENT ON COLUMN academy_certificates.enrollment_id IS 'One-to-one relationship with enrollment (UNIQUE constraint)';