## 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>
297 lines
9.4 KiB
PL/PgSQL
297 lines
9.4 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: education
|
|
-- TABLE: enrollments
|
|
-- DESCRIPTION: Inscripciones de usuarios a cursos
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Enum para estado de inscripcion
|
|
DO $$ BEGIN
|
|
CREATE TYPE education.enrollment_status AS ENUM (
|
|
'active', -- Inscripcion activa
|
|
'completed', -- Curso completado
|
|
'expired', -- Acceso expirado
|
|
'cancelled', -- Cancelada por usuario
|
|
'refunded', -- Reembolsada
|
|
'suspended' -- Suspendida por admin
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Enum para tipo de acceso
|
|
DO $$ BEGIN
|
|
CREATE TYPE education.enrollment_type AS ENUM (
|
|
'free', -- Acceso gratuito
|
|
'purchased', -- Comprado individualmente
|
|
'subscription', -- Via suscripcion VIP
|
|
'gift', -- Regalado
|
|
'promotional', -- Promocional/Codigo
|
|
'bundle', -- Parte de un bundle
|
|
'admin_granted' -- Otorgado por admin
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla de Inscripciones
|
|
CREATE TABLE IF NOT EXISTS education.enrollments (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id UUID NOT NULL REFERENCES users.users(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,
|
|
|
|
-- Tipo y estado
|
|
type education.enrollment_type NOT NULL DEFAULT 'free',
|
|
status education.enrollment_status NOT NULL DEFAULT 'active',
|
|
|
|
-- Progreso general
|
|
progress_percent DECIMAL(5, 2) NOT NULL DEFAULT 0 CHECK (progress_percent BETWEEN 0 AND 100),
|
|
lessons_completed INTEGER NOT NULL DEFAULT 0,
|
|
quizzes_completed INTEGER NOT NULL DEFAULT 0,
|
|
quizzes_passed INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Tiempo
|
|
total_time_spent_minutes INTEGER NOT NULL DEFAULT 0,
|
|
last_activity_at TIMESTAMPTZ,
|
|
last_lesson_id UUID,
|
|
last_position_seconds INTEGER, -- Posicion en el ultimo video
|
|
|
|
-- Pago (si aplica)
|
|
payment_id UUID,
|
|
amount_paid DECIMAL(10, 2),
|
|
currency VARCHAR(3) DEFAULT 'USD',
|
|
credits_used INTEGER DEFAULT 0,
|
|
|
|
-- Acceso
|
|
access_starts_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
access_ends_at TIMESTAMPTZ, -- NULL = acceso permanente
|
|
|
|
-- Completado
|
|
completed_at TIMESTAMPTZ,
|
|
completion_time_days INTEGER, -- Dias desde inscripcion hasta completar
|
|
|
|
-- Certificado
|
|
certificate_issued BOOLEAN NOT NULL DEFAULT FALSE,
|
|
certificate_id UUID,
|
|
certificate_issued_at TIMESTAMPTZ,
|
|
|
|
-- Calificacion final
|
|
final_score DECIMAL(5, 2),
|
|
final_grade VARCHAR(2), -- 'A', 'B', 'C', 'D', 'F'
|
|
|
|
-- Gamificacion
|
|
xp_earned INTEGER NOT NULL DEFAULT 0,
|
|
badges_earned UUID[],
|
|
|
|
-- Codigo promocional usado
|
|
promo_code VARCHAR(50),
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
notes TEXT, -- Notas del admin
|
|
|
|
-- Timestamps
|
|
enrolled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT enrollments_unique_user_course UNIQUE (user_id, course_id)
|
|
);
|
|
|
|
COMMENT ON TABLE education.enrollments IS
|
|
'Inscripciones de usuarios a cursos con tracking de progreso';
|
|
|
|
COMMENT ON COLUMN education.enrollments.access_ends_at IS
|
|
'NULL indica acceso permanente al curso';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_user
|
|
ON education.enrollments(user_id, status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_course
|
|
ON education.enrollments(course_id, status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_tenant
|
|
ON education.enrollments(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_status
|
|
ON education.enrollments(status);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_active
|
|
ON education.enrollments(user_id, course_id)
|
|
WHERE status = 'active';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_completed
|
|
ON education.enrollments(course_id, completed_at DESC)
|
|
WHERE status = 'completed';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_progress
|
|
ON education.enrollments(course_id, progress_percent DESC)
|
|
WHERE status = 'active';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_enrollments_last_activity
|
|
ON education.enrollments(last_activity_at DESC)
|
|
WHERE status = 'active';
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS enrollment_updated_at ON education.enrollments;
|
|
CREATE TRIGGER enrollment_updated_at
|
|
BEFORE UPDATE ON education.enrollments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_education_timestamp();
|
|
|
|
-- Trigger para actualizar estadisticas del curso
|
|
CREATE OR REPLACE FUNCTION education.update_course_enrollment_stats()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF TG_OP = 'INSERT' THEN
|
|
UPDATE education.courses
|
|
SET enrollment_count = enrollment_count + 1
|
|
WHERE id = NEW.course_id;
|
|
|
|
ELSIF TG_OP = 'DELETE' THEN
|
|
UPDATE education.courses
|
|
SET enrollment_count = enrollment_count - 1
|
|
WHERE id = OLD.course_id;
|
|
|
|
ELSIF TG_OP = 'UPDATE' THEN
|
|
-- Si cambio a completado
|
|
IF NEW.status = 'completed' AND OLD.status != 'completed' THEN
|
|
UPDATE education.courses
|
|
SET completion_count = completion_count + 1
|
|
WHERE id = NEW.course_id;
|
|
-- Si cambio de completado a otro estado
|
|
ELSIF OLD.status = 'completed' AND NEW.status != 'completed' THEN
|
|
UPDATE education.courses
|
|
SET completion_count = completion_count - 1
|
|
WHERE id = NEW.course_id;
|
|
END IF;
|
|
END IF;
|
|
|
|
RETURN COALESCE(NEW, OLD);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS enrollment_stats ON education.enrollments;
|
|
CREATE TRIGGER enrollment_stats
|
|
AFTER INSERT OR UPDATE OF status OR DELETE ON education.enrollments
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_course_enrollment_stats();
|
|
|
|
-- Funcion para calcular progreso
|
|
CREATE OR REPLACE FUNCTION education.calculate_enrollment_progress(p_enrollment_id UUID)
|
|
RETURNS DECIMAL(5, 2) AS $$
|
|
DECLARE
|
|
v_enrollment RECORD;
|
|
v_total_lessons INTEGER;
|
|
v_completed_lessons INTEGER;
|
|
v_progress DECIMAL(5, 2);
|
|
BEGIN
|
|
SELECT * INTO v_enrollment
|
|
FROM education.enrollments
|
|
WHERE id = p_enrollment_id;
|
|
|
|
IF v_enrollment IS NULL THEN
|
|
RETURN 0;
|
|
END IF;
|
|
|
|
-- Contar lecciones totales del curso
|
|
SELECT COUNT(*) INTO v_total_lessons
|
|
FROM education.lessons
|
|
WHERE course_id = v_enrollment.course_id
|
|
AND status = 'published'
|
|
AND is_mandatory = TRUE;
|
|
|
|
-- Contar lecciones completadas
|
|
SELECT COUNT(*) INTO v_completed_lessons
|
|
FROM education.lesson_progress lp
|
|
WHERE lp.enrollment_id = p_enrollment_id
|
|
AND lp.is_completed = TRUE;
|
|
|
|
IF v_total_lessons = 0 THEN
|
|
RETURN 0;
|
|
END IF;
|
|
|
|
v_progress := (v_completed_lessons::DECIMAL / v_total_lessons * 100);
|
|
|
|
-- Actualizar enrollment
|
|
UPDATE education.enrollments
|
|
SET progress_percent = v_progress,
|
|
lessons_completed = v_completed_lessons,
|
|
status = CASE WHEN v_progress >= 100 THEN 'completed' ELSE status END,
|
|
completed_at = CASE WHEN v_progress >= 100 AND completed_at IS NULL THEN NOW() ELSE completed_at END
|
|
WHERE id = p_enrollment_id;
|
|
|
|
RETURN v_progress;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Vista de mis cursos (para estudiante)
|
|
CREATE OR REPLACE VIEW education.v_my_courses AS
|
|
SELECT
|
|
e.id AS enrollment_id,
|
|
e.user_id,
|
|
e.course_id,
|
|
c.title,
|
|
c.slug,
|
|
c.thumbnail_url,
|
|
c.difficulty,
|
|
c.total_lessons,
|
|
e.status,
|
|
e.progress_percent,
|
|
e.lessons_completed,
|
|
e.last_activity_at,
|
|
e.enrolled_at,
|
|
e.completed_at,
|
|
e.certificate_issued
|
|
FROM education.enrollments e
|
|
JOIN education.courses c ON e.course_id = c.id
|
|
WHERE e.status NOT IN ('cancelled', 'refunded')
|
|
ORDER BY e.last_activity_at DESC NULLS LAST;
|
|
|
|
-- Vista de estudiantes de un curso (para instructor)
|
|
CREATE OR REPLACE VIEW education.v_course_students AS
|
|
SELECT
|
|
e.id AS enrollment_id,
|
|
e.user_id,
|
|
u.email,
|
|
u.first_name,
|
|
u.last_name,
|
|
e.course_id,
|
|
e.status,
|
|
e.progress_percent,
|
|
e.lessons_completed,
|
|
e.quizzes_passed,
|
|
e.final_score,
|
|
e.total_time_spent_minutes,
|
|
e.enrolled_at,
|
|
e.last_activity_at,
|
|
e.completed_at
|
|
FROM education.enrollments e
|
|
JOIN users.users u ON e.user_id = u.id
|
|
ORDER BY e.enrolled_at DESC;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE education.enrollments ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY enrollments_tenant_isolation ON education.enrollments
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
-- Los usuarios solo ven sus propias inscripciones
|
|
CREATE POLICY enrollments_user_isolation ON education.enrollments
|
|
FOR SELECT
|
|
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON education.enrollments TO trading_app;
|
|
GRANT SELECT ON education.enrollments TO trading_readonly;
|
|
GRANT SELECT ON education.v_my_courses TO trading_app;
|
|
GRANT SELECT ON education.v_course_students TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION education.calculate_enrollment_progress TO trading_app;
|