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

1544 lines
45 KiB
TypeScript

import { unstable_cache } from "next/cache"
import { ApiLang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { badRequestError } from "@/server/errors/trpc"
import {
contentStackBaseWithServiceProcedure,
publicProcedure,
router,
safeProtectedServiceProcedure,
serviceProcedure,
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"
import { cache } from "@/utils/cache"
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
import { getVerifiedUser, parsedUser } from "../user/query"
import { additionalDataSchema } from "./schemas/additionalData"
import { meetingRoomsSchema } from "./schemas/meetingRoom"
import {
breakfastPackageInputSchema,
cityCoordinatesInputSchema,
getAdditionalDataInputSchema,
getHotelsByHotelIdsAvailabilityInputSchema,
getHotelsInput,
getMeetingRoomsInputSchema,
hotelInputSchema,
hotelsAvailabilityInputSchema,
nearbyHotelIdsInput,
ratesInputSchema,
roomPackagesInputSchema,
roomsAvailabilityInputSchema,
selectedRoomAvailabilityInputSchema,
} from "./input"
import { metrics } from "./metrics"
import {
breakfastPackagesSchema,
getNearbyHotelIdsSchema,
hotelsAvailabilitySchema,
hotelSchema,
packagesSchema,
ratesSchema,
roomsAvailabilitySchema,
} from "./output"
import tempRatesData from "./tempRatesData.json"
import {
getCitiesByCountry,
getCountries,
getHotelIdsByCityId,
getHotelIdsByCountry,
getLocations,
TWENTYFOUR_HOURS,
} from "./utils"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { HotelData } from "@/types/hotel"
import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage"
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
export const getHotel = cache(
async (input: HotelInput, serviceToken: string) => {
const callable = unstable_cache(
async function (
hotelId: HotelInput["hotelId"],
language: HotelInput["language"],
isCardOnlyPayment?: HotelInput["isCardOnlyPayment"]
) {
/**
* Since API expects the params appended and not just
* a comma separated string we need to initialize the
* SearchParams with a sequence of pairs
* (include=City&include=NearbyHotels&include=Restaurants etc.)
**/
const params = new URLSearchParams([
["include", "AdditionalData"],
["include", "City"],
["include", "NearbyHotels"],
["include", "Restaurants"],
["include", "RoomCategories"],
["language", toApiLang(language)],
])
metrics.hotel.counter.add(1, {
hotelId,
language,
})
console.info(
"api.hotels.hotelData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${serviceToken}`,
},
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
next: {
revalidate: env.CACHE_TIME_HOTELS,
tags: [`${language}:hotel:${hotelId}`],
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.hotel.fail.add(1, {
hotelId,
language,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelData error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = hotelSchema.safeParse(apiJson)
if (!validateHotelData.success) {
metrics.hotel.fail.add(1, {
hotelId,
language,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.hotelData validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
metrics.hotel.success.add(1, {
hotelId,
language,
})
console.info(
"api.hotels.hotelData success",
JSON.stringify({
query: { hotelId, params: params },
})
)
const hotelData = validateHotelData.data
if (isCardOnlyPayment) {
hotelData.hotel.merchantInformationData.alternatePaymentOptions = []
}
const gallery = hotelData.additionalData?.gallery
if (gallery) {
const smallerImages = gallery.smallerImages
const hotelGalleryImages =
hotelData.hotel.hotelType === HotelTypeEnum.Signature
? smallerImages.slice(0, 10)
: smallerImages.slice(0, 6)
hotelData.hotel.galleryImages = hotelGalleryImages
}
return hotelData
},
[`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`],
{
revalidate: env.CACHE_TIME_HOTELS,
tags: [
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
],
}
)
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
}
)
export const hotelQueryRouter = router({
availability: router({
hotelsByCity: serviceProcedure
.input(hotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
} = input
const params: Record<string, string | number> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: apiLang,
}
metrics.hotelsAvailability.counter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.hotelsAvailability start",
JSON.stringify({ query: { cityId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.city(cityId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.hotelsAvailability.fail.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsAvailability error",
JSON.stringify({
query: { cityId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
hotelsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
metrics.hotelsAvailability.fail.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.hotelsAvailability validation error",
JSON.stringify({
query: { cityId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
metrics.hotelsAvailability.success.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.hotelsAvailability success",
JSON.stringify({
query: { cityId, params: params },
})
)
return {
availability: validateAvailabilityData.data.data.flatMap(
(hotels) => hotels.attributes
),
}
}),
hotelsByHotelIds: serviceProcedure
.input(getHotelsByHotelIdsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
} = input
const params: Record<string, string | number | number[]> = {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: apiLang,
}
metrics.hotelsByHotelIdAvailability.counter.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.hotelsByHotelIdAvailability start",
JSON.stringify({ query: { params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotels(),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.hotelsByHotelIdAvailability.fail.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsByHotelIdAvailability error",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
hotelsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
metrics.hotelsByHotelIdAvailability.fail.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.hotelsByHotelIdAvailability validation error",
JSON.stringify({
query: { params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
metrics.hotelsByHotelIdAvailability.success.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.hotelsByHotelIdAvailability success",
JSON.stringify({
query: { params },
})
)
return {
availability: validateAvailabilityData.data.data.flatMap(
(hotels) => hotels.attributes
),
}
}),
rooms: serviceProcedure
.input(roomsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
rateCode,
} = input
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: apiLang,
}
metrics.roomAvailability.counter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.roomsAvailability start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.roomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.roomsAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
metrics.roomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.roomsAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
metrics.roomAvailability.success.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.roomsAvailability success",
JSON.stringify({
query: { hotelId, params: params },
})
)
if (rateCode) {
validateAvailabilityData.data.mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.filter(
(rate) => rate.rateCode === rateCode
)[0].mustBeGuaranteed
}
return validateAvailabilityData.data
}),
room: serviceProcedure
.input(selectedRoomAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
rateCode,
roomTypeCode,
packageCodes,
} = input
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: toApiLang(ctx.lang),
}
metrics.selectedRoomAvailability.counter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponseAvailability = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponseAvailability.ok) {
const text = await apiResponseAvailability.text()
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
}),
})
console.error(
"api.hotels.selectedRoomAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
},
})
)
return null
}
const apiJsonAvailability = await apiResponseAvailability.json()
const validateAvailabilityData =
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
if (!validateAvailabilityData.success) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.selectedRoomAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
const hotelData = await getHotel(
{
hotelId,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
const availableRooms =
validateAvailabilityData.data.roomConfigurations.filter((room) => {
if (packageCodes) {
return (
room.status === "Available" &&
room.features.some(
(feature) =>
packageCodes.includes(feature.code) && feature.inventory > 0
)
)
}
return room.status === "Available"
})
const selectedRoom = availableRooms.find(
(room) => room.roomTypeCode === roomTypeCode
)
const availableRoomsInCategory = availableRooms.filter(
(room) => room.roomType === selectedRoom?.roomType
)
if (!selectedRoom) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "not_found",
error: `Couldn't find selected room with input: ${roomTypeCode}`,
})
console.error("No matching room found")
return null
}
const rateTypes = selectedRoom.products.find(
(rate) =>
rate.productType.public?.rateCode === rateCode ||
rate.productType.member?.rateCode === rateCode
)
if (!rateTypes) {
metrics.selectedRoomAvailability.fail.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "not_found",
error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`,
})
console.error("No matching rate found")
return null
}
const rates = rateTypes.productType
const rateDefinition =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)
const bedTypes = availableRoomsInCategory
.map((availRoom) => {
const matchingRoom = hotelData?.roomCategories
?.find((room) =>
room.roomTypes
.map((roomType) => roomType.code)
.includes(availRoom.roomTypeCode)
)
?.roomTypes.find(
(roomType) => roomType.code === availRoom.roomTypeCode
)
if (matchingRoom) {
return {
description: matchingRoom.description,
size: matchingRoom.mainBed.widthRange,
value: matchingRoom.code,
type: matchingRoom.mainBed.type,
extraBed: matchingRoom.fixedExtraBed
? {
type: matchingRoom.fixedExtraBed.type,
description: matchingRoom.fixedExtraBed.description,
}
: undefined,
}
}
})
.filter((bed): bed is BedTypeSelection => Boolean(bed))
metrics.selectedRoomAvailability.success.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.selectedRoomAvailability success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
selectedRoom,
rateDetails: rateDefinition?.generalTerms,
cancellationText: rateDefinition?.cancellationText ?? "",
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
memberRate: rates?.member,
publicRate: rates.public,
bedTypes,
}
}),
}),
rates: router({
get: publicProcedure
.input(ratesInputSchema)
.query(async ({ input, ctx }) => {
// TODO: Do a real API call when the endpoint is ready
// const { hotelId } = input
// const params = new URLSearchParams()
// const apiLang = toApiLang(language)
// params.set("hotelId", hotelId.toString())
// params.set("language", apiLang)
console.info("api.hotels.rates start", JSON.stringify({}))
const validatedHotelData = ratesSchema.safeParse(tempRatesData)
if (!tempRatesData) {
console.error(
"api.hotels.rates error",
JSON.stringify({ error: null })
)
//Can't return null here since consuming component does not handle null yet
// return null
}
if (!validatedHotelData.success) {
console.error(
"api.hotels.rates validation error",
JSON.stringify({
error: validatedHotelData.error,
})
)
throw badRequestError()
}
console.info("api.hotels.rates success", JSON.stringify({}))
return validatedHotelData.data
}),
}),
get: serviceProcedure
.input(hotelInputSchema)
.query(async ({ ctx, input }) => {
return getHotel(input, ctx.serviceToken)
}),
hotels: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsInput)
.query(async function ({ ctx, input }) {
const { locationFilter, hotelsToInclude } = input
const language = ctx.lang
const apiLang = toApiLang(language)
const options: RequestOptionsWithOutBody = {
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: env.CACHE_TIME_HOTELS,
},
}
let hotelsToFetch: string[] = []
metrics.hotels.counter.add(1, {
input: JSON.stringify(input),
language,
})
console.info(
"api.hotel.hotels start",
JSON.stringify({
query: {
...input,
language,
},
})
)
if (hotelsToInclude.length) {
hotelsToFetch = hotelsToInclude
} else if (locationFilter?.city) {
const locationsParams = new URLSearchParams({
language: apiLang,
})
const locations = await getLocations(
language,
options,
locationsParams,
null
)
if (!locations || "error" in locations) {
return []
}
const cityId = locations
.filter(
(loc): loc is CityLocation =>
"type" in loc && loc.type === "cities"
)
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
if (!cityId) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { ...input, language },
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
)
return []
}
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: cityId,
onlyBasicInfo: "true",
})
const hotelIds = await getHotelIdsByCityId(
cityId,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
cityId,
language,
error_type: "not_found",
error: `No hotelIds found for cityId: ${cityId}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { cityId, language },
error: `No hotelIds found for cityId: ${cityId}`,
})
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
} else if (locationFilter?.country) {
const hotelIdsParams = new URLSearchParams({
language: ApiLang.En,
country: locationFilter.country,
onlyBasicInfo: "true",
})
const hotelIds = await getHotelIdsByCountry(
locationFilter.country,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
country: locationFilter.country,
language,
error_type: "not_found",
error: `No hotelIds found for country: ${locationFilter.country}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { country: locationFilter.country, language },
error: `No hotelIds found for cityId: ${locationFilter.country}`,
})
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
}
if (!hotelsToFetch.length) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: JSON.stringify(input),
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
)
return []
}
const hotels = await Promise.all(
hotelsToFetch.map(async (hotelId) => {
const [hotelData, url] = await Promise.all([
getHotel(
{ hotelId, isCardOnlyPayment: false, language },
ctx.serviceToken
),
getHotelPageUrl(language, hotelId),
])
return {
data: hotelData,
url,
}
})
)
metrics.hotels.success.add(1, {
input: JSON.stringify(input),
language,
})
console.info(
"api.hotels success",
JSON.stringify({
query: {
input: JSON.stringify(input),
language,
},
})
)
return hotels.filter(
(hotel): hotel is { data: HotelData; url: HotelPageUrl } =>
!!hotel.data
)
}),
}),
nearbyHotelIds: serviceProcedure
.input(nearbyHotelIdsInput)
.query(async function ({ ctx, input }) {
const { lang } = ctx
const apiLang = toApiLang(lang)
const { hotelId } = input
const params: Record<string, string | number> = {
language: apiLang,
}
metrics.nearbyHotelIds.counter.add(1, {
hotelId,
})
console.info(
"api.hotels.nearbyHotelIds start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.nearbyHotelIds.fail.add(1, {
hotelId,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.nearbyHotelIds error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
if (!validateHotelData.success) {
metrics.nearbyHotelIds.fail.add(1, {
hotelId,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.nearbyHotelIds validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
metrics.nearbyHotelIds.success.add(1, {
hotelId,
})
console.info(
"api.hotels.nearbyHotelIds success",
JSON.stringify({
query: { hotelId, params },
})
)
return validateHotelData.data.map((id: string) => parseInt(id, 10))
}),
locations: router({
get: serviceProcedure.query(async function ({ ctx }) {
const searchParams = new URLSearchParams()
searchParams.set("language", toApiLang(ctx.lang))
const options: RequestOptionsWithOutBody = {
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: env.CACHE_TIME_HOTELS,
},
}
const countries = await getCountries(options, searchParams, ctx.lang)
if (!countries) {
return null
}
const countryNames = countries.data.map((country) => country.name)
const citiesByCountry = await getCitiesByCountry(
countryNames,
options,
searchParams,
ctx.lang
)
const locations = await getLocations(
ctx.lang,
options,
searchParams,
citiesByCountry
)
if (Array.isArray(locations)) {
return {
data: locations,
}
}
return locations
}),
}),
map: router({
city: serviceProcedure
.input(cityCoordinatesInputSchema)
.query(async function ({ input }) {
const apiKey = process.env.GOOGLE_STATIC_MAP_KEY
const { city, hotel } = input
async function fetchCoordinates(address: string) {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
const response = await fetch(url)
const data = await response.json()
if (data.status !== "OK") {
console.error(`Geocode error: ${data.status}`)
return null
}
const location = data.results[0]?.geometry?.location
if (!location) {
console.error("No location found in geocode response")
return null
}
return location
}
let location = await fetchCoordinates(city)
if (!location) {
location = await fetchCoordinates(`${city}, ${hotel.address}`)
}
if (!location) {
throw new Error("Unable to fetch coordinates")
}
return location
}),
}),
meetingRooms: safeProtectedServiceProcedure
.input(getMeetingRoomsInputSchema)
.query(async function ({ ctx, input }) {
const { hotelId, language } = input
const params: Record<string, string | string[]> = {
hotelId,
language,
}
const metricsData = { ...params, hotelId: input.hotelId }
metrics.meetingRooms.counter.add(1, metricsData)
console.info(
"api.hotels.meetingRooms start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.meetingRooms(input.hotelId),
{
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: env.CACHE_TIME_HOTELS,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.meetingRooms.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.meetingRooms error",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validatedMeetingRooms = meetingRoomsSchema.safeParse(apiJson)
if (!validatedMeetingRooms.success) {
console.error(
"api.hotels.meetingRooms validation error",
JSON.stringify({
query: { params },
error: validatedMeetingRooms.error,
})
)
throw badRequestError()
}
metrics.meetingRooms.success.add(1, {
hotelId,
})
console.info(
"api.hotels.meetingRooms success",
JSON.stringify({ query: { params } })
)
return validatedMeetingRooms.data.data
}),
additionalData: safeProtectedServiceProcedure
.input(getAdditionalDataInputSchema)
.query(async function ({ ctx, input }) {
const { hotelId, language } = input
const params: Record<string, string | string[]> = {
hotelId,
language,
}
const metricsData = { ...params, hotelId: input.hotelId }
metrics.additionalData.counter.add(1, metricsData)
console.info(
"api.hotels.additionalData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId),
{
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: env.CACHE_TIME_HOTELS,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.additionalData.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.additionalData error",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validatedAdditionalData = additionalDataSchema.safeParse(apiJson)
if (!validatedAdditionalData.success) {
console.error(
"api.hotels.additionalData validation error",
JSON.stringify({
query: { params },
error: validatedAdditionalData.error,
})
)
throw badRequestError()
}
metrics.additionalData.success.add(1, {
hotelId,
})
console.info(
"api.hotels.additionalData success",
JSON.stringify({ query: { params } })
)
return validatedAdditionalData.data
}),
packages: router({
get: serviceProcedure
.input(roomPackagesInputSchema)
.query(async ({ input, ctx }) => {
const { hotelId, startDate, endDate, adults, children, packageCodes } =
input
const { lang } = ctx
const apiLang = toApiLang(lang)
const searchParams = new URLSearchParams({
startDate,
endDate,
adults: adults.toString(),
children: children.toString(),
language: apiLang,
})
packageCodes.forEach((code) => {
searchParams.append("packageCodes", code)
})
const params = searchParams.toString()
metrics.packages.counter.add(1, {
hotelId,
})
console.info(
"api.hotels.packages start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Package.Packages.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
searchParams
)
if (!apiResponse.ok) {
metrics.packages.fail.add(1, {
hotelId,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
}),
})
console.error(
"api.hotels.packages error",
JSON.stringify({ query: { hotelId, params } })
)
}
const apiJson = await apiResponse.json()
const validatedPackagesData = packagesSchema.safeParse(apiJson)
if (!validatedPackagesData.success) {
metrics.packages.fail.add(1, {
hotelId,
error_type: "validation_error",
error: JSON.stringify(validatedPackagesData.error),
})
console.error(
"api.hotels.packages validation error",
JSON.stringify({
query: { hotelId, params },
error: validatedPackagesData.error,
})
)
throw badRequestError()
}
metrics.packages.success.add(1, {
hotelId,
})
console.info(
"api.hotels.packages success",
JSON.stringify({ query: { hotelId, params: params } })
)
return validatedPackagesData.data
}),
breakfast: safeProtectedServiceProcedure
.input(breakfastPackageInputSchema)
.query(async function ({ ctx, input }) {
const { lang } = ctx
const apiLang = toApiLang(lang)
const params = {
Adults: input.adults,
EndDate: dt(input.toDate).format("YYYY-MM-DD"),
StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
language: apiLang,
}
const metricsData = { ...params, hotelId: input.hotelId }
metrics.breakfastPackage.counter.add(1, metricsData)
console.info(
"api.package.breakfast start",
JSON.stringify({ query: metricsData })
)
const apiResponse = await api.get(
api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
{
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: 60,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.breakfastPackage.fail.add(1, {
...metricsData,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsAvailability error",
JSON.stringify({
query: metricsData,
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson)
if (!breakfastPackages.success) {
metrics.breakfastPackage.fail.add(1, {
...metricsData,
error_type: "validation_error",
error: JSON.stringify(breakfastPackages.error),
})
console.error(
"api.package.breakfast validation error",
JSON.stringify({
query: metricsData,
error: breakfastPackages.error,
})
)
return null
}
metrics.breakfastPackage.success.add(1, metricsData)
console.info(
"api.package.breakfast success",
JSON.stringify({
query: metricsData,
})
)
if (ctx.session?.token) {
const apiUser = await getVerifiedUser({ session: ctx.session })
if (apiUser && !("error" in apiUser)) {
const user = parsedUser(apiUser.data, false)
if (
user.membership &&
["L6", "L7"].includes(user.membership.membershipLevel)
) {
const freeBreakfastPackage = breakfastPackages.data.find(
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
)
if (freeBreakfastPackage?.localPrice) {
return [freeBreakfastPackage]
}
}
}
}
return breakfastPackages.data.filter(
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
)
}),
}),
})