Files
web/server/routers/hotels/output.ts
2025-01-30 13:51:47 +01:00

275 lines
7.2 KiB
TypeScript

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 getHotelIdsByCityIdSchema = 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))
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))