import { z } from "zod" import { Lang } from "@scandic-hotels/common/constants/language" import { getCacheClient } from "@scandic-hotels/common/dataCache" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { isDefined } from "@scandic-hotels/common/utils/isDefined" import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { safeProtectedServiceProcedure } from "../../procedures" import { getCityPageUrls } from "../../routers/contentstack/destinationCityPage/utils" import { getCountryPageUrls } from "../../routers/contentstack/destinationCountryPage/utils" import { getHotelPageUrls } from "../../routers/contentstack/hotelPage/utils" import { ApiCountry, type Country } from "../../types/country" import { getCitiesByCountry } from "../hotels/services/getCitiesByCountry" import { getCountries } from "../hotels/services/getCountries" import { getLocationsByCountries } from "../hotels/services/getLocationsByCountries" import { filterAndCategorizeAutoComplete } from "./util/filterAndCategorizeAutoComplete" import { mapLocationToAutoCompleteLocation } from "./util/mapLocationToAutoCompleteLocation" import type { AutoCompleteLocation } from "./schema" const destinationsAutoCompleteInputSchema = z.object({ query: z.string(), selectedHotelId: z.string().optional(), selectedCity: z.string().optional(), lang: z.nativeEnum(Lang), includeTypes: z.array(z.enum(["hotels", "cities", "countries"])), }) type DestinationsAutoCompleteOutput = { hits: { hotels: AutoCompleteLocation[] cities: AutoCompleteLocation[] countries: AutoCompleteLocation[] } currentSelection: { hotel: (AutoCompleteLocation & { type: "hotels" }) | null city: (AutoCompleteLocation & { type: "cities" }) | null } } export const getDestinationsAutoCompleteRoute = safeProtectedServiceProcedure .input(destinationsAutoCompleteInputSchema) .query(async ({ ctx, input }): Promise => { const lang = input.lang || ctx.lang const [locations, error] = await safeTry( getAutoCompleteDestinationsData({ lang, serviceToken: ctx.serviceToken, }) ) if (error || !locations) { throw new Error("Unable to fetch autocomplete destinations data", { cause: error, }) } const hits = filterAndCategorizeAutoComplete({ locations: locations, query: input.query, includeTypes: input.includeTypes, }) const selectedHotel = locations.find( (location) => location.type === "hotels" && location.id === input.selectedHotelId ) const selectedCity = locations.find( (location) => location.type === "cities" && location.cityIdentifier === input.selectedCity ) return { hits: hits, currentSelection: { city: isCity(selectedCity) ? selectedCity : null, hotel: isHotel(selectedHotel) ? selectedHotel : null, }, } }) function isHotel( location: AutoCompleteLocation | null | undefined ): location is AutoCompleteLocation & { type: "hotels" } { return !!location && location.type === "hotels" } function isCity( location: AutoCompleteLocation | null | undefined ): location is AutoCompleteLocation & { type: "cities" } { return !!location && location.type === "cities" } export async function getAutoCompleteDestinationsData({ lang, serviceToken, warmup = false, }: { lang: Lang serviceToken: string warmup?: boolean }) { const autoCompleteLogger = createLogger("autocomplete-destinations") const cacheClient = await getCacheClient() return await cacheClient.cacheOrGet( `autocomplete:destinations:locations:${lang}`, async () => { const hotelUrlsPromise = safeTry(getHotelPageUrls(lang)) const cityUrlsPromise = safeTry(getCityPageUrls(lang)) const countryUrlsPromise = safeTry(getCountryPageUrls(lang)) const countries = await getCountries({ lang: lang, serviceToken, }) if (!countries) { autoCompleteLogger.error("Unable to fetch countries") throw new Error("Unable to fetch countries") } const countryNames = countries.data.map((country) => country.name) const [citiesByCountry, citiesByCountryError] = await safeTry( getCitiesByCountry({ countries: countryNames, serviceToken: serviceToken, lang, }) ) if (citiesByCountryError || !citiesByCountry) { autoCompleteLogger.error("Unable to fetch cities by country") throw new Error("Unable to fetch cities by country") } const [locations, locationsError] = await safeTry( getLocationsByCountries({ lang: lang, serviceToken: serviceToken, citiesByCountry: citiesByCountry, }) ) if (locationsError || !locations) { autoCompleteLogger.error("Unable to fetch locations") throw new Error("Unable to fetch locations") } const activeLocations = locations.filter((location) => { return ( location.type === "cities" || (location.type === "hotels" && location.isActive) ) }) const [hotelUrls, hotelUrlsError] = await hotelUrlsPromise const [cityUrls, cityUrlsError] = await cityUrlsPromise const [countryUrls, countryUrlsError] = await countryUrlsPromise if ( hotelUrlsError || cityUrlsError || countryUrlsError || !hotelUrls || !cityUrls || !countryUrls ) { autoCompleteLogger.error("Unable to fetch location URLs") throw new Error("Unable to fetch location URLs") } const hotelsAndCities = activeLocations .map((location) => { let url: string | undefined if (location.type === "cities") { url = cityUrls.find( (c) => c.city && location.cityIdentifier && c.city === location.cityIdentifier )?.url } if (location.type === "hotels") { url = hotelUrls.find( (h) => h.hotelId && location.id && h.hotelId === location.id )?.url } return { ...location, url } }) .map(mapLocationToAutoCompleteLocation) .filter(isDefined) const countryAutoCompleteLocations = countries.data.map((country) => { const url = countryUrls.find( (c) => c.country && ApiCountry[lang][c.country as Country] === country.name )?.url return { id: country.id, name: country.name, type: "countries", searchTokens: [country.name], destination: "", url, } satisfies AutoCompleteLocation }) return [...hotelsAndCities, ...countryAutoCompleteLocations] }, "1d", { cacheStrategy: warmup ? "fetch-then-cache" : "cache-first" } ) }