import { z } from "zod" import { toLang } from "@/server/utils" import { occupancySchema } from "./schemas/availability/occupancy" import { productTypeSchema } from "./schemas/availability/productType" import { citySchema } from "./schemas/city" import { attributesSchema, includedSchema, relationshipsSchema as hotelRelationshipsSchema, } from "./schemas/hotel" import { locationCitySchema } from "./schemas/location/city" import { locationHotelSchema } from "./schemas/location/hotel" import { breakfastPackageSchema, packageSchema } from "./schemas/packages" import { rateSchema } from "./schemas/rate" import { relationshipsSchema } from "./schemas/relationships" import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration" import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition" import type { AdditionalData, City, NearbyHotel, Restaurant, Room, } from "@/types/hotel" // NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html export const hotelSchema = z .object({ data: z.object({ attributes: attributesSchema, id: z.string(), language: z.string().transform((val) => { const lang = toLang(val) if (!lang) { throw new Error("Invalid language") } return lang }), relationships: hotelRelationshipsSchema, type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels". }), // NOTE: We can pass an "include" param to the hotel API to retrieve // additional data for an individual hotel. included: includedSchema, }) .transform(({ data: { attributes, ...data }, included }) => { const additionalData = included.find( (inc): inc is AdditionalData => inc!.type === "additionalData" ) ?? ({} as AdditionalData) const cities = included.filter((inc): inc is City => inc!.type === "cities") const nearbyHotels = included.filter( (inc): inc is NearbyHotel => inc!.type === "hotels" ) const restaurants = included.filter( (inc): inc is Restaurant => inc!.type === "restaurants" ) const roomCategories = included.filter( (inc): inc is Room => inc!.type === "roomcategories" ) return { additionalData, cities, hotel: { ...data, ...attributes, }, nearbyHotels, restaurants, roomCategories, } }) export const hotelsAvailabilitySchema = z.object({ data: z.array( z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), hotelId: z.number(), occupancy: occupancySchema, productType: productTypeSchema, status: z.string(), }), relationships: relationshipsSchema.optional(), type: z.string().optional(), }) ), }) export const roomsAvailabilitySchema = z .object({ data: z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), hotelId: z.number(), mustBeGuaranteed: z.boolean().optional(), occupancy: occupancySchema.optional(), rateDefinitions: z.array(rateDefinitionSchema), roomConfigurations: z.array(roomConfigurationSchema), }), relationships: relationshipsSchema.optional(), type: z.string().optional(), }), }) .transform((o) => o.data.attributes) export const ratesSchema = z.array(rateSchema) export const citiesByCountrySchema = z.object({ data: z.array( citySchema.transform((data) => { return { ...data.attributes, id: data.id, type: data.type, } }) ), }) export const countriesSchema = z.object({ data: z .array( z.object({ attributes: z.object({ currency: z.string().default("N/A"), name: z.string(), }), hotelInformationSystemId: z.number().optional(), id: z.string().optional().default(""), language: z.string().optional(), type: z.literal("countries"), }) ) .transform((data) => { return data.map((country) => { return { ...country.attributes, hotelInformationSystemId: country.hotelInformationSystemId, id: country.id, language: country.language, type: country.type, } }) }), }) export const citiesSchema = z .object({ data: z.array(citySchema), }) .transform(({ data }) => { if (data.length) { const city = data[0] return { ...city.attributes, id: city.id, type: city.type, } } return null }) export const locationsSchema = z.object({ data: z .array( z .discriminatedUnion("type", [locationCitySchema, locationHotelSchema]) .transform((location) => { if (location.type === "cities") { return { ...location.attributes, country: location?.country ?? "", id: location.id, type: location.type, } } return { ...location.attributes, id: location.id, relationships: { city: { cityIdentifier: "", ianaTimeZoneId: "", id: "", isPublished: false, keywords: [], name: "", timeZoneId: "", type: "cities", url: location?.relationships?.city?.links?.related ?? "", }, }, type: location.type, } }) ) .transform((data) => data .filter((node) => !!node) .sort((a, b) => { if (a.type === b.type) { return a.name.localeCompare(b.name) } else { return a.type === "cities" ? -1 : 1 } }) ), }) export const breakfastPackagesSchema = z .object({ data: z.object({ attributes: z.object({ hotelId: z.number(), packages: z.array(breakfastPackageSchema), }), type: z.literal("breakfastpackage"), }), }) .transform(({ data }) => data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm)) ) export const packagesSchema = z .object({ data: z .object({ attributes: z.object({ hotelId: z.number(), packages: z.array(packageSchema).default([]), }), relationships: z .object({ links: z.array( z.object({ type: z.string(), url: z.string(), }) ), }) .optional(), type: z.string(), }) .optional(), }) .transform(({ data }) => data?.attributes.packages) export const getHotelIdsSchema = z .object({ data: z.array( z.object({ attributes: z.object({ isPublished: z.boolean(), }), id: z.string(), }) ), }) .transform(({ data }) => { const filteredHotels = data.filter( (hotel) => !!hotel.attributes.isPublished ) return filteredHotels.map((hotel) => hotel.id) }) export const getNearbyHotelIdsSchema = z .object({ data: z.array( z.object({ // We only care about the hotel id id: z.string(), }) ), }) .transform((data) => data.data.map((hotel) => hotel.id))