trading-platform-database-v2/ddl/schemas/education/tables/004_lessons.sql
rckrdmrd cd6590ec25 [DDL] feat: Sprint 2 - Add education schema with 11 tables
## Schema: education (Complete Learning Management System)

### Course Structure (3 tables):
- categories: Hierarchical course categories with materialized paths
- courses: Full course catalog with pricing, access models, gamification
- modules: Course sections/modules with sequencing

### Content (3 tables):
- lessons: Individual lessons (video, article, interactive)
- quizzes: Assessments with configurable rules
- quiz_questions: Question bank with multiple types

### Progress Tracking (3 tables):
- enrollments: User enrollments with progress tracking
- lesson_progress: Detailed per-lesson progress
- quiz_attempts: Quiz attempt history with grading

### Completion (2 tables):
- course_reviews: Student reviews with moderation
- certificates: Verifiable completion certificates

## Features:
- 8 custom ENUMs for education domain
- Multi-tenancy with RLS policies
- Automatic progress calculation triggers
- Quiz grading and statistics
- Certificate generation with verification codes
- Rating aggregation for courses
- Gamification support (XP, badges)

Total: 11 tables, ~95KB of DDL

Roadmap: orchestration/planes/ROADMAP-IMPLEMENTACION-DDL-2026-Q1.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:48:39 -06:00

186 lines
5.7 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: education
-- TABLE: lessons
-- DESCRIPTION: Lecciones individuales de un modulo
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Tabla de Lecciones
CREATE TABLE IF NOT EXISTS education.lessons (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
module_id UUID NOT NULL REFERENCES education.modules(id) ON DELETE CASCADE,
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Informacion basica
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL,
description TEXT,
-- Tipo de contenido
content_type education.content_type NOT NULL DEFAULT 'video',
-- Orden
sequence_number INTEGER NOT NULL DEFAULT 0,
-- Contenido principal
content TEXT, -- Contenido HTML/Markdown
content_json JSONB, -- Contenido estructurado
-- Video
video_url TEXT,
video_provider VARCHAR(50), -- 'youtube', 'vimeo', 'bunny', 'self'
video_id VARCHAR(100),
video_duration_seconds INTEGER,
video_thumbnail_url TEXT,
-- Recursos
resources JSONB DEFAULT '[]'::JSONB, -- Array de archivos descargables
-- [{ "name": "PDF", "url": "...", "type": "pdf", "size": 1024 }]
-- Interactividad
has_quiz BOOLEAN NOT NULL DEFAULT FALSE,
quiz_id UUID, -- Quiz asociado a esta leccion
-- Estado
status education.publish_status NOT NULL DEFAULT 'draft',
is_preview BOOLEAN NOT NULL DEFAULT FALSE,
is_mandatory BOOLEAN NOT NULL DEFAULT TRUE, -- Obligatoria para completar curso
-- Duracion
estimated_duration_minutes INTEGER,
-- Requisitos
requires_previous_completion BOOLEAN NOT NULL DEFAULT FALSE,
-- Gamificacion
xp_reward INTEGER DEFAULT 10,
-- AI Generated content
is_ai_generated BOOLEAN NOT NULL DEFAULT FALSE,
ai_generation_prompt TEXT,
-- Metadata
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT lessons_unique_sequence UNIQUE (module_id, sequence_number),
CONSTRAINT lessons_unique_slug UNIQUE (module_id, slug)
);
COMMENT ON TABLE education.lessons IS
'Lecciones individuales que componen un modulo del curso';
COMMENT ON COLUMN education.lessons.content_type IS
'Tipo de contenido: video, article, quiz, interactive, download, live, assignment';
-- Indices
CREATE INDEX IF NOT EXISTS idx_lessons_module
ON education.lessons(module_id, sequence_number);
CREATE INDEX IF NOT EXISTS idx_lessons_course
ON education.lessons(course_id);
CREATE INDEX IF NOT EXISTS idx_lessons_tenant
ON education.lessons(tenant_id);
CREATE INDEX IF NOT EXISTS idx_lessons_status
ON education.lessons(status);
CREATE INDEX IF NOT EXISTS idx_lessons_content_type
ON education.lessons(content_type);
CREATE INDEX IF NOT EXISTS idx_lessons_preview
ON education.lessons(course_id, is_preview)
WHERE is_preview = TRUE;
CREATE INDEX IF NOT EXISTS idx_lessons_with_quiz
ON education.lessons(quiz_id)
WHERE quiz_id IS NOT NULL;
-- Trigger para updated_at
DROP TRIGGER IF EXISTS lesson_updated_at ON education.lessons;
CREATE TRIGGER lesson_updated_at
BEFORE UPDATE ON education.lessons
FOR EACH ROW
EXECUTE FUNCTION education.update_education_timestamp();
-- Trigger para actualizar conteos
CREATE OR REPLACE FUNCTION education.update_lesson_counts()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
-- Actualizar modulo
UPDATE education.modules
SET lesson_count = lesson_count + 1
WHERE id = NEW.module_id;
-- Actualizar curso
UPDATE education.courses
SET total_lessons = total_lessons + 1,
last_content_update = NOW()
WHERE id = NEW.course_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE education.modules
SET lesson_count = lesson_count - 1
WHERE id = OLD.module_id;
UPDATE education.courses
SET total_lessons = total_lessons - 1,
last_content_update = NOW()
WHERE id = OLD.course_id;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS lesson_counts ON education.lessons;
CREATE TRIGGER lesson_counts
AFTER INSERT OR DELETE ON education.lessons
FOR EACH ROW
EXECUTE FUNCTION education.update_lesson_counts();
-- Vista de lecciones de un curso
CREATE OR REPLACE VIEW education.v_course_lessons AS
SELECT
l.id,
l.module_id,
l.course_id,
m.title AS module_title,
m.sequence_number AS module_sequence,
l.title,
l.slug,
l.content_type,
l.sequence_number,
l.video_duration_seconds,
l.estimated_duration_minutes,
l.is_preview,
l.is_mandatory,
l.has_quiz,
l.status
FROM education.lessons l
JOIN education.modules m ON l.module_id = m.id
WHERE l.status = 'published'
ORDER BY m.sequence_number, l.sequence_number;
-- RLS Policy para multi-tenancy
ALTER TABLE education.lessons ENABLE ROW LEVEL SECURITY;
CREATE POLICY lessons_tenant_isolation ON education.lessons
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE, DELETE ON education.lessons TO trading_app;
GRANT SELECT ON education.lessons TO trading_readonly;
GRANT SELECT ON education.v_course_lessons TO trading_app;