fix: clean up hotel and its typings

This commit is contained in:
Simon Emanuelsson
2024-12-17 16:17:25 +01:00
parent ec74af8814
commit 13a164242f
110 changed files with 1931 additions and 1559 deletions

View File

@@ -5,7 +5,7 @@ import { dt } from "@/lib/dt"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { router, serviceProcedure } from "@/server/trpc"
import { getHotelData } from "../hotels/query"
import { getHotel } from "../hotels/query"
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { getBookedHotelRoom } from "./utils"
@@ -84,8 +84,12 @@ export const bookingQueryRouter = router({
throw badRequestError()
}
const hotelData = await getHotelData(
{ hotelId: booking.data.hotelId, language: ctx.lang },
const hotelData = await getHotel(
{
hotelId: booking.data.hotelId,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
@@ -123,14 +127,12 @@ export const bookingQueryRouter = router({
* Add hotels check in and out times to booking check in and out date
* as that is date only (YYYY-MM-DD)
*/
const checkInTime =
hotelData.data.attributes.hotelFacts.checkin.checkInTime
const checkInTime = hotelData.hotel.hotelFacts.checkin.checkInTime
const [checkInHour, checkInMinute] = checkInTime.split(":")
const checkIn = dt(booking.data.checkInDate)
.set("hour", Number(checkInHour))
.set("minute", Number(checkInMinute))
const checkOutTime =
hotelData.data.attributes.hotelFacts.checkin.checkOutTime
const checkOutTime = hotelData.hotel.hotelFacts.checkin.checkOutTime
const [checkOutHour, checkOutMinute] = checkOutTime.split(":")
const checkOut = dt(booking.data.checkOutDate)
.set("hour", Number(checkOutHour))
@@ -140,13 +142,10 @@ export const bookingQueryRouter = router({
booking.data.checkOutDate = checkOut.toDate()
return {
...hotelData,
booking: booking.data,
hotel: {
...hotelData.data.attributes,
included: hotelData.included,
},
room: getBookedHotelRoom(
hotelData.included.rooms,
hotelData.roomCategories,
booking.data.roomTypeCode
),
}

View File

@@ -1,8 +1,8 @@
import type { RoomData } from "@/types/hotel"
import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getBookedHotelRoom(
rooms: RoomData[] | undefined,
rooms: Room[] | undefined,
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
) {
if (!rooms?.length || !roomTypeCode) {

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
import { hotelAttributesSchema } from "../../hotels/output"
import { attributesSchema as hotelAttributesSchema } from "../../hotels/schemas/hotel"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { getDescription, getImage, getTitle } from "./utils"

View File

@@ -15,7 +15,7 @@ import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { getHotelData } from "../../hotels/query"
import { getHotel } from "../../hotels/query"
import { metadataSchema } from "./output"
import { affix } from "./utils"
@@ -171,15 +171,19 @@ export const metadataQueryRouter = router({
}>(GetHotelPageMetadata, variables)
const hotelPageData = hotelPageResponse.hotel_page
const hotelData = hotelPageData.hotel_page_id
? await getHotelData(
{ hotelId: hotelPageData.hotel_page_id, language: ctx.lang },
? await getHotel(
{
hotelId: hotelPageData.hotel_page_id,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
: null
return getTransformedMetadata({
...hotelPageData,
hotelData: hotelData?.data.attributes,
hotelData: hotelData?.hotel,
})
default:
return null

View File

@@ -1,9 +1,11 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { Country } from "@/types/enums/country"
export const getHotelsAvailabilityInputSchema = z.object({
export const hotelsAvailabilityInputSchema = z.object({
cityId: z.string(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
@@ -21,7 +23,7 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
bookingCode: z.string().optional().default(""),
})
export const getRoomsAvailabilityInputSchema = z.object({
export const roomsAvailabilityInputSchema = z.object({
hotelId: z.number(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
@@ -31,7 +33,7 @@ export const getRoomsAvailabilityInputSchema = z.object({
rateCode: z.string().optional(),
})
export const getSelectedRoomAvailabilityInputSchema = z.object({
export const selectedRoomAvailabilityInputSchema = z.object({
hotelId: z.string(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
@@ -44,25 +46,23 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({
})
export type GetSelectedRoomAvailabilityInput = z.input<
typeof getSelectedRoomAvailabilityInputSchema
typeof selectedRoomAvailabilityInputSchema
>
export type GetRoomsAvailabilityInput = z.input<
typeof getRoomsAvailabilityInputSchema
typeof roomsAvailabilityInputSchema
>
export const getRatesInputSchema = z.object({
export const ratesInputSchema = z.object({
hotelId: z.string(),
})
export const getHotelDataInputSchema = z.object({
export const hotelInputSchema = z.object({
hotelId: z.string(),
language: z.string(),
isCardOnlyPayment: z.boolean().optional(),
isCardOnlyPayment: z.boolean().default(false),
language: z.nativeEnum(Lang),
})
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
export const getHotelsInput = z.object({
locationFilter: z
.object({
@@ -73,13 +73,13 @@ export const getHotelsInput = z.object({
.nullable(),
hotelsToInclude: z.array(z.string()),
})
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> {}
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> { }
export const nearbyHotelIdsInput = z.object({
hotelId: z.string(),
})
export const getBreakfastPackageInputSchema = z.object({
export const breakfastPackageInputSchema = z.object({
adults: z.number().min(1, { message: "at least one adult is required" }),
fromDate: z
.string()
@@ -92,7 +92,7 @@ export const getBreakfastPackageInputSchema = z.object({
.pipe(z.coerce.date()),
})
export const getRoomPackagesInputSchema = z.object({
export const roomPackagesInputSchema = z.object({
hotelId: z.string(),
startDate: z.string(),
endDate: z.string(),
@@ -100,7 +100,7 @@ export const getRoomPackagesInputSchema = z.object({
children: z.number().optional().default(0),
packageCodes: z.array(z.string()).optional().default([]),
})
export const getCityCoordinatesInputSchema = z.object({
export const cityCoordinatesInputSchema = z.object({
city: z.string(),
hotel: z.object({
address: z.string().optional(),

View File

@@ -0,0 +1,65 @@
import { metrics as opentelemetryMetrics } from "@opentelemetry/api"
const meter = opentelemetryMetrics.getMeter("trpc.hotels")
export const metrics = {
additionalData: {
counter: meter.createCounter("trpc.hotels.additionalData"),
fail: meter.createCounter("trpc.hotels.additionalData-fail"),
success: meter.createCounter("trpc.hotels.additionalData-success"),
},
breakfastPackage: {
counter: meter.createCounter("trpc.package.breakfast"),
fail: meter.createCounter("trpc.package.breakfast-fail"),
success: meter.createCounter("trpc.package.breakfast-success"),
},
hotel: {
counter: meter.createCounter("trpc.hotel.get"),
fail: meter.createCounter("trpc.hotel.get-fail"),
success: meter.createCounter("trpc.hotel.get-success"),
},
hotels: {
counter: meter.createCounter("trpc.hotel.hotels.get"),
fail: meter.createCounter("trpc.hotel.hotels.get-fail"),
success: meter.createCounter("trpc.hotel.hotels.get-success"),
},
hotelIds: {
counter: meter.createCounter("trpc.hotel.hotel-ids.get"),
fail: meter.createCounter("trpc.hotel.hotel-ids.get-fail"),
success: meter.createCounter("trpc.hotel.hotel-ids.get-success"),
},
hotelsAvailability: {
counter: meter.createCounter("trpc.hotel.availability.hotels"),
fail: meter.createCounter("trpc.hotel.availability.hotels-fail"),
success: meter.createCounter("trpc.hotel.availability.hotels-success"),
},
hotelsByHotelIdAvailability: {
counter: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id"),
fail: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-fail"),
success: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-success"),
},
meetingRooms: {
counter: meter.createCounter("trpc.hotels.meetingRooms"),
fail: meter.createCounter("trpc.hotels.meetingRooms-fail"),
success: meter.createCounter("trpc.hotels.meetingRooms-success"),
},
nearbyHotelIds: {
counter: meter.createCounter("trpc.hotel.nearby-hotel-ids.get"),
fail: meter.createCounter("trpc.hotel.nearby-hotel-ids.get-fail"),
success: meter.createCounter("trpc.hotel.nearby-hotel-ids.get-success"),
},
packages: {
counter: meter.createCounter("trpc.hotel.packages.get"),
fail: meter.createCounter("trpc.hotel.packages.get-fail"),
success: meter.createCounter("trpc.hotel.packages.get-success"),
},
roomAvailability: {
counter: meter.createCounter("trpc.hotel.availability.rooms"),
fail: meter.createCounter("trpc.hotel.availability.rooms-fail"),
success: meter.createCounter("trpc.hotel.availability.rooms-success"),
},
selectedRoomAvailability: {
counter: meter.createCounter("trpc.hotel.availability.room"),
fail: meter.createCounter("trpc.hotel.availability.room-fail"),
success: meter.createCounter("trpc.hotel.availability.room-success"),
},
}

View File

@@ -1,668 +1,120 @@
import { z } from "zod"
import { ChildBedTypeEnum, type PaymentMethodEnum } from "@/constants/booking"
import { toLang } from "@/server/utils"
import { additionalDataSchema } from "./schemas/additionalData"
import { imageSchema } from "./schemas/image"
import { restaurantSchema } from "./schemas/restaurants"
import { roomSchema } from "./schemas/room"
import { specialAlertsSchema } from "./schemas/specialAlerts"
import { getPoiGroupByCategoryName } from "./utils"
import { occupancySchema } from "./schemas/availability/occupancy"
import { productTypeSchema } from "./schemas/availability/productType"
import { citySchema } from "./schemas/city"
import {
attributesSchema,
includesSchema,
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 { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { FacilityEnum } from "@/types/enums/facilities"
import { PackageTypeEnum } from "@/types/enums/packages"
import type { AdditionalData, RestaurantData, RoomData } from "@/types/hotel"
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(),
})
export 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 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 hotelContentSchema = z.object({
images: imageSchema,
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string(),
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
restaurantsOverviewPage: z.object({
restaurantsOverviewPageLinkText: z.string().optional(),
restaurantsOverviewPageLink: z.string().optional(),
restaurantsContentDescriptionShort: z.string().optional(),
restaurantsContentDescriptionMedium: z.string().optional(),
}), // TODO remove, use new /additionalData endpoint
})
const detailedFacilitySchema = z.object({
id: z.number(),
name: z.string(),
public: z.boolean(),
sortOrder: z.number(),
filter: z.string().optional(),
icon: z.string().optional(),
})
export const facilitySchema = z.object({
headingText: z.string().default(""),
heroImages: z.array(imageSchema),
}) // TODO remove, use new /additionalData endpoint
export const gallerySchema = z.object({
heroImages: z.array(imageSchema),
smallerImages: z.array(imageSchema),
}) // TODO remove, use new /additionalData endpoint
const healthFacilitySchema = z.object({
type: z.string(),
content: z.object({
images: z.array(imageSchema),
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(),
}),
})
export const pointOfInterestSchema = z
.object({
name: z.string().optional(),
distance: z.number().optional(),
category: z.object({
name: z.string().optional(),
group: z.string().optional(),
}),
location: locationSchema.optional(),
isHighlighted: z.boolean().optional(),
})
.transform((poi) => ({
name: poi.name,
distance: poi.distance,
categoryName: poi.category.name,
group: getPoiGroupByCategoryName(poi.category.name),
coordinates: {
lat: poi.location?.latitude ?? 0,
lng: poi.location?.longitude ?? 0,
},
}))
const parkingPricingSchema = z.object({
freeParking: z.boolean(),
paymentType: z.string().optional(),
localCurrency: z.object({
currency: z.string().default("N/A"),
range: z.object({
min: z.number().optional(),
max: z.number().optional(),
}),
ordinary: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
weekend: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
}),
requestedCurrency: z
.object({
currency: z.string().default("N/A"),
range: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
.optional(),
ordinary: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
weekend: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
})
.optional(),
})
export const parkingSchema = z.object({
type: z.string().optional(),
name: z.string().optional(),
address: z.string().optional(),
numberOfParkingSpots: z.number().optional(),
numberOfChargingSpaces: z.number().optional(),
distanceToHotel: z.number().optional(),
canMakeReservation: z.boolean(),
externalParkingUrl: z.string().optional(),
pricing: parkingPricingSchema,
})
const socialMediaSchema = z.object({
instagram: z.string().optional(),
facebook: z.string().optional(),
})
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 merchantInformationSchema = z.object({
webMerchantId: z.string(),
cards: z.record(z.string(), z.boolean()).transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
alternatePaymentOptions: z
.record(z.string(), z.boolean())
.transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
})
const hotelFacilityDetailSchema = z
.object({
description: z.string(),
heading: z.string(),
})
.optional()
/** Possibly more values */
const hotelFacilityDetailsSchema = z.object({
breakfast: hotelFacilityDetailSchema,
checkout: hotelFacilityDetailSchema,
gym: hotelFacilityDetailSchema,
internet: hotelFacilityDetailSchema,
laundry: hotelFacilityDetailSchema,
luggage: hotelFacilityDetailSchema,
shop: hotelFacilityDetailSchema,
telephone: hotelFacilityDetailSchema,
})
const hotelInformationSchema = z
.object({
description: z.string(),
heading: z.string(),
link: z.string().optional(),
})
.optional()
const hotelInformationsSchema = z.object({
accessibility: hotelInformationSchema,
safety: hotelInformationSchema,
sustainability: hotelInformationSchema,
})
const hotelFactsSchema = z.object({
checkin: checkinSchema,
ecoLabels: ecoLabelsSchema,
hotelFacilityDetail: hotelFacilityDetailsSchema.default({}),
hotelInformation: hotelInformationsSchema.default({}),
interior: interiorSchema,
receptionHours: receptionHoursSchema,
yearBuilt: z.string(),
})
type DetailedFacility = { id: FacilityEnum } & z.infer<
typeof detailedFacilitySchema
>
export const hotelAttributesSchema = z.object({
address: addressSchema,
cityId: z.string(),
cityName: z.string(),
conferencesAndMeetings: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint
contactInformation: contactInformationSchema,
detailedFacilities: z.array(detailedFacilitySchema).transform(
(facilities) =>
facilities
// Filter away facilities with ID:s that we don't recognize
.filter(
(f) => f.id !== undefined && f.id !== null && f.id in FacilityEnum
)
.sort((a, b) => b.sortOrder - a.sortOrder) as DetailedFacility[]
),
gallery: gallerySchema.optional(), // TODO remove, use new /additionalData endpoint
galleryImages: z.array(imageSchema).optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelType: z.string().optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: z.array(z.string()),
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: z.string(),
operaId: z.string(),
parking: z.array(parkingSchema),
pointsOfInterest: z
.array(pointOfInterestSchema)
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
),
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
vat: z.number(),
})
const includedSchema = z
.array(z.union([roomSchema, restaurantSchema, additionalDataSchema]))
.transform((data) => {
const rooms = data.filter((d) => d.type === "roomcategories") as RoomData[]
const restaurants = data.filter(
(d) => d.type === "restaurants"
) as RestaurantData[]
const additionalData = data.filter(
(d) => d.type === "additionalData"
) as AdditionalData[]
return {
rooms,
restaurants,
additionalData,
}
})
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 getHotelDataSchema = z.object({
data: z.object({
id: z.string(),
type: z.literal("hotels"), // 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
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".
}),
attributes: hotelAttributesSchema,
relationships: relationshipsSchema,
}),
// NOTE: We can pass an "include" param to the hotel API to retrieve
// additional data for an individual hotel.
included: includedSchema.optional().transform((incl) => {
// NOTE: We can pass an "include" param to the hotel API to retrieve
// additional data for an individual hotel.
included: includesSchema,
})
.transform(({ data: { attributes, ...data }, included }) => {
const additionalData = included.find((inc): inc is AdditionalData => inc!.type === "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 {
restaurants: incl?.restaurants,
rooms: incl?.rooms,
additionalData: incl?.additionalData[0],
}
}),
})
export const childrenSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
const occupancySchema = z.object({
adults: z.number(),
children: z.array(childrenSchema).default([]),
})
const linksSchema = z.object({
links: z.array(
z.object({
url: z.string().url(),
type: z.string(),
})
),
})
export const priceSchema = z.object({
pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(),
currency: z.string().default("N/A"),
})
export const productTypePriceSchema = z.object({
rateCode: z.string(),
rateType: z.string().optional(),
localPrice: priceSchema,
requestedPrice: priceSchema.optional(),
})
const productSchema = z.object({
productType: z.object({
public: productTypePriceSchema.default({
rateCode: "",
rateType: "",
localPrice: {
currency: "N/A",
pricePerNight: 0,
pricePerStay: 0,
additionalData,
cities,
hotel: {
...data,
...attributes,
},
requestedPrice: undefined,
}),
member: productTypePriceSchema.optional(),
}),
})
nearbyHotels,
restaurants,
roomCategories,
}
})
const hotelsAvailabilitySchema = z.object({
export const hotelsAvailabilitySchema = z.object({
data: z.array(
z.object({
attributes: z.object({
checkInDate: z.string(),
checkOutDate: z.string(),
occupancy: occupancySchema,
status: z.string(),
hotelId: z.number(),
productType: z
.object({
public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(),
})
.optional(),
occupancy: occupancySchema,
productType: productTypeSchema,
status: z.string(),
}),
relationships: linksSchema.optional(),
relationships: relationshipsSchema.optional(),
type: z.string().optional(),
})
),
})
export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema
export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
export type ProductType =
HotelsAvailability["data"][number]["attributes"]["productType"]
export type ProductTypePrices = z.infer<typeof productTypePriceSchema>
export type HotelsAvailabilityItem =
HotelsAvailability["data"][number]["attributes"]
const roomConfigurationSchema = z.object({
status: z.string(),
roomTypeCode: z.string(),
roomType: z.string(),
roomsLeft: z.number(),
features: z
.array(
z.object({
inventory: z.number(),
code: z.enum([
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
]),
})
)
.default([]),
products: z.array(productSchema).default([]),
})
const rateDefinitionSchema = z.object({
title: z.string(),
breakfastIncluded: z.boolean(),
rateType: z.string().optional(),
rateCode: z.string(),
generalTerms: z.array(z.string()),
cancellationRule: z.string(),
cancellationText: z.string(),
mustBeGuaranteed: z.boolean(),
})
const roomsAvailabilitySchema = z
export const roomsAvailabilitySchema = z
.object({
data: z.object({
attributes: z.object({
checkInDate: z.string(),
checkOutDate: z.string(),
occupancy: occupancySchema.optional(),
hotelId: z.number(),
bookingCode: z.string().optional(),
roomConfigurations: z.array(roomConfigurationSchema),
rateDefinitions: z.array(rateDefinitionSchema),
mustBeGuaranteed: z.boolean().optional(),
occupancy: occupancySchema.optional(),
rateDefinitions: z.array(rateDefinitionSchema),
roomConfigurations: z.array(roomConfigurationSchema),
}),
relationships: linksSchema.optional(),
relationships: relationshipsSchema.optional(),
type: z.string().optional(),
}),
})
.transform((o) => o.data.attributes)
export const getRoomsAvailabilitySchema = roomsAvailabilitySchema
export type RoomsAvailability = z.infer<typeof roomsAvailabilitySchema>
export type RoomConfiguration = z.infer<typeof roomConfigurationSchema>
export type Product = z.infer<typeof productSchema>
export type RateDefinition = z.infer<typeof rateDefinitionSchema>
export const ratesSchema = z.array(rateSchema)
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<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>
export const apiCitiesByCountrySchema = z.object({
export const citiesByCountrySchema = z.object({
data: z.array(
z
.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
name: z.string(),
keywords: z.array(z.string()).optional(),
timeZoneId: z.string().optional(),
ianaTimeZoneId: z.string().optional(),
isPublished: z.boolean().optional().default(false),
}),
id: z.string(),
type: z.literal("cities"),
})
.transform((data) => {
return {
...data.attributes,
id: data.id,
type: data.type,
}
})
citySchema.transform((data) => {
return {
...data.attributes,
id: data.id,
type: data.type,
}
})
),
})
export interface CitiesByCountry
extends z.output<typeof apiCitiesByCountrySchema> {}
export type CitiesGroupedByCountry = Record<string, CitiesByCountry["data"]>
export const apiCountriesSchema = z.object({
export const countriesSchema = z.object({
data: z
.array(
z.object({
@@ -689,35 +141,9 @@ export const apiCountriesSchema = z.object({
}),
})
export interface Countries extends z.output<typeof apiCountriesSchema> {}
export const apiLocationCitySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
}),
country: z.string().optional().default(""),
id: z.string().optional().default(""),
type: z.literal("cities"),
})
export const apiCitySchema = z
export const citiesSchema = z
.object({
data: z.array(
z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
name: z.string(),
keywords: z.array(z.string()),
timeZoneId: z.string().optional(),
ianaTimeZoneId: z.string().optional(),
isPublished: z.boolean().optional().default(false),
}),
id: z.string().optional(),
type: z.literal("cities"),
})
),
data: z.array(citySchema),
})
.transform(({ data }) => {
if (data.length) {
@@ -731,46 +157,11 @@ export const apiCitySchema = z
return null
})
export const apiLocationHotelSchema = z.object({
attributes: z.object({
distanceToCentre: z.number().optional(),
images: z
.object({
large: z.string().optional(),
medium: z.string().optional(),
small: z.string().optional(),
tiny: z.string().optional(),
})
.optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
operaId: z.string().optional(),
}),
id: z.string().optional().default(""),
relationships: z
.object({
city: z
.object({
links: z
.object({
related: z.string().optional(),
})
.optional(),
})
.optional(),
})
.optional(),
type: z.literal("hotels"),
})
export const apiLocationsSchema = z.object({
export const locationsSchema = z.object({
data: z
.array(
z
.discriminatedUnion("type", [
apiLocationCitySchema,
apiLocationHotelSchema,
])
.discriminatedUnion("type", [locationCitySchema, locationHotelSchema])
.transform((location) => {
if (location.type === "cities") {
return {
@@ -813,20 +204,6 @@ export const apiLocationsSchema = z.object({
),
})
export const packagePriceSchema = z.object({
currency: z.string().default("N/A"),
price: z.string(),
totalPrice: z.string(),
})
export const breakfastPackageSchema = z.object({
code: z.string(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
packageType: z.literal(PackageTypeEnum.BreakfastAdult),
})
export const breakfastPackagesSchema = z
.object({
data: z.object({
@@ -838,38 +215,23 @@ export const breakfastPackagesSchema = z
}),
})
.transform(({ data }) =>
data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm))
data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm))
)
export const packagesSchema = z.object({
code: z.nativeEnum(RoomPackageCodeEnum),
itemCode: z.string().optional(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
inventories: z.array(
z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
),
})
export const getRoomPackagesSchema = z
export const packagesSchema = z
.object({
data: z
.object({
attributes: z.object({
hotelId: z.number(),
packages: z.array(packagesSchema).optional().default([]),
packages: z.array(packageSchema).default([]),
}),
relationships: z
.object({
links: z.array(
z.object({
url: z.string(),
type: z.string(),
url: z.string(),
})
),
})
@@ -878,7 +240,7 @@ export const getRoomPackagesSchema = z
})
.optional(),
})
.transform((data) => data.data?.attributes?.packages ?? [])
.transform(({ data }) => data?.attributes.packages)
export const getHotelIdsByCityIdSchema = z
.object({
@@ -900,50 +262,4 @@ export const getNearbyHotelIdsSchema = z
})
),
})
.transform((data) => data.data.map((hotel) => hotel.id))
export const getMeetingRoomsSchema = z.object({
data: z.array(
z.object({
attributes: z.object({
name: z.string(),
email: z.string().optional(),
phoneNumber: z.string(),
size: z.number(),
doorWidth: z.number(),
doorHeight: z.number(),
length: z.number(),
width: z.number(),
height: z.number(),
floorNumber: z.number(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
short: z.string().optional(),
medium: z.string().optional(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
}),
seatings: z.array(
z.object({
type: z.string(),
capacity: z.number(),
})
),
lighting: z.string(),
sortOrder: z.number().optional(),
}),
id: z.string(),
type: z.string(),
})
),
})
.transform((data) => data.data.map((hotel) => hotel.id))

File diff suppressed because it is too large Load Diff

View File

@@ -60,8 +60,13 @@ export const additionalDataSchema = z
}),
type: z.literal("additionalData"),
})
.transform(({ attributes, type }) => ({
...attributes,
type,
id: attributes.id,
}))
export function transformAdditionalData(
data: z.output<typeof additionalDataSchema>
) {
return {
...data.attributes,
id: data.attributes.id,
type: data.type
}
}

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import { ChildBedTypeEnum } from "@/constants/booking"
export const childrenSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
export const occupancySchema = z.object({
adults: z.number(),
children: z.array(childrenSchema),
})

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
import { productTypePriceSchema } from "../productTypePrice"
export const productTypeSchema = z
.object({
public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(),
})
.optional()

View File

@@ -0,0 +1,14 @@
import { z } from "zod"
export const citySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().default(""),
ianaTimeZoneId: z.string().default(""),
isPublished: z.boolean().default(false),
keywords: z.array(z.string()).default([]),
name: z.string(),
timeZoneId: z.string().default(""),
}),
id: z.string(),
type: z.literal("cities"),
})

View File

@@ -0,0 +1,73 @@
import { z } from "zod"
import { addressSchema } from "./hotel/address"
import { contactInformationSchema } from "./hotel/contactInformation"
import { hotelContentSchema } from "./hotel/content"
import { detailedFacilitiesSchema } from "./hotel/detailedFacility"
import { hotelFactsSchema } from "./hotel/facts"
import { gallerySchema } from "./hotel/gallery"
import { healthFacilitySchema } from "./hotel/healthFacilities"
import { includeSchema } from "./hotel/include/include"
import { locationSchema } from "./hotel/location"
import { merchantInformationSchema } from "./hotel/merchantInformation"
import { parkingSchema } from "./hotel/parking"
import { pointOfInterestsSchema } from "./hotel/poi"
import { ratingsSchema } from "./hotel/rating"
import { rewardNightSchema } from "./hotel/rewardNight"
import { socialMediaSchema } from "./hotel/socialMedia"
import { specialAlertsSchema } from "./hotel/specialAlerts"
import { specialNeedGroupSchema } from "./hotel/specialNeedGroups"
import { imageSchema } from "./image"
import { facilitySchema } from "./additionalData"
export const attributesSchema = z.object({
accessibilityElevatorPitchText: z.string().optional(),
address: addressSchema,
cityId: z.string(),
cityName: z.string(),
conferencesAndMeetings: facilitySchema.optional(),
contactInformation: contactInformationSchema,
detailedFacilities: detailedFacilitiesSchema,
gallery: gallerySchema.optional(),
galleryImages: z.array(imageSchema).optional(),
healthAndWellness: facilitySchema.optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelRoomElevatorPitchText: z.string().optional(),
hotelType: z.string().optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: z.array(z.string()),
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: z.string(),
operaId: z.string(),
parking: z.array(parkingSchema),
pointsOfInterest: pointOfInterestsSchema,
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(),
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
specialNeedGroups: z.array(specialNeedGroupSchema),
vat: z.number(),
})
export const includesSchema = z
.array(includeSchema)
.default([])
.transform((data) => data.filter((item) => !!item))
const relationshipSchema = z.object({
links: z.object({
related: z.string(),
}),
})
export const relationshipsSchema = z.object({
meetingRooms: relationshipSchema,
nearbyHotels: relationshipSchema,
restaurants: relationshipSchema,
roomCategories: relationshipSchema,
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
export const addressSchema = z.object({
city: z.string(),
country: z.string(),
streetAddress: z.string(),
zipCode: z.string(),
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
export const contactInformationSchema = z.object({
email: z.string(),
faxNumber: z.string().optional(),
phoneNumber: z.string(),
websiteUrl: z.string(),
})

View File

@@ -0,0 +1,40 @@
import { z } from "zod"
import { imageSchema } from "../image"
export const hotelContentSchema = z.object({
images: imageSchema.default({
metaData: {
altText: "default image",
altText_En: "default image",
copyRight: "default image",
title: "default image",
},
imageSizes: {
large: "https://placehold.co/1280x720",
medium: "https://placehold.co/1280x720",
small: "https://placehold.co/1280x720",
tiny: "https://placehold.co/1280x720",
},
}),
restaurantsOverviewPage: z.object({
restaurantsContentDescriptionMedium: z.string().optional(),
restaurantsContentDescriptionShort: z.string().optional(),
restaurantsOverviewPageLink: z.string().optional(),
restaurantsOverviewPageLinkText: z.string().optional(),
}),
texts: z.object({
descriptions: z.object({
medium: z.string(),
short: z.string(),
}),
facilityInformation: z.string().optional(),
meetingDescription: z
.object({
medium: z.string().optional(),
short: z.string().optional(),
})
.optional(),
surroundingInformation: z.string(),
}),
})

View File

@@ -0,0 +1,18 @@
import { z } from "zod"
import { FacilityEnum } from "@/types/enums/facilities"
const detailedFacilitySchema = z.object({
filter: z.string().optional(),
icon: z.string().optional(),
id: z.nativeEnum(FacilityEnum),
name: z.string(),
public: z.boolean(),
sortOrder: z.number(),
})
export const detailedFacilitiesSchema = z
.array(detailedFacilitySchema)
.transform((facilities) =>
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
)

View File

@@ -0,0 +1,80 @@
import { z } from "zod"
const ecoLabelsSchema = z.object({
euEcoLabel: z.boolean(),
greenGlobeLabel: z.boolean(),
nordicEcoLabel: z.boolean(),
svanenEcoLabelCertificateNumber: z.string().optional(),
})
export const checkinSchema = z.object({
checkInTime: z.string(),
checkOutTime: z.string(),
onlineCheckout: z.boolean(),
onlineCheckOutAvailableFrom: z.string().nullable().optional(),
})
const hotelFacilityDetailSchema = z
.object({
description: z.string(),
heading: z.string(),
})
.optional()
/** Possibly more values */
const hotelFacilityDetailsSchema = z.object({
breakfast: hotelFacilityDetailSchema,
checkout: hotelFacilityDetailSchema,
gym: hotelFacilityDetailSchema,
internet: hotelFacilityDetailSchema,
laundry: hotelFacilityDetailSchema,
luggage: hotelFacilityDetailSchema,
shop: hotelFacilityDetailSchema,
telephone: hotelFacilityDetailSchema,
})
const hotelInformationSchema = z
.object({
description: z.string(),
heading: z.string(),
link: z.string().optional(),
})
.optional()
const hotelInformationsSchema = z.object({
accessibility: hotelInformationSchema,
safety: hotelInformationSchema,
sustainability: hotelInformationSchema,
})
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(),
})
export const hotelFactsSchema = z.object({
checkin: checkinSchema,
ecoLabels: ecoLabelsSchema,
hotelFacilityDetail: hotelFacilityDetailsSchema.default({}),
hotelInformation: hotelInformationsSchema.default({}),
interior: interiorSchema,
receptionHours: receptionHoursSchema,
yearBuilt: z.string(),
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
import { imageSchema } from "../image"
export const gallerySchema = z.object({
heroImages: z.array(imageSchema),
smallerImages: z.array(imageSchema),
})

View File

@@ -0,0 +1,41 @@
import { z } from "zod"
import { imageSchema } from "../image"
const healthFacilitiesOpenHoursSchema = z.object({
alwaysOpen: z.boolean(),
closingTime: z.string().optional(),
isClosed: z.boolean(),
openingTime: z.string().optional(),
sortOrder: z.number().optional(),
})
export const healthFacilitySchema = z.object({
content: z.object({
images: z.array(imageSchema),
texts: z.object({
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
}),
}),
details: z.array(
z.object({
name: z.string(),
type: z.string(),
value: z.string().optional(),
})
),
openingDetails: z.object({
manualOpeningHours: z.string().optional(),
openingHours: z.object({
ordinary: healthFacilitiesOpenHoursSchema,
weekends: healthFacilitiesOpenHoursSchema,
}),
useManualOpeningHours: z.boolean(),
}),
type: z.string(),
})

View File

@@ -0,0 +1,37 @@
import { z } from "zod"
import { citySchema } from "@/server/routers/hotels/schemas/city"
import { nearbyHotelsSchema } from "@/server/routers/hotels/schemas/hotel/include/nearbyHotels"
import { restaurantsSchema } from "@/server/routers/hotels/schemas/hotel/include/restaurants"
import {
roomCategoriesSchema,
transformRoomCategories,
} from "@/server/routers/hotels/schemas/hotel/include/roomCategories"
import { additionalDataSchema, transformAdditionalData } from "../../additionalData"
export const includeSchema = z
.discriminatedUnion("type", [
additionalDataSchema,
citySchema,
nearbyHotelsSchema,
restaurantsSchema,
roomCategoriesSchema,
])
.transform((data) => {
switch (data.type) {
case "additionalData":
return transformAdditionalData(data)
case "cities":
case "hotels":
case "restaurants":
return {
...data.attributes,
id: data.id,
type: data.type,
}
case "roomcategories":
return transformRoomCategories(data)
default:
return null
}
})

View File

@@ -0,0 +1,41 @@
import { z } from "zod"
import { attributesSchema } from "@/server/routers/hotels/schemas/hotel"
export const nearbyHotelsSchema = z.object({
attributes: z.lazy(() =>
z
.object({
displayWebPage: z
.object({
healthGym: z.boolean().default(false),
meetingRoom: z.boolean().default(false),
parking: z.boolean().default(false),
specialNeeds: z.boolean().default(false),
})
.default({
healthGym: false,
meetingRoom: false,
parking: false,
specialNeeds: false,
}),
})
.merge(
attributesSchema.pick({
address: true,
cityId: true,
cityName: true,
detailedFacilities: true,
hotelContent: true,
isActive: true,
isPublished: true,
location: true,
name: true,
operaId: true,
ratings: true,
})
)
),
id: z.string(),
type: z.literal("hotels"),
})

View File

@@ -0,0 +1,72 @@
import { z } from "zod"
import { imageSchema } from "@/server/routers/hotels/schemas/image"
import { CurrencyEnum } from "@/types/enums/currency"
const contentSchema = z.object({
images: z.array(imageSchema).default([]),
texts: z.object({
descriptions: z.object({
medium: z.string().default(""),
short: z.string().default(""),
}),
}),
})
const externalBreakfastSchema = z.object({
isAvailable: z.boolean().default(false),
localPriceForExternalGuests: z.object({
amount: z.number().default(0),
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.SEK),
}),
})
const menuItemSchema = z.object({
name: z.string(),
url: z.string().url(),
})
const daySchema = z.object({
alwaysOpen: z.boolean().default(false),
closingTime: z.string().default(""),
isClosed: z.boolean().default(false),
openingTime: z.string().default(""),
sortOrder: z.number().int().default(0),
})
const openingDetailsSchema = z.object({
alternateOpeningHours: z.object({
isActive: z.boolean().default(false),
}),
openingHours: z.object({
friday: daySchema,
isActive: z.boolean().default(false),
monday: daySchema,
name: z.string().default(""),
saturday: daySchema,
sunday: daySchema,
thursday: daySchema,
tuesday: daySchema,
wednesday: daySchema,
}),
})
export const restaurantsSchema = z.object({
attributes: z.object({
bookTableUrl: z.string().default(""),
content: contentSchema,
// When using .email().default("") is not sufficent
// so .optional also needs to be chained
email: z.string().email().optional(),
externalBreakfast: externalBreakfastSchema,
isPublished: z.boolean().default(false),
menus: z.array(menuItemSchema).default([]),
name: z.string().default(""),
openingDetails: z.array(openingDetailsSchema).default([]),
restaurantPage: z.boolean().default(false),
specialAlerts: z.array(z.object({})).default([]),
}),
id: z.string(),
type: z.literal("restaurants"),
})

View File

@@ -0,0 +1,97 @@
import { z } from "zod"
import { imageMetaDataSchema, imageSizesSchema } from "../../image"
const minMaxSchema = z.object({
max: z.number(),
min: z.number(),
})
const bedTypeSchema = z.object({
description: z.string().default(""),
type: z.string(),
widthRange: minMaxSchema,
})
const occupancySchema = z.object({
adults: z.number(),
children: z.number(),
total: z.number(),
})
const roomContentSchema = z.object({
images: z.array(
z.object({
imageSizes: imageSizesSchema,
metaData: imageMetaDataSchema,
})
),
texts: z.object({
descriptions: z.object({
medium: z.string().optional(),
short: z.string().optional(),
}),
}),
})
const roomTypesSchema = z.object({
code: z.string(),
description: z.string(),
fixedExtraBed: bedTypeSchema,
isLackingCribs: z.boolean(),
isLackingExtraBeds: z.boolean(),
mainBed: bedTypeSchema,
name: z.string(),
occupancy: occupancySchema,
roomCount: z.number(),
roomSize: minMaxSchema,
})
const roomFacilitiesSchema = z.object({
availableInAllRooms: z.boolean(),
icon: z.string().optional(),
isUniqueSellingPoint: z.boolean(),
name: z.string(),
sortOrder: z.number(),
})
export const roomCategoriesSchema = z.object({
attributes: z.object({
content: roomContentSchema,
name: z.string(),
occupancy: minMaxSchema,
roomFacilities: z.array(roomFacilitiesSchema),
roomSize: minMaxSchema,
roomTypes: z.array(roomTypesSchema),
sortOrder: z.number(),
}),
id: z.string(),
type: z.literal("roomcategories"),
})
export function transformRoomCategories(
data: z.output<typeof roomCategoriesSchema>
) {
return {
descriptions: data.attributes.content.texts.descriptions,
id: data.id,
images: data.attributes.content.images,
name: data.attributes.name,
occupancy: data.attributes.occupancy,
roomFacilities: data.attributes.roomFacilities,
roomSize: data.attributes.roomSize,
roomTypes: data.attributes.roomTypes,
sortOrder: data.attributes.sortOrder,
type: data.type,
totalOccupancy:
data.attributes.occupancy.min === data.attributes.occupancy.max
? {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.max}`,
}
: {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.min}-${data.attributes.occupancy.max}`,
},
}
}

View File

@@ -0,0 +1,7 @@
import { z } from "zod"
export const locationSchema = z.object({
distanceToCentre: z.number(),
latitude: z.number(),
longitude: z.number(),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
import type { PaymentMethodEnum } from "@/constants/booking"
export const merchantInformationSchema = z.object({
alternatePaymentOptions: z
.record(z.string(), z.boolean())
.transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
cards: z.record(z.string(), z.boolean()).transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
webMerchantId: z.string(),
})

View File

@@ -0,0 +1,41 @@
import { z } from "zod"
const periodSchema = z.object({
amount: z.number().optional(),
endTime: z.string().optional(),
period: z.string().optional(),
startTime: z.string().optional(),
})
const currencySchema = z
.object({
currency: z.string().optional(),
ordinary: z.array(periodSchema).optional(),
range: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
.optional(),
weekend: z.array(periodSchema).optional(),
})
.optional()
const pricingSchema = z.object({
freeParking: z.boolean(),
localCurrency: currencySchema,
paymentType: z.string().optional(),
requestedCurrency: currencySchema,
})
export const parkingSchema = z.object({
address: z.string().optional(),
canMakeReservation: z.boolean(),
distanceToHotel: z.number().optional(),
externalParkingUrl: z.string().optional(),
name: z.string().optional(),
numberOfChargingSpaces: z.number().optional(),
numberOfParkingSpots: z.number().optional(),
pricing: pricingSchema,
type: z.string().optional(),
})

View File

@@ -0,0 +1,32 @@
import { z } from "zod"
import { getPoiGroupByCategoryName } from "../../utils"
import { locationSchema } from "./location"
export const pointOfInterestSchema = z
.object({
category: z.object({
name: z.string().optional(),
group: z.string().optional(),
}),
distance: z.number().optional(),
isHighlighted: z.boolean().optional(),
location: locationSchema.optional(),
name: z.string().optional(),
})
.transform((poi) => ({
categoryName: poi.category.name,
coordinates: {
lat: poi.location?.latitude ?? 0,
lng: poi.location?.longitude ?? 0,
},
distance: poi.distance,
group: getPoiGroupByCategoryName(poi.category.name),
name: poi.name,
}))
export const pointOfInterestsSchema = z
.array(pointOfInterestSchema)
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
)

View File

@@ -0,0 +1,31 @@
import { z } from "zod"
const awardSchema = z.object({
displayName: z.string(),
images: z.object({
large: z.string(),
medium: z.string(),
small: z.string(),
}),
})
const reviewsSchema = z
.object({
widgetHtmlTagId: z.string(),
widgetScriptEmbedUrlIframe: z.string(),
widgetScriptEmbedUrlJavaScript: z.string(),
})
.optional()
export const ratingsSchema = z
.object({
tripAdvisor: z.object({
awards: z.array(awardSchema),
numberOfReviews: z.number(),
rating: z.number(),
ratingImageUrl: z.string(),
reviews: reviewsSchema,
webUrl: z.string(),
}),
})
.optional()

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
export const rewardNightSchema = z.object({
campaign: z.object({
end: z.string(),
points: z.number(),
start: z.string(),
}),
points: z.number(),
})

View File

@@ -0,0 +1,6 @@
import { z } from "zod"
export const socialMediaSchema = z.object({
facebook: z.string().optional(),
instagram: z.string().optional(),
})

View File

@@ -0,0 +1,35 @@
import { z } from "zod"
import { dt } from "@/lib/dt"
import { AlertTypeEnum } from "@/types/enums/alert"
const specialAlertSchema = z.object({
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
endDate: z.string().optional(),
startDate: z.string().optional(),
title: z.string().optional(),
type: z.string(),
})
export const specialAlertsSchema = z
.array(specialAlertSchema)
.transform((data) => {
const now = dt().utc().format("YYYY-MM-DD")
const filteredAlerts = data.filter((alert) => {
const shouldShowNow =
alert.startDate && alert.endDate
? alert.startDate <= now && alert.endDate >= now
: true
const hasText = alert.description || alert.title
return shouldShowNow && hasText
})
return filteredAlerts.map((alert, idx) => ({
heading: alert.title || null,
id: `alert-${alert.type}-${idx}`,
text: alert.description || null,
type: AlertTypeEnum.Info,
}))
})
.default([])

View File

@@ -0,0 +1,11 @@
import { z } from "zod"
const specialNeedSchema = z.object({
details: z.string(),
name: z.string(),
})
export const specialNeedGroupSchema = z.object({
name: z.string(),
specialNeeds: z.array(specialNeedSchema),
})

View File

@@ -0,0 +1,7 @@
import { z } from "zod"
export const hotelFilterSchema = z.object({
hotelFacilities: z.array(z.string()),
hotelSurroundings: z.array(z.string()),
roomFacilities: z.array(z.string()),
})

View File

@@ -1,17 +1,17 @@
import { z } from "zod"
const imageSizesSchema = z.object({
tiny: z.string(),
small: z.string(),
medium: z.string(),
export const imageSizesSchema = z.object({
large: z.string(),
medium: z.string(),
small: z.string(),
tiny: z.string(),
})
const imageMetaDataSchema = z.object({
title: z.string(),
export const imageMetaDataSchema = z.object({
altText: z.string(),
altText_En: z.string(),
copyRight: z.string(),
title: z.string(),
})
const DEFAULT_IMAGE_OBJ = {

View File

@@ -0,0 +1,12 @@
import { z } from "zod"
export const locationCitySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
}),
country: z.string().optional().default(""),
id: z.string().optional().default(""),
type: z.literal("cities"),
})

View File

@@ -0,0 +1,33 @@
import { z } from "zod"
export const locationHotelSchema = z.object({
attributes: z.object({
distanceToCentre: z.number().optional(),
images: z
.object({
large: z.string().optional(),
medium: z.string().optional(),
small: z.string().optional(),
tiny: z.string().optional(),
})
.optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
operaId: z.string().optional(),
}),
id: z.string().optional().default(""),
relationships: z
.object({
city: z
.object({
links: z
.object({
related: z.string().optional(),
})
.optional(),
})
.optional(),
})
.optional(),
type: z.literal("hotels"),
})

View File

@@ -0,0 +1,50 @@
import { z } from "zod"
import { imageSchema } from "./image"
export const meetingRoomsSchema = z
.object({
data: z.array(
z.object({
attributes: z.object({
name: z.string(),
email: z.string().optional(),
phoneNumber: z.string(),
size: z.number(),
doorWidth: z.number(),
doorHeight: z.number(),
length: z.number(),
width: z.number(),
height: z.number(),
floorNumber: z.number(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
short: z.string().optional(),
medium: z.string().optional(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
}),
seatings: z.array(
z.object({
type: z.string(),
capacity: z.number(),
})
),
lighting: z.string(),
sortOrder: z.number().optional(),
}),
id: z.string(),
type: z.string(),
})
),
})

View File

@@ -1,16 +1,9 @@
import { z } from "zod"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { PackageTypeEnum } from "@/types/enums/packages"
export const getRoomPackagesInputSchema = z.object({
hotelId: z.string(),
startDate: z.string(),
endDate: z.string(),
adults: z.number(),
children: z.number().optional().default(0),
packageCodes: z.array(z.string()).optional().default([]),
})
// TODO: Remove optional and default when the API change has been deployed
export const packagePriceSchema = z
.object({
currency: z.string().default("N/A"),
@@ -22,40 +15,27 @@ export const packagePriceSchema = z
currency: "N/A",
price: "0",
totalPrice: "0",
}) // TODO: Remove optional and default when the API change has been deployed
})
export const packagesSchema = z.object({
const inventorySchema = z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
export const packageSchema = z.object({
code: z.nativeEnum(RoomPackageCodeEnum),
description: z.string(),
inventories: z.array(inventorySchema),
itemCode: z.string().default(""),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
})
export const breakfastPackageSchema = z.object({
code: z.string(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
inventories: z.array(
z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
),
packageType: z.literal(PackageTypeEnum.BreakfastAdult),
})
export const getRoomPackagesSchema = z
.object({
data: z.object({
attributes: z.object({
hotelId: z.number(),
packages: z.array(packagesSchema).default([]),
}),
relationships: z
.object({
links: z.array(
z.object({
url: z.string(),
type: z.string(),
})
),
})
.optional(),
type: z.string(),
}),
})
.transform((data) => data.data.attributes.packages)

View File

@@ -0,0 +1,16 @@
import { z } from "zod"
import { CurrencyEnum } from "@/types/enums/currency"
export const priceSchema = z.object({
currency: z.nativeEnum(CurrencyEnum),
pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(),
})
export const productTypePriceSchema = z.object({
localPrice: priceSchema,
rateCode: z.string(),
rateType: z.string().optional(),
requestedPrice: priceSchema.optional(),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
const flexibilityPrice = z.object({
member: z.number(),
standard: z.number(),
})
export const rateSchema = z.object({
breakfastIncluded: z.boolean(),
description: z.string(),
id: z.number(),
imageSrc: z.string(),
name: z.string(),
prices: z.object({
currency: z.string(),
freeCancellation: flexibilityPrice,
freeRebooking: flexibilityPrice,
nonRefundable: flexibilityPrice,
}),
size: z.string(),
})

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
export const relationshipsSchema = z.object({
links: z.array(
z.object({
type: z.string(),
url: z.string().url(),
})
),
})

View File

@@ -0,0 +1,23 @@
import { z } from "zod"
import { productSchema } from "./product"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export const roomConfigurationSchema = z.object({
features: z.array(
z.object({
inventory: z.number(),
code: z.enum([
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
]),
})
),
products: z.array(productSchema),
roomsLeft: z.number(),
roomType: z.string(),
roomTypeCode: z.string(),
status: z.string(),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
import { productTypePriceSchema } from "../productTypePrice"
import { CurrencyEnum } from "@/types/enums/currency"
export const productSchema = z.object({
productType: z.object({
member: productTypePriceSchema.optional(),
public: productTypePriceSchema.default({
localPrice: {
currency: CurrencyEnum.SEK,
pricePerNight: 0,
pricePerStay: 0,
},
rateCode: "",
rateType: "",
requestedPrice: undefined,
}),
}),
})

View File

@@ -0,0 +1,12 @@
import { z } from "zod"
export const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean(),
cancellationRule: z.string(),
cancellationText: z.string(),
generalTerms: z.array(z.string()),
mustBeGuaranteed: z.boolean(),
rateCode: z.string(),
rateType: z.string().optional(),
title: z.string(),
})

View File

@@ -4,25 +4,24 @@ import { unstable_cache } from "next/cache"
import * as api from "@/lib/api"
import {
apiCitiesByCountrySchema,
apiCitySchema,
apiCountriesSchema,
apiLocationsSchema,
type CitiesGroupedByCountry,
citiesByCountrySchema,
citiesSchema,
countriesSchema,
getHotelIdsByCityIdSchema,
locationsSchema,
} from "./output"
import {
getHotelIdsCounter,
getHotelIdsFailCounter,
getHotelIdsSuccessCounter,
} from "./telemetry"
import type { Country } from "@/types/enums/country"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import { PointOfInterestGroupEnum } from "@/types/hotel"
import type { HotelLocation } from "@/types/trpc/routers/hotel/locations"
import type {
CitiesGroupedByCountry,
Countries,
HotelLocation,
} from "@/types/trpc/routers/hotel/locations"
import type { Lang } from "@/constants/languages"
import type { Endpoint } from "@/lib/api/endpoints"
import { Country } from "@/types/enums/country"
import { metrics } from "./metrics"
export function getPoiGroupByCategoryName(category: string | undefined) {
if (!category) return PointOfInterestGroupEnum.LOCATION
@@ -75,7 +74,7 @@ export async function getCity(
}
const cityJson = await cityResponse.json()
const city = apiCitySchema.safeParse(cityJson)
const city = citiesSchema.safeParse(cityJson)
if (!city.success) {
console.info(`Validation of city failed`)
console.info(`cityUrl: ${locationCityUrl}`)
@@ -108,7 +107,7 @@ export async function getCountries(
}
const countriesJson = await countryResponse.json()
const countries = apiCountriesSchema.safeParse(countriesJson)
const countries = countriesSchema.safeParse(countriesJson)
if (!countries.success) {
console.info(`Validation for countries failed`)
console.error(countries.error)
@@ -149,8 +148,7 @@ export async function getCitiesByCountry(
}
const countryJson = await countryResponse.json()
const citiesByCountry =
apiCitiesByCountrySchema.safeParse(countryJson)
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
if (!citiesByCountry.success) {
console.info(`Failed to validate Cities by Country payload`)
console.error(citiesByCountry.error)
@@ -200,7 +198,7 @@ export async function getLocations(
}
const apiJson = await apiResponse.json()
const verifiedLocations = apiLocationsSchema.safeParse(apiJson)
const verifiedLocations = locationsSchema.safeParse(apiJson)
if (!verifiedLocations.success) {
console.info(`Locations Verification Failed`)
console.error(verifiedLocations.error)
@@ -273,7 +271,7 @@ export async function getHotelIdsByCityId(
) {
return unstable_cache(
async function (params: URLSearchParams) {
getHotelIdsCounter.add(1, { params: params.toString() })
metrics.hotelIds.counter.add(1, { params: params.toString() })
console.info(
"api.hotel.hotel-ids start",
JSON.stringify({ params: params.toString() })
@@ -286,7 +284,7 @@ export async function getHotelIdsByCityId(
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
params: params.toString(),
error_type: "http_error",
error: responseMessage,
@@ -309,7 +307,7 @@ export async function getHotelIdsByCityId(
const apiJson = await apiResponse.json()
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
if (!validatedHotelIds.success) {
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
params: params.toString(),
error_type: "validation_error",
error: JSON.stringify(validatedHotelIds.error),
@@ -324,7 +322,7 @@ export async function getHotelIdsByCityId(
return null
}
getHotelIdsSuccessCounter.add(1, { cityId })
metrics.hotelIds.success.add(1, { cityId })
console.info(
"api.hotel.hotel-ids success",
JSON.stringify({ params: params.toString() })
@@ -344,7 +342,7 @@ export async function getHotelIdsByCountry(
) {
return unstable_cache(
async function (params: URLSearchParams) {
getHotelIdsCounter.add(1, { country })
metrics.hotelIds.counter.add(1, { country })
console.info(
"api.hotel.hotel-ids start",
JSON.stringify({ query: { country } })
@@ -357,7 +355,7 @@ export async function getHotelIdsByCountry(
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
country,
error_type: "http_error",
error: responseMessage,
@@ -380,7 +378,7 @@ export async function getHotelIdsByCountry(
const apiJson = await apiResponse.json()
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
if (!validatedHotelIds.success) {
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
country,
error_type: "validation_error",
error: JSON.stringify(validatedHotelIds.error),
@@ -395,7 +393,7 @@ export async function getHotelIdsByCountry(
return null
}
getHotelIdsSuccessCounter.add(1, { country })
metrics.hotelIds.success.add(1, { country })
console.info(
"api.hotel.hotel-ids success",
JSON.stringify({ query: { country } })