import { randomInt } from 'node:crypto'; import type { DayOfWeek } from '@repo/api-contract'; export type Slot = { startTime: string; endTime: string; }; const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; const BOOKING_CODE_LENGTH = 6; export const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [ 'SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY', ]; 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): { bookingDate: Date; dayOfWeek: DayOfWeek } { const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date); if (!match) { throw new Error('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 ) { throw new Error('La fecha enviada no es valida.'); } const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()]; if (!dayOfWeek) { throw new Error('No se pudo resolver el dia de la semana.'); } return { bookingDate, dayOfWeek }; } export function formatIsoDate(date: Date): string { return date.toISOString().slice(0, 10); } export function getDayOfWeekValue(date: Date): DayOfWeek { const dayOfWeek = DAY_OF_WEEK_BY_INDEX[date.getUTCDay()]; if (!dayOfWeek) { throw new Error('No se pudo resolver el dia de la semana.'); } return 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 function hasOverlap(slot: Slot, existing: Slot): boolean { return slot.startTime < existing.endTime && slot.endTime > existing.startTime; } type PriceableCourt = { basePrice: unknown; priceRules: Array<{ dayOfWeek: DayOfWeek | null; startTime: string | null; endTime: string | null; price: unknown; }>; }; export function resolveSlotPrice(court: PriceableCourt, dayOfWeek: DayOfWeek, slot: Slot): number { const slotStart = toMinutes(slot.startTime); const slotEnd = toMinutes(slot.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 assertAvailabilityRanges( availability: Array<{ dayOfWeek: DayOfWeek; startTime: string; endTime: string; }> ) { const grouped = new Map>(); for (const range of availability) { const start = toMinutes(range.startTime); const end = toMinutes(range.endTime); if (start >= end) { throw new Error(`El rango ${range.startTime}-${range.endTime} es invalido.`); } const current = grouped.get(range.dayOfWeek) ?? []; current.push({ start, end }); grouped.set(range.dayOfWeek, current); } for (const ranges of grouped.values()) { ranges.sort((a, b) => a.start - b.start); for (let index = 1; index < ranges.length; index += 1) { const previous = ranges[index - 1]; const current = ranges[index]; if (previous.end > current.start) { throw new Error('Hay rangos horarios superpuestos para el mismo dia.'); } } } }