## 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>
380 lines
12 KiB
PL/PgSQL
380 lines
12 KiB
PL/PgSQL
-- ============================================================================
|
|
-- SCHEMA: education
|
|
-- TABLE: certificates
|
|
-- DESCRIPTION: Certificados de finalizacion de cursos
|
|
-- VERSION: 1.0.0
|
|
-- CREATED: 2026-01-16
|
|
-- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026
|
|
-- ============================================================================
|
|
|
|
-- Enum para tipo de certificado
|
|
DO $$ BEGIN
|
|
CREATE TYPE education.certificate_type AS ENUM (
|
|
'completion', -- Certificado de finalizacion
|
|
'achievement', -- Certificado de logro especifico
|
|
'participation', -- Certificado de participacion
|
|
'excellence', -- Certificado de excelencia (alta puntuacion)
|
|
'professional' -- Certificacion profesional
|
|
);
|
|
EXCEPTION
|
|
WHEN duplicate_object THEN null;
|
|
END $$;
|
|
|
|
-- Tabla de Certificados
|
|
CREATE TABLE IF NOT EXISTS education.certificates (
|
|
-- Identificadores
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
certificate_number VARCHAR(50) NOT NULL UNIQUE, -- Numero unico verificable
|
|
enrollment_id UUID NOT NULL REFERENCES education.enrollments(id) ON DELETE CASCADE,
|
|
course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Tipo
|
|
type education.certificate_type NOT NULL DEFAULT 'completion',
|
|
|
|
-- Datos del certificado
|
|
title VARCHAR(200) NOT NULL,
|
|
description TEXT,
|
|
recipient_name VARCHAR(200) NOT NULL, -- Nombre como aparece en certificado
|
|
|
|
-- Logro
|
|
final_score DECIMAL(5, 2),
|
|
grade VARCHAR(2),
|
|
completion_date DATE NOT NULL DEFAULT CURRENT_DATE,
|
|
hours_completed INTEGER,
|
|
|
|
-- Template y diseño
|
|
template_id UUID,
|
|
template_data JSONB DEFAULT '{}'::JSONB, -- Datos para el template
|
|
background_url TEXT,
|
|
|
|
-- Archivos generados
|
|
pdf_url TEXT,
|
|
image_url TEXT,
|
|
linkedin_url TEXT, -- URL para compartir en LinkedIn
|
|
|
|
-- Verificacion
|
|
verification_code VARCHAR(20) NOT NULL, -- Codigo corto para verificar
|
|
verification_url TEXT,
|
|
qr_code_url TEXT,
|
|
|
|
-- Blockchain (opcional)
|
|
blockchain_hash VARCHAR(66), -- Hash de verificacion en blockchain
|
|
blockchain_network VARCHAR(50),
|
|
blockchain_tx_id VARCHAR(66),
|
|
|
|
-- Firmantes
|
|
signed_by JSONB DEFAULT '[]'::JSONB, -- [{ "name": "...", "title": "...", "signature_url": "..." }]
|
|
|
|
-- Expiracion (para certificaciones profesionales)
|
|
expires_at TIMESTAMPTZ,
|
|
is_expired BOOLEAN NOT NULL DEFAULT FALSE,
|
|
renewal_available BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
-- Estado
|
|
is_valid BOOLEAN NOT NULL DEFAULT TRUE,
|
|
revoked_at TIMESTAMPTZ,
|
|
revocation_reason TEXT,
|
|
|
|
-- Compartido
|
|
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
|
public_url TEXT,
|
|
share_count INTEGER NOT NULL DEFAULT 0,
|
|
view_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
-- Metadata
|
|
metadata JSONB DEFAULT '{}'::JSONB,
|
|
|
|
-- Timestamps
|
|
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
-- Constraints
|
|
CONSTRAINT certificates_unique_enrollment UNIQUE (enrollment_id, type)
|
|
);
|
|
|
|
COMMENT ON TABLE education.certificates IS
|
|
'Certificados de finalizacion emitidos a estudiantes';
|
|
|
|
COMMENT ON COLUMN education.certificates.certificate_number IS
|
|
'Numero unico para verificacion publica del certificado';
|
|
|
|
COMMENT ON COLUMN education.certificates.verification_code IS
|
|
'Codigo corto (6-8 caracteres) para verificacion rapida';
|
|
|
|
-- Indices
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_enrollment
|
|
ON education.certificates(enrollment_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_course
|
|
ON education.certificates(course_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_user
|
|
ON education.certificates(user_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_tenant
|
|
ON education.certificates(tenant_id);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_number
|
|
ON education.certificates(certificate_number);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_verification
|
|
ON education.certificates(verification_code);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_type
|
|
ON education.certificates(type);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_valid
|
|
ON education.certificates(is_valid)
|
|
WHERE is_valid = TRUE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_public
|
|
ON education.certificates(is_public, issued_at DESC)
|
|
WHERE is_public = TRUE;
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_certificates_expires
|
|
ON education.certificates(expires_at)
|
|
WHERE expires_at IS NOT NULL AND is_valid = TRUE;
|
|
|
|
-- Trigger para updated_at
|
|
DROP TRIGGER IF EXISTS certificate_updated_at ON education.certificates;
|
|
CREATE TRIGGER certificate_updated_at
|
|
BEFORE UPDATE ON education.certificates
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.update_education_timestamp();
|
|
|
|
-- Funcion para generar numero de certificado
|
|
CREATE OR REPLACE FUNCTION education.generate_certificate_number()
|
|
RETURNS TEXT AS $$
|
|
DECLARE
|
|
v_year TEXT := TO_CHAR(NOW(), 'YYYY');
|
|
v_seq TEXT;
|
|
BEGIN
|
|
-- Formato: CERT-YYYY-XXXXXX (ej: CERT-2026-000123)
|
|
SELECT LPAD(COALESCE(MAX(
|
|
NULLIF(SUBSTRING(certificate_number FROM 11), '')::INTEGER
|
|
), 0) + 1, 6, '0')
|
|
INTO v_seq
|
|
FROM education.certificates
|
|
WHERE certificate_number LIKE 'CERT-' || v_year || '-%';
|
|
|
|
RETURN 'CERT-' || v_year || '-' || v_seq;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para generar codigo de verificacion
|
|
CREATE OR REPLACE FUNCTION education.generate_verification_code()
|
|
RETURNS TEXT AS $$
|
|
DECLARE
|
|
v_chars TEXT := 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; -- Sin I, O, 0, 1 para evitar confusion
|
|
v_code TEXT := '';
|
|
i INTEGER;
|
|
BEGIN
|
|
FOR i IN 1..8 LOOP
|
|
v_code := v_code || SUBSTRING(v_chars FROM FLOOR(RANDOM() * LENGTH(v_chars) + 1)::INTEGER FOR 1);
|
|
END LOOP;
|
|
RETURN v_code;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Trigger para generar numeros automaticamente
|
|
CREATE OR REPLACE FUNCTION education.set_certificate_numbers()
|
|
RETURNS TRIGGER AS $$
|
|
BEGIN
|
|
IF NEW.certificate_number IS NULL THEN
|
|
NEW.certificate_number := education.generate_certificate_number();
|
|
END IF;
|
|
|
|
IF NEW.verification_code IS NULL THEN
|
|
NEW.verification_code := education.generate_verification_code();
|
|
END IF;
|
|
|
|
RETURN NEW;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
DROP TRIGGER IF EXISTS certificate_numbers ON education.certificates;
|
|
CREATE TRIGGER certificate_numbers
|
|
BEFORE INSERT ON education.certificates
|
|
FOR EACH ROW
|
|
EXECUTE FUNCTION education.set_certificate_numbers();
|
|
|
|
-- Funcion para emitir certificado
|
|
CREATE OR REPLACE FUNCTION education.issue_certificate(
|
|
p_enrollment_id UUID,
|
|
p_type education.certificate_type DEFAULT 'completion'
|
|
)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_enrollment RECORD;
|
|
v_course RECORD;
|
|
v_user RECORD;
|
|
v_cert_id UUID;
|
|
BEGIN
|
|
-- Obtener enrollment
|
|
SELECT * INTO v_enrollment
|
|
FROM education.enrollments
|
|
WHERE id = p_enrollment_id;
|
|
|
|
IF v_enrollment IS NULL THEN
|
|
RAISE EXCEPTION 'Enrollment not found';
|
|
END IF;
|
|
|
|
-- Verificar que el curso este completado
|
|
IF v_enrollment.status != 'completed' AND p_type = 'completion' THEN
|
|
RAISE EXCEPTION 'Course not completed';
|
|
END IF;
|
|
|
|
-- Obtener curso
|
|
SELECT * INTO v_course
|
|
FROM education.courses
|
|
WHERE id = v_enrollment.course_id;
|
|
|
|
-- Obtener usuario
|
|
SELECT * INTO v_user
|
|
FROM users.users
|
|
WHERE id = v_enrollment.user_id;
|
|
|
|
-- Verificar si ya existe
|
|
IF EXISTS (
|
|
SELECT 1 FROM education.certificates
|
|
WHERE enrollment_id = p_enrollment_id AND type = p_type
|
|
) THEN
|
|
RAISE EXCEPTION 'Certificate already issued';
|
|
END IF;
|
|
|
|
-- Crear certificado
|
|
INSERT INTO education.certificates (
|
|
enrollment_id, course_id, user_id, tenant_id,
|
|
type, title, recipient_name,
|
|
final_score, grade, hours_completed
|
|
) VALUES (
|
|
p_enrollment_id, v_enrollment.course_id, v_enrollment.user_id, v_enrollment.tenant_id,
|
|
p_type,
|
|
v_course.title || ' - Certificate of ' ||
|
|
CASE p_type
|
|
WHEN 'completion' THEN 'Completion'
|
|
WHEN 'excellence' THEN 'Excellence'
|
|
WHEN 'participation' THEN 'Participation'
|
|
ELSE 'Achievement'
|
|
END,
|
|
COALESCE(v_user.first_name || ' ' || v_user.last_name, v_user.email),
|
|
v_enrollment.final_score,
|
|
v_enrollment.final_grade,
|
|
v_course.estimated_duration_minutes / 60
|
|
)
|
|
RETURNING id INTO v_cert_id;
|
|
|
|
-- Actualizar enrollment
|
|
UPDATE education.enrollments
|
|
SET certificate_issued = TRUE,
|
|
certificate_id = v_cert_id,
|
|
certificate_issued_at = NOW()
|
|
WHERE id = p_enrollment_id;
|
|
|
|
RETURN v_cert_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Funcion para verificar certificado
|
|
CREATE OR REPLACE FUNCTION education.verify_certificate(
|
|
p_code VARCHAR(50)
|
|
)
|
|
RETURNS TABLE (
|
|
is_valid BOOLEAN,
|
|
certificate_number VARCHAR(50),
|
|
recipient_name VARCHAR(200),
|
|
course_title VARCHAR(200),
|
|
completion_date DATE,
|
|
grade VARCHAR(2),
|
|
issued_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ
|
|
) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
SELECT
|
|
c.is_valid AND (c.expires_at IS NULL OR c.expires_at > NOW()),
|
|
c.certificate_number,
|
|
c.recipient_name,
|
|
co.title,
|
|
c.completion_date,
|
|
c.grade,
|
|
c.issued_at,
|
|
c.expires_at
|
|
FROM education.certificates c
|
|
JOIN education.courses co ON c.course_id = co.id
|
|
WHERE c.certificate_number = p_code
|
|
OR c.verification_code = p_code;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Vista de mis certificados
|
|
CREATE OR REPLACE VIEW education.v_my_certificates AS
|
|
SELECT
|
|
c.id,
|
|
c.certificate_number,
|
|
c.course_id,
|
|
co.title AS course_title,
|
|
co.thumbnail_url AS course_thumbnail,
|
|
c.type,
|
|
c.title AS certificate_title,
|
|
c.recipient_name,
|
|
c.final_score,
|
|
c.grade,
|
|
c.completion_date,
|
|
c.pdf_url,
|
|
c.image_url,
|
|
c.verification_code,
|
|
c.is_public,
|
|
c.public_url,
|
|
c.expires_at,
|
|
c.is_valid,
|
|
c.issued_at
|
|
FROM education.certificates c
|
|
JOIN education.courses co ON c.course_id = co.id
|
|
WHERE c.is_valid = TRUE
|
|
ORDER BY c.issued_at DESC;
|
|
|
|
-- Vista de certificados publicos de un curso
|
|
CREATE OR REPLACE VIEW education.v_course_certificates AS
|
|
SELECT
|
|
c.id,
|
|
c.course_id,
|
|
c.recipient_name,
|
|
c.type,
|
|
c.grade,
|
|
c.completion_date,
|
|
c.issued_at
|
|
FROM education.certificates c
|
|
WHERE c.is_public = TRUE
|
|
AND c.is_valid = TRUE
|
|
ORDER BY c.issued_at DESC;
|
|
|
|
-- RLS Policy para multi-tenancy
|
|
ALTER TABLE education.certificates ENABLE ROW LEVEL SECURITY;
|
|
|
|
CREATE POLICY certificates_tenant_isolation ON education.certificates
|
|
FOR ALL
|
|
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
|
|
|
CREATE POLICY certificates_user_isolation ON education.certificates
|
|
FOR SELECT
|
|
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
|
|
|
-- Policy especial para verificacion publica
|
|
CREATE POLICY certificates_public_verify ON education.certificates
|
|
FOR SELECT
|
|
USING (is_public = TRUE OR is_valid = TRUE);
|
|
|
|
-- Grants
|
|
GRANT SELECT, INSERT, UPDATE ON education.certificates TO trading_app;
|
|
GRANT SELECT ON education.certificates TO trading_readonly;
|
|
GRANT SELECT ON education.v_my_certificates TO trading_app;
|
|
GRANT SELECT ON education.v_course_certificates TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION education.issue_certificate TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION education.verify_certificate TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION education.generate_certificate_number TO trading_app;
|
|
GRANT EXECUTE ON FUNCTION education.generate_verification_code TO trading_app;
|