-- ============================================================================ -- SCHEMA: education -- TABLE: modules -- DESCRIPTION: Modulos/secciones de un curso -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 2 - DDL Implementation Roadmap Q1-2026 -- ============================================================================ -- Tabla de Modulos CREATE TABLE IF NOT EXISTS education.modules ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), course_id UUID NOT NULL REFERENCES education.courses(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, -- Informacion basica title VARCHAR(200) NOT NULL, slug VARCHAR(200) NOT NULL, description TEXT, -- Orden y estructura sequence_number INTEGER NOT NULL DEFAULT 0, -- Estado status education.publish_status NOT NULL DEFAULT 'draft', is_preview BOOLEAN NOT NULL DEFAULT FALSE, -- Disponible como preview gratuito is_locked BOOLEAN NOT NULL DEFAULT FALSE, -- Bloqueado hasta cumplir requisitos -- Requisitos para desbloquear unlock_requirements JSONB DEFAULT '{}'::JSONB, -- { "modules": [], "min_score": 70 } -- Duracion estimated_duration_minutes INTEGER, -- Estadisticas (cache) lesson_count INTEGER NOT NULL DEFAULT 0, quiz_count INTEGER NOT NULL DEFAULT 0, -- Metadata metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT modules_unique_sequence UNIQUE (course_id, sequence_number), CONSTRAINT modules_unique_slug UNIQUE (course_id, slug) ); COMMENT ON TABLE education.modules IS 'Modulos o secciones que organizan las lecciones de un curso'; COMMENT ON COLUMN education.modules.is_preview IS 'Si TRUE, el modulo esta disponible como preview gratuito'; -- Indices CREATE INDEX IF NOT EXISTS idx_modules_course ON education.modules(course_id, sequence_number); CREATE INDEX IF NOT EXISTS idx_modules_tenant ON education.modules(tenant_id); CREATE INDEX IF NOT EXISTS idx_modules_status ON education.modules(status); CREATE INDEX IF NOT EXISTS idx_modules_preview ON education.modules(course_id, is_preview) WHERE is_preview = TRUE; -- Trigger para updated_at DROP TRIGGER IF EXISTS module_updated_at ON education.modules; CREATE TRIGGER module_updated_at BEFORE UPDATE ON education.modules FOR EACH ROW EXECUTE FUNCTION education.update_education_timestamp(); -- Trigger para actualizar conteo en curso CREATE OR REPLACE FUNCTION education.update_course_module_count() RETURNS TRIGGER AS $$ BEGIN IF TG_OP = 'INSERT' THEN UPDATE education.courses SET total_modules = total_modules + 1, last_content_update = NOW() WHERE id = NEW.course_id; ELSIF TG_OP = 'DELETE' THEN UPDATE education.courses SET total_modules = total_modules - 1, last_content_update = NOW() WHERE id = OLD.course_id; END IF; RETURN COALESCE(NEW, OLD); END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS module_course_count ON education.modules; CREATE TRIGGER module_course_count AFTER INSERT OR DELETE ON education.modules FOR EACH ROW EXECUTE FUNCTION education.update_course_module_count(); -- Vista de modulos con lecciones CREATE OR REPLACE VIEW education.v_course_modules AS SELECT m.id, m.course_id, m.title, m.slug, m.description, m.sequence_number, m.status, m.is_preview, m.is_locked, m.estimated_duration_minutes, m.lesson_count, m.quiz_count FROM education.modules m WHERE m.status = 'published' ORDER BY m.course_id, m.sequence_number; -- RLS Policy para multi-tenancy ALTER TABLE education.modules ENABLE ROW LEVEL SECURITY; CREATE POLICY modules_tenant_isolation ON education.modules FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT, INSERT, UPDATE, DELETE ON education.modules TO trading_app; GRANT SELECT ON education.modules TO trading_readonly; GRANT SELECT ON education.v_course_modules TO trading_app;