-- 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);