496 lines
12 KiB
TypeScript
496 lines
12 KiB
TypeScript
import { z } from "zod"
|
|
|
|
import { toLang } from "@/server/utils"
|
|
|
|
const RatingsSchema = z
|
|
.object({
|
|
tripAdvisor: z.object({
|
|
numberOfReviews: z.number(),
|
|
rating: z.number(),
|
|
ratingImageUrl: z.string(),
|
|
webUrl: z.string(),
|
|
awards: z.array(
|
|
z.object({
|
|
displayName: z.string(),
|
|
images: z.object({
|
|
small: z.string(),
|
|
medium: z.string(),
|
|
large: z.string(),
|
|
}),
|
|
})
|
|
),
|
|
reviews: z.object({
|
|
widgetHtmlTagId: z.string(),
|
|
widgetScriptEmbedUrlIframe: z.string(),
|
|
widgetScriptEmbedUrlJavaScript: z.string(),
|
|
}),
|
|
}),
|
|
})
|
|
.optional()
|
|
|
|
const AddressSchema = z.object({
|
|
streetAddress: z.string(),
|
|
city: z.string(),
|
|
zipCode: z.string(),
|
|
country: z.string(),
|
|
})
|
|
|
|
const ContactInformationSchema = z.object({
|
|
phoneNumber: z.string(),
|
|
faxNumber: z.string().optional(),
|
|
email: z.string(),
|
|
websiteUrl: z.string(),
|
|
})
|
|
|
|
const CheckinSchema = z.object({
|
|
checkInTime: z.string(),
|
|
checkOutTime: z.string(),
|
|
onlineCheckOutAvailableFrom: z.string().nullable().optional(),
|
|
onlineCheckout: z.boolean(),
|
|
})
|
|
|
|
const EcoLabelsSchema = z.object({
|
|
euEcoLabel: z.boolean(),
|
|
greenGlobeLabel: z.boolean(),
|
|
nordicEcoLabel: z.boolean(),
|
|
svanenEcoLabelCertificateNumber: z.string().optional(),
|
|
})
|
|
|
|
const HotelFacilityDetailSchema = z.object({
|
|
heading: z.string(),
|
|
description: z.string(),
|
|
})
|
|
|
|
const HotelFacilitySchema = z.object({
|
|
breakfast: HotelFacilityDetailSchema,
|
|
checkout: HotelFacilityDetailSchema,
|
|
gym: HotelFacilityDetailSchema,
|
|
internet: HotelFacilityDetailSchema,
|
|
laundry: HotelFacilityDetailSchema,
|
|
luggage: HotelFacilityDetailSchema,
|
|
shop: HotelFacilityDetailSchema,
|
|
telephone: HotelFacilityDetailSchema,
|
|
})
|
|
|
|
const HotelInformationDetailSchema = z.object({
|
|
heading: z.string(),
|
|
description: z.string(),
|
|
link: z.string().optional(),
|
|
})
|
|
|
|
const HotelInformationSchema = z.object({
|
|
accessibility: HotelInformationDetailSchema,
|
|
safety: HotelInformationDetailSchema,
|
|
sustainability: HotelInformationDetailSchema,
|
|
})
|
|
|
|
const InteriorSchema = z.object({
|
|
numberOfBeds: z.number(),
|
|
numberOfCribs: z.number(),
|
|
numberOfFloors: z.number(),
|
|
numberOfRooms: z.object({
|
|
connected: z.number(),
|
|
forAllergics: z.number().optional(),
|
|
forDisabled: z.number(),
|
|
nonSmoking: z.number(),
|
|
pet: z.number(),
|
|
withExtraBeds: z.number(),
|
|
total: z.number(),
|
|
}),
|
|
})
|
|
|
|
const ReceptionHoursSchema = z.object({
|
|
alwaysOpen: z.boolean(),
|
|
isClosed: z.boolean(),
|
|
openingTime: z.string().optional(),
|
|
closingTime: z.string().optional(),
|
|
})
|
|
|
|
const LocationSchema = z.object({
|
|
distanceToCentre: z.number(),
|
|
latitude: z.number(),
|
|
longitude: z.number(),
|
|
})
|
|
|
|
const ImageMetaDataSchema = z.object({
|
|
title: z.string(),
|
|
altText: z.string(),
|
|
altText_En: z.string(),
|
|
copyRight: z.string(),
|
|
})
|
|
|
|
const ImageSizesSchema = z.object({
|
|
tiny: z.string(),
|
|
small: z.string(),
|
|
medium: z.string(),
|
|
large: z.string(),
|
|
})
|
|
|
|
const HotelContentSchema = z.object({
|
|
images: z.object({
|
|
metaData: ImageMetaDataSchema,
|
|
imageSizes: ImageSizesSchema,
|
|
}),
|
|
texts: z.object({
|
|
facilityInformation: z.string(),
|
|
surroundingInformation: z.string(),
|
|
descriptions: z.object({
|
|
short: z.string(),
|
|
medium: z.string(),
|
|
}),
|
|
}),
|
|
restaurantsOverviewPage: z.object({
|
|
restaurantsOverviewPageLinkText: z.string(),
|
|
restaurantsOverviewPageLink: z.string(),
|
|
restaurantsContentDescriptionShort: z.string(),
|
|
restaurantsContentDescriptionMedium: z.string(),
|
|
}),
|
|
})
|
|
|
|
const DetailedFacilitySchema = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
code: z.string().optional(),
|
|
applyToAllHotels: z.boolean(),
|
|
public: z.boolean(),
|
|
icon: z.string(), //Check output.
|
|
iconName: z.string().optional(),
|
|
sortOrder: z.number(),
|
|
})
|
|
|
|
const HealthFacilitySchema = z.object({
|
|
type: z.string(),
|
|
content: z.object({
|
|
images: z.array(
|
|
z.object({
|
|
metaData: ImageMetaDataSchema,
|
|
imageSizes: ImageSizesSchema,
|
|
})
|
|
),
|
|
texts: z.object({
|
|
facilityInformation: z.string().optional(),
|
|
surroundingInformation: z.string().optional(),
|
|
descriptions: z.object({
|
|
short: z.string(),
|
|
medium: z.string(),
|
|
}),
|
|
}),
|
|
}),
|
|
openingDetails: z.object({
|
|
useManualOpeningHours: z.boolean(),
|
|
manualOpeningHours: z.string().optional(),
|
|
openingHours: z.object({
|
|
ordinary: z.object({
|
|
alwaysOpen: z.boolean(),
|
|
isClosed: z.boolean(),
|
|
openingTime: z.string().optional(),
|
|
closingTime: z.string().optional(),
|
|
sortOrder: z.number().optional(),
|
|
}),
|
|
weekends: z.object({
|
|
alwaysOpen: z.boolean(),
|
|
isClosed: z.boolean(),
|
|
openingTime: z.string().optional(),
|
|
closingTime: z.string().optional(),
|
|
sortOrder: z.number().optional(),
|
|
}),
|
|
}),
|
|
}),
|
|
details: z.array(
|
|
z.object({
|
|
name: z.string(),
|
|
type: z.string(),
|
|
value: z.string().optional(),
|
|
})
|
|
),
|
|
})
|
|
|
|
const RewardNightSchema = z.object({
|
|
points: z.number(),
|
|
campaign: z.object({
|
|
start: z.string(),
|
|
end: z.string(),
|
|
points: z.number(),
|
|
}),
|
|
})
|
|
|
|
const PointsOfInterestSchema = z.object({
|
|
name: z.string(),
|
|
distance: z.number(),
|
|
category: z.object({
|
|
name: z.string(),
|
|
group: z.string(),
|
|
}),
|
|
location: LocationSchema,
|
|
isHighlighted: z.boolean(),
|
|
})
|
|
|
|
const ParkingPricingSchema = z.object({
|
|
freeParking: z.boolean(),
|
|
paymentType: z.string(),
|
|
localCurrency: z.object({
|
|
currency: z.string(),
|
|
range: z.object({
|
|
min: z.number(),
|
|
max: z.number().optional(),
|
|
}),
|
|
ordinary: z.array(
|
|
z.object({
|
|
period: z.string(),
|
|
amount: z.number().optional(),
|
|
startTime: z.string(),
|
|
endTime: z.string(),
|
|
})
|
|
),
|
|
weekend: z.array(
|
|
z.object({
|
|
period: z.string(),
|
|
amount: z.number().optional(),
|
|
startTime: z.string(),
|
|
endTime: z.string(),
|
|
})
|
|
),
|
|
}),
|
|
requestedCurrency: z
|
|
.object({
|
|
currency: z.string(),
|
|
range: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
}),
|
|
ordinary: z.array(
|
|
z.object({
|
|
period: z.string(),
|
|
amount: z.number(),
|
|
startTime: z.string(),
|
|
endTime: z.string(),
|
|
})
|
|
),
|
|
weekend: z.array(
|
|
z.object({
|
|
period: z.string(),
|
|
amount: z.number(),
|
|
startTime: z.string(),
|
|
endTime: z.string(),
|
|
})
|
|
),
|
|
})
|
|
.optional(),
|
|
})
|
|
|
|
const ParkingSchema = z.object({
|
|
type: z.string(),
|
|
name: z.string(),
|
|
address: z.string(),
|
|
numberOfParkingSpots: z.number(),
|
|
numberOfChargingSpaces: z.number(),
|
|
distanceToHotel: z.number(),
|
|
canMakeReservation: z.boolean(),
|
|
pricing: ParkingPricingSchema,
|
|
})
|
|
|
|
const SpecialNeedSchema = z.object({
|
|
name: z.string(),
|
|
details: z.string(),
|
|
})
|
|
|
|
const SpecialNeedGroupSchema = z.object({
|
|
name: z.string(),
|
|
specialNeeds: z.array(SpecialNeedSchema),
|
|
})
|
|
|
|
const SocialMediaSchema = z.object({
|
|
instagram: z.string().optional(),
|
|
facebook: z.string().optional(),
|
|
})
|
|
|
|
const MetaSpecialAlertSchema = z.object({
|
|
type: z.string(),
|
|
description: z.string().optional(),
|
|
displayInBookingFlow: z.boolean(),
|
|
startDate: z.string(),
|
|
endDate: z.string(),
|
|
})
|
|
|
|
const MetaSchema = z.object({
|
|
specialAlerts: z.array(MetaSpecialAlertSchema),
|
|
})
|
|
|
|
const RelationshipsSchema = z.object({
|
|
restaurants: z.object({
|
|
links: z.object({
|
|
related: z.string(),
|
|
}),
|
|
}),
|
|
nearbyHotels: z.object({
|
|
links: z.object({
|
|
related: z.string(),
|
|
}),
|
|
}),
|
|
roomCategories: z.object({
|
|
links: z.object({
|
|
related: z.string(),
|
|
}),
|
|
}),
|
|
meetingRooms: z.object({
|
|
links: z.object({
|
|
related: z.string(),
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const RoomContentSchema = z.object({
|
|
images: z.array(
|
|
z.object({
|
|
metaData: ImageMetaDataSchema,
|
|
imageSizes: ImageSizesSchema,
|
|
})
|
|
),
|
|
texts: z.object({
|
|
descriptions: z.object({
|
|
short: z.string(),
|
|
medium: z.string(),
|
|
}),
|
|
}),
|
|
})
|
|
|
|
const RoomTypesSchema = z.object({
|
|
name: z.string(),
|
|
description: z.string(),
|
|
code: z.string(),
|
|
roomCount: z.number(),
|
|
mainBed: z.object({
|
|
type: z.string(),
|
|
description: z.string(),
|
|
widthRange: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
}),
|
|
}),
|
|
fixedExtraBed: z.object({
|
|
type: z.string(),
|
|
description: z.string().optional(),
|
|
widthRange: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
}),
|
|
}),
|
|
roomSize: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
}),
|
|
occupancy: z.object({
|
|
total: z.number(),
|
|
adults: z.number(),
|
|
children: z.number(),
|
|
}),
|
|
isLackingCribs: z.boolean(),
|
|
isLackingExtraBeds: z.boolean(),
|
|
})
|
|
|
|
const RoomFacilitiesSchema = z.object({
|
|
availableInAllRooms: z.boolean(),
|
|
name: z.string(),
|
|
isUniqueSellingPoint: z.boolean(),
|
|
sortOrder: z.number(),
|
|
})
|
|
|
|
export const RoomSchema = z.object({
|
|
attributes: z.object({
|
|
name: z.string(),
|
|
sortOrder: z.number(),
|
|
content: RoomContentSchema,
|
|
roomTypes: z.array(RoomTypesSchema),
|
|
roomFacilities: z.array(RoomFacilitiesSchema),
|
|
occupancy: z.object({
|
|
total: z.number(),
|
|
adults: z.number(),
|
|
children: z.number(),
|
|
}),
|
|
roomSize: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
}),
|
|
}),
|
|
id: z.string(),
|
|
type: z.enum(["roomcategories"]),
|
|
})
|
|
|
|
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
|
export const getHotelDataSchema = z.object({
|
|
data: z.object({
|
|
id: z.string(),
|
|
type: z.string(), // No enum here but the standard return appears to be "hotels".
|
|
language: z
|
|
.string()
|
|
.refine((val) => toLang(val) !== undefined, {
|
|
message: "Invalid language",
|
|
})
|
|
.transform((val) => {
|
|
const lang = toLang(val)
|
|
if (!lang) {
|
|
throw new Error("Invalid language")
|
|
}
|
|
return lang
|
|
}),
|
|
attributes: z.object({
|
|
name: z.string(),
|
|
operaId: z.string(),
|
|
keywords: z.array(z.string()),
|
|
isPublished: z.boolean(),
|
|
cityId: z.string(),
|
|
cityName: z.string(),
|
|
ratings: RatingsSchema,
|
|
address: AddressSchema,
|
|
contactInformation: ContactInformationSchema,
|
|
hotelFacts: z.object({
|
|
checkin: CheckinSchema,
|
|
ecoLabels: EcoLabelsSchema,
|
|
hotelFacilityDetail: HotelFacilitySchema,
|
|
hotelInformation: HotelInformationSchema,
|
|
interior: InteriorSchema,
|
|
receptionHours: ReceptionHoursSchema,
|
|
yearBuilt: z.string(),
|
|
}),
|
|
location: LocationSchema,
|
|
hotelContent: HotelContentSchema,
|
|
detailedFacilities: z.array(DetailedFacilitySchema),
|
|
healthFacilities: z.array(HealthFacilitySchema),
|
|
rewardNight: RewardNightSchema,
|
|
pointsOfInterest: z.array(PointsOfInterestSchema),
|
|
parking: z.array(ParkingSchema),
|
|
specialNeedGroups: z.array(SpecialNeedGroupSchema),
|
|
socialMedia: SocialMediaSchema,
|
|
meta: MetaSchema.optional(),
|
|
isActive: z.boolean(),
|
|
}),
|
|
relationships: RelationshipsSchema,
|
|
}),
|
|
// NOTE: We can pass an "include" param to the hotel API to retrieve
|
|
// additional data for an individual hotel.
|
|
included: z.array(RoomSchema).optional(),
|
|
})
|
|
|
|
const rate = z.object({
|
|
id: z.number(),
|
|
name: z.string(),
|
|
description: z.string(),
|
|
size: z.string(),
|
|
pricePerNight: z.number(),
|
|
currency: z.string(),
|
|
imageSrc: z.string(),
|
|
})
|
|
|
|
export const getRatesSchema = z.array(rate)
|
|
|
|
export type Rate = z.infer<typeof rate>
|
|
|
|
const hotelFilter = z.object({
|
|
roomFacilities: z.array(z.string()),
|
|
hotelFacilities: z.array(z.string()),
|
|
hotelSurroundings: z.array(z.string()),
|
|
})
|
|
|
|
export const getFiltersSchema = hotelFilter
|
|
export type HotelFilter = z.infer<typeof hotelFilter>
|