import { BookingService } from '../../../src/services/booking.service'; import { ApiError } from '../../../src/middleware/errorHandler'; import prisma from '../../../src/config/database'; import { BookingStatus } from '../../../src/utils/constants'; import * as emailService from '../../../src/services/email.service'; // Mock dependencies jest.mock('../../../src/config/database', () => ({ __esModule: true, default: { court: { findFirst: jest.fn(), findUnique: jest.fn(), }, courtSchedule: { findFirst: jest.fn(), }, booking: { findFirst: jest.fn(), findUnique: jest.fn(), findMany: jest.fn(), create: jest.fn(), update: jest.fn(), }, }, })); jest.mock('../../../src/services/email.service'); jest.mock('../../../src/config/logger', () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), })); describe('BookingService', () => { const mockCourt = { id: 'court-123', name: 'Cancha 1', pricePerHour: 2000, isActive: true, }; const mockSchedule = { id: 'schedule-123', courtId: 'court-123', dayOfWeek: 1, // Monday openTime: '08:00', closeTime: '23:00', }; const mockBooking = { id: 'booking-123', userId: 'user-123', courtId: 'court-123', date: new Date('2026-02-01'), startTime: '10:00', endTime: '11:00', status: BookingStatus.PENDING, totalPrice: 2000, notes: null, court: mockCourt, user: { id: 'user-123', firstName: 'Test', lastName: 'User', email: 'test@example.com', }, }; beforeEach(() => { jest.clearAllMocks(); }); describe('createBooking', () => { const validBookingInput = { userId: 'user-123', courtId: 'court-123', date: new Date('2026-02-01'), startTime: '10:00', endTime: '11:00', notes: 'Test booking', }; it('should create a booking successfully', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule); (prisma.booking.findFirst as jest.Mock).mockResolvedValue(null); // No conflict (prisma.booking.create as jest.Mock).mockResolvedValue(mockBooking); (emailService.sendBookingConfirmation as jest.Mock).mockResolvedValue(undefined); // Act const result = await BookingService.createBooking(validBookingInput); // Assert expect(prisma.court.findFirst).toHaveBeenCalledWith({ where: { id: validBookingInput.courtId, isActive: true }, }); expect(prisma.booking.create).toHaveBeenCalled(); expect(emailService.sendBookingConfirmation).toHaveBeenCalled(); expect(result).toHaveProperty('id'); expect(result).toHaveProperty('benefitsApplied'); }); it('should throw error when date is in the past', async () => { // Arrange const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); const pastBookingInput = { ...validBookingInput, date: yesterday, }; // Act & Assert await expect(BookingService.createBooking(pastBookingInput)).rejects.toThrow(ApiError); await expect(BookingService.createBooking(pastBookingInput)).rejects.toThrow('No se pueden hacer reservas en fechas pasadas'); }); it('should throw error when court not found or inactive', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow(ApiError); await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow('Cancha no encontrada o inactiva'); }); it('should throw error when no schedule for day', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow(ApiError); await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow('La cancha no tiene horario disponible para este día'); }); it('should throw error when time is outside schedule', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue({ ...mockSchedule, openTime: '08:00', closeTime: '12:00', }); const lateBookingInput = { ...validBookingInput, startTime: '13:00', endTime: '14:00', }; // Act & Assert await expect(BookingService.createBooking(lateBookingInput)).rejects.toThrow(ApiError); }); it('should throw error when end time is before or equal to start time', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule); const invalidBookingInput = { ...validBookingInput, startTime: '10:00', endTime: '10:00', }; // Act & Assert await expect(BookingService.createBooking(invalidBookingInput)).rejects.toThrow(ApiError); await expect(BookingService.createBooking(invalidBookingInput)).rejects.toThrow('La hora de fin debe ser posterior a la de inicio'); }); it('should throw error when there is a time conflict', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule); (prisma.booking.findFirst as jest.Mock).mockResolvedValue({ id: 'existing-booking', startTime: '10:00', endTime: '11:00', }); // Act & Assert await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow(ApiError); await expect(BookingService.createBooking(validBookingInput)).rejects.toThrow('La cancha no está disponible en ese horario'); }); it('should not fail if confirmation email fails', async () => { // Arrange (prisma.court.findFirst as jest.Mock).mockResolvedValue(mockCourt); (prisma.courtSchedule.findFirst as jest.Mock).mockResolvedValue(mockSchedule); (prisma.booking.findFirst as jest.Mock).mockResolvedValue(null); (prisma.booking.create as jest.Mock).mockResolvedValue(mockBooking); (emailService.sendBookingConfirmation as jest.Mock).mockRejectedValue(new Error('Email failed')); // Act const result = await BookingService.createBooking(validBookingInput); // Assert expect(result).toHaveProperty('id'); }); }); describe('getAllBookings', () => { it('should return all bookings with filters', async () => { // Arrange const mockBookings = [mockBooking]; (prisma.booking.findMany as jest.Mock).mockResolvedValue(mockBookings); // Act const result = await BookingService.getAllBookings({ userId: 'user-123', courtId: 'court-123', }); // Assert expect(prisma.booking.findMany).toHaveBeenCalledWith({ where: { userId: 'user-123', courtId: 'court-123' }, include: expect.any(Object), orderBy: expect.any(Array), }); expect(result).toEqual(mockBookings); }); it('should return all bookings without filters', async () => { // Arrange const mockBookings = [mockBooking, { ...mockBooking, id: 'booking-456' }]; (prisma.booking.findMany as jest.Mock).mockResolvedValue(mockBookings); // Act const result = await BookingService.getAllBookings({}); // Assert expect(prisma.booking.findMany).toHaveBeenCalledWith({ where: {}, include: expect.any(Object), orderBy: expect.any(Array), }); }); }); describe('getBookingById', () => { it('should return booking by id', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking); // Act const result = await BookingService.getBookingById('booking-123'); // Assert expect(prisma.booking.findUnique).toHaveBeenCalledWith({ where: { id: 'booking-123' }, include: expect.any(Object), }); expect(result).toEqual(mockBooking); }); it('should throw error when booking not found', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(null); // Act & Assert await expect(BookingService.getBookingById('non-existent')).rejects.toThrow(ApiError); await expect(BookingService.getBookingById('non-existent')).rejects.toThrow('Reserva no encontrada'); }); }); describe('getUserBookings', () => { it('should return user bookings', async () => { // Arrange const userBookings = [mockBooking]; (prisma.booking.findMany as jest.Mock).mockResolvedValue(userBookings); // Act const result = await BookingService.getUserBookings('user-123'); // Assert expect(prisma.booking.findMany).toHaveBeenCalledWith({ where: { userId: 'user-123' }, include: expect.any(Object), orderBy: expect.any(Array), }); expect(result).toEqual(userBookings); }); it('should return upcoming bookings when specified', async () => { // Arrange const userBookings = [mockBooking]; (prisma.booking.findMany as jest.Mock).mockResolvedValue(userBookings); // Act const result = await BookingService.getUserBookings('user-123', true); // Assert expect(prisma.booking.findMany).toHaveBeenCalledWith({ where: { userId: 'user-123', date: expect.any(Object), status: expect.any(Object), }, include: expect.any(Object), orderBy: expect.any(Array), }); }); }); describe('cancelBooking', () => { it('should cancel booking successfully', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking); (prisma.booking.update as jest.Mock).mockResolvedValue({ ...mockBooking, status: BookingStatus.CANCELLED, }); (emailService.sendBookingCancellation as jest.Mock).mockResolvedValue(undefined); // Act const result = await BookingService.cancelBooking('booking-123', 'user-123'); // Assert expect(prisma.booking.update).toHaveBeenCalledWith({ where: { id: 'booking-123' }, data: { status: BookingStatus.CANCELLED }, include: expect.any(Object), }); expect(emailService.sendBookingCancellation).toHaveBeenCalled(); expect(result.status).toBe(BookingStatus.CANCELLED); }); it('should throw error when user tries to cancel another user booking', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking); // Act & Assert await expect(BookingService.cancelBooking('booking-123', 'different-user')).rejects.toThrow(ApiError); await expect(BookingService.cancelBooking('booking-123', 'different-user')).rejects.toThrow('No tienes permiso para cancelar esta reserva'); }); it('should throw error when booking is already cancelled', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue({ ...mockBooking, status: BookingStatus.CANCELLED, }); // Act & Assert await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow(ApiError); await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow('La reserva ya está cancelada'); }); it('should throw error when booking is completed', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue({ ...mockBooking, status: BookingStatus.COMPLETED, }); // Act & Assert await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow(ApiError); await expect(BookingService.cancelBooking('booking-123', 'user-123')).rejects.toThrow('No se puede cancelar una reserva completada'); }); }); describe('confirmBooking', () => { it('should confirm pending booking', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking); (prisma.booking.update as jest.Mock).mockResolvedValue({ ...mockBooking, status: BookingStatus.CONFIRMED, }); // Act const result = await BookingService.confirmBooking('booking-123'); // Assert expect(prisma.booking.update).toHaveBeenCalledWith({ where: { id: 'booking-123' }, data: { status: BookingStatus.CONFIRMED }, include: expect.any(Object), }); expect(result.status).toBe(BookingStatus.CONFIRMED); }); it('should throw error when booking is not pending', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue({ ...mockBooking, status: BookingStatus.CONFIRMED, }); // Act & Assert await expect(BookingService.confirmBooking('booking-123')).rejects.toThrow(ApiError); await expect(BookingService.confirmBooking('booking-123')).rejects.toThrow('Solo se pueden confirmar reservas pendientes'); }); }); describe('updateBooking', () => { it('should update booking successfully', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking); (prisma.booking.update as jest.Mock).mockResolvedValue({ ...mockBooking, notes: 'Updated notes', }); // Act const result = await BookingService.updateBooking('booking-123', { notes: 'Updated notes' }, 'user-123'); // Assert expect(prisma.booking.update).toHaveBeenCalled(); expect(result.notes).toBe('Updated notes'); }); it('should throw error when user tries to update another user booking', async () => { // Arrange (prisma.booking.findUnique as jest.Mock).mockResolvedValue(mockBooking); // Act & Assert await expect(BookingService.updateBooking('booking-123', { notes: 'test' }, 'different-user')).rejects.toThrow(ApiError); await expect(BookingService.updateBooking('booking-123', { notes: 'test' }, 'different-user')).rejects.toThrow('No tienes permiso para modificar esta reserva'); }); }); });