import prisma from '../config/database'; import { ApiError } from '../middleware/errorHandler'; import { UserBonusStatus, BookingStatus } from '../utils/constants'; import logger from '../config/logger'; export interface PurchaseBonusInput { userId: string; bonusPackId: string; paymentId: string; } export interface UseBonusInput { userId: string; bookingId: string; } export class UserBonusService { // Comprar un bono static async purchaseBonus(userId: string, bonusPackId: string, paymentId: string) { // Verificar que el bono exista y esté activo const bonusPack = await prisma.bonusPack.findFirst({ where: { id: bonusPackId, isActive: true }, }); if (!bonusPack) { throw new ApiError('Pack de bonos no encontrado o inactivo', 404); } // Calcular fechas const purchaseDate = new Date(); const expirationDate = new Date(); expirationDate.setDate(expirationDate.getDate() + bonusPack.validityDays); // Crear el bono del usuario const userBonus = await prisma.userBonus.create({ data: { userId, bonusPackId, totalBookings: bonusPack.numberOfBookings, usedBookings: 0, remainingBookings: bonusPack.numberOfBookings, purchaseDate, expirationDate, status: UserBonusStatus.ACTIVE, paymentId, }, include: { bonusPack: { select: { name: true, numberOfBookings: true, validityDays: true, }, }, }, }); logger.info(`Bono comprado: ${userBonus.id} por usuario: ${userId}`); return userBonus; } // Obtener mis bonos activos static async getMyBonuses(userId: string, includeExpired = false) { const where: any = { userId }; if (!includeExpired) { where.status = UserBonusStatus.ACTIVE; } const bonuses = await prisma.userBonus.findMany({ where, include: { bonusPack: { select: { name: true, description: true, }, }, }, orderBy: [ { expirationDate: 'asc' }, { createdAt: 'desc' }, ], }); // Verificar y actualizar bonos expirados const now = new Date(); const updatedBonuses = []; for (const bonus of bonuses) { if (bonus.status === UserBonusStatus.ACTIVE && bonus.expirationDate < now) { // Actualizar a expirado await prisma.userBonus.update({ where: { id: bonus.id }, data: { status: UserBonusStatus.EXPIRED }, }); bonus.status = UserBonusStatus.EXPIRED; } updatedBonuses.push(bonus); } return updatedBonuses; } // Obtener bono por ID (verificar que pertenezca al usuario) static async getBonusById(id: string, userId: string) { const bonus = await prisma.userBonus.findFirst({ where: { id, userId }, include: { bonusPack: { select: { name: true, description: true, price: true, }, }, usages: { include: { booking: { select: { id: true, date: true, startTime: true, endTime: true, court: { select: { name: true, }, }, }, }, }, orderBy: { usedAt: 'desc', }, }, }, }); if (!bonus) { throw new ApiError('Bono no encontrado', 404); } // Verificar si está expirado if (bonus.status === UserBonusStatus.ACTIVE && bonus.expirationDate < new Date()) { await prisma.userBonus.update({ where: { id: bonus.id }, data: { status: UserBonusStatus.EXPIRED }, }); bonus.status = UserBonusStatus.EXPIRED; } return bonus; } // Usar un bono para una reserva static async useBonusForBooking(userId: string, bookingId: string, userBonusId?: string) { // Verificar que la reserva exista y pertenezca al usuario const booking = await prisma.booking.findFirst({ where: { id: bookingId, userId }, include: { court: true, }, }); if (!booking) { throw new ApiError('Reserva no encontrada', 404); } // Verificar que la reserva esté confirmada o pendiente if (booking.status !== BookingStatus.PENDING && booking.status !== BookingStatus.CONFIRMED) { throw new ApiError('Solo se pueden aplicar bonos a reservas pendientes o confirmadas', 400); } // Verificar que no tenga ya un bono aplicado const existingUsage = await prisma.bonusUsage.findUnique({ where: { bookingId }, }); if (existingUsage) { throw new ApiError('Esta reserva ya tiene un bono aplicado', 400); } // Si se especifica un bono específico, usar ese let userBonus; if (userBonusId) { userBonus = await prisma.userBonus.findFirst({ where: { id: userBonusId, userId, status: UserBonusStatus.ACTIVE, remainingBookings: { gt: 0 }, }, include: { bonusPack: true, }, }); if (!userBonus) { throw new ApiError('Bono no encontrado, no activo o sin reservas disponibles', 404); } } else { // Buscar el bono activo más próximo a expirar (FIFO) const now = new Date(); userBonus = await prisma.userBonus.findFirst({ where: { userId, status: UserBonusStatus.ACTIVE, remainingBookings: { gt: 0 }, expirationDate: { gt: now }, }, include: { bonusPack: true, }, orderBy: { expirationDate: 'asc', }, }); if (!userBonus) { throw new ApiError('No tienes bonos activos disponibles para usar', 400); } } // Verificar que el bono no esté expirado if (userBonus.expirationDate < new Date()) { await prisma.userBonus.update({ where: { id: userBonus.id }, data: { status: UserBonusStatus.EXPIRED }, }); throw new ApiError('El bono ha expirado', 400); } // Ejecutar la transacción const result = await prisma.$transaction(async (tx) => { // Descontar 1 del bono const newRemaining = userBonus!.remainingBookings - 1; const newStatus = newRemaining === 0 ? UserBonusStatus.DEPLETED : UserBonusStatus.ACTIVE; const updatedBonus = await tx.userBonus.update({ where: { id: userBonus!.id }, data: { usedBookings: { increment: 1 }, remainingBookings: newRemaining, status: newStatus, }, }); // Crear registro de uso const usage = await tx.bonusUsage.create({ data: { userBonusId: userBonus!.id, bookingId, usedAt: new Date(), }, }); // Actualizar la reserva (precio = 0 cuando se usa bono) const updatedBooking = await tx.booking.update({ where: { id: bookingId }, data: { totalPrice: 0 }, }); return { updatedBonus, usage, updatedBooking }; }); logger.info(`Bono usado: ${userBonus.id} para booking: ${bookingId}`); return { message: 'Bono aplicado exitosamente', bonusUsage: result.usage, remainingBookings: result.updatedBonus.remainingBookings, booking: result.updatedBooking, }; } // Verificar y marcar bonos expirados (para cron job) static async checkExpiredBonuses() { const now = new Date(); const expiredBonuses = await prisma.userBonus.findMany({ where: { status: UserBonusStatus.ACTIVE, expirationDate: { lt: now }, }, }); if (expiredBonuses.length === 0) { return { count: 0, bonuses: [] }; } // Actualizar todos los bonos expirados const updatePromises = expiredBonuses.map((bonus) => prisma.userBonus.update({ where: { id: bonus.id }, data: { status: UserBonusStatus.EXPIRED }, }) ); await Promise.all(updatePromises); logger.info(`${expiredBonuses.length} bonos marcados como expirados`); return { count: expiredBonuses.length, bonuses: expiredBonuses.map((b) => ({ id: b.id, userId: b.userId, expirationDate: b.expirationDate, })), }; } // Obtener historial de uso de un bono static async getBonusUsageHistory(userBonusId: string, userId: string) { // Verificar que el bono pertenezca al usuario const bonus = await prisma.userBonus.findFirst({ where: { id: userBonusId, userId }, }); if (!bonus) { throw new ApiError('Bono no encontrado', 404); } const usages = await prisma.bonusUsage.findMany({ where: { userBonusId }, include: { booking: { select: { id: true, date: true, startTime: true, endTime: true, court: { select: { name: true, }, }, }, }, }, orderBy: { usedAt: 'desc', }, }); return { bonusId: userBonusId, totalBookings: bonus.totalBookings, usedBookings: bonus.usedBookings, remainingBookings: bonus.remainingBookings, usages, }; } // Obtener bonos disponibles para un usuario (para mostrar en checkout) static async getAvailableBonuses(userId: string) { const now = new Date(); return prisma.userBonus.findMany({ where: { userId, status: UserBonusStatus.ACTIVE, remainingBookings: { gt: 0 }, expirationDate: { gt: now }, }, include: { bonusPack: { select: { name: true, description: true, }, }, }, orderBy: { expirationDate: 'asc', }, }); } } export default UserBonusService;