trading-platform-database-v2/ddl/schemas/education/tables/003_modules.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

135 lines
4.1 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: education
-- TABLE: modules
-- DESCRIPTION: Modulos/secciones de un curso
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Tabla de Modulos
CREATE TABLE IF NOT EXISTS education.modules (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
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,
-- Orden y estructura
sequence_number INTEGER NOT NULL DEFAULT 0,
-- Estado
status education.publish_status NOT NULL DEFAULT 'draft',
is_preview BOOLEAN NOT NULL DEFAULT FALSE, -- Disponible como preview gratuito
is_locked BOOLEAN NOT NULL DEFAULT FALSE, -- Bloqueado hasta cumplir requisitos
-- Requisitos para desbloquear
unlock_requirements JSONB DEFAULT '{}'::JSONB, -- { "modules": [], "min_score": 70 }
-- Duracion
estimated_duration_minutes INTEGER,
-- Estadisticas (cache)
lesson_count INTEGER NOT NULL DEFAULT 0,
quiz_count INTEGER NOT NULL DEFAULT 0,
-- Metadata
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Constraints
CONSTRAINT modules_unique_sequence UNIQUE (course_id, sequence_number),
CONSTRAINT modules_unique_slug UNIQUE (course_id, slug)
);
COMMENT ON TABLE education.modules IS
'Modulos o secciones que organizan las lecciones de un curso';
COMMENT ON COLUMN education.modules.is_preview IS
'Si TRUE, el modulo esta disponible como preview gratuito';
-- Indices
CREATE INDEX IF NOT EXISTS idx_modules_course
ON education.modules(course_id, sequence_number);
CREATE INDEX IF NOT EXISTS idx_modules_tenant
ON education.modules(tenant_id);
CREATE INDEX IF NOT EXISTS idx_modules_status
ON education.modules(status);
CREATE INDEX IF NOT EXISTS idx_modules_preview
ON education.modules(course_id, is_preview)
WHERE is_preview = TRUE;
-- Trigger para updated_at
DROP TRIGGER IF EXISTS module_updated_at ON education.modules;
CREATE TRIGGER module_updated_at
BEFORE UPDATE ON education.modules
FOR EACH ROW
EXECUTE FUNCTION education.update_education_timestamp();
-- Trigger para actualizar conteo en curso
CREATE OR REPLACE FUNCTION education.update_course_module_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE education.courses
SET total_modules = total_modules + 1,
last_content_update = NOW()
WHERE id = NEW.course_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE education.courses
SET total_modules = total_modules - 1,
last_content_update = NOW()
WHERE id = OLD.course_id;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS module_course_count ON education.modules;
CREATE TRIGGER module_course_count
AFTER INSERT OR DELETE ON education.modules
FOR EACH ROW
EXECUTE FUNCTION education.update_course_module_count();
-- Vista de modulos con lecciones
CREATE OR REPLACE VIEW education.v_course_modules AS
SELECT
m.id,
m.course_id,
m.title,
m.slug,
m.description,
m.sequence_number,
m.status,
m.is_preview,
m.is_locked,
m.estimated_duration_minutes,
m.lesson_count,
m.quiz_count
FROM education.modules m
WHERE m.status = 'published'
ORDER BY m.course_id, m.sequence_number;
-- RLS Policy para multi-tenancy
ALTER TABLE education.modules ENABLE ROW LEVEL SECURITY;
CREATE POLICY modules_tenant_isolation ON education.modules
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE, DELETE ON education.modules TO trading_app;
GRANT SELECT ON education.modules TO trading_readonly;
GRANT SELECT ON education.v_course_modules TO trading_app;