Files
app-padel/backend/_future_services/bonusPack.service.ts
Ivan Alcaraz b8a964dc2c 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
2026-01-31 09:02:25 +00:00

137 lines
3.6 KiB
TypeScript

import prisma from '../config/database';
import { ApiError } from '../middleware/errorHandler';
import logger from '../config/logger';
export interface CreateBonusPackInput {
name: string;
description?: string;
numberOfBookings: number;
price: number;
validityDays: number;
}
export interface UpdateBonusPackInput {
name?: string;
description?: string;
numberOfBookings?: number;
price?: number;
validityDays?: number;
isActive?: boolean;
}
export class BonusPackService {
// Crear un tipo de bono (admin)
static async createBonusPack(adminId: string, data: CreateBonusPackInput) {
// Validar que el precio sea positivo
if (data.price < 0) {
throw new ApiError('El precio no puede ser negativo', 400);
}
// Validar que la cantidad de reservas sea positiva
if (data.numberOfBookings <= 0) {
throw new ApiError('La cantidad de reservas debe ser mayor a 0', 400);
}
// Validar que los días de validez sean positivos
if (data.validityDays <= 0) {
throw new ApiError('Los días de validez deben ser mayor a 0', 400);
}
const bonusPack = await prisma.bonusPack.create({
data: {
name: data.name,
description: data.description,
numberOfBookings: data.numberOfBookings,
price: data.price,
validityDays: data.validityDays,
isActive: true,
},
});
logger.info(`BonusPack creado: ${bonusPack.id} por admin: ${adminId}`);
return bonusPack;
}
// Obtener todos los bonos activos (público)
static async getBonusPacks(includeInactive = false) {
const where = includeInactive ? {} : { isActive: true };
return prisma.bonusPack.findMany({
where,
orderBy: {
price: 'asc',
},
});
}
// Obtener un bono por ID
static async getBonusPackById(id: string) {
const bonusPack = await prisma.bonusPack.findUnique({
where: { id },
});
if (!bonusPack) {
throw new ApiError('Pack de bonos no encontrado', 404);
}
return bonusPack;
}
// Actualizar un tipo de bono (admin)
static async updateBonusPack(id: string, adminId: string, data: UpdateBonusPackInput) {
const bonusPack = await prisma.bonusPack.findUnique({
where: { id },
});
if (!bonusPack) {
throw new ApiError('Pack de bonos no encontrado', 404);
}
// Validaciones si se actualizan ciertos campos
if (data.price !== undefined && data.price < 0) {
throw new ApiError('El precio no puede ser negativo', 400);
}
if (data.numberOfBookings !== undefined && data.numberOfBookings <= 0) {
throw new ApiError('La cantidad de reservas debe ser mayor a 0', 400);
}
if (data.validityDays !== undefined && data.validityDays <= 0) {
throw new ApiError('Los días de validez deben ser mayor a 0', 400);
}
const updated = await prisma.bonusPack.update({
where: { id },
data,
});
logger.info(`BonusPack actualizado: ${id} por admin: ${adminId}`);
return updated;
}
// Eliminar (desactivar) un tipo de bono (admin)
static async deleteBonusPack(id: string, adminId: string) {
const bonusPack = await prisma.bonusPack.findUnique({
where: { id },
});
if (!bonusPack) {
throw new ApiError('Pack de bonos no encontrado', 404);
}
// Desactivar en lugar de eliminar físicamente
const updated = await prisma.bonusPack.update({
where: { id },
data: { isActive: false },
});
logger.info(`BonusPack desactivado: ${id} por admin: ${adminId}`);
return updated;
}
}
export default BonusPackService;