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

270 lines
8.5 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: education
-- TABLE: courses
-- DESCRIPTION: Cursos de trading y educacion financiera
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Enum para tipo de curso
DO $$ BEGIN
CREATE TYPE education.course_type AS ENUM (
'course', -- Curso completo
'workshop', -- Taller practico
'masterclass', -- Masterclass
'webinar', -- Webinar grabado
'bootcamp', -- Bootcamp intensivo
'certification' -- Programa de certificacion
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para modelo de acceso
DO $$ BEGIN
CREATE TYPE education.access_model AS ENUM (
'free', -- Gratis
'freemium', -- Gratis con contenido premium
'paid', -- De pago
'subscription', -- Requiere suscripcion VIP
'bundle' -- Parte de un bundle
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla de Cursos
CREATE TABLE IF NOT EXISTS education.courses (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
category_id UUID REFERENCES education.categories(id) ON DELETE SET NULL,
-- Informacion basica
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) NOT NULL,
subtitle VARCHAR(255),
description TEXT,
short_description VARCHAR(500),
-- Clasificacion
type education.course_type NOT NULL DEFAULT 'course',
difficulty education.difficulty_level NOT NULL DEFAULT 'beginner',
-- Instructor/Autor
instructor_id UUID REFERENCES users.users(id),
instructor_name VARCHAR(100),
instructor_bio TEXT,
instructor_avatar_url TEXT,
-- Media
thumbnail_url TEXT,
banner_url TEXT,
preview_video_url TEXT,
trailer_url TEXT,
-- Contenido
objectives JSONB DEFAULT '[]'::JSONB, -- Array de objetivos de aprendizaje
requirements JSONB DEFAULT '[]'::JSONB, -- Prerrequisitos
target_audience JSONB DEFAULT '[]'::JSONB, -- Publico objetivo
syllabus JSONB DEFAULT '[]'::JSONB, -- Temario resumido
-- Duracion y estructura
estimated_duration_minutes INTEGER, -- Duracion total estimada
total_lessons INTEGER NOT NULL DEFAULT 0,
total_quizzes INTEGER NOT NULL DEFAULT 0,
total_modules INTEGER NOT NULL DEFAULT 0,
-- Acceso y precio
access_model education.access_model NOT NULL DEFAULT 'free',
price DECIMAL(10, 2) DEFAULT 0,
currency VARCHAR(3) DEFAULT 'USD',
discount_price DECIMAL(10, 2),
discount_ends_at TIMESTAMPTZ,
-- Requisitos de suscripcion
required_vip_tier VARCHAR(50), -- NULL = no requiere VIP
credits_cost INTEGER DEFAULT 0, -- Costo en creditos del wallet
-- Estado
status education.publish_status NOT NULL DEFAULT 'draft',
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
is_new BOOLEAN NOT NULL DEFAULT TRUE,
is_bestseller BOOLEAN NOT NULL DEFAULT FALSE,
-- SEO
meta_title VARCHAR(100),
meta_description VARCHAR(255),
meta_keywords VARCHAR(255)[],
-- Estadisticas (cache)
enrollment_count INTEGER NOT NULL DEFAULT 0,
completion_count INTEGER NOT NULL DEFAULT 0,
average_rating DECIMAL(3, 2) DEFAULT 0,
review_count INTEGER NOT NULL DEFAULT 0,
view_count INTEGER NOT NULL DEFAULT 0,
-- Certificacion
has_certificate BOOLEAN NOT NULL DEFAULT FALSE,
certificate_template_id UUID,
passing_score INTEGER DEFAULT 70, -- % minimo para certificado
-- Gamificacion
xp_reward INTEGER DEFAULT 0, -- XP al completar
badge_id UUID, -- Badge al completar
-- Configuracion
allow_reviews BOOLEAN NOT NULL DEFAULT TRUE,
allow_discussions BOOLEAN NOT NULL DEFAULT TRUE,
enforce_sequence BOOLEAN NOT NULL DEFAULT FALSE, -- Forzar orden de lecciones
-- Metadata
tags VARCHAR(50)[],
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at TIMESTAMPTZ,
last_content_update TIMESTAMPTZ,
-- Constraints
CONSTRAINT courses_unique_slug_per_tenant UNIQUE (tenant_id, slug),
CONSTRAINT courses_price_check CHECK (price >= 0),
CONSTRAINT courses_rating_check CHECK (average_rating >= 0 AND average_rating <= 5)
);
COMMENT ON TABLE education.courses IS
'Cursos de trading y educacion financiera';
COMMENT ON COLUMN education.courses.enforce_sequence IS
'Si TRUE, el usuario debe completar lecciones en orden';
-- Indices
CREATE INDEX IF NOT EXISTS idx_courses_tenant
ON education.courses(tenant_id);
CREATE INDEX IF NOT EXISTS idx_courses_category
ON education.courses(category_id);
CREATE INDEX IF NOT EXISTS idx_courses_instructor
ON education.courses(instructor_id);
CREATE INDEX IF NOT EXISTS idx_courses_slug
ON education.courses(tenant_id, slug);
CREATE INDEX IF NOT EXISTS idx_courses_status
ON education.courses(status)
WHERE status = 'published';
CREATE INDEX IF NOT EXISTS idx_courses_featured
ON education.courses(tenant_id, is_featured)
WHERE is_featured = TRUE AND status = 'published';
CREATE INDEX IF NOT EXISTS idx_courses_difficulty
ON education.courses(difficulty);
CREATE INDEX IF NOT EXISTS idx_courses_access
ON education.courses(access_model);
CREATE INDEX IF NOT EXISTS idx_courses_rating
ON education.courses(average_rating DESC)
WHERE status = 'published';
CREATE INDEX IF NOT EXISTS idx_courses_popular
ON education.courses(enrollment_count DESC)
WHERE status = 'published';
-- GIN index para tags
CREATE INDEX IF NOT EXISTS idx_courses_tags_gin
ON education.courses USING GIN (tags);
-- Trigger para updated_at
DROP TRIGGER IF EXISTS course_updated_at ON education.courses;
CREATE TRIGGER course_updated_at
BEFORE UPDATE ON education.courses
FOR EACH ROW
EXECUTE FUNCTION education.update_education_timestamp();
-- Trigger para actualizar conteo en categoria
CREATE OR REPLACE FUNCTION education.update_category_course_count()
RETURNS TRIGGER AS $$
BEGIN
-- Decrementar categoria anterior
IF OLD IS NOT NULL AND OLD.category_id IS NOT NULL THEN
UPDATE education.categories
SET course_count = course_count - 1
WHERE id = OLD.category_id;
END IF;
-- Incrementar categoria nueva
IF NEW IS NOT NULL AND NEW.category_id IS NOT NULL AND NEW.status = 'published' THEN
UPDATE education.categories
SET course_count = course_count + 1
WHERE id = NEW.category_id;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS course_category_count ON education.courses;
CREATE TRIGGER course_category_count
AFTER INSERT OR UPDATE OF category_id, status OR DELETE ON education.courses
FOR EACH ROW
EXECUTE FUNCTION education.update_category_course_count();
-- Vista de cursos publicados
CREATE OR REPLACE VIEW education.v_published_courses AS
SELECT
c.id,
c.tenant_id,
c.category_id,
cat.name AS category_name,
c.title,
c.slug,
c.subtitle,
c.short_description,
c.type,
c.difficulty,
c.instructor_name,
c.thumbnail_url,
c.estimated_duration_minutes,
c.total_lessons,
c.access_model,
c.price,
c.discount_price,
c.is_featured,
c.is_new,
c.is_bestseller,
c.enrollment_count,
c.average_rating,
c.review_count,
c.has_certificate,
c.tags
FROM education.courses c
LEFT JOIN education.categories cat ON c.category_id = cat.id
WHERE c.status = 'published'
ORDER BY c.is_featured DESC, c.enrollment_count DESC;
-- Vista de cursos populares
CREATE OR REPLACE VIEW education.v_popular_courses AS
SELECT *
FROM education.v_published_courses
ORDER BY enrollment_count DESC
LIMIT 20;
-- RLS Policy para multi-tenancy
ALTER TABLE education.courses ENABLE ROW LEVEL SECURITY;
CREATE POLICY courses_tenant_isolation ON education.courses
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE, DELETE ON education.courses TO trading_app;
GRANT SELECT ON education.courses TO trading_readonly;
GRANT SELECT ON education.v_published_courses TO trading_app;
GRANT SELECT ON education.v_popular_courses TO trading_app;