-- ============================================================================ -- 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;