import { beforeEach, expect, test } from 'bun:test'; import { prismaMock, transactionMock, uuidV7Mock } from '../support/prisma.mock'; const { AdminBookingServiceError, rescheduleAdminBooking } = await import( '@/modules/admin-booking/services/admin-booking.service' ); function makeBooking(overrides?: Record) { return { id: 'booking-1', bookingCode: 'ABC123', courtId: 'court-1', bookingDate: new Date('2026-07-15T00:00:00.000Z'), startTime: '10:00', endTime: '11:00', customerName: 'Juan Perez', customerPhone: '1144444444', customerEmail: 'juan@example.com', status: 'CONFIRMED', recurringGroupId: null, createdAt: new Date('2026-07-14T00:00:00.000Z'), updatedAt: new Date('2026-07-14T00:00:00.000Z'), court: { id: 'court-1', name: 'Cancha 1', complexId: 'complex-1', slotDurationMinutes: 60, sport: { id: 'sport-1', name: 'Tenis', slug: 'tenis' }, complex: { id: 'complex-1', complexName: 'Complejo Test' }, }, ...overrides, }; } // July 15, 2026 is a Wednesday const WEDNESDAY = 'WEDNESDAY'; function makeCourt(overrides?: Record) { return { id: 'court-2', name: 'Cancha 2', complexId: 'complex-1', slotDurationMinutes: 60, basePrice: 15000, isUnderMaintenance: false, availabilities: [{ id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '18:00' }], priceRules: [], sport: { id: 'sport-1', name: 'Tenis', slug: 'tenis' }, ...overrides, }; } beforeEach(() => { prismaMock._reset(); transactionMock.mockClear(); uuidV7Mock.mockReset(); uuidV7Mock.mockReturnValue('00000000-0000-4000-8000-000000000001'); }); test('reschedules a booking changing both court and time', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never); prismaMock.court.findFirst.mockResolvedValue(makeCourt() as never); prismaMock.courtBooking.findFirst .mockResolvedValueOnce(makeBooking() as never) // initial fetch .mockResolvedValueOnce(null as never); // overlap check -> no overlap transactionMock.mockImplementation(async (fn: (tx: typeof prismaMock) => Promise) => fn(prismaMock) ); prismaMock.courtBookingLog.create.mockResolvedValue({} as never); prismaMock.courtBooking.update.mockResolvedValue( makeBooking({ courtId: 'court-2', startTime: '14:00', endTime: '15:00', }) as never ); const result = await rescheduleAdminBooking('user-1', 'booking-1', { courtId: 'court-2', startTime: '14:00', }); expect(result.courtId).toBe('court-2'); expect(result.startTime).toBe('14:00'); expect(result.courtName).toBe('Cancha 2'); expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1); expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({ where: { id: 'booking-1' }, data: { courtId: 'court-2', startTime: '14:00', endTime: '15:00', }, }); const logArgs = prismaMock.courtBookingLog.create.mock.calls[0]?.[0] as { data: { previousCourtId: string; previousStartTime: string; previousEndTime: string }; }; expect(logArgs.data.previousCourtId).toBe('court-1'); expect(logArgs.data.previousStartTime).toBe('10:00'); expect(logArgs.data.previousEndTime).toBe('11:00'); }); test('reschedules a booking changing only the time', async () => { prismaMock.courtBooking.findFirst .mockResolvedValueOnce(makeBooking() as never) // initial fetch .mockResolvedValueOnce(null as never); // overlap check // Same court, different time prismaMock.court.findFirst.mockResolvedValue( makeCourt({ id: 'court-1', name: 'Cancha 1', availabilities: [ { id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '18:00' }, ], }) as never ); transactionMock.mockImplementation(async (fn: (tx: typeof prismaMock) => Promise) => fn(prismaMock) ); prismaMock.courtBookingLog.create.mockResolvedValue({} as never); prismaMock.courtBooking.update.mockResolvedValue( makeBooking({ startTime: '15:00', endTime: '16:00', }) as never ); const result = await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '15:00', }); expect(result.startTime).toBe('15:00'); expect(result.courtId).toBe('court-1'); expect(prismaMock.courtBookingLog.create).toHaveBeenCalledTimes(1); expect(prismaMock.courtBooking.update).toHaveBeenCalledWith({ where: { id: 'booking-1' }, data: { courtId: 'court-1', startTime: '15:00', endTime: '16:00', }, }); }); test('rejects when booking is not found', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(null as never); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(404); expect(transactionMock).not.toHaveBeenCalled(); }); test('rejects when booking status is not CONFIRMED', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue( makeBooking({ status: 'COMPLETED' }) as never ); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(409); expect(transactionMock).not.toHaveBeenCalled(); }); test('rejects when slot is not available in target court', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never); // Court with no availabilities that match the requested time prismaMock.court.findFirst.mockResolvedValue( makeCourt({ id: 'court-2', availabilities: [ { id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '09:00' }, ], }) as never ); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', { courtId: 'court-2', startTime: '14:00', }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(409); }); test('rejects when target court has a different sport', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never); prismaMock.court.findFirst.mockResolvedValue( makeCourt({ sport: { id: 'sport-2', name: 'FĂștbol', slug: 'futbol' }, }) as never ); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', { courtId: 'court-2', startTime: '14:00', }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(409); expect(transactionMock).not.toHaveBeenCalled(); }); test('rejects when target court is under maintenance', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never); prismaMock.court.findFirst.mockResolvedValue( makeCourt({ isUnderMaintenance: true, }) as never ); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', { courtId: 'court-2', startTime: '14:00', }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(409); }); test('rejects when there is an overlapping booking', async () => { prismaMock.courtBooking.findFirst .mockResolvedValueOnce(makeBooking() as never) // initial fetch .mockResolvedValueOnce({ id: 'overlap-1' } as never); // overlap found prismaMock.court.findFirst.mockResolvedValue( makeCourt({ availabilities: [ { id: 'avail-1', dayOfWeek: WEDNESDAY, startTime: '08:00', endTime: '18:00' }, ], }) as never ); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', { courtId: 'court-2', startTime: '14:00', }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(409); expect(transactionMock).not.toHaveBeenCalled(); }); test('rejects when neither courtId nor startTime changes', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', {}); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(400); expect(transactionMock).not.toHaveBeenCalled(); }); test('rejects when target court does not belong to the same complex', async () => { prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never); // Court belongs to a different complex prismaMock.court.findFirst.mockResolvedValue(null as never); let caught: unknown; try { await rescheduleAdminBooking('user-1', 'booking-1', { courtId: 'other-complex-court', startTime: '14:00', }); } catch (error) { caught = error; } expect(caught).toBeInstanceOf(AdminBookingServiceError); expect((caught as InstanceType).status).toBe(404); expect(transactionMock).not.toHaveBeenCalled(); });