import { Lang } from "@scandic-hotels/common/constants/language" import { getCacheClient } from "@scandic-hotels/common/dataCache" import { dt } from "@scandic-hotels/common/dt" 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" 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) { 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 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 = {} 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) { 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 === 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 = { 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") { 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 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 = { 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 }), }), })