From 4eb7ade40710e9d805f968373683f2369d079d97 Mon Sep 17 00:00:00 2001
From: Nurfog
Date: Sun, 15 Feb 2026 13:54:01 -0300
Subject: [PATCH] feat: Implement course monetization with Mercado Pago, update
roadmap status, and refine discussion service handlers.
---
README.md | 2 +
roadmap.md | 17 ++---
.../lms-service/src/handlers_discussions.rs | 65 ++++++++++++-------
web/experience/src/app/courses/[id]/page.tsx | 14 ++--
web/studio/package-lock.json | 14 +++-
5 files changed, 72 insertions(+), 40 deletions(-)
diff --git a/README.md b/README.md
index 3e85a8f..3ba35d3 100644
--- a/README.md
+++ b/README.md
@@ -37,6 +37,7 @@ El proyecto ha sido optimizado para reducir la complejidad de la infraestructura
- **Efficient Docker Builds**: Imágenes de contenedor optimizadas para desarrollo rápido y despliegue ligero.
- **Discussion Forums**: Sistema completo de foros por curso con hilos de discusión, respuestas anidadas, votación, moderación por instructores y suscripciones.
- **Split Authentication Flow**: Flujos de autenticación diferenciados para usuarios personales (email/password) y empresas (dominio corporativo).
+- **Course Monetization**: Integración con Mercado Pago para venta de cursos, con inscripciones automáticas y paneles de precios para instructores.
## Requisitos del Sistema
@@ -581,6 +582,7 @@ Obtiene una lista de todas las organizaciones registradas.
- **Discussion Forums**: Complete forum system with threaded replies, voting, instructor moderation, and subscriptions.
- **Course Announcements**: Instructor-to-student communication system with automatic notifications and pinning functionality.
- **Split Authentication**: Separate login flows for personal users and enterprise organizations with SSO support.
+- **Mercado Pago Monetization**: Integrated payment gateway with automatic course unlocking and transaction tracking.
## 📄 Licencia
Este proyecto es código abierto y está disponible bajo los términos de la licencia especificada en el repositorio.
\ No newline at end of file
diff --git a/roadmap.md b/roadmap.md
index 7a109db..a04170c 100644
--- a/roadmap.md
+++ b/roadmap.md
@@ -192,11 +192,13 @@
- [ ] **Bookmarks**: Sistema de favoritos para lecciones importantes.
- [ ] **Progress Dashboard**: Gráficos de progreso temporal y predicción de finalización.
-## Fase 18: Monetización y Estandarización (Nuevas Sugerencias)
-- [ ] **E-Commerce & Monetización**:
- - [ ] Integración con Mercado Pago y Stripe.
- - [ ] Sistema de precios por curso y organización.
- - [ ] Dashboard de transacciones y conciliación.
+## Fase 18: Monetización y Estandarización ✅
+- [x] **E-Commerce & Monetización**: (Completado)
+ - [x] Integración con Mercado Pago (Preferencia de pago y Webhooks).
+ - [x] Sistema de precios y moneda por curso.
+ - [x] Inscripción automática tras pago exitoso.
+ - [x] Verificación de seguridad de acceso a lecciones basada en inscripción.
+ - [x] Dashboard de transacciones básico en base de datos.
- [ ] **Interoperabilidad**:
- [ ] Implementación de LTI 1.3 (Tool Provider).
- [ ] Conectividad con LMS externos (Moodle/Canvas).
@@ -213,10 +215,9 @@
---
-**Estado Actual**: La plataforma cuenta con un motor de IA avanzado, gestión multi-tenant completa, tutoría inteligente con memoria histórica, una **interfaz 100% responsiva**, flujos de autenticación diferenciados, **sistema de foros de discusión funcional** y **gestión de anuncios del curso con notificaciones automáticas**.
+**Estado Actual**: La plataforma cuenta con un motor de IA avanzado, gestión multi-tenant completa, tutoría inteligente con memoria histórica, una **interfaz 100% responsiva**, flujos de autenticación diferenciados, **sistema de foros de discusión funcional**, **gestión de anuncios** y **monetización integrada con Mercado Pago**.
**Próximas Prioridades**:
-1. **Monetización Core**: Preparación de infraestructura para pagos y precios.
-2. **LTI 1.3 Research**: Bases para la interoperabilidad.
+1. **LTI 1.3 Research**: Bases para la interoperabilidad con LMS externos.
3. **Course Wiki**: Espacio colaborativo para documentación de cursos.
4. **Student Notes**: Anotaciones personales exportables.
diff --git a/services/lms-service/src/handlers_discussions.rs b/services/lms-service/src/handlers_discussions.rs
index 6129925..686fe88 100644
--- a/services/lms-service/src/handlers_discussions.rs
+++ b/services/lms-service/src/handlers_discussions.rs
@@ -5,10 +5,7 @@ use axum::{
};
use common::auth::Claims;
use common::middleware::Org;
-use common::models::{
- DiscussionThread, DiscussionPost,
- ThreadWithAuthor, PostWithAuthor,
-};
+use common::models::{DiscussionPost, DiscussionThread, PostWithAuthor, ThreadWithAuthor};
use serde::Deserialize;
use sqlx::PgPool;
use uuid::Uuid;
@@ -63,12 +60,12 @@ pub async fn list_threads(
FROM discussion_threads t
LEFT JOIN users u ON t.author_id = u.id
LEFT JOIN discussion_posts p ON t.id = p.thread_id
- WHERE t.course_id = $1 AND t.organization_id = $2"
+ WHERE t.course_id = $1 AND t.organization_id = $2",
);
let mut bind_count = 2;
- if let Some(lesson_id) = params.lesson_id {
+ if let Some(_lesson_id) = params.lesson_id {
bind_count += 1;
query.push_str(&format!(" AND t.lesson_id = ${}", bind_count));
}
@@ -80,7 +77,9 @@ pub async fn list_threads(
query.push_str(&format!(" AND t.author_id = ${}", bind_count));
}
"unanswered" => {
- query.push_str(" AND NOT EXISTS (SELECT 1 FROM discussion_posts WHERE thread_id = t.id)");
+ query.push_str(
+ " AND NOT EXISTS (SELECT 1 FROM discussion_posts WHERE thread_id = t.id)",
+ );
}
"resolved" => {
query.push_str(" AND EXISTS (SELECT 1 FROM discussion_posts WHERE thread_id = t.id AND is_endorsed = true)");
@@ -146,7 +145,7 @@ pub async fn create_thread(
let _ = sqlx::query(
"INSERT INTO discussion_subscriptions (organization_id, thread_id, user_id)
VALUES ($1, $2, $3)
- ON CONFLICT DO NOTHING"
+ ON CONFLICT DO NOTHING",
)
.bind(org_ctx.id)
.bind(thread.id)
@@ -181,7 +180,7 @@ pub async fn get_thread_detail(
LEFT JOIN users u ON t.author_id = u.id
LEFT JOIN discussion_posts p ON t.id = p.thread_id
WHERE t.id = $1 AND t.organization_id = $2
- GROUP BY t.id, u.full_name, u.avatar_url"
+ GROUP BY t.id, u.full_name, u.avatar_url",
)
.bind(thread_id)
.bind(org_ctx.id)
@@ -205,7 +204,13 @@ fn get_thread_posts_recursive<'a>(
parent_id: Option,
user_id: Uuid,
org_id: Uuid,
-) -> std::pin::Pin, (StatusCode, String)>> + Send + 'a>> {
+) -> std::pin::Pin<
+ Box<
+ dyn std::future::Future
-
{courseData.pacing_mode === 'instructor_led' ? : }
{courseData.pacing_mode === 'instructor_led' ? 'Dirigido por un Instructor' : 'A tu Ritmo'}
@@ -210,7 +212,7 @@ export default function CourseOutlinePage({ params }: { params: { id: string } }