217 lines
5.7 KiB
TypeScript
217 lines
5.7 KiB
TypeScript
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<Date> {
|
|
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<DayOfWeek> {
|
|
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(),
|
|
};
|
|
}
|