feat: add city/state/country to complex, new settings page with sidebar, and Biome linting
- Added city, state, and country optional fields to Complex model - Updated onboarding to include optional location fields - Created new settings page with sidebar navigation (Datos del Complejo, Canchas) - Replaced ESLint with Biome for frontend and backend linting - Added parallel dev script with concurrently - Migrated register-routes to use direct app.route() pattern
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import { randomInt } from 'node:crypto';
|
||||
import { db } from '@/lib/prisma';
|
||||
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service';
|
||||
import type {
|
||||
CreatePublicBookingInput,
|
||||
DayOfWeek,
|
||||
PublicAvailabilityQuery,
|
||||
} from '@repo/api-contract'
|
||||
import { randomInt } from 'node:crypto'
|
||||
import { v7 as uuidv7 } from 'uuid'
|
||||
import { db } from '@/lib/prisma'
|
||||
import { evaluatePlanUsage, parsePlanRules } from '@/modules/plan/services/plan-rules.service'
|
||||
} from '@repo/api-contract';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
type Slot = {
|
||||
startTime: string
|
||||
endTime: string
|
||||
}
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
};
|
||||
|
||||
type ComplexWithPublicBookingData = Awaited<ReturnType<typeof getComplexWithBookingData>>
|
||||
type ComplexWithPublicBookingData = Awaited<ReturnType<typeof getComplexWithBookingData>>;
|
||||
|
||||
const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
|
||||
'SUNDAY',
|
||||
@@ -23,44 +23,44 @@ const DAY_OF_WEEK_BY_INDEX: DayOfWeek[] = [
|
||||
'THURSDAY',
|
||||
'FRIDAY',
|
||||
'SATURDAY',
|
||||
]
|
||||
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
const BOOKING_CODE_LENGTH = 6
|
||||
];
|
||||
const BOOKING_CODE_ALPHABET = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
const BOOKING_CODE_LENGTH = 6;
|
||||
|
||||
export class PublicBookingServiceError extends Error {
|
||||
status: 400 | 403 | 404 | 409
|
||||
status: 400 | 403 | 404 | 409;
|
||||
|
||||
constructor(message: string, status: 400 | 403 | 404 | 409) {
|
||||
super(message)
|
||||
this.name = 'PublicBookingServiceError'
|
||||
this.status = status
|
||||
super(message);
|
||||
this.name = 'PublicBookingServiceError';
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
function toMinutes(value: string): number {
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part))
|
||||
return hours * 60 + minutes
|
||||
const [hours, minutes] = value.split(':').map((part) => Number(part));
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
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')}`
|
||||
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')}`;
|
||||
}
|
||||
|
||||
function parseIsoDate(date: string): { bookingDate: Date; dayOfWeek: DayOfWeek } {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date)
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
|
||||
|
||||
if (!match) {
|
||||
throw new PublicBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400)
|
||||
throw new PublicBookingServiceError('La fecha debe tener formato YYYY-MM-DD.', 400);
|
||||
}
|
||||
|
||||
const year = Number(match[1])
|
||||
const month = Number(match[2])
|
||||
const day = Number(match[3])
|
||||
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))
|
||||
const bookingDate = new Date(Date.UTC(year, month - 1, day));
|
||||
|
||||
if (
|
||||
Number.isNaN(bookingDate.getTime()) ||
|
||||
@@ -68,48 +68,48 @@ function parseIsoDate(date: string): { bookingDate: Date; dayOfWeek: DayOfWeek }
|
||||
bookingDate.getUTCMonth() + 1 !== month ||
|
||||
bookingDate.getUTCDate() !== day
|
||||
) {
|
||||
throw new PublicBookingServiceError('La fecha enviada no es valida.', 400)
|
||||
throw new PublicBookingServiceError('La fecha enviada no es valida.', 400);
|
||||
}
|
||||
|
||||
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()]
|
||||
const dayOfWeek = DAY_OF_WEEK_BY_INDEX[bookingDate.getUTCDay()];
|
||||
|
||||
if (!dayOfWeek) {
|
||||
throw new PublicBookingServiceError('No se pudo resolver el dia de la semana.', 400)
|
||||
throw new PublicBookingServiceError('No se pudo resolver el dia de la semana.', 400);
|
||||
}
|
||||
|
||||
return { bookingDate, dayOfWeek }
|
||||
return { bookingDate, dayOfWeek };
|
||||
}
|
||||
|
||||
function formatIsoDate(date: Date): string {
|
||||
return date.toISOString().slice(0, 10)
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function generateBookingCode(): string {
|
||||
let code = ''
|
||||
let code = '';
|
||||
|
||||
for (let index = 0; index < BOOKING_CODE_LENGTH; index += 1) {
|
||||
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)]
|
||||
code += BOOKING_CODE_ALPHABET[randomInt(0, BOOKING_CODE_ALPHABET.length)];
|
||||
}
|
||||
|
||||
return code
|
||||
return code;
|
||||
}
|
||||
|
||||
function hasOverlap(slot: Slot, existing: Slot): boolean {
|
||||
return slot.startTime < existing.endTime && slot.endTime > existing.startTime
|
||||
return slot.startTime < existing.endTime && slot.endTime > existing.startTime;
|
||||
}
|
||||
|
||||
function buildSlots(
|
||||
availability: Array<{
|
||||
startTime: string
|
||||
endTime: string
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
}>,
|
||||
slotDurationMinutes: number,
|
||||
slotDurationMinutes: number
|
||||
): Slot[] {
|
||||
const slots: Slot[] = []
|
||||
const slots: Slot[] = [];
|
||||
|
||||
for (const range of availability) {
|
||||
const start = toMinutes(range.startTime)
|
||||
const end = toMinutes(range.endTime)
|
||||
const start = toMinutes(range.startTime);
|
||||
const end = toMinutes(range.endTime);
|
||||
|
||||
for (
|
||||
let current = start;
|
||||
@@ -119,19 +119,19 @@ function buildSlots(
|
||||
slots.push({
|
||||
startTime: minutesToTime(current),
|
||||
endTime: minutesToTime(current + slotDurationMinutes),
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return slots
|
||||
return slots;
|
||||
}
|
||||
|
||||
function resolveSports(data: ComplexWithPublicBookingData) {
|
||||
const map = new Map<string, { id: string; name: string; slug: string }>()
|
||||
const map = new Map<string, { id: string; name: string; slug: string }>();
|
||||
|
||||
for (const court of data.courts) {
|
||||
if (!court.sport.isActive) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!map.has(court.sport.id)) {
|
||||
@@ -139,41 +139,41 @@ function resolveSports(data: ComplexWithPublicBookingData) {
|
||||
id: court.sport.id,
|
||||
name: court.sport.name,
|
||||
slug: court.sport.slug,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name, 'es'))
|
||||
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name, 'es'));
|
||||
}
|
||||
|
||||
function validateSportSelection(
|
||||
availableSports: Array<{ id: string }>,
|
||||
sportId: string | undefined,
|
||||
sportId: string | undefined
|
||||
): { sportSelectionRequired: boolean; selectedSportId: string | undefined } {
|
||||
const sportSelectionRequired = availableSports.length > 1
|
||||
const sportSelectionRequired = availableSports.length > 1;
|
||||
|
||||
if (sportSelectionRequired && !sportId) {
|
||||
throw new PublicBookingServiceError(
|
||||
'Debes seleccionar un deporte para consultar disponibilidad.',
|
||||
400,
|
||||
)
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (sportId && !availableSports.some((sport) => sport.id === sportId)) {
|
||||
throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400)
|
||||
throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400);
|
||||
}
|
||||
|
||||
if (sportSelectionRequired) {
|
||||
return {
|
||||
sportSelectionRequired,
|
||||
selectedSportId: sportId,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
sportSelectionRequired,
|
||||
selectedSportId: sportId ?? availableSports[0]?.id,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function getComplexWithBookingData(complexSlug: string) {
|
||||
@@ -205,48 +205,48 @@ async function getComplexWithBookingData(complexSlug: string) {
|
||||
orderBy: { createdAt: 'asc' },
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (!complex) {
|
||||
throw new PublicBookingServiceError('Complejo no encontrado.', 404)
|
||||
throw new PublicBookingServiceError('Complejo no encontrado.', 404);
|
||||
}
|
||||
|
||||
if (complex.plan) {
|
||||
const rules = parsePlanRules(complex.plan.rules)
|
||||
const rules = parsePlanRules(complex.plan.rules);
|
||||
|
||||
if (!rules.features.publicBookingPage) {
|
||||
throw new PublicBookingServiceError(
|
||||
'El complejo no tiene habilitada la reserva publica.',
|
||||
403,
|
||||
)
|
||||
403
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return complex
|
||||
return complex;
|
||||
}
|
||||
|
||||
function mapBookingResponse(input: {
|
||||
bookingId: string
|
||||
bookingCode: string
|
||||
bookingDate: Date
|
||||
createdAt: Date
|
||||
startTime: string
|
||||
endTime: string
|
||||
customerName: string
|
||||
customerPhone: string
|
||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED'
|
||||
bookingId: string;
|
||||
bookingCode: string;
|
||||
bookingDate: Date;
|
||||
createdAt: Date;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
customerName: string;
|
||||
customerPhone: string;
|
||||
status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED';
|
||||
court: {
|
||||
id: string
|
||||
name: string
|
||||
complexId: string
|
||||
complexName: string
|
||||
complexSlug: string
|
||||
id: string;
|
||||
name: string;
|
||||
complexId: string;
|
||||
complexName: string;
|
||||
complexSlug: string;
|
||||
sport: {
|
||||
id: string
|
||||
name: string
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
id: string;
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
};
|
||||
}) {
|
||||
return {
|
||||
id: input.bookingId,
|
||||
@@ -264,11 +264,11 @@ function mapBookingResponse(input: {
|
||||
customerPhone: input.customerPhone,
|
||||
status: input.status,
|
||||
createdAt: input.createdAt.toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function getPublicBookingConfirmation(complexSlug: string, bookingCode: string) {
|
||||
const normalizedBookingCode = bookingCode.toUpperCase()
|
||||
const normalizedBookingCode = bookingCode.toUpperCase();
|
||||
const booking = await db.courtBooking.findFirst({
|
||||
where: {
|
||||
bookingCode: normalizedBookingCode,
|
||||
@@ -304,10 +304,10 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (!booking) {
|
||||
throw new PublicBookingServiceError('Reserva no encontrada.', 404)
|
||||
throw new PublicBookingServiceError('Reserva no encontrada.', 404);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -325,39 +325,34 @@ export async function getPublicBookingConfirmation(complexSlug: string, bookingC
|
||||
},
|
||||
status: booking.status,
|
||||
createdAt: booking.createdAt.toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function listPublicAvailability(
|
||||
complexSlug: string,
|
||||
query: PublicAvailabilityQuery,
|
||||
) {
|
||||
const { bookingDate, dayOfWeek } = parseIsoDate(query.date)
|
||||
const complex = await getComplexWithBookingData(complexSlug)
|
||||
const sports = resolveSports(complex)
|
||||
const sportSelectionRequired = sports.length > 1
|
||||
export async function listPublicAvailability(complexSlug: string, query: PublicAvailabilityQuery) {
|
||||
const { bookingDate, dayOfWeek } = parseIsoDate(query.date);
|
||||
const complex = await getComplexWithBookingData(complexSlug);
|
||||
const sports = resolveSports(complex);
|
||||
const sportSelectionRequired = sports.length > 1;
|
||||
|
||||
if (query.sportId && !sports.some((sport) => sport.id === query.sportId)) {
|
||||
throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400)
|
||||
throw new PublicBookingServiceError('El deporte seleccionado no pertenece al complejo.', 400);
|
||||
}
|
||||
|
||||
const selectedSportId = sportSelectionRequired
|
||||
? query.sportId
|
||||
: (query.sportId ?? sports[0]?.id)
|
||||
const selectedSportId = sportSelectionRequired ? query.sportId : (query.sportId ?? sports[0]?.id);
|
||||
|
||||
const candidateCourts = complex.courts.filter((court) => {
|
||||
if (!court.sport.isActive) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectedSportId) {
|
||||
return court.sportId === selectedSportId
|
||||
return court.sportId === selectedSportId;
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
return true;
|
||||
});
|
||||
|
||||
const courtIds = candidateCourts.map((court) => court.id)
|
||||
const courtIds = candidateCourts.map((court) => court.id);
|
||||
|
||||
const bookings =
|
||||
courtIds.length > 0
|
||||
@@ -375,31 +370,31 @@ export async function listPublicAvailability(
|
||||
endTime: true,
|
||||
},
|
||||
})
|
||||
: []
|
||||
: [];
|
||||
|
||||
const bookingsByCourt = new Map<string, Slot[]>()
|
||||
const bookingsByCourt = new Map<string, Slot[]>();
|
||||
|
||||
for (const booking of bookings) {
|
||||
const current = bookingsByCourt.get(booking.courtId) ?? []
|
||||
const current = bookingsByCourt.get(booking.courtId) ?? [];
|
||||
current.push({
|
||||
startTime: booking.startTime,
|
||||
endTime: booking.endTime,
|
||||
})
|
||||
bookingsByCourt.set(booking.courtId, current)
|
||||
});
|
||||
bookingsByCourt.set(booking.courtId, current);
|
||||
}
|
||||
|
||||
const courts = candidateCourts
|
||||
.map((court) => {
|
||||
const dayAvailability = court.availabilities.filter(
|
||||
(availability) => availability.dayOfWeek === dayOfWeek,
|
||||
)
|
||||
(availability) => availability.dayOfWeek === dayOfWeek
|
||||
);
|
||||
|
||||
const allSlots = buildSlots(dayAvailability, court.slotDurationMinutes)
|
||||
const occupiedSlots = bookingsByCourt.get(court.id) ?? []
|
||||
const allSlots = buildSlots(dayAvailability, court.slotDurationMinutes);
|
||||
const occupiedSlots = bookingsByCourt.get(court.id) ?? [];
|
||||
|
||||
const availableSlots = allSlots.filter(
|
||||
(slot) => !occupiedSlots.some((occupied) => hasOverlap(slot, occupied)),
|
||||
)
|
||||
(slot) => !occupiedSlots.some((occupied) => hasOverlap(slot, occupied))
|
||||
);
|
||||
|
||||
return {
|
||||
courtId: court.id,
|
||||
@@ -412,9 +407,9 @@ export async function listPublicAvailability(
|
||||
slotDurationMinutes: court.slotDurationMinutes,
|
||||
availabilityDay: dayOfWeek,
|
||||
availableSlots,
|
||||
}
|
||||
};
|
||||
})
|
||||
.filter((court) => court.availableSlots.length > 0)
|
||||
.filter((court) => court.availableSlots.length > 0);
|
||||
|
||||
return {
|
||||
complexId: complex.id,
|
||||
@@ -424,69 +419,66 @@ export async function listPublicAvailability(
|
||||
sportSelectionRequired,
|
||||
sports,
|
||||
courts,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function createPublicBooking(
|
||||
complexSlug: string,
|
||||
input: CreatePublicBookingInput,
|
||||
) {
|
||||
const { bookingDate, dayOfWeek } = parseIsoDate(input.date)
|
||||
const complex = await getComplexWithBookingData(complexSlug)
|
||||
const sports = resolveSports(complex)
|
||||
const { sportSelectionRequired } = validateSportSelection(sports, input.sportId)
|
||||
export async function createPublicBooking(complexSlug: string, input: CreatePublicBookingInput) {
|
||||
const { bookingDate, dayOfWeek } = parseIsoDate(input.date);
|
||||
const complex = await getComplexWithBookingData(complexSlug);
|
||||
const sports = resolveSports(complex);
|
||||
const { sportSelectionRequired } = validateSportSelection(sports, input.sportId);
|
||||
|
||||
const selectedCourt = complex.courts.find(
|
||||
(court) => court.id === input.courtId && court.sport.isActive,
|
||||
)
|
||||
(court) => court.id === input.courtId && court.sport.isActive
|
||||
);
|
||||
|
||||
if (!selectedCourt) {
|
||||
throw new PublicBookingServiceError('La cancha seleccionada no existe en el complejo.', 404)
|
||||
throw new PublicBookingServiceError('La cancha seleccionada no existe en el complejo.', 404);
|
||||
}
|
||||
|
||||
if (sportSelectionRequired && !input.sportId) {
|
||||
throw new PublicBookingServiceError(
|
||||
'Debes seleccionar un deporte para reservar en este complejo.',
|
||||
400,
|
||||
)
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
if (input.sportId && input.sportId !== selectedCourt.sportId) {
|
||||
throw new PublicBookingServiceError(
|
||||
'La cancha seleccionada no corresponde al deporte indicado.',
|
||||
400,
|
||||
)
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
const dayAvailability = selectedCourt.availabilities.filter(
|
||||
(availability) => availability.dayOfWeek === dayOfWeek,
|
||||
)
|
||||
(availability) => availability.dayOfWeek === dayOfWeek
|
||||
);
|
||||
|
||||
const validSlots = buildSlots(dayAvailability, selectedCourt.slotDurationMinutes)
|
||||
const selectedStartMinutes = toMinutes(input.startTime)
|
||||
const selectedEndMinutes = selectedStartMinutes + selectedCourt.slotDurationMinutes
|
||||
const validSlots = buildSlots(dayAvailability, selectedCourt.slotDurationMinutes);
|
||||
const selectedStartMinutes = toMinutes(input.startTime);
|
||||
const selectedEndMinutes = selectedStartMinutes + selectedCourt.slotDurationMinutes;
|
||||
|
||||
const selectedSlot = {
|
||||
startTime: input.startTime,
|
||||
endTime: minutesToTime(selectedEndMinutes),
|
||||
}
|
||||
};
|
||||
|
||||
const slotExists = validSlots.some(
|
||||
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime,
|
||||
)
|
||||
(slot) => slot.startTime === selectedSlot.startTime && slot.endTime === selectedSlot.endTime
|
||||
);
|
||||
|
||||
if (!slotExists) {
|
||||
throw new PublicBookingServiceError(
|
||||
'El horario seleccionado no esta disponible para esa cancha.',
|
||||
409,
|
||||
)
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
try {
|
||||
const booking = await db.$transaction(async (tx) => {
|
||||
if (complex.plan) {
|
||||
const rules = parsePlanRules(complex.plan.rules)
|
||||
const rules = parsePlanRules(complex.plan.rules);
|
||||
const bookingsForDate = await tx.courtBooking.count({
|
||||
where: {
|
||||
bookingDate,
|
||||
@@ -495,19 +487,19 @@ export async function createPublicBooking(
|
||||
complexId: complex.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
const violations = evaluatePlanUsage(rules, {
|
||||
courtsCount: complex.courts.length,
|
||||
bookingsToday: bookingsForDate,
|
||||
})
|
||||
});
|
||||
|
||||
const maxBookingsViolation = violations.find(
|
||||
(violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED',
|
||||
)
|
||||
(violation) => violation.code === 'MAX_BOOKINGS_PER_DAY_REACHED'
|
||||
);
|
||||
|
||||
if (maxBookingsViolation) {
|
||||
throw new PublicBookingServiceError(maxBookingsViolation.message, 409)
|
||||
throw new PublicBookingServiceError(maxBookingsViolation.message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,10 +515,10 @@ export async function createPublicBooking(
|
||||
gt: selectedSlot.startTime,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
|
||||
if (overlappingBooking) {
|
||||
throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409)
|
||||
throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409);
|
||||
}
|
||||
|
||||
return tx.courtBooking.create({
|
||||
@@ -552,8 +544,8 @@ export async function createPublicBooking(
|
||||
customerPhone: true,
|
||||
status: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
return mapBookingResponse({
|
||||
bookingId: booking.id,
|
||||
@@ -577,40 +569,40 @@ export async function createPublicBooking(
|
||||
slug: selectedCourt.sport.slug,
|
||||
},
|
||||
},
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof PublicBookingServiceError) {
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
|
||||
const prismaError = error as {
|
||||
code?: string
|
||||
code?: string;
|
||||
meta?: {
|
||||
target?: string[] | string
|
||||
}
|
||||
}
|
||||
target?: string[] | string;
|
||||
};
|
||||
};
|
||||
|
||||
if (prismaError.code === 'P2002') {
|
||||
const targets = Array.isArray(prismaError.meta?.target)
|
||||
? prismaError.meta?.target
|
||||
: [prismaError.meta?.target]
|
||||
: [prismaError.meta?.target];
|
||||
const isBookingCodeCollision = targets.some((target) =>
|
||||
String(target).includes('booking_code'),
|
||||
)
|
||||
String(target).includes('booking_code')
|
||||
);
|
||||
|
||||
if (isBookingCodeCollision) {
|
||||
continue
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409)
|
||||
throw new PublicBookingServiceError('El horario seleccionado ya fue reservado.', 409);
|
||||
}
|
||||
|
||||
throw error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new PublicBookingServiceError(
|
||||
'No se pudo generar un codigo de reserva unico. Intenta nuevamente.',
|
||||
409,
|
||||
)
|
||||
409
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user