Files
web/packages/trpc/lib/routers/hotels/query.ts
Joakim Jäderberg daf765f3d5 Merged in feature/wrap-logging (pull request #2511)
Feature/wrap logging

* feat: change all logging to go through our own logger function so that we can control log levels

* move packages/trpc to using our own logger

* merge


Approved-by: Linus Flood
2025-07-03 12:37:04 +00:00

1191 lines
37 KiB
TypeScript

import { Lang } from "@scandic-hotels/common/constants/language"
import { getCacheClient } from "@scandic-hotels/common/dataCache"
import { dt } from "@scandic-hotels/common/dt"
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { createCounter } from "@scandic-hotels/common/telemetry"
import { env } from "../../../env/server"
import { router } from "../.."
import * as api from "../../api"
import { SEARCH_TYPE_REDEMPTION } from "../../constants/booking"
import { BreakfastPackageEnum } from "../../enums/breakfast"
import { RateEnum } from "../../enums/rate"
import { RateTypeEnum } from "../../enums/rateType"
import { AvailabilityEnum } from "../../enums/selectHotel"
import { badRequestError, unauthorizedError } from "../../errors"
import {
contentStackBaseWithServiceProcedure,
publicProcedure,
safeProtectedServiceProcedure,
serviceProcedure,
} from "../../procedures"
import { getCityPageUrls } from "../../routers/contentstack/destinationCityPage/utils"
import { getHotelPageUrls } from "../../routers/contentstack/hotelPage/utils"
import {
ancillaryPackageInputSchema,
breakfastPackageInputSchema,
cityCoordinatesInputSchema,
enterDetailsRoomsAvailabilityInputSchema,
getAdditionalDataInputSchema,
getDestinationsMapDataInput,
getHotelsByCityIdentifierInput,
getHotelsByCountryInput,
getHotelsByCSFilterInput,
getHotelsByHotelIdsAvailabilityInputSchema,
getLocationsInput,
getLocationsUrlsInput,
getMeetingRoomsInputSchema,
hotelInputSchema,
hotelsAvailabilityInputSchema,
myStayRoomAvailabilityInputSchema,
nearbyHotelIdsInput,
roomPackagesInputSchema,
selectRateRoomAvailabilityInputSchema,
selectRateRoomsAvailabilityInputSchema,
} from "../../routers/hotels/input"
import {
ancillaryPackagesSchema,
breakfastPackagesSchema,
getNearbyHotelIdsSchema,
} from "../../routers/hotels/output"
import { toApiLang } from "../../utils"
import { getVerifiedUser } from "../user/utils"
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
import { meetingRoomsSchema } from "./schemas/meetingRoom"
import {
getCitiesByCountry,
getCountries,
getHotel,
getHotelIdsByCityId,
getHotelIdsByCityIdentifier,
getHotelIdsByCountry,
getHotelsByHotelIds,
getLocations,
} from "./utils"
import {
getBedTypes,
getHotelsAvailabilityByCity,
getHotelsAvailabilityByHotelIds,
getPackages,
getRoomsAvailability,
getSelectedRoomAvailability,
mergeRoomTypes,
selectRateRedirectURL,
} from "./utils"
import type {
DestinationPagesHotelData,
HotelDataWithUrl,
} from "../../types/hotel"
import type { CityLocation } from "../../types/locations"
import type { Room } from "../../types/room"
const hotelQueryLogger = createLogger("hotelQueryRouter")
export const hotelQueryRouter = router({
availability: router({
hotelsByCity: safeProtectedServiceProcedure
.input(hotelsAvailabilityInputSchema)
.use(async ({ ctx, input, next }) => {
if (input.redemption) {
if (ctx.session?.token.access_token) {
const verifiedUser = await getVerifiedUser({ session: ctx.session })
if (!verifiedUser?.error) {
return next({
ctx: {
token: ctx.session.token.access_token,
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
},
input,
})
}
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
input,
})
})
.query(async ({ ctx, input }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
} = input
// In case of redemption do not cache result
if (redemption) {
return getHotelsAvailabilityByCity(
input,
apiLang,
ctx.token,
ctx.userPoints
)
}
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
async () => {
return getHotelsAvailabilityByCity(input, apiLang, ctx.token)
},
env.CACHE_TIME_CITY_SEARCH
)
}),
hotelsByHotelIds: serviceProcedure
.input(getHotelsByHotelIdsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken)
}),
enterDetails: safeProtectedServiceProcedure
.input(enterDetailsRoomsAvailabilityInputSchema)
.use(async ({ ctx, input, next }) => {
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
if (ctx.session?.token.access_token) {
const verifiedUser = await getVerifiedUser({ session: ctx.session })
if (!verifiedUser?.error) {
return next({
ctx: {
token: ctx.session.token.access_token,
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
},
})
}
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
})
})
.query(async function ({ ctx, input }) {
const availability = await getRoomsAvailability(
input,
ctx.token,
ctx.serviceToken,
ctx.userPoints
)
const hotelData = await getHotel(
{
hotelId: input.booking.hotelId,
isCardOnlyPayment: false,
language: input.lang || ctx.lang,
},
ctx.serviceToken
)
const selectedRooms = []
for (const [idx, room] of availability.entries()) {
if (!room || "error" in room) {
hotelQueryLogger.error(
`Availability failed: ${room.error}`,
room.details
)
selectedRooms.push(null)
continue
}
const bookingRoom = input.booking.rooms[idx]
const selected = getSelectedRoomAvailability(
bookingRoom.rateCode,
room.rateDefinitions,
room.roomConfigurations,
bookingRoom.roomTypeCode,
ctx.userPoints
)
if (!selected) {
hotelQueryLogger.error("Unable to find selected room")
selectedRooms.push(null)
continue
}
const {
rateDefinition,
rateDefinitions,
product,
rooms,
selectedRoom,
} = selected
const bedTypes = getBedTypes(
rooms,
selectedRoom.roomType,
hotelData?.roomCategories
)
const counterRateCode = input.booking.rooms[idx].counterRateCode
const rateCode = input.booking.rooms[idx].rateCode
let memberRateDefinition = undefined
if ("member" in product && product.member && counterRateCode) {
memberRateDefinition = rateDefinitions.find(
(rate) =>
(rate.rateCode === counterRateCode ||
rate.rateCode === rateCode) &&
rate.isMemberRate
)
}
const selectedPackages = input.booking.rooms[idx].packages
selectedRooms.push({
bedTypes,
breakfastIncluded: rateDefinition.breakfastIncluded,
cancellationText: rateDefinition.cancellationText,
cancellationRule: rateDefinition.cancellationRule,
isAvailable: selectedRoom.status === AvailabilityEnum.Available,
isFlexRate: product.rate === RateEnum.flex,
memberMustBeGuaranteed: memberRateDefinition?.mustBeGuaranteed,
mustBeGuaranteed: rateDefinition.mustBeGuaranteed,
packages: room.packages.filter((pkg) =>
selectedPackages?.includes(pkg.code)
),
rate: product.rate,
rateDefinitionTitle: rateDefinition.title,
rateDetails: rateDefinition.generalTerms,
memberRateDetails: memberRateDefinition?.generalTerms,
// Send rate Title when it is a booking code rate
rateTitle:
rateDefinition.rateType !== RateTypeEnum.Regular
? rateDefinition.title
: undefined,
rateType: rateDefinition.rateType,
roomRate: product,
roomType: selectedRoom.roomType,
roomTypeCode: selectedRoom.roomTypeCode,
})
}
const totalBedsAvailableForRoomTypeCode: Record<string, number> = {}
for (const selectedRoom of selectedRooms) {
if (selectedRoom) {
if (!totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]) {
totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] =
selectedRoom.bedTypes.reduce(
(total, bedType) => total + bedType.roomsLeft,
0
)
}
}
}
for (const [idx, selectedRoom] of selectedRooms.entries()) {
if (selectedRoom) {
const totalBedsLeft =
totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]
if (totalBedsLeft <= 0) {
selectedRooms[idx] = null
continue
}
totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] =
totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] - 1
}
}
if (selectedRooms.some((sr) => !sr)) {
return selectRateRedirectURL(input, selectedRooms.map(Boolean))
}
// Make TS show appropriate type
return selectedRooms.filter((sr): sr is Room => !!sr)
}),
myStay: safeProtectedServiceProcedure
.input(myStayRoomAvailabilityInputSchema)
.use(async ({ ctx, input, next }) => {
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
if (ctx.session?.token.access_token) {
const verifiedUser = await getVerifiedUser({ session: ctx.session })
if (!verifiedUser?.error) {
return next({
ctx: {
token: ctx.session.token.access_token,
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
},
})
}
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
})
})
.query(async function ({ ctx, input }) {
const [availability] = await getRoomsAvailability(
{
booking: {
...input.booking,
rooms: [input.booking.room],
},
lang: input.lang,
},
ctx.token,
ctx.serviceToken,
ctx.userPoints
)
if (!availability || "error" in availability) {
return null
}
const bookingRoom = input.booking.room
const selected = getSelectedRoomAvailability(
bookingRoom.rateCode,
availability.rateDefinitions,
availability.roomConfigurations,
bookingRoom.roomTypeCode,
ctx.userPoints
)
if (!selected) {
hotelQueryLogger.error("Unable to find selected room")
return null
}
return {
product: selected.product,
selectedRoom: selected.selectedRoom,
}
}),
selectRate: router({
room: safeProtectedServiceProcedure
.input(selectRateRoomAvailabilityInputSchema)
.use(async ({ ctx, input, next }) => {
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
if (ctx.session?.token.access_token) {
const verifiedUser = await getVerifiedUser({
session: ctx.session,
})
if (!verifiedUser?.error) {
return next({
ctx: {
token: ctx.session.token.access_token,
userPoints:
verifiedUser?.data.membership?.currentPoints ?? 0,
},
})
}
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
})
})
.query(async function ({ ctx, input }) {
const [availability] = await getRoomsAvailability(
{
booking: {
...input.booking,
rooms: [input.booking.room],
},
lang: input.lang,
},
ctx.token,
ctx.serviceToken,
ctx.userPoints
)
if (!availability || "error" in availability) {
return null
}
const roomConfigurations = mergeRoomTypes(
availability.roomConfigurations
)
return {
...availability,
roomConfigurations,
}
}),
rooms: safeProtectedServiceProcedure
.input(selectRateRoomsAvailabilityInputSchema)
.use(async ({ ctx, input, next }) => {
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
if (ctx.session?.token.access_token) {
const verifiedUser = await getVerifiedUser({
session: ctx.session,
})
if (!verifiedUser?.error) {
return next({
ctx: {
token: ctx.session.token.access_token,
userPoints:
verifiedUser?.data.membership?.currentPoints ?? 0,
},
})
}
}
throw unauthorizedError()
}
return next({
ctx: {
token: ctx.serviceToken,
},
})
})
.query(async function ({ ctx, input }) {
input.booking.rooms = input.booking.rooms.map((room) => ({
...room,
bookingCode: room.bookingCode || input.booking.bookingCode,
}))
const availability = await getRoomsAvailability(
input,
ctx.token,
ctx.serviceToken,
ctx.userPoints
)
for (const room of availability) {
if (!room || "error" in room) {
continue
}
room.roomConfigurations = mergeRoomTypes(room.roomConfigurations)
}
return availability
}),
}),
hotelsByCityWithBookingCode: serviceProcedure
.input(hotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const bookingCodeAvailabilityResponse =
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
// Get regular availability of hotels which don't have availability with booking code.
const unavailableHotelIds = bookingCodeAvailabilityResponse.availability
.filter((hotel) => {
return hotel.status === "NotAvailable"
})
.flatMap((hotel) => {
return hotel.hotelId
})
// All hotels have availability with booking code no need to fetch regular prices.
// return response as is without any filtering as below.
if (!unavailableHotelIds || !unavailableHotelIds.length) {
return bookingCodeAvailabilityResponse
}
const unavailableHotelsInput = {
...input,
bookingCode: "",
hotelIds: unavailableHotelIds,
}
const unavailableHotels = await getHotelsAvailabilityByHotelIds(
unavailableHotelsInput,
apiLang,
ctx.serviceToken
)
// No regular rates available due to network or API failure (no need to filter & merge).
if (!unavailableHotels) {
return bookingCodeAvailabilityResponse
}
// Filtering the response hotels to merge bookingCode rates and regular rates in single response.
return {
availability: bookingCodeAvailabilityResponse.availability
.filter((hotel) => {
return hotel.status === "Available"
})
.concat(unavailableHotels.availability),
}
}),
}),
get: serviceProcedure
.input(hotelInputSchema)
.query(async ({ ctx, input }) => {
const { hotelId, language } = input
const [hotelData, hotelPages] = await Promise.all([
getHotel(input, ctx.serviceToken),
getHotelPageUrls(language),
])
const hotelPage = hotelPages.find((page) => page.hotelId === hotelId)
return hotelData
? {
...hotelData,
url: hotelPage?.url ?? null,
}
: null
}),
hotels: router({
byCountry: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsByCountryInput)
.query(async ({ ctx, input }) => {
const { lang, serviceToken } = ctx
const { country } = input
const hotelIds = await getHotelIdsByCountry({
country,
serviceToken: ctx.serviceToken,
})
return await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
}),
}),
byCityIdentifier: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsByCityIdentifierInput)
.query(async ({ ctx, input }) => {
const { lang, serviceToken } = ctx
const { cityIdentifier } = input
const hotelIds = await getHotelIdsByCityIdentifier(
cityIdentifier,
serviceToken
)
return await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
}),
}),
byCSFilter: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsByCSFilterInput)
.query(async function ({ ctx, input }) {
const { locationFilter, hotelsToInclude } = input
const language = ctx.lang
let hotelsToFetch: string[] = []
const getHotelsByCSFilterCounter = createCounter(
"trpc.hotel.hotels",
"byCSFilter"
)
const metricsGetHotelsByCSFilter = getHotelsByCSFilterCounter.init({
input,
language,
})
metricsGetHotelsByCSFilter.start()
if (hotelsToInclude.length) {
hotelsToFetch = hotelsToInclude
} else if (locationFilter?.city) {
const locations = await getLocations({
lang: language,
serviceToken: ctx.serviceToken,
citiesByCountry: 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) {
metricsGetHotelsByCSFilter.dataError(
`CityId not found for cityIdentifier: ${locationFilter.city}`,
{
cityIdentifier: locationFilter.city,
}
)
return []
}
const hotelIds = await getHotelIdsByCityId({
cityId,
serviceToken: ctx.serviceToken,
})
if (!hotelIds?.length) {
metricsGetHotelsByCSFilter.dataError(
`No hotelIds found for cityId: ${cityId}`,
{
cityId,
}
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
} else if (locationFilter?.country) {
const hotelIds = await getHotelIdsByCountry({
country: locationFilter.country,
serviceToken: ctx.serviceToken,
})
if (!hotelIds?.length) {
metricsGetHotelsByCSFilter.dataError(
`No hotelIds found for country: ${locationFilter.country}`,
{
country: locationFilter.country,
}
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
}
if (!hotelsToFetch.length) {
metricsGetHotelsByCSFilter.dataError(
`Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
input
)
return []
}
const hotelPages = await getHotelPageUrls(language)
const hotels = await Promise.all(
hotelsToFetch.map(async (hotelId) => {
const hotelData = await getHotel(
{ hotelId, isCardOnlyPayment: false, language },
ctx.serviceToken
)
const hotelPage = hotelPages.find(
(page) => page.hotelId === hotelId
)
return hotelData
? {
...hotelData,
url: hotelPage?.url ?? null,
}
: null
})
)
metricsGetHotelsByCSFilter.success()
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
}),
}),
getDestinationsMapData: serviceProcedure
.input(getDestinationsMapDataInput)
.query(async function ({ input, ctx }) {
const lang = input?.lang ?? ctx.lang
const warmup = input?.warmup ?? false
const fetchHotels = async () => {
const countries = await getCountries({
// Countries need to be in English regardless of incoming lang because
// we use the names as input for API endpoints.
lang: Lang.en,
serviceToken: ctx.serviceToken,
})
if (!countries) {
throw new Error("Unable to fetch countries")
}
const countryNames = countries.data.map((country) => country.name)
const hotelData: DestinationPagesHotelData[] = (
await Promise.all(
countryNames.map(async (country) => {
const hotelIds = await getHotelIdsByCountry({
country,
serviceToken: ctx.serviceToken,
})
const hotels = await getHotelsByHotelIds({
hotelIds,
lang: lang,
serviceToken: ctx.serviceToken,
})
return hotels
})
)
).flat()
return hotelData
}
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`${lang}:getDestinationsMapData`,
fetchHotels,
"max",
{
cacheStrategy: warmup ? "fetch-then-cache" : "cache-first",
}
)
}),
}),
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,
}
const cacheClient = await getCacheClient()
return cacheClient.cacheOrGet(
`${apiLang}:nearbyHotels:${hotelId}`,
async () => {
const nearbyHotelsCounter = createCounter(
"trpc.hotel",
"nearbyHotelIds"
)
const metricsNearbyHotels = nearbyHotelsCounter.init({
params,
hotelId,
})
metricsNearbyHotels.start()
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
await metricsNearbyHotels.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
if (!validateHotelData.success) {
metricsNearbyHotels.validationError(validateHotelData.error)
throw badRequestError()
}
metricsNearbyHotels.success()
return validateHotelData.data.map((id: string) => parseInt(id, 10))
},
env.CACHE_TIME_HOTELS
)
}),
locations: router({
get: serviceProcedure.input(getLocationsInput).query(async function ({
ctx,
input,
}) {
const lang = input.lang ?? ctx.lang
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`${lang}:getLocations`,
async () => {
const countries = await getCountries({
lang: lang,
serviceToken: ctx.serviceToken,
})
if (!countries) {
throw new Error("Unable to fetch countries")
}
const countryNames = countries.data.map((country) => country.name)
const citiesByCountry = await getCitiesByCountry({
countries: countryNames,
serviceToken: ctx.serviceToken,
lang,
})
const locations = await getLocations({
lang,
serviceToken: ctx.serviceToken,
citiesByCountry,
})
if (!locations || "error" in locations) {
throw new Error("Unable to fetch locations")
}
return locations
},
"max"
)
}),
urls: publicProcedure
.input(getLocationsUrlsInput)
.query(async ({ input }) => {
const { lang } = input
const locationsUrlsCounter = createCounter(
"trpc.hotel.locations",
"urls"
)
const metricsLocationsUrls = locationsUrlsCounter.init({
lang,
})
metricsLocationsUrls.start()
const [hotelPageUrlsResult, cityPageUrlsResult] =
await Promise.allSettled([
getHotelPageUrls(lang),
getCityPageUrls(lang),
])
if (
hotelPageUrlsResult.status === "rejected" ||
cityPageUrlsResult.status === "rejected"
) {
metricsLocationsUrls.dataError(`Failed to get data for page URLs`, {
hotelPageUrlsResult,
cityPageUrlsResult,
})
return null
}
metricsLocationsUrls.success()
return {
hotels: hotelPageUrlsResult.value,
cities: cityPageUrlsResult.value,
}
}),
}),
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 cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`coordinates:${address}`,
async function () {
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
const response = await fetch(url, {
signal: AbortSignal.timeout(15_000),
})
const data = await response.json()
if (data.status !== "OK") {
hotelQueryLogger.error(`Geocode error: ${data.status}`)
return null
}
const location = data.results[0]?.geometry?.location
if (!location) {
hotelQueryLogger.error("No location found in geocode response")
return null
}
return location
},
"1d"
)
}
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 meetingRoomsCounter = createCounter("trpc.hotel", "meetingRooms")
const metricsMeetingRooms = meetingRoomsCounter.init({
params,
})
metricsMeetingRooms.start()
const cacheClient = await getCacheClient()
return cacheClient.cacheOrGet(
`${language}:hotels:meetingRooms:${hotelId}`,
async () => {
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.meetingRooms(input.hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
if (apiResponse.status === 404) {
// This is expected when the hotel does not have meeting rooms
metricsMeetingRooms.success()
return []
}
await metricsMeetingRooms.httpError(apiResponse)
throw new Error("Failed to fetch meeting rooms", {
cause: apiResponse,
})
}
const apiJson = await apiResponse.json()
const validatedMeetingRooms = meetingRoomsSchema.safeParse(apiJson)
if (!validatedMeetingRooms.success) {
metricsMeetingRooms.validationError(validatedMeetingRooms.error)
throw badRequestError()
}
metricsMeetingRooms.success()
return validatedMeetingRooms.data.data
},
env.CACHE_TIME_HOTELS
)
}),
additionalData: safeProtectedServiceProcedure
.input(getAdditionalDataInputSchema)
.query(async function ({ ctx, input }) {
const { hotelId, language } = input
const params: Record<string, string | string[]> = {
hotelId,
language,
}
const additionalDataCounter = createCounter(
"trpc.hotel",
"additionalData"
)
const metricsAdditionalData = additionalDataCounter.init({
params,
})
metricsAdditionalData.start()
const cacheClient = await getCacheClient()
return cacheClient.cacheOrGet(
`${language}:hotels:additionalData:${hotelId}`,
async () => {
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
await metricsAdditionalData.httpError(apiResponse)
throw new Error("Unable to fetch additional data for hotel")
}
const apiJson = await apiResponse.json()
const validatedAdditionalData =
additionalDataSchema.safeParse(apiJson)
if (!validatedAdditionalData.success) {
metricsAdditionalData.validationError(validatedAdditionalData.error)
throw badRequestError()
}
metricsAdditionalData.success()
return validatedAdditionalData.data
},
env.CACHE_TIME_HOTELS
)
}),
packages: router({
get: serviceProcedure
.input(roomPackagesInputSchema)
.query(async ({ ctx, input }) => {
return getPackages(input, ctx.serviceToken)
}),
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 breakfastCounter = createCounter(
"trpc.hotel.packages",
"breakfast"
)
const metricsBreakfast = breakfastCounter.init({
params,
hotelId: input.hotelId,
})
metricsBreakfast.start()
const cacheClient = await getCacheClient()
const breakfastPackages = await cacheClient.cacheOrGet(
`${apiLang}:adults${input.adults}:startDate:${params.StartDate}:endDate:${params.EndDate}:hotel:${input.hotelId}`,
async () => {
const apiResponse = await api.get(
api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
await metricsBreakfast.httpError(apiResponse)
throw new Error("Unable to fetch breakfast packages")
}
const apiJson = await apiResponse.json()
const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson)
if (!breakfastPackages.success) {
metricsBreakfast.validationError(breakfastPackages.error)
throw new Error("Unable to parse breakfast packages")
}
return breakfastPackages.data
},
"1h"
)
// Since the BRF0 package is out of scope for release we'll disable this handling
// of membership levels for now, to be reanabled once needed
// 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.find(
// (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
// )
// if (freeBreakfastPackage?.localPrice) {
// return [freeBreakfastPackage]
// }
// }
// }
// }
metricsBreakfast.success()
return breakfastPackages.filter(
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
)
}),
ancillary: safeProtectedServiceProcedure
.input(ancillaryPackageInputSchema)
.query(async function ({ ctx, input }) {
const { lang } = ctx
const apiLang = toApiLang(lang)
const params = {
EndDate: dt(input.toDate).format("YYYY-MM-DD"),
StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
language: apiLang,
}
const ancillaryCounter = createCounter(
"trpc.hotel.packages",
"ancillary"
)
const metricsAncillary = ancillaryCounter.init({
params,
hotelId: input.hotelId,
})
metricsAncillary.start()
const cacheClient = await getCacheClient()
const result = await cacheClient.cacheOrGet(
`${apiLang}:hotel:${input.hotelId}:ancillaries:startDate:${params.StartDate}:endDate:${params.EndDate}`,
async () => {
const apiResponse = await api.get(
api.endpoints.v1.Package.Ancillary.hotel(input.hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
await metricsAncillary.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const ancillaryPackages = ancillaryPackagesSchema.safeParse(apiJson)
if (!ancillaryPackages.success) {
metricsAncillary.validationError(ancillaryPackages.error)
return null
}
return ancillaryPackages.data
},
"1h"
)
metricsAncillary.success()
return result
}),
}),
})