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> { 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)); }