Initial commit
This commit is contained in:
12
packages/api-contract/package.json
Normal file
12
packages/api-contract/package.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "@repo/api-contract",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"hono": "^4.12.10"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
}
|
||||
}
|
||||
83
packages/api-contract/src/complex.ts
Normal file
83
packages/api-contract/src/complex.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const complexSchema = z.object({
|
||||
id: z.uuid(),
|
||||
complexName: z.string(),
|
||||
physicalAddress: z.string().nullable(),
|
||||
complexSlug: z.string(),
|
||||
adminEmail: z.email(),
|
||||
planCode: z.string().max(10).nullable(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
|
||||
export const createComplexSchema = z.object({
|
||||
complexName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, 'El nombre del complejo debe tener al menos 3 caracteres.')
|
||||
.max(120, 'El nombre del complejo no puede superar los 120 caracteres.'),
|
||||
physicalAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(5, 'La direccion debe tener al menos 5 caracteres.')
|
||||
.max(200, 'La direccion no puede superar los 200 caracteres.'),
|
||||
planCode: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'El planCode no puede estar vacío.')
|
||||
.max(10, 'El planCode no puede superar los 10 caracteres.')
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const updateComplexSchema = z
|
||||
.object({
|
||||
complexName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, 'El nombre del complejo debe tener al menos 3 caracteres.')
|
||||
.max(120, 'El nombre del complejo no puede superar los 120 caracteres.')
|
||||
.optional(),
|
||||
physicalAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(5, 'La direccion debe tener al menos 5 caracteres.')
|
||||
.max(200, 'La direccion no puede superar los 200 caracteres.')
|
||||
.nullable()
|
||||
.optional(),
|
||||
complexSlug: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, 'El slug del complejo debe tener al menos 3 caracteres.')
|
||||
.max(120, 'El slug del complejo no puede superar los 120 caracteres.')
|
||||
.optional(),
|
||||
adminEmail: z.email('El adminEmail debe ser un email válido.').optional(),
|
||||
planCode: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'El planCode no puede estar vacío.')
|
||||
.max(10, 'El planCode no puede superar los 10 caracteres.')
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.refine(
|
||||
(payload) =>
|
||||
Boolean(
|
||||
payload.complexName ||
|
||||
payload.physicalAddress !== undefined ||
|
||||
payload.complexSlug ||
|
||||
payload.adminEmail ||
|
||||
payload.planCode !== undefined,
|
||||
),
|
||||
{
|
||||
message: 'Debes enviar al menos un campo para actualizar.',
|
||||
},
|
||||
)
|
||||
|
||||
export type Complex = z.infer<typeof complexSchema>
|
||||
export type CreateComplexInput = z.infer<typeof createComplexSchema>
|
||||
export type UpdateComplexInput = z.infer<typeof updateComplexSchema>
|
||||
export type CreateComplexPayload = CreateComplexInput
|
||||
export type CreateComplexResponse = Complex
|
||||
export type UpdateComplexPayload = UpdateComplexInput
|
||||
export type UpdateComplexResponse = Complex
|
||||
187
packages/api-contract/src/court.ts
Normal file
187
packages/api-contract/src/court.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
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>
|
||||
81
packages/api-contract/src/index.ts
Normal file
81
packages/api-contract/src/index.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
export { API_ROUTE_PATHS, registerApiRoutes } from './register-routes'
|
||||
export {
|
||||
planFeatureFlagsSchema,
|
||||
planRulesSchema,
|
||||
planRulesV1Schema,
|
||||
planSchema,
|
||||
planSummarySchema,
|
||||
} from './plan'
|
||||
export type { Plan, PlanFeatureFlags, PlanRules, PlanRulesV1, PlanSummary } from './plan'
|
||||
export {
|
||||
onboardingCompleteResponseSchema,
|
||||
onboardingCompleteSchema,
|
||||
onboardingResendOtpResponseSchema,
|
||||
onboardingResendOtpSchema,
|
||||
onboardingStartResponseSchema,
|
||||
onboardingStartSchema,
|
||||
onboardingVerifyOtpResponseSchema,
|
||||
onboardingVerifyOtpSchema,
|
||||
} from './onboarding'
|
||||
export type {
|
||||
OnboardingCompleteInput,
|
||||
OnboardingCompleteResponse,
|
||||
OnboardingResendOtpInput,
|
||||
OnboardingResendOtpResponse,
|
||||
OnboardingStartInput,
|
||||
OnboardingStartResponse,
|
||||
OnboardingVerifyOtpInput,
|
||||
OnboardingVerifyOtpResponse,
|
||||
} from './onboarding'
|
||||
export { complexSchema, createComplexSchema, updateComplexSchema } from './complex'
|
||||
export type {
|
||||
Complex,
|
||||
CreateComplexInput,
|
||||
CreateComplexPayload,
|
||||
CreateComplexResponse,
|
||||
UpdateComplexInput,
|
||||
UpdateComplexPayload,
|
||||
UpdateComplexResponse,
|
||||
} from './complex'
|
||||
export {
|
||||
dayOfWeekSchema,
|
||||
courtAvailabilityInputSchema,
|
||||
courtAvailabilitySchema,
|
||||
courtPriceRuleSchema,
|
||||
courtSchema,
|
||||
createCourtSchema,
|
||||
updateCourtSchema,
|
||||
} from './court'
|
||||
export type {
|
||||
DayOfWeek,
|
||||
Court,
|
||||
CourtAvailability,
|
||||
CourtAvailabilityInput,
|
||||
CourtPriceRule,
|
||||
CreateCourtInput,
|
||||
UpdateCourtInput,
|
||||
} from './court'
|
||||
export {
|
||||
createPublicBookingSchema,
|
||||
publicAvailabilityCourtSchema,
|
||||
publicAvailabilityQuerySchema,
|
||||
publicAvailabilityResponseSchema,
|
||||
publicBookingConfirmationSchema,
|
||||
publicBookingSchema,
|
||||
publicBookingSlotSchema,
|
||||
publicBookingSportSchema,
|
||||
} from './public-booking'
|
||||
export type {
|
||||
CreatePublicBookingInput,
|
||||
PublicAvailabilityCourt,
|
||||
PublicAvailabilityQuery,
|
||||
PublicAvailabilityResponse,
|
||||
PublicBooking,
|
||||
PublicBookingConfirmation,
|
||||
PublicBookingSlot,
|
||||
PublicBookingSport,
|
||||
} from './public-booking'
|
||||
export { sportSchema, createSportSchema, updateSportSchema } from './sport'
|
||||
export type { Sport, CreateSportInput, UpdateSportInput } from './sport'
|
||||
export { userProfileSchema } from './user'
|
||||
export type { UserProfile, UserProfileResponse } from './user'
|
||||
98
packages/api-contract/src/onboarding.ts
Normal file
98
packages/api-contract/src/onboarding.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const otpSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.regex(/^\d{6}$/, 'El OTP debe tener 6 digitos numericos.')
|
||||
|
||||
export const onboardingStartSchema = z.object({
|
||||
fullName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, 'El nombre debe tener al menos 2 caracteres.')
|
||||
.max(80, 'El nombre no puede superar los 80 caracteres.'),
|
||||
email: z.email('Ingresa un email valido.'),
|
||||
})
|
||||
|
||||
export const onboardingVerifyOtpSchema = z.object({
|
||||
requestId: z.uuid('El requestId de onboarding no es valido.'),
|
||||
otp: otpSchema,
|
||||
})
|
||||
|
||||
export const onboardingResendOtpSchema = z.object({
|
||||
requestId: z.uuid('El requestId de onboarding no es valido.'),
|
||||
})
|
||||
|
||||
export const onboardingCompleteSchema = z.object({
|
||||
onboardingRequestId: z.uuid('El onboardingRequestId no es valido.'),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, 'La contrasena debe tener al menos 8 caracteres.')
|
||||
.max(72, 'La contrasena no puede superar los 72 caracteres.'),
|
||||
complexName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(3, 'El nombre del complejo debe tener al menos 3 caracteres.')
|
||||
.max(120, 'El nombre del complejo no puede superar los 120 caracteres.'),
|
||||
physicalAddress: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(5, 'La direccion debe tener al menos 5 caracteres.')
|
||||
.max(200, 'La direccion no puede superar los 200 caracteres.'),
|
||||
planCode: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'Debes seleccionar un plan.')
|
||||
.max(10, 'El plan seleccionado no es valido.'),
|
||||
})
|
||||
|
||||
export const onboardingStartResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
requestId: z.uuid(),
|
||||
email: z.email(),
|
||||
expiresAt: z.string().datetime(),
|
||||
cooldownSeconds: z.int().nonnegative(),
|
||||
})
|
||||
|
||||
export const onboardingVerifyOtpResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
verified: z.boolean(),
|
||||
requestId: z.uuid(),
|
||||
email: z.email().nullable(),
|
||||
expiresAt: z.string().datetime().nullable(),
|
||||
remainingAttempts: z.int().nonnegative(),
|
||||
cooldownSeconds: z.int().nonnegative(),
|
||||
})
|
||||
|
||||
export const onboardingResendOtpResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
requestId: z.uuid(),
|
||||
email: z.email(),
|
||||
expiresAt: z.string().datetime(),
|
||||
cooldownSeconds: z.int().nonnegative(),
|
||||
remainingAttempts: z.int().nonnegative(),
|
||||
})
|
||||
|
||||
export const onboardingCompleteResponseSchema = z.object({
|
||||
message: z.string(),
|
||||
userId: z.uuid(),
|
||||
complexId: z.uuid(),
|
||||
complexSlug: z.string(),
|
||||
supabaseUserId: z.uuid(),
|
||||
})
|
||||
|
||||
export type OnboardingStartInput = z.infer<typeof onboardingStartSchema>
|
||||
export type OnboardingVerifyOtpInput = z.infer<typeof onboardingVerifyOtpSchema>
|
||||
export type OnboardingResendOtpInput = z.infer<typeof onboardingResendOtpSchema>
|
||||
export type OnboardingCompleteInput = z.infer<typeof onboardingCompleteSchema>
|
||||
|
||||
export type OnboardingStartResponse = z.infer<typeof onboardingStartResponseSchema>
|
||||
export type OnboardingVerifyOtpResponse = z.infer<
|
||||
typeof onboardingVerifyOtpResponseSchema
|
||||
>
|
||||
export type OnboardingResendOtpResponse = z.infer<
|
||||
typeof onboardingResendOtpResponseSchema
|
||||
>
|
||||
export type OnboardingCompleteResponse = z.infer<
|
||||
typeof onboardingCompleteResponseSchema
|
||||
>
|
||||
56
packages/api-contract/src/plan.ts
Normal file
56
packages/api-contract/src/plan.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const nonNegativeInt = z.int().nonnegative()
|
||||
const positiveInt = z.int().positive()
|
||||
|
||||
export const planFeatureFlagsSchema = z.object({
|
||||
onlinePayments: z.boolean().default(false),
|
||||
publicBookingPage: z.boolean().default(false),
|
||||
advancedReports: z.boolean().default(false),
|
||||
whatsappReminders: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export const planRulesV1Schema = z.object({
|
||||
version: z.literal('v1'),
|
||||
limits: z.object({
|
||||
maxCourts: positiveInt,
|
||||
maxBookingsPerDay: positiveInt,
|
||||
maxActiveUsers: positiveInt.optional(),
|
||||
maxConcurrentBookingsPerSlot: positiveInt.optional(),
|
||||
}),
|
||||
policies: z
|
||||
.object({
|
||||
maxAdvanceBookingDays: positiveInt.optional(),
|
||||
minCancellationNoticeHours: nonNegativeInt.optional(),
|
||||
allowOverbooking: z.boolean().default(false),
|
||||
})
|
||||
.default({ allowOverbooking: false }),
|
||||
features: planFeatureFlagsSchema.default({
|
||||
onlinePayments: false,
|
||||
publicBookingPage: false,
|
||||
advancedReports: false,
|
||||
whatsappReminders: false,
|
||||
}),
|
||||
})
|
||||
|
||||
export const planRulesSchema = z.discriminatedUnion('version', [planRulesV1Schema])
|
||||
|
||||
export const planSchema = z.object({
|
||||
code: z.string().trim().min(1).max(10),
|
||||
name: z.string().trim().min(1).max(30),
|
||||
price: z.number().nonnegative(),
|
||||
lastUpdatedAt: z.string().datetime(),
|
||||
rules: planRulesSchema,
|
||||
})
|
||||
|
||||
export const planSummarySchema = z.object({
|
||||
code: z.string().trim().min(1).max(10),
|
||||
name: z.string().trim().min(1).max(30),
|
||||
price: z.number().nonnegative(),
|
||||
})
|
||||
|
||||
export type PlanFeatureFlags = z.infer<typeof planFeatureFlagsSchema>
|
||||
export type PlanRulesV1 = z.infer<typeof planRulesV1Schema>
|
||||
export type PlanRules = z.infer<typeof planRulesSchema>
|
||||
export type Plan = z.infer<typeof planSchema>
|
||||
export type PlanSummary = z.infer<typeof planSummarySchema>
|
||||
98
packages/api-contract/src/public-booking.ts
Normal file
98
packages/api-contract/src/public-booking.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { z } from 'zod'
|
||||
import { dayOfWeekSchema } from './court'
|
||||
|
||||
const TIME_REGEX = /^([01]\d|2[0-3]):([0-5]\d)$/
|
||||
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/
|
||||
const BOOKING_CODE_REGEX = /^[A-Z0-9]{6,8}$/
|
||||
|
||||
export const publicAvailabilityQuerySchema = z.object({
|
||||
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
|
||||
sportId: z.uuid('El sportId debe ser un UUID valido.').optional(),
|
||||
})
|
||||
|
||||
export const publicBookingSportSchema = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
})
|
||||
|
||||
export const publicBookingSlotSchema = z.object({
|
||||
startTime: z.string().regex(TIME_REGEX),
|
||||
endTime: z.string().regex(TIME_REGEX),
|
||||
})
|
||||
|
||||
export const publicAvailabilityCourtSchema = z.object({
|
||||
courtId: z.uuid(),
|
||||
courtName: z.string(),
|
||||
sport: publicBookingSportSchema,
|
||||
slotDurationMinutes: z.int().positive(),
|
||||
availabilityDay: dayOfWeekSchema,
|
||||
availableSlots: z.array(publicBookingSlotSchema),
|
||||
})
|
||||
|
||||
export const publicAvailabilityResponseSchema = z.object({
|
||||
complexId: z.uuid(),
|
||||
complexName: z.string(),
|
||||
complexSlug: z.string(),
|
||||
date: z.string().regex(ISO_DATE_REGEX),
|
||||
sportSelectionRequired: z.boolean(),
|
||||
sports: z.array(publicBookingSportSchema),
|
||||
courts: z.array(publicAvailabilityCourtSchema),
|
||||
})
|
||||
|
||||
export const createPublicBookingSchema = z.object({
|
||||
date: z.string().regex(ISO_DATE_REGEX, 'La fecha debe tener formato YYYY-MM-DD.'),
|
||||
sportId: z.uuid('El sportId debe ser un UUID valido.').optional(),
|
||||
courtId: z.uuid('El courtId debe ser un UUID valido.'),
|
||||
startTime: z.string().regex(TIME_REGEX, 'La hora debe tener formato HH:mm.'),
|
||||
customerName: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, 'El nombre debe tener al menos 2 caracteres.')
|
||||
.max(120, 'El nombre no puede superar los 120 caracteres.'),
|
||||
customerPhone: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(6, 'El telefono debe tener al menos 6 caracteres.')
|
||||
.max(30, 'El telefono no puede superar los 30 caracteres.'),
|
||||
})
|
||||
|
||||
export const publicBookingSchema = z.object({
|
||||
id: z.uuid(),
|
||||
bookingCode: z.string().regex(BOOKING_CODE_REGEX),
|
||||
complexId: z.uuid(),
|
||||
complexName: z.string(),
|
||||
complexSlug: z.string(),
|
||||
courtId: z.uuid(),
|
||||
courtName: z.string(),
|
||||
sport: publicBookingSportSchema,
|
||||
date: z.string().regex(ISO_DATE_REGEX),
|
||||
startTime: z.string().regex(TIME_REGEX),
|
||||
endTime: z.string().regex(TIME_REGEX),
|
||||
customerName: z.string(),
|
||||
customerPhone: z.string(),
|
||||
status: z.enum(['CONFIRMED', 'CANCELLED']),
|
||||
createdAt: z.string().datetime(),
|
||||
})
|
||||
|
||||
export const publicBookingConfirmationSchema = z.object({
|
||||
bookingCode: z.string().regex(BOOKING_CODE_REGEX),
|
||||
complexName: z.string(),
|
||||
complexSlug: z.string(),
|
||||
date: z.string().regex(ISO_DATE_REGEX),
|
||||
startTime: z.string().regex(TIME_REGEX),
|
||||
endTime: z.string().regex(TIME_REGEX),
|
||||
courtName: z.string(),
|
||||
sport: publicBookingSportSchema,
|
||||
status: z.enum(['CONFIRMED', 'CANCELLED']),
|
||||
createdAt: z.string().datetime(),
|
||||
})
|
||||
|
||||
export type PublicAvailabilityQuery = z.infer<typeof publicAvailabilityQuerySchema>
|
||||
export type PublicBookingSport = z.infer<typeof publicBookingSportSchema>
|
||||
export type PublicBookingSlot = z.infer<typeof publicBookingSlotSchema>
|
||||
export type PublicAvailabilityCourt = z.infer<typeof publicAvailabilityCourtSchema>
|
||||
export type PublicAvailabilityResponse = z.infer<typeof publicAvailabilityResponseSchema>
|
||||
export type CreatePublicBookingInput = z.infer<typeof createPublicBookingSchema>
|
||||
export type PublicBooking = z.infer<typeof publicBookingSchema>
|
||||
export type PublicBookingConfirmation = z.infer<typeof publicBookingConfirmationSchema>
|
||||
95
packages/api-contract/src/register-routes.ts
Normal file
95
packages/api-contract/src/register-routes.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import type { Env, Hono, Schema } from 'hono'
|
||||
|
||||
type HonoLike<E extends Env = Env, S extends Schema = Schema, B extends string = string> =
|
||||
Hono<E, S, B>
|
||||
|
||||
type ApiRouteModules<
|
||||
UE extends Env = Env,
|
||||
US extends Schema = Schema,
|
||||
UB extends string = string,
|
||||
PLE extends Env = Env,
|
||||
PLS extends Schema = Schema,
|
||||
PLB extends string = string,
|
||||
OE extends Env = Env,
|
||||
OS extends Schema = Schema,
|
||||
OB extends string = string,
|
||||
CE extends Env = Env,
|
||||
CS extends Schema = Schema,
|
||||
CB extends string = string,
|
||||
SPE extends Env = Env,
|
||||
SPS extends Schema = Schema,
|
||||
SPB extends string = string,
|
||||
COE extends Env = Env,
|
||||
COS extends Schema = Schema,
|
||||
COB extends string = string,
|
||||
> = {
|
||||
user: HonoLike<UE, US, UB>
|
||||
plans: HonoLike<PLE, PLS, PLB>
|
||||
onboarding: HonoLike<OE, OS, OB>
|
||||
complexes: HonoLike<CE, CS, CB>
|
||||
sports: HonoLike<SPE, SPS, SPB>
|
||||
courts: HonoLike<COE, COS, COB>
|
||||
}
|
||||
|
||||
export const API_ROUTE_PATHS = {
|
||||
user: '/api/user',
|
||||
plans: '/api/plans',
|
||||
onboarding: '/api/onboarding',
|
||||
complexes: '/api/complexes',
|
||||
sports: '/api/sports',
|
||||
courts: '/api/courts',
|
||||
} as const
|
||||
|
||||
export function registerApiRoutes<
|
||||
E extends Env,
|
||||
S extends Schema,
|
||||
B extends string,
|
||||
UE extends Env,
|
||||
US extends Schema,
|
||||
UB extends string,
|
||||
PLE extends Env,
|
||||
PLS extends Schema,
|
||||
PLB extends string,
|
||||
OE extends Env,
|
||||
OS extends Schema,
|
||||
OB extends string,
|
||||
CE extends Env,
|
||||
CS extends Schema,
|
||||
CB extends string,
|
||||
SPE extends Env,
|
||||
SPS extends Schema,
|
||||
SPB extends string,
|
||||
COE extends Env,
|
||||
COS extends Schema,
|
||||
COB extends string,
|
||||
>(
|
||||
app: HonoLike<E, S, B>,
|
||||
modules: ApiRouteModules<
|
||||
UE,
|
||||
US,
|
||||
UB,
|
||||
PLE,
|
||||
PLS,
|
||||
PLB,
|
||||
OE,
|
||||
OS,
|
||||
OB,
|
||||
CE,
|
||||
CS,
|
||||
CB,
|
||||
SPE,
|
||||
SPS,
|
||||
SPB,
|
||||
COE,
|
||||
COS,
|
||||
COB
|
||||
>,
|
||||
) {
|
||||
return app
|
||||
.route(API_ROUTE_PATHS.user, modules.user)
|
||||
.route(API_ROUTE_PATHS.plans, modules.plans)
|
||||
.route(API_ROUTE_PATHS.onboarding, modules.onboarding)
|
||||
.route(API_ROUTE_PATHS.complexes, modules.complexes)
|
||||
.route(API_ROUTE_PATHS.sports, modules.sports)
|
||||
.route(API_ROUTE_PATHS.courts, modules.courts)
|
||||
}
|
||||
36
packages/api-contract/src/sport.ts
Normal file
36
packages/api-contract/src/sport.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const sportSchema = z.object({
|
||||
id: z.uuid(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
isActive: z.boolean(),
|
||||
createdAt: z.string().datetime(),
|
||||
updatedAt: z.string().datetime(),
|
||||
})
|
||||
|
||||
export const createSportSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, 'El deporte debe tener al menos 2 caracteres.')
|
||||
.max(80, 'El deporte no puede superar los 80 caracteres.'),
|
||||
})
|
||||
|
||||
export const updateSportSchema = z
|
||||
.object({
|
||||
name: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(2, 'El deporte debe tener al menos 2 caracteres.')
|
||||
.max(80, 'El deporte no puede superar los 80 caracteres.')
|
||||
.optional(),
|
||||
isActive: z.boolean().optional(),
|
||||
})
|
||||
.refine((payload) => payload.name !== undefined || payload.isActive !== undefined, {
|
||||
message: 'Debes enviar al menos un campo para actualizar.',
|
||||
})
|
||||
|
||||
export type Sport = z.infer<typeof sportSchema>
|
||||
export type CreateSportInput = z.infer<typeof createSportSchema>
|
||||
export type UpdateSportInput = z.infer<typeof updateSportSchema>
|
||||
13
packages/api-contract/src/user.ts
Normal file
13
packages/api-contract/src/user.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const userProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
fullName: z.string(),
|
||||
email: z.email().nullable(),
|
||||
role: z.string(),
|
||||
avatarUrl: z.url().nullable(),
|
||||
createdAt: z.string(),
|
||||
})
|
||||
|
||||
export type UserProfile = z.infer<typeof userProfileSchema>
|
||||
export type UserProfileResponse = UserProfile
|
||||
Reference in New Issue
Block a user