import { randomInt } from 'node:crypto'; import { Errors } from '@/lib/errors'; import { db } from '@/lib/prisma'; import type { Result } from '@/lib/result'; import { err, ok } from '@/lib/result'; import type { AdminBooking, DayOfWeek } from '@repo/api-contract'; export type Slot = { startTime: string; endTime: string }; const DAY_OF_WEEK_BY_INDEX = [ 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', ] as const; const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const BOOKING_CODE_LENGTH = 6; export function toMinutes(value: string): number { const [hours, minutes] = value.split(':').map((part) => Number(part)); return hours * 60 + minutes; } export function minutesToTime(minutes: number): string { const safeMinutes = Math.max(0, minutes); const hours = Math.floor(safeMinutes / 60); const mins = safeMinutes % 60; return `${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}`; } export function parseIsoDate(date: string): Result { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date); if (!match) { return err(Errors.validation('La fecha debe tener formato YYYY-MM-DD.')); } const year = Number(match[1]); const month = Number(match[2]); const day = Number(match[3]); const bookingDate = new Date(Date.UTC(year, month - 1, day)); if ( Number.isNaN(bookingDate.getTime()) || bookingDate.getUTCFullYear() !== year || bookingDate.getUTCMonth() + 1 !== month || bookingDate.getUTCDate() !== day ) { return err(Errors.validation('La fecha enviada no es valida.')); } return ok(bookingDate); } export function formatIsoDate(date: Date): string { return date.toISOString().slice(0, 10); } export function getDayOfWeek(date: Date): Result { const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()]; if (!dayOfWeek) { return err(Errors.validation('No se pudo resolver el dia de la semana.')); } return ok(dayOfWeek); } export function generateBookingCode(): string { let code = ''; for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) { code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)]; } return code; } export function buildSlots( availability: Array<{ startTime: string; endTime: string }>, slotDurationMinutes: number ): Slot[] { const slots: Slot[] = []; for (const range of availability) { const start = toMinutes(range.startTime); const end = toMinutes(range.endTime); for ( let current = start; current + slotDurationMinutes <= end; current += slotDurationMinutes ) { slots.push({ startTime: minutesToTime(current), endTime: minutesToTime(current + slotDurationMinutes), }); } } return slots; } export async function ensureComplexAccess( complexId: string, userId: string ): Promise< Result<{ id: string; complexName: string; plan: { rules: unknown } | null; }> > { const complexUser = await db.complexUser.findUnique({ where: { complexId_userId: { complexId, userId }, }, include: { complex: { select: { id: true, complexName: true, plan: { select: { rules: true } } }, }, }, }); if (!complexUser) { return err(Errors.forbidden('No tienes permisos para administrar este complejo.')); } return ok(complexUser.complex); } export function resolvePrice( court: { basePrice: unknown; priceRules: Array<{ dayOfWeek: string | null; startTime: string | null; endTime: string | null; price: unknown; }>; }, dayOfWeek: string, startTime: string, endTime: string ): number { const slotStart = toMinutes(startTime); const slotEnd = toMinutes(endTime); const matchingRules = court.priceRules .filter((rule) => { if (rule.dayOfWeek && rule.dayOfWeek !== dayOfWeek) return false; if (!rule.startTime || !rule.endTime) return true; return slotStart >= toMinutes(rule.startTime) && slotEnd <= toMinutes(rule.endTime); }) .sort((first, second) => { const firstSpecificity = (first.dayOfWeek ? 2 : 0) + (first.startTime && first.endTime ? 1 : 0); const secondSpecificity = (second.dayOfWeek ? 2 : 0) + (second.startTime && second.endTime ? 1 : 0); return secondSpecificity - firstSpecificity; }); return Number(matchingRules[0]?.price ?? court.basePrice); } export function mapBookingResponse(booking: { id: string; bookingCode: string; bookingDate: Date; startTime: string; endTime: string; customerName: string; customerPhone: string; customerEmail: string; status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED' | 'NOSHOW'; recurringGroupId: string | null; createdAt: Date; updatedAt: Date; court: { id: string; name: string; complex: { id: string; complexName: string }; sport: { id: string; name: string; slug: string }; }; price?: number; }): AdminBooking { return { id: booking.id, bookingCode: booking.bookingCode, complexId: booking.court.complex.id, complexName: booking.court.complex.complexName, courtId: booking.court.id, courtName: booking.court.name, sport: { id: booking.court.sport.id, name: booking.court.sport.name, slug: booking.court.sport.slug, }, date: formatIsoDate(booking.bookingDate), startTime: booking.startTime, endTime: booking.endTime, customerName: booking.customerName, customerPhone: booking.customerPhone, customerEmail: booking.customerEmail, price: booking.price ?? 0, status: booking.status, recurringGroupId: booking.recurringGroupId, createdAt: booking.createdAt.toISOString(), updatedAt: booking.updatedAt.toISOString(), }; }