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

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;