FASE 4 COMPLETADA: Pagos y Monetización con MercadoPago

Implementados 4 módulos con agent swarm:

1. MERCADOPAGO INTEGRADO
   - SDK oficial de MercadoPago
   - Crear preferencias de pago
   - Webhooks para notificaciones
   - Reembolsos y cancelaciones
   - Estados: PENDING, PROCESSING, COMPLETED, REFUNDED

2. SISTEMA DE BONOS Y PACKS
   - Pack 5, Pack 10, Pack Mensual
   - Compra online con MP
   - Uso FIFO automático
   - Control de expiración
   - Aplicación en reservas

3. SUSCRIPCIONES/MEMBRESÍAS
   - Planes: Básico, Premium, Anual VIP
   - Beneficios: descuentos, reservas gratis, prioridad
   - Cobro recurrente vía MP
   - Estados: ACTIVE, PAUSED, CANCELLED
   - Aplicación automática en reservas

4. CLASES CON PROFESORES
   - Registro de coaches con verificación
   - Tipos: Individual, Grupal, Clínica
   - Horarios y disponibilidad
   - Reservas con pago integrado
   - Sistema de reseñas

Endpoints nuevos:
- /payments/* - Pagos MercadoPago
- /bonus-packs/*, /bonuses/* - Bonos
- /subscription-plans/*, /subscriptions/* - Suscripciones
- /coaches/* - Profesores
- /classes/*, /class-enrollments/* - Clases

Variables de entorno:
- MERCADOPAGO_ACCESS_TOKEN
- MERCADOPAGO_PUBLIC_KEY
- MERCADOPAGO_WEBHOOK_SECRET

Datos de prueba:
- 3 Bonus Packs
- 3 Planes de suscripción
- 1 Coach verificado (admin)
- 3 Clases disponibles
This commit is contained in:
2026-01-31 09:02:25 +00:00
parent 6494e2b38b
commit b8a964dc2c
44 changed files with 7084 additions and 9 deletions

View File

@@ -0,0 +1,20 @@
import { Router } from 'express';
import { ClassController } from '../controllers/class.controller';
import { authenticate, authorize } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { createClassSchema, createClassBookingSchema } from '../validators/class.validator';
const router = Router();
// Rutas públicas
router.get('/', ClassController.getClasses);
router.get('/:id', ClassController.getClassById);
router.get('/:id/sessions', ClassController.getClassBookings);
// Rutas protegidas (solo coaches)
router.post('/', authenticate, validate(createClassSchema), ClassController.createClass);
router.put('/:id', authenticate, ClassController.updateClass);
router.delete('/:id', authenticate, ClassController.deleteClass);
router.post('/:id/sessions', authenticate, validate(createClassBookingSchema), ClassController.createClassBooking);
export default router;

View File

@@ -0,0 +1,103 @@
import { Request, Response, NextFunction } from 'express';
import { ClassEnrollmentService } from '../services/classEnrollment.service';
import { ApiError } from '../middleware/errorHandler';
export class ClassEnrollmentController {
// Inscribirse en una clase
static async enrollInClass(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.user!.userId;
const result = await ClassEnrollmentService.enrollInClass(userId, req.body);
res.status(201).json({
success: true,
message: 'Inscripción creada exitosamente',
data: result,
});
} catch (error) {
next(error);
}
}
// Webhook de MercadoPago
static async webhook(req: Request, res: Response, next: NextFunction) {
try {
// Responder inmediatamente a MP
res.status(200).send('OK');
// Procesar el webhook de forma asíncrona
await ClassEnrollmentService.processPaymentWebhook(req.body);
} catch (error) {
// Loggear error pero no enviar respuesta (ya se envió 200)
console.error('Error procesando webhook:', error);
}
}
// Cancelar inscripción
static async cancelEnrollment(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.user!.userId;
const { id } = req.params;
const result = await ClassEnrollmentService.cancelEnrollment(userId, id);
res.status(200).json({
success: true,
message: result.message,
});
} catch (error) {
next(error);
}
}
// Obtener mis inscripciones
static async getMyEnrollments(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.user!.userId;
const status = req.query.status as string | undefined;
const enrollments = await ClassEnrollmentService.getMyEnrollments(userId, status);
res.status(200).json({
success: true,
count: enrollments.length,
data: enrollments,
});
} catch (error) {
next(error);
}
}
// Obtener inscripción por ID
static async getEnrollmentById(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.user!.userId;
const { id } = req.params;
const enrollment = await ClassEnrollmentService.getEnrollmentById(id, userId);
res.status(200).json({
success: true,
data: enrollment,
});
} catch (error) {
next(error);
}
}
// Marcar asistencia (solo coach)
static async markAttendance(req: Request, res: Response, next: NextFunction) {
try {
const userId = req.user!.userId;
const { id } = req.params;
const enrollment = await ClassEnrollmentService.markAttendance(id, userId);
res.status(200).json({
success: true,
message: 'Asistencia marcada exitosamente',
data: enrollment,
});
} catch (error) {
next(error);
}
}
}
export default ClassEnrollmentController;

View File

@@ -0,0 +1,19 @@
import { Router } from 'express';
import { ClassEnrollmentController } from '../controllers/classEnrollment.controller';
import { authenticate, authorize } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { enrollmentSchema } from '../validators/class.validator';
const router = Router();
// Webhook de MercadoPago (público)
router.post('/webhook', ClassEnrollmentController.webhook);
// Rutas protegidas
router.post('/', authenticate, validate(enrollmentSchema), ClassEnrollmentController.enrollInClass);
router.get('/my', authenticate, ClassEnrollmentController.getMyEnrollments);
router.get('/:id', authenticate, ClassEnrollmentController.getEnrollmentById);
router.delete('/:id', authenticate, ClassEnrollmentController.cancelEnrollment);
router.put('/:id/attend', authenticate, ClassEnrollmentController.markAttendance);
export default router;

View File

@@ -0,0 +1,29 @@
import { Router } from 'express';
import { CoachController } from '../controllers/coach.controller';
import { authenticate, authorize } from '../middleware/auth';
import { validate } from '../middleware/validate';
import { registerCoachSchema, reviewSchema } from '../validators/class.validator';
import { UserRole } from '../utils/constants';
const router = Router();
// Rutas públicas
router.get('/', CoachController.getCoaches);
router.get('/:id', CoachController.getCoachById);
router.get('/:id/availability', CoachController.getAvailability);
router.get('/:id/reviews', CoachController.getReviews);
// Rutas protegidas (usuarios autenticados)
router.post('/register', authenticate, validate(registerCoachSchema), CoachController.registerAsCoach);
router.get('/me/profile', authenticate, CoachController.getMyProfile);
router.put('/me', authenticate, CoachController.updateMyProfile);
router.post('/me/availability', authenticate, CoachController.addAvailability);
router.post('/:id/reviews', authenticate, validate(reviewSchema), CoachController.addReview);
// Eliminar disponibilidad
router.delete('/availability/:id', authenticate, CoachController.removeAvailability);
// Rutas de admin
router.put('/:id/verify', authenticate, authorize(UserRole.ADMIN, UserRole.SUPERADMIN), CoachController.verifyCoach);
export default router;