feat/SW-1418 destination page map data * feat(SW-1418): implement dynamic map on overview page * feat(SW-1418): update folder structure * feat(SW-1418): use getHotelsByHotelIds Approved-by: Erik Tiekstra Approved-by: Matilda Landström
504 lines
14 KiB
TypeScript
504 lines
14 KiB
TypeScript
import deepmerge from "deepmerge"
|
|
import { unstable_cache } from "next/cache"
|
|
|
|
import { Lang } from "@/constants/languages"
|
|
import { env } from "@/env/server"
|
|
import * as api from "@/lib/api"
|
|
import { toApiLang } from "@/server/utils"
|
|
|
|
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
|
|
import { metrics } from "./metrics"
|
|
import {
|
|
citiesByCountrySchema,
|
|
citiesSchema,
|
|
countriesSchema,
|
|
getHotelIdsSchema,
|
|
locationsSchema,
|
|
} from "./output"
|
|
import { getHotel } from "./query"
|
|
|
|
import type { Country } from "@/types/enums/country"
|
|
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
import type { HotelDataWithUrl } from "@/types/hotel"
|
|
import type {
|
|
CitiesGroupedByCountry,
|
|
CityLocation,
|
|
HotelLocation,
|
|
} from "@/types/trpc/routers/hotel/locations"
|
|
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 []
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validatedHotelIds = getHotelIdsSchema.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 []
|
|
}
|
|
|
|
metrics.hotelIds.success.add(1, { cityId })
|
|
console.info(
|
|
"api.hotel.hotel-ids success",
|
|
JSON.stringify({
|
|
params: params.toString(),
|
|
response: validatedHotelIds.data,
|
|
})
|
|
)
|
|
|
|
return validatedHotelIds.data
|
|
},
|
|
[`hotelsByCityId`, params.toString()],
|
|
{ revalidate: env.CACHE_TIME_HOTELS }
|
|
)(params)
|
|
}
|
|
|
|
export async function getHotelIdsByCountry(
|
|
country: string,
|
|
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 []
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validatedHotelIds = getHotelIdsSchema.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 []
|
|
}
|
|
|
|
metrics.hotelIds.success.add(1, { country })
|
|
console.info(
|
|
"api.hotel.hotel-ids success",
|
|
JSON.stringify({ query: { country } })
|
|
)
|
|
|
|
return validatedHotelIds.data
|
|
},
|
|
[`hotelsByCountry`, params.toString()],
|
|
{ revalidate: env.CACHE_TIME_HOTELS }
|
|
)(params)
|
|
}
|
|
|
|
export async function getHotelIdsByCityIdentifier(
|
|
cityIdentifier: string,
|
|
serviceToken: string
|
|
) {
|
|
const apiLang = toApiLang(Lang.en)
|
|
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
|
|
|
|
if (!city) {
|
|
return []
|
|
}
|
|
|
|
const hotelIdsParams = new URLSearchParams({
|
|
language: apiLang,
|
|
city: city.id,
|
|
})
|
|
const options: RequestOptionsWithOutBody = {
|
|
// needs to clear default option as only
|
|
// cache or next.revalidate is permitted
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
const hotelIds = await getHotelIdsByCityId(city.id, options, hotelIdsParams)
|
|
return hotelIds
|
|
}
|
|
|
|
export async function getCityByCityIdentifier(
|
|
cityIdentifier: string,
|
|
serviceToken: string
|
|
) {
|
|
const lang = Lang.en
|
|
const apiLang = toApiLang(lang)
|
|
const options: RequestOptionsWithOutBody = {
|
|
// needs to clear default option as only
|
|
// cache or next.revalidate is permitted
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
const params = new URLSearchParams({
|
|
language: apiLang,
|
|
})
|
|
const locations = await getLocations(lang, options, params, null)
|
|
if (!locations || "error" in locations) {
|
|
return null
|
|
}
|
|
|
|
const city = locations
|
|
.filter((loc): loc is CityLocation => loc.type === "cities")
|
|
.find((loc) => loc.cityIdentifier === cityIdentifier)
|
|
|
|
return city ?? null
|
|
}
|
|
|
|
export async function getHotelsByHotelIds(
|
|
hotelIds: string[],
|
|
lang: Lang,
|
|
serviceToken: string
|
|
) {
|
|
const hotels = await Promise.all(
|
|
hotelIds.map(async (hotelId) => {
|
|
const [hotelData, url] = await Promise.all([
|
|
getHotel(
|
|
{ hotelId, language: lang, isCardOnlyPayment: false },
|
|
serviceToken
|
|
),
|
|
getHotelPageUrl(lang, hotelId),
|
|
])
|
|
|
|
return hotelData ? { ...hotelData, url } : null
|
|
})
|
|
)
|
|
|
|
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
|
|
}
|