import deepmerge from "deepmerge" import { z } from "zod" import { getCacheClient } from "@scandic-hotels/common/dataCache" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { chunk } from "@scandic-hotels/common/utils/chunk" import * as api from "../../../api" import { serverErrorByStatus } from "../../../errors" import { toApiLang } from "../../../utils" import { locationCitySchema } from "../schemas/location/city" import { locationHotelSchema } from "../schemas/location/hotel" import { getCity } from "./getCity" 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 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, { apiResponse }) } 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((x) => enrichCity(x, citiesByCountry)) const chunkedHotels = chunk( data.filter((x) => x.type === "hotels"), 10 ) const hotels = ( await Promise.all( chunkedHotels.flatMap(async (chunk) => { return await Promise.all( chunk.flatMap(async (hotel) => enrichHotel(hotel, serviceToken)) ) }) ) ).flat() let locations: z.infer["data"] = [ ...cities, ...hotels, ] return locations }, "1d" ) } async function enrichHotel( hotel: Extract< z.infer["data"][number], { type: "hotels" } >, serviceToken: string ): Promise< Extract["data"][number], { type: "hotels" }> > { if (hotel.type !== "hotels") { return hotel } if (!hotel.relationships.city?.url) { return hotel } const city = await getCity({ cityUrl: hotel.relationships.city.url, serviceToken, }) if (!city) { return hotel } return deepmerge(hotel, { relationships: { city, }, }) } function enrichCity( city: Extract< z.infer["data"][number], { type: "cities" } >, citiesByCountry: CitiesNamesByCountry | null ): Extract< z.infer["data"][number], { type: "cities" } > { 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: "", ianaTimeZoneId: "", id: "", isPublished: false, keywords: [], name: "", timeZoneId: "", type: "cities", url: location?.relationships?.city?.links?.related ?? "", }, }, type: location.type, operaId: location.attributes.operaId ?? "", } }) ), })