import { REDEMPTION } from "@/constants/booking" import { Lang } from "@/constants/languages" import { env } from "@/env/server" import * as api from "@/lib/api" import { dt } from "@/lib/dt" import { badRequestError, unauthorizedError } from "@/server/errors/trpc" import { getCityPageUrls } from "@/server/routers/contentstack/destinationCityPage/utils" import { contentStackBaseWithServiceProcedure, publicProcedure, router, safeProtectedServiceProcedure, serviceProcedure, } from "@/server/trpc" import { toApiLang } from "@/server/utils" import { getCacheClient } from "@/services/dataCache" import { getHotelPageUrls } from "../contentstack/hotelPage/utils" import { getVerifiedUser } from "../user/query" import { additionalDataSchema } from "./schemas/hotel/include/additionalData" import { meetingRoomsSchema } from "./schemas/meetingRoom" import { ancillaryPackageInputSchema, breakfastPackageInputSchema, cityCoordinatesInputSchema, enterDetailsRoomsAvailabilityInputSchema, getAdditionalDataInputSchema, getDestinationsMapDataInput, getHotelsByCityIdentifierInput, getHotelsByCountryInput, getHotelsByCSFilterInput, getHotelsByHotelIdsAvailabilityInputSchema, getLocationsInput, getLocationsUrlsInput, getMeetingRoomsInputSchema, hotelInputSchema, hotelsAvailabilityInputSchema, myStayRoomAvailabilityInputSchema, nearbyHotelIdsInput, roomPackagesInputSchema, selectRateRoomAvailabilityInputSchema, selectRateRoomsAvailabilityInputSchema, } from "./input" import { metrics } from "./metrics" import { ancillaryPackagesSchema, breakfastPackagesSchema, getNearbyHotelIdsSchema, } from "./output" import { locationsUrlsCounter, locationsUrlsFailCounter, locationsUrlsSuccessCounter, } from "./telemetry" import { getBedTypes, getCitiesByCountry, getCountries, getHotel, getHotelIdsByCityId, getHotelIdsByCityIdentifier, getHotelIdsByCountry, getHotelsAvailabilityByCity, getHotelsAvailabilityByHotelIds, getHotelsByHotelIds, getLocations, getPackages, getRoomsAvailability, getSelectedRoomAvailability, mergeRoomTypes, } from "./utils" import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel" import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { RateEnum } from "@/types/enums/rate" import { RateTypeEnum } from "@/types/enums/rateType" import type { DestinationPagesHotelData, HotelDataWithUrl } from "@/types/hotel" import type { CityLocation } from "@/types/trpc/routers/hotel/locations" 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 === 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) { console.info(`Availability failed: ${room.error}`) console.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) { console.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 let memberRateDefinition = undefined if ("member" in product && product.member && counterRateCode) { memberRateDefinition = rateDefinitions.find( (rate) => rate.rateCode === counterRateCode && rate.isMemberRate ) } const selectedPackages = input.booking.rooms[idx].packages selectedRooms.push({ bedTypes, breakfastIncluded: rateDefinition.breakfastIncluded, cancellationText: rateDefinition.cancellationText, 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, // 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, }) } return selectedRooms }), myStay: safeProtectedServiceProcedure .input(myStayRoomAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { if (input.booking.searchType === 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) { console.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 === 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 === 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.rateCode ? 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) metrics.hotelsAvailabilityBookingCode.counter.add(1, { ...input, }) const bookingCodeAvailabilityResponse = await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken) // If API or network failed with no response if (!bookingCodeAvailabilityResponse) { metrics.hotelsAvailabilityBookingCode.fail.add(1, { ...input, error_type: "unknown", }) return null } // 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 ) metrics.hotelsAvailabilityBookingCode.success.add(1, { ...input, }) console.info("api.hotels.hotelsAvailabilityBookingCode success") // 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 }) => { return getHotel(input, ctx.serviceToken) }), 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[] = [] 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 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) { 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 hotelIds = await getHotelIdsByCityId({ cityId, serviceToken: ctx.serviceToken, }) 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 hotelIds = await getHotelIdsByCountry({ country: locationFilter.country, serviceToken: ctx.serviceToken, }) 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 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 }) ) 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 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 } if (warmup) { return await fetchHotels() } const cacheClient = await getCacheClient() return await cacheClient.cacheOrGet( `${lang}:getDestinationsMapData`, fetchHotels, "max" ) }), }), nearbyHotelIds: serviceProcedure .input(nearbyHotelIdsInput) .query(async function ({ ctx, input }) { const { lang } = ctx const apiLang = toApiLang(lang) const { hotelId } = input const params: Record = { language: apiLang, } const cacheClient = await getCacheClient() return cacheClient.cacheOrGet( `${apiLang}:nearbyHotels:${hotelId}`, async () => { 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)) }, 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 procedureName = "hotels.locations.urls" const { lang } = input locationsUrlsCounter.add(1, { lang }) console.info( `${procedureName}: start`, JSON.stringify({ query: { lang } }) ) const [hotelPageUrlsResult, cityPageUrlsResult] = await Promise.allSettled([ getHotelPageUrls(lang), getCityPageUrls(lang), ]) if ( hotelPageUrlsResult.status === "rejected" || cityPageUrlsResult.status === "rejected" ) { locationsUrlsFailCounter.add(1, { lang, error_type: "no_data", response: JSON.stringify({ hotelPageUrlsResult, cityPageUrlsResult, }), }) console.error(`${procedureName}: no data`, { variables: { lang }, error_type: "no_data", response: { hotelPageUrlsResult, cityPageUrlsResult, }, }) return null } locationsUrlsSuccessCounter.add(1, { lang }) console.info(`${procedureName}: success`, { variables: { lang }, }) 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) 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 }, "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 = { 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 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) { 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, }, }) ) throw new Error("Failed to fetch meeting rooms") } 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 }, env.CACHE_TIME_HOTELS ) }), additionalData: safeProtectedServiceProcedure .input(getAdditionalDataInputSchema) .query(async function ({ ctx, input }) { const { hotelId, language } = input const params: Record = { 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 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) { 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, }, }) ) throw new Error("Unable to fetch additional data for hotel") } 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 }, 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 metricsData = { ...params, hotelId: input.hotelId } metrics.breakfastPackage.counter.add(1, metricsData) console.info( "api.package.breakfast start", JSON.stringify({ query: metricsData }) ) 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) { 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.package.breakfast error", JSON.stringify({ query: metricsData, error: { status: apiResponse.status, statusText: apiResponse.statusText, text, }, }) ) throw new Error("Unable to fetch breakfast packages") } 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, }) ) throw new Error("Unable to parse breakfast packages") } metrics.breakfastPackage.success.add(1, metricsData) console.info( "api.package.breakfast success", JSON.stringify({ query: metricsData, }) ) 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] // } // } // } // } 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 cacheClient = await getCacheClient() return await cacheClient.cacheOrGet( `${apiLang}:hotel:${input.hotelId}:ancillaries:startDate:${params.StartDate}:endDate:${params.EndDate}`, async () => { const metricsData = { ...params, hotelId: input.hotelId } metrics.ancillaryPackage.counter.add(1, metricsData) console.info( "api.package.ancillary start", JSON.stringify({ query: metricsData }) ) const apiResponse = await api.get( api.endpoints.v1.Package.Ancillary.hotel(input.hotelId), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, }, }, params ) if (!apiResponse.ok) { const text = await apiResponse.text() metrics.ancillaryPackage.fail.add(1, { ...metricsData, error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, statusText: apiResponse.statusText, text, }), }) console.error( "api.package.ancillary start error", JSON.stringify({ query: metricsData, error: { status: apiResponse.status, statusText: apiResponse.statusText, text, }, }) ) return null } const apiJson = await apiResponse.json() const ancillaryPackages = ancillaryPackagesSchema.safeParse(apiJson) if (!ancillaryPackages.success) { metrics.ancillaryPackage.fail.add(1, { ...metricsData, error_type: "validation_error", error: JSON.stringify(ancillaryPackages.error), }) console.error( "api.package.ancillary validation error", JSON.stringify({ query: metricsData, error: ancillaryPackages.error, }) ) return null } metrics.ancillaryPackage.success.add(1, metricsData) console.info( "api.package.ancillary success", JSON.stringify({ query: metricsData, }) ) return ancillaryPackages.data }, "1h" ) }), }), })