## 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>
270 lines
8.5 KiB
PL/PgSQL
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;
|