Files
playzer/packages/api-contract/src/court.ts
Jose Selesan 9ae270609d Initial commit
2026-04-08 22:53:11 -03:00

188 lines
5.4 KiB
TypeScript

import { z } from 'zod'
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/
function toMinutes(value: string): number {
const [hours, minutes] = value.split(':').map((part) => Number(part))
return hours * 60 + minutes
}
function hasOverlaps(
availability: Array<{
dayOfWeek: DayOfWeek
startTime: string
endTime: string
}>,
): boolean {
const grouped = new Map<
DayOfWeek,
Array<{
start: number
end: number
}>
>()
for (const range of availability) {
const start = toMinutes(range.startTime)
const end = toMinutes(range.endTime)
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) {
return true
}
}
}
return false
}
function refineAvailability(
availability: Array<{
dayOfWeek: DayOfWeek
startTime: string
endTime: string
}>,
context: z.RefinementCtx,
) {
for (const range of availability) {
if (toMinutes(range.startTime) >= toMinutes(range.endTime)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: `El rango ${range.startTime}-${range.endTime} es invalido. La hora de inicio debe ser menor a la de fin.`,
})
}
}
if (hasOverlaps(availability)) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: 'Hay rangos horarios superpuestos para el mismo dia.',
})
}
}
export const dayOfWeekSchema = z.enum([
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
'SUNDAY',
])
export type DayOfWeek = z.infer<typeof dayOfWeekSchema>
export const courtAvailabilitySchema = z.object({
id: z.uuid(),
dayOfWeek: dayOfWeekSchema,
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
})
export const courtAvailabilityInputSchema = z.object({
dayOfWeek: dayOfWeekSchema,
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
endTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
})
export const courtPriceRuleSchema = z.object({
id: z.uuid(),
dayOfWeek: dayOfWeekSchema.nullable(),
startTime: z.string().nullable(),
endTime: z.string().nullable(),
price: z.number().nonnegative(),
isActive: z.boolean(),
})
export const courtSportSchema = z.object({
id: z.uuid(),
name: z.string(),
slug: z.string(),
})
export const courtSchema = z.object({
id: z.uuid(),
complexId: z.uuid(),
name: z.string(),
sportId: z.uuid(),
sport: courtSportSchema,
slotDurationMinutes: z.int().positive(),
basePrice: z.number().nonnegative(),
availability: z.array(courtAvailabilitySchema),
priceRules: z.array(courtPriceRuleSchema),
hasCustomPricing: z.boolean(),
createdAt: z.string().datetime(),
updatedAt: z.string().datetime(),
})
export const createCourtSchema = z
.object({
name: z
.string()
.trim()
.min(2, 'La cancha debe tener al menos 2 caracteres.')
.max(120, 'La cancha no puede superar los 120 caracteres.'),
sportId: z.uuid('El sportId debe ser un UUID valido.'),
slotDurationMinutes: z
.int()
.min(15, 'La duracion minima por turno es 15 minutos.')
.max(480, 'La duracion maxima por turno es 480 minutos.'),
basePrice: z.number().nonnegative('El precio base no puede ser negativo.'),
availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.'),
})
.superRefine((payload, context) => refineAvailability(payload.availability, context))
export const updateCourtSchema = z
.object({
name: z
.string()
.trim()
.min(2, 'La cancha debe tener al menos 2 caracteres.')
.max(120, 'La cancha no puede superar los 120 caracteres.')
.optional(),
sportId: z.uuid('El sportId debe ser un UUID valido.').optional(),
slotDurationMinutes: z
.int()
.min(15, 'La duracion minima por turno es 15 minutos.')
.max(480, 'La duracion maxima por turno es 480 minutos.')
.optional(),
basePrice: z.number().nonnegative('El precio base no puede ser negativo.').optional(),
availability: z.array(courtAvailabilityInputSchema).min(1, 'Debes definir al menos un rango horario.').optional(),
})
.superRefine((payload, context) => {
if (
payload.name === undefined &&
payload.sportId === undefined &&
payload.slotDurationMinutes === undefined &&
payload.basePrice === undefined &&
payload.availability === undefined
) {
context.addIssue({
code: z.ZodIssueCode.custom,
message: 'Debes enviar al menos un campo para actualizar.',
})
}
if (payload.availability) {
refineAvailability(payload.availability, context)
}
})
export type Court = z.infer<typeof courtSchema>
export type CourtAvailability = z.infer<typeof courtAvailabilitySchema>
export type CourtAvailabilityInput = z.infer<typeof courtAvailabilityInputSchema>
export type CourtPriceRule = z.infer<typeof courtPriceRuleSchema>
export type CreateCourtInput = z.infer<typeof createCourtSchema>
export type UpdateCourtInput = z.infer<typeof updateCourtSchema>