veza/veza-backend-api/migrations/951_education_courses_v0123.sql
senke 329f53ada3 feat(v0.12.3): database migrations for education courses
Tables: courses, lessons, course_enrollments, lesson_progress,
certificates, course_reviews with proper indexes and constraints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 09:44:54 +01:00

135 lines
5.7 KiB
SQL

-- v0.12.3: Formation & Éducation (F276-F305)
-- Courses, lessons, enrollments, progress, certificates, reviews
-- Courses catalog
CREATE TABLE IF NOT EXISTS courses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
description TEXT NOT NULL DEFAULT '',
category VARCHAR(100),
tags VARCHAR(50)[] DEFAULT '{}',
cover_image_url TEXT,
price_cents INTEGER NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
pricing_model VARCHAR(50) NOT NULL DEFAULT 'fixed',
minimum_price_cents INTEGER DEFAULT 0,
status VARCHAR(50) NOT NULL DEFAULT 'draft',
level VARCHAR(50) DEFAULT 'beginner',
language VARCHAR(5) DEFAULT 'en',
total_duration_seconds INTEGER NOT NULL DEFAULT 0,
lesson_count INTEGER NOT NULL DEFAULT 0,
enrollment_count INTEGER NOT NULL DEFAULT 0,
review_count INTEGER NOT NULL DEFAULT 0,
average_rating NUMERIC(3,2) DEFAULT 0,
published_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_courses_creator ON courses(creator_id);
CREATE INDEX IF NOT EXISTS idx_courses_status ON courses(status, published_at DESC);
CREATE INDEX IF NOT EXISTS idx_courses_category ON courses(category);
CREATE INDEX IF NOT EXISTS idx_courses_slug ON courses(slug);
-- Course lessons
CREATE TABLE IF NOT EXISTS lessons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
order_index INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
description TEXT,
video_file_path VARCHAR(512),
duration_seconds INTEGER NOT NULL DEFAULT 0,
is_preview_free BOOLEAN NOT NULL DEFAULT false,
transcoding_status VARCHAR(50) NOT NULL DEFAULT 'pending',
hls_master_playlist_url TEXT,
thumbnail_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_lesson_order UNIQUE (course_id, order_index),
CONSTRAINT chk_order_positive CHECK (order_index > 0)
);
CREATE INDEX IF NOT EXISTS idx_lessons_course_order ON lessons(course_id, order_index);
CREATE INDEX IF NOT EXISTS idx_lessons_transcoding ON lessons(transcoding_status);
-- Course enrollments (purchases)
CREATE TABLE IF NOT EXISTS course_enrollments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
purchased_price_cents INTEGER NOT NULL DEFAULT 0,
currency VARCHAR(3) NOT NULL DEFAULT 'USD',
status VARCHAR(50) NOT NULL DEFAULT 'active',
access_expires_at TIMESTAMPTZ,
purchased_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
refunded_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_enrollment_user_course UNIQUE (user_id, course_id)
);
CREATE INDEX IF NOT EXISTS idx_enrollments_user ON course_enrollments(user_id, purchased_at DESC);
CREATE INDEX IF NOT EXISTS idx_enrollments_course ON course_enrollments(course_id, status);
-- Lesson progress per user
CREATE TABLE IF NOT EXISTS lesson_progress (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
lesson_id UUID NOT NULL REFERENCES lessons(id) ON DELETE CASCADE,
enrollment_id UUID NOT NULL REFERENCES course_enrollments(id) ON DELETE CASCADE,
watched_percentage INTEGER DEFAULT 0,
watched_duration_seconds INTEGER DEFAULT 0,
playback_position_seconds INTEGER DEFAULT 0,
is_completed BOOLEAN NOT NULL DEFAULT false,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_progress_user_lesson UNIQUE (user_id, lesson_id),
CONSTRAINT chk_percentage CHECK (watched_percentage >= 0 AND watched_percentage <= 100)
);
CREATE INDEX IF NOT EXISTS idx_progress_enrollment ON lesson_progress(enrollment_id);
-- Completion certificates (declarative, not gamified)
CREATE TABLE IF NOT EXISTS certificates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
enrollment_id UUID NOT NULL REFERENCES course_enrollments(id) ON DELETE CASCADE,
certificate_code VARCHAR(255) NOT NULL UNIQUE,
issue_date TIMESTAMPTZ NOT NULL DEFAULT NOW(),
status VARCHAR(50) NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uq_cert_user_course UNIQUE (user_id, course_id)
);
CREATE INDEX IF NOT EXISTS idx_certificates_code ON certificates(certificate_code);
-- Course reviews
CREATE TABLE IF NOT EXISTS course_reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
course_id UUID NOT NULL REFERENCES courses(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
enrollment_id UUID NOT NULL REFERENCES course_enrollments(id),
rating INTEGER NOT NULL,
title VARCHAR(255),
content TEXT NOT NULL,
status VARCHAR(50) NOT NULL DEFAULT 'approved',
helpful_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_review_user_course UNIQUE (user_id, course_id),
CONSTRAINT chk_rating CHECK (rating >= 1 AND rating <= 5)
);
CREATE INDEX IF NOT EXISTS idx_reviews_course ON course_reviews(course_id, rating DESC);