Files
playzer/apps/backend/test/admin-booking/reschedule-admin-booking.service.test.ts
2026-06-26 14:05:37 -03:00

290 lines
8.9 KiB
TypeScript

import { beforeEach, expect, test } from 'bun:test';
import { prismaMock, transactionMock, uuidV7Mock } from '../support/prisma.mock';
const { rescheduleAdminBooking } = await import(
'@/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.business'
);
function makeBooking(overrides?: Record<string, unknown>) {
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<string, unknown>) {
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 <T>(fn: (tx: typeof prismaMock) => Promise<T>) =>
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.ok).toBe(true);
if (!result.ok) return;
expect(result.value.courtId).toBe('court-2');
expect(result.value.startTime).toBe('14:00');
expect(result.value.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 <T>(fn: (tx: typeof prismaMock) => Promise<T>) =>
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.ok).toBe(true);
if (!result.ok) return;
expect(result.value.startTime).toBe('15:00');
expect(result.value.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);
const result = await rescheduleAdminBooking('user-1', 'non-existent', { startTime: '14:00' });
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('not_found');
expect(transactionMock).not.toHaveBeenCalled();
});
test('rejects when booking status is not CONFIRMED', async () => {
prismaMock.courtBooking.findFirst.mockResolvedValue(
makeBooking({ status: 'COMPLETED' }) as never
);
const result = await rescheduleAdminBooking('user-1', 'booking-1', { startTime: '14:00' });
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
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
);
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
});
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
);
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
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
);
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
});
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
);
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'court-2',
startTime: '14:00',
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('conflict');
expect(transactionMock).not.toHaveBeenCalled();
});
test('rejects when neither courtId nor startTime changes', async () => {
prismaMock.courtBooking.findFirst.mockResolvedValue(makeBooking() as never);
const result = await rescheduleAdminBooking('user-1', 'booking-1', {});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('validation');
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);
const result = await rescheduleAdminBooking('user-1', 'booking-1', {
courtId: 'other-complex-court',
startTime: '14:00',
});
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error.type).toBe('not_found');
expect(transactionMock).not.toHaveBeenCalled();
});