Refactor to vertical slice
This commit is contained in:
216
apps/backend/src/modules/admin-booking/shared/helpers.ts
Normal file
216
apps/backend/src/modules/admin-booking/shared/helpers.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
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: string } | 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(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user