import { z } from "zod" import { getCacheClient } from "@scandic-hotels/common/dataCache" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import * as api from "../../../api" import { extractResponseDetails, serverErrorByStatus } from "../../../errors" import { toApiLang } from "../../../utils" import { locationCitySchema } from "../schemas/location/city" import { locationHotelSchema } from "../schemas/location/hotel" import type { Country } from "@scandic-hotels/common/constants/country" import type { Lang } from "@scandic-hotels/common/constants/language" const hotelUtilsLogger = createLogger("getLocationsByCountries") type CitiesNamesByCountry = Record< Country | (string & {}), Array<{ name: string }> > | null type Hotel = Extract< z.infer["data"][number], { type: "hotels" } > type City = Extract< z.infer["data"][number], { type: "cities" } > export async function getLocationsByCountries({ lang, citiesByCountry, serviceToken, }: { lang: Lang citiesByCountry: CitiesNamesByCountry | null serviceToken: string }) { const cacheClient = await getCacheClient() const countryKeys = Object.keys(citiesByCountry ?? {}) let cacheKey = `${lang}:locations` if (countryKeys.length > 0) { cacheKey += `:${countryKeys.toSorted().join(",")}` } return await cacheClient.cacheOrGet( cacheKey.toLowerCase(), async () => { const apiResponse = await api.get( api.endpoints.v1.Hotel.locations, { headers: { Authorization: `Bearer ${serviceToken}`, }, }, { language: toApiLang(lang), } ) if (!apiResponse.ok) { throw serverErrorByStatus( apiResponse.status, await extractResponseDetails(apiResponse), "getLocationsByCountries failed" ) } const apiJson = await apiResponse.json() const verifiedLocations = locationsSchema.safeParse(apiJson) if (!verifiedLocations.success) { hotelUtilsLogger.error( `Locations Verification Failed`, verifiedLocations.error ) throw new Error("Unable to parse api response for locations", { cause: verifiedLocations.error, }) } const data = cleanData(verifiedLocations.data.data) const cities = data .filter((x) => x.type === "cities") .map((city) => addCountryDataToCity(city, citiesByCountry)) const hotels = data .filter((x) => x.type === "hotels") .map((hotel) => addCityDataToHotel(hotel, cities)) let locations: z.infer["data"] = [ ...cities, ...hotels, ] return locations }, "1d" ) } function addCityDataToHotel(hotel: Hotel, cities: City[]) { const city = cities.find((c) => c.id === hotel.relationships.city.id) if (!city) { hotelUtilsLogger.warn( `City with id ${hotel.relationships.city.id} not found for hotel ${hotel.id}` ) return hotel } return { ...hotel, relationships: { city: { ...city, cityIdentifier: city.cityIdentifier, url: hotel.relationships.city.url, keywords: city.keyWords ?? [], }, }, } satisfies Hotel } function addCountryDataToCity( city: City, citiesByCountry: CitiesNamesByCountry | null ): City { if (!citiesByCountry) { return city } const country = Object.keys(citiesByCountry).find((country) => citiesByCountry[country].find((loc) => loc.name === city.name) ) if (!country) { hotelUtilsLogger.error( `Location cannot be found in any of the countries cities`, city ) return city } return { ...city, country, } } function cleanData(data: z.infer["data"]) { return data .filter((node) => { if (node?.isPublished !== true) { return false } if (node.type === "hotels" && !node.operaId) { return false } if (node.type === "cities" && !node.cityIdentifier) { return false } return true }) .toSorted((a, b) => { if (a.type === b.type) { return a.name.localeCompare(b.name) } else { return a.type === "cities" ? -1 : 1 } }) } export const locationsSchema = z.object({ data: z.array( z .discriminatedUnion("type", [locationCitySchema, locationHotelSchema]) .transform((location) => { if (location.type === "cities") { return { ...location.attributes, country: location.attributes.countryName || "", id: location.id, type: location.type, } } return { ...location.attributes, id: location.id, relationships: { city: { cityIdentifier: undefined as string | undefined, id: extractCityId( location.relationships?.city?.links?.related ?? "" ), isPublished: false, keywords: [] as string[], name: undefined as string | undefined, type: "cities", url: location?.relationships?.city?.links?.related ?? "", }, }, type: location.type, operaId: location.attributes.operaId ?? "", } }) ), }) function extractCityId(cityUrl: string): string | null { try { const url = new URL(cityUrl) if (!url.pathname.toLowerCase().includes("/cities/")) { return null } const id = url.pathname.split("/").at(-1) return id ?? null } catch { return null } }