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(), }), }) .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(), 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().optional(), 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().optional(), numberOfChargingSpaces: z.number().optional(), 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().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 occupancySchema = z.object({ adults: z.number(), children: z.number(), }) const bestPricePerStaySchema = z.object({ currency: z.string(), amount: z.number(), regularAmount: z.number(), memberAmount: z.number(), discountRate: z.number(), discountAmount: z.number(), points: z.number(), numberOfVouchers: z.number(), numberOfBonusCheques: z.number(), }) const bestPricePerNightSchema = z.object({ currency: z.string(), amount: z.number(), regularAmount: z.number(), memberAmount: z.number(), discountRate: z.number(), discountAmount: z.number(), points: z.number(), numberOfVouchers: z.number(), numberOfBonusCheques: z.number(), }) const linksSchema = z.object({ links: z.array( z.object({ url: z.string().url(), type: z.string(), }) ), }) const availabilitySchema = z.object({ data: z.array( z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), occupancy: occupancySchema.optional(), status: z.string(), hotelId: z.number(), ratePlanSet: z.string().optional(), bestPricePerStay: bestPricePerStaySchema.optional(), bestPricePerNight: bestPricePerNightSchema.optional(), }), relationships: linksSchema.optional(), type: z.string().optional(), }) ), }) export const getAvailabilitySchema = availabilitySchema export type Availability = z.infer export type AvailabilityPrices = Availability["data"][number]["attributes"]["bestPricePerNight"] const flexibilityPrice = z.object({ standard: z.number(), member: z.number(), }) const rate = z.object({ id: z.number(), name: z.string(), description: z.string(), size: z.string(), imageSrc: z.string(), breakfastIncluded: z.boolean(), prices: z.object({ currency: z.string(), nonRefundable: flexibilityPrice, freeRebooking: flexibilityPrice, freeCancellation: flexibilityPrice, }), }) export const getRatesSchema = z.array(rate) export type Rate = z.infer 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