import deepmerge from "deepmerge" import { unstable_cache } from "next/cache" import * as api from "@/lib/api" import { metrics } from "./metrics" import { citiesByCountrySchema, citiesSchema, countriesSchema, getHotelIdsByCityIdSchema, locationsSchema, } from "./output" import type { Country } from "@/types/enums/country" import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest" import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { CitiesGroupedByCountry, Countries, HotelLocation, } from "@/types/trpc/routers/hotel/locations" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" export function getPoiGroupByCategoryName(category: string | undefined) { if (!category) return PointOfInterestGroupEnum.LOCATION switch (category) { case "Airport": case "Bus terminal": case "Transportations": return PointOfInterestGroupEnum.PUBLIC_TRANSPORT case "Amusement park": case "Museum": case "Sports": case "Theatre": case "Tourist": case "Zoo": return PointOfInterestGroupEnum.ATTRACTIONS case "Nearby companies": case "Fair": return PointOfInterestGroupEnum.BUSINESS case "Parking / Garage": return PointOfInterestGroupEnum.PARKING case "Shopping": case "Restaurant": return PointOfInterestGroupEnum.SHOPPING_DINING case "Hospital": default: return PointOfInterestGroupEnum.LOCATION } } export const locationsAffix = "locations" export const TWENTYFOUR_HOURS = 60 * 60 * 24 export async function getCity( cityUrl: string, options: RequestOptionsWithOutBody, lang: Lang, relationshipCity: HotelLocation["relationships"]["city"] ) { return unstable_cache( async function (locationCityUrl: string) { const url = new URL(locationCityUrl) const cityResponse = await api.get( url.pathname as Endpoint, options, url.searchParams ) if (!cityResponse.ok) { return null } const cityJson = await cityResponse.json() const city = citiesSchema.safeParse(cityJson) if (!city.success) { console.info(`Validation of city failed`) console.info(`cityUrl: ${locationCityUrl}`) console.error(city.error) return null } return city.data }, [cityUrl, `${lang}:${relationshipCity}`], { revalidate: TWENTYFOUR_HOURS } )(cityUrl) } export async function getCountries( options: RequestOptionsWithOutBody, params: URLSearchParams, lang: Lang ) { return unstable_cache( async function (searchParams) { const countryResponse = await api.get( api.endpoints.v1.Hotel.countries, options, searchParams ) if (!countryResponse.ok) { return null } const countriesJson = await countryResponse.json() const countries = countriesSchema.safeParse(countriesJson) if (!countries.success) { console.info(`Validation for countries failed`) console.error(countries.error) return null } return countries.data }, [`${lang}:${locationsAffix}:countries`, params.toString()], { revalidate: TWENTYFOUR_HOURS } )(params) } export async function getCitiesByCountry( countries: string[], options: RequestOptionsWithOutBody, params: URLSearchParams, lang: Lang, onlyPublished = false, // false by default as it might be used in other places affix: string = locationsAffix ) { return unstable_cache( async function ( searchParams: URLSearchParams, searchedCountries: string[] ) { const citiesGroupedByCountry: CitiesGroupedByCountry = {} await Promise.all( searchedCountries.map(async (country) => { const countryResponse = await api.get( api.endpoints.v1.Hotel.Cities.country(country), options, searchParams ) if (!countryResponse.ok) { return null } const countryJson = await countryResponse.json() const citiesByCountry = citiesByCountrySchema.safeParse(countryJson) if (!citiesByCountry.success) { console.info(`Failed to validate Cities by Country payload`) console.error(citiesByCountry.error) return null } const cities = onlyPublished ? citiesByCountry.data.data.filter((city) => city.isPublished) : citiesByCountry.data.data citiesGroupedByCountry[country] = cities return true }) ) return citiesGroupedByCountry }, [ `${lang}:${affix}:cities-by-country`, params.toString(), JSON.stringify(countries), ], { revalidate: TWENTYFOUR_HOURS } )(params, countries) } export async function getLocations( lang: Lang, options: RequestOptionsWithOutBody, params: URLSearchParams, citiesByCountry: CitiesGroupedByCountry | null ) { return unstable_cache( async function ( searchParams: URLSearchParams, groupedCitiesByCountry: CitiesGroupedByCountry | null ) { const apiResponse = await api.get( api.endpoints.v1.Hotel.locations, options, searchParams ) if (!apiResponse.ok) { if (apiResponse.status === 401) { return { error: true, cause: "unauthorized" } as const } else if (apiResponse.status === 403) { return { error: true, cause: "forbidden" } as const } return null } const apiJson = await apiResponse.json() const verifiedLocations = locationsSchema.safeParse(apiJson) if (!verifiedLocations.success) { console.info(`Locations Verification Failed`) console.error(verifiedLocations.error) return null } return await Promise.all( verifiedLocations.data.data.map(async (location) => { if (location.type === "cities") { if (groupedCitiesByCountry) { const country = Object.keys(groupedCitiesByCountry).find( (country) => { if ( groupedCitiesByCountry[country].find( (loc) => loc.name === location.name ) ) { return true } return false } ) if (country) { return { ...location, country, } } else { console.info( `Location cannot be found in any of the countries cities` ) console.info(location) } } } else if (location.type === "hotels") { if (location.relationships.city?.url) { const city = await getCity( location.relationships.city.url, options, lang, location.relationships.city ) if (city) { return deepmerge(location, { relationships: { city, }, }) } } } return location }) ) }, [ `${lang}:${locationsAffix}`, params.toString(), JSON.stringify(citiesByCountry), ], { revalidate: TWENTYFOUR_HOURS } )(params, citiesByCountry) } export async function getHotelIdsByCityId( cityId: string, options: RequestOptionsWithOutBody, params: URLSearchParams ) { return unstable_cache( async function (params: URLSearchParams) { metrics.hotelIds.counter.add(1, { params: params.toString() }) console.info( "api.hotel.hotel-ids start", JSON.stringify({ params: params.toString() }) ) const apiResponse = await api.get( api.endpoints.v1.Hotel.hotels, options, params ) if (!apiResponse.ok) { const responseMessage = await apiResponse.text() metrics.hotelIds.fail.add(1, { params: params.toString(), error_type: "http_error", error: responseMessage, }) console.error( "api.hotel.hotel-ids fetch error", JSON.stringify({ params: params.toString(), error: { status: apiResponse.status, statusText: apiResponse.statusText, text: responseMessage, }, }) ) return null } const apiJson = await apiResponse.json() const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson) if (!validatedHotelIds.success) { metrics.hotelIds.fail.add(1, { params: params.toString(), error_type: "validation_error", error: JSON.stringify(validatedHotelIds.error), }) console.error( "api.hotel.hotel-ids validation error", JSON.stringify({ params: params.toString(), error: validatedHotelIds.error, }) ) return null } metrics.hotelIds.success.add(1, { cityId }) console.info( "api.hotel.hotel-ids success", JSON.stringify({ params: params.toString() }) ) return validatedHotelIds.data }, [`hotelsByCityId`, params.toString()], { revalidate: TWENTYFOUR_HOURS } )(params) } export async function getHotelIdsByCountry( country: Country, options: RequestOptionsWithOutBody, params: URLSearchParams ) { return unstable_cache( async function (params: URLSearchParams) { metrics.hotelIds.counter.add(1, { country }) console.info( "api.hotel.hotel-ids start", JSON.stringify({ query: { country } }) ) const apiResponse = await api.get( api.endpoints.v1.Hotel.hotels, options, params ) if (!apiResponse.ok) { const responseMessage = await apiResponse.text() metrics.hotelIds.fail.add(1, { country, error_type: "http_error", error: responseMessage, }) console.error( "api.hotel.hotel-ids fetch error", JSON.stringify({ query: { country }, error: { status: apiResponse.status, statusText: apiResponse.statusText, text: responseMessage, }, }) ) return null } const apiJson = await apiResponse.json() const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson) if (!validatedHotelIds.success) { metrics.hotelIds.fail.add(1, { country, error_type: "validation_error", error: JSON.stringify(validatedHotelIds.error), }) console.error( "api.hotel.hotel-ids validation error", JSON.stringify({ query: { country }, error: validatedHotelIds.error, }) ) return null } metrics.hotelIds.success.add(1, { country }) console.info( "api.hotel.hotel-ids success", JSON.stringify({ query: { country } }) ) return validatedHotelIds.data }, [`hotelsByCountry`, params.toString()], { revalidate: TWENTYFOUR_HOURS } )(params) }