Files
playzer/apps/backend/src/modules/admin-booking/features/reschedule-admin-booking/reschedule-admin-booking.business.ts
2026-06-26 14:05:37 -03:00

179 lines
5.3 KiB
TypeScript

import { Errors } from '@/lib/errors';
import { db } from '@/lib/prisma';
import type { Result } from '@/lib/result';
import { err, ok } from '@/lib/result';
import { isSlotInPast } from '@/lib/slot-validator';
import type { AdminBooking, RescheduleAdminBookingInput } from '@repo/api-contract';
import { v7 as uuidv7 } from 'uuid';
import {
buildSlots,
getDayOfWeek,
mapBookingResponse,
minutesToTime,
resolvePrice,
toMinutes,
} from '../../shared/helpers';
export async function rescheduleAdminBooking(
userId: string,
bookingId: string,
input: RescheduleAdminBookingInput
): Promise<Result<AdminBooking>> {
const booking = await db.courtBooking.findFirst({
where: {
id: bookingId,
court: {
complex: { users: { some: { userId } } },
},
},
include: {
court: {
select: {
id: true,
name: true,
complexId: true,
slotDurationMinutes: true,
sport: { select: { id: true, name: true, slug: true } },
complex: { select: { id: true, complexName: true } },
},
},
},
});
if (!booking) {
return err(Errors.notFound('Reserva no encontrada.'));
}
if (booking.status !== 'CONFIRMED') {
return err(Errors.conflict('Solo se pueden reprogramar reservas en estado confirmada.'));
}
const targetCourtId = input.courtId ?? booking.courtId;
const targetStartTime = input.startTime ?? booking.startTime;
if (targetCourtId === booking.courtId && targetStartTime === booking.startTime) {
return err(Errors.validation('Debe proporcionar al menos una cancha o un horario diferente.'));
}
const dayOfWeekResult = getDayOfWeek(booking.bookingDate);
if (!dayOfWeekResult.ok) return err(dayOfWeekResult.error);
const dayOfWeek = dayOfWeekResult.value;
const targetCourt = await db.court.findFirst({
where: { id: targetCourtId, complexId: booking.court.complexId },
include: {
availabilities: {
where: { dayOfWeek },
orderBy: { startTime: 'asc' },
},
priceRules: {
where: { isActive: true },
orderBy: [{ dayOfWeek: 'asc' }, { startTime: 'asc' }],
},
sport: { select: { id: true, name: true, slug: true } },
},
});
if (!targetCourt) {
return err(Errors.notFound('La cancha seleccionada no existe en el complejo.'));
}
if (targetCourt.sport.id !== booking.court.sport.id) {
return err(
Errors.conflict('La cancha seleccionada no es del mismo deporte que la reserva original.')
);
}
if (targetCourt.isUnderMaintenance) {
return err(Errors.conflict('La cancha seleccionada se encuentra en mantenimiento.'));
}
const validSlots = buildSlots(targetCourt.availabilities, targetCourt.slotDurationMinutes);
const selectedEndMinutes = toMinutes(targetStartTime) + targetCourt.slotDurationMinutes;
const selectedSlot = { startTime: targetStartTime, endTime: minutesToTime(selectedEndMinutes) };
const slotExists = validSlots.some(
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
);
if (!slotExists) {
return err(Errors.conflict('El horario seleccionado no está disponible para esa cancha.'));
}
if (isSlotInPast(booking.bookingDate, targetStartTime, targetCourt.slotDurationMinutes)) {
return err(Errors.validation('No se pueden reprogramar reservas en el pasado.'));
}
const overlappingBooking = await db.courtBooking.findFirst({
where: {
id: { not: bookingId },
courtId: targetCourtId,
bookingDate: booking.bookingDate,
status: 'CONFIRMED',
startTime: { lt: selectedSlot.endTime },
endTime: { gt: selectedSlot.startTime },
},
});
if (overlappingBooking) {
return err(Errors.conflict('El horario seleccionado ya fue reservado por otra reserva.'));
}
const newPrice = resolvePrice(
targetCourt,
dayOfWeek,
selectedSlot.startTime,
selectedSlot.endTime
);
await db.$transaction(async (tx) => {
await tx.courtBookingLog.create({
data: {
id: uuidv7(),
bookingCode: booking.bookingCode,
courtId: booking.court.id,
bookingDate: booking.bookingDate,
startTime: booking.startTime,
endTime: booking.endTime,
customerName: booking.customerName,
customerPhone: booking.customerPhone,
customerEmail: booking.customerEmail,
previousStatus: booking.status,
newStatus: booking.status,
previousCourtId: booking.court.id,
previousStartTime: booking.startTime,
previousEndTime: booking.endTime,
changedAt: new Date(),
},
});
await tx.courtBooking.update({
where: { id: booking.id },
data: {
courtId: targetCourtId,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
},
});
});
const updatedBooking = {
...booking,
startTime: selectedSlot.startTime,
endTime: selectedSlot.endTime,
price: newPrice,
};
if (targetCourtId !== booking.courtId) {
updatedBooking.court = {
id: targetCourt.id,
name: targetCourt.name,
slotDurationMinutes: targetCourt.slotDurationMinutes,
sport: targetCourt.sport,
complex: booking.court.complex,
} as typeof booking.court;
}
return ok(mapBookingResponse(updatedBooking));
}