Merged in feat/SW-1456-country-dynamic-map (pull request #1310)

feat(SW-1456): Added map and fetching hotels by country

* feat(SW-1456): Added map and fetching hotels by country


Approved-by: Fredrik Thorsson
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-02-12 09:15:33 +00:00
parent 3901c3ac77
commit bcfa84324f
33 changed files with 2211 additions and 187 deletions

View File

@@ -244,7 +244,6 @@ export const destinationOverviewPageQueryRouter = router({
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: city.id,
onlyBasicInfo: "true",
})
const hotels = await getHotelIdsByCityId(

View File

@@ -63,7 +63,7 @@ export const hotelInputSchema = z.object({
language: z.nativeEnum(Lang),
})
export const getHotelsInput = z.object({
export const getHotelsByCSFilterInput = z.object({
locationFilter: z
.object({
city: z.string().nullable(),
@@ -73,7 +73,8 @@ export const getHotelsInput = z.object({
.nullable(),
hotelsToInclude: z.array(z.string()),
})
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> {}
export interface GetHotelsByCSFilterInput
extends z.infer<typeof getHotelsByCSFilterInput> {}
export const nearbyHotelIdsInput = z.object({
hotelId: z.string(),
@@ -116,3 +117,7 @@ export const getAdditionalDataInputSchema = z.object({
hotelId: z.string(),
language: z.string(),
})
export const getHotelsByCountryInput = z.object({
country: z.nativeEnum(Country),
})

View File

@@ -24,8 +24,9 @@ import {
breakfastPackageInputSchema,
cityCoordinatesInputSchema,
getAdditionalDataInputSchema,
getHotelsByCountryInput,
getHotelsByCSFilterInput,
getHotelsByHotelIdsAvailabilityInputSchema,
getHotelsInput,
getMeetingRoomsInputSchema,
hotelInputSchema,
hotelsAvailabilityInputSchema,
@@ -59,8 +60,7 @@ import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHote
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { HotelData } from "@/types/hotel"
import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage"
import type { HotelDataWithUrl } from "@/types/hotel"
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
@@ -833,207 +833,251 @@ export const hotelQueryRouter = router({
return getHotel(input, ctx.serviceToken)
}),
hotels: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsInput)
.query(async function ({ ctx, input }) {
const { locationFilter, hotelsToInclude } = input
byCountry: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsByCountryInput)
.query(async ({ ctx, input }) => {
const { lang, serviceToken } = ctx
const { country } = input
const language = ctx.lang
const apiLang = toApiLang(language)
const options: RequestOptionsWithOutBody = {
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: env.CACHE_TIME_HOTELS,
},
}
let hotelsToFetch: string[] = []
metrics.hotels.counter.add(1, {
input: JSON.stringify(input),
language,
})
console.info(
"api.hotel.hotels start",
JSON.stringify({
query: {
...input,
language,
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,
},
})
)
if (hotelsToInclude.length) {
hotelsToFetch = hotelsToInclude
} else if (locationFilter?.city) {
const locationsParams = new URLSearchParams({
language: apiLang,
})
const locations = await getLocations(
language,
options,
locationsParams,
null
)
if (!locations || "error" in locations) {
return []
}
const cityId = locations
.filter(
(loc): loc is CityLocation =>
"type" in loc && loc.type === "cities"
)
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
if (!cityId) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { ...input, language },
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
)
return []
}
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: cityId,
onlyBasicInfo: "true",
})
const hotelIds = await getHotelIdsByCityId(
cityId,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
cityId,
language,
error_type: "not_found",
error: `No hotelIds found for cityId: ${cityId}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { cityId, language },
error: `No hotelIds found for cityId: ${cityId}`,
})
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
} else if (locationFilter?.country) {
const hotelIdsParams = new URLSearchParams({
language: ApiLang.En,
country: locationFilter.country,
onlyBasicInfo: "true",
country,
})
const hotelIds = await getHotelIdsByCountry(
locationFilter.country,
country,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
const hotels = await Promise.all(
hotelIds.map(async (hotelId) => {
const [hotelData, url] = await Promise.all([
getHotel(
{ hotelId, isCardOnlyPayment: false, language: lang },
ctx.serviceToken
),
getHotelPageUrl(lang, hotelId),
])
return hotelData ? { ...hotelData, url } : null
})
)
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
}),
}),
byCSFilter: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsByCSFilterInput)
.query(async function ({ ctx, input }) {
const { locationFilter, hotelsToInclude } = input
const language = ctx.lang
const apiLang = toApiLang(language)
const options: RequestOptionsWithOutBody = {
// needs to clear default option as only
// cache or next.revalidate is permitted
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: env.CACHE_TIME_HOTELS,
},
}
let hotelsToFetch: string[] = []
metrics.hotels.counter.add(1, {
input: JSON.stringify(input),
language,
})
console.info(
"api.hotel.hotels start",
JSON.stringify({
query: {
...input,
language,
},
})
)
if (hotelsToInclude.length) {
hotelsToFetch = hotelsToInclude
} else if (locationFilter?.city) {
const locationsParams = new URLSearchParams({
language: apiLang,
})
const locations = await getLocations(
language,
options,
locationsParams,
null
)
if (!locations || "error" in locations) {
return []
}
const cityId = locations
.filter(
(loc): loc is CityLocation =>
"type" in loc && loc.type === "cities"
)
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
if (!cityId) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { ...input, language },
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
})
)
return []
}
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: cityId,
})
const hotelIds = await getHotelIdsByCityId(
cityId,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
cityId,
language,
error_type: "not_found",
error: `No hotelIds found for cityId: ${cityId}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { cityId, language },
error: `No hotelIds found for cityId: ${cityId}`,
})
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
} else if (locationFilter?.country) {
const hotelIdsParams = new URLSearchParams({
language: ApiLang.En,
country: locationFilter.country,
})
const hotelIds = await getHotelIdsByCountry(
locationFilter.country,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
metrics.hotels.fail.add(1, {
country: locationFilter.country,
language,
error_type: "not_found",
error: `No hotelIds found for country: ${locationFilter.country}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { country: locationFilter.country, language },
error: `No hotelIds found for cityId: ${locationFilter.country}`,
})
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
hotelsToFetch = filteredHotelIds
}
if (!hotelsToFetch.length) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `No hotelIds found for country: ${locationFilter.country}`,
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: { country: locationFilter.country, language },
error: `No hotelIds found for cityId: ${locationFilter.country}`,
query: JSON.stringify(input),
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
)
return []
}
const filteredHotelIds = hotelIds.filter(
(id) => !locationFilter.excluded.includes(id)
)
const hotels = await Promise.all(
hotelsToFetch.map(async (hotelId) => {
const [hotelData, url] = await Promise.all([
getHotel(
{ hotelId, isCardOnlyPayment: false, language },
ctx.serviceToken
),
getHotelPageUrl(language, hotelId),
])
hotelsToFetch = filteredHotelIds
}
if (!hotelsToFetch.length) {
metrics.hotels.fail.add(1, {
input: JSON.stringify(input),
language,
error_type: "not_found",
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
query: JSON.stringify(input),
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
return hotelData
? {
...hotelData,
url,
}
: null
})
)
return []
}
const hotels = await Promise.all(
hotelsToFetch.map(async (hotelId) => {
const [hotelData, url] = await Promise.all([
getHotel(
{ hotelId, isCardOnlyPayment: false, language },
ctx.serviceToken
),
getHotelPageUrl(language, hotelId),
])
return {
data: hotelData,
url,
}
metrics.hotels.success.add(1, {
input: JSON.stringify(input),
language,
})
)
metrics.hotels.success.add(1, {
input: JSON.stringify(input),
language,
})
console.info(
"api.hotels success",
JSON.stringify({
query: {
input: JSON.stringify(input),
language,
},
})
)
console.info(
"api.hotels success",
JSON.stringify({
query: {
input: JSON.stringify(input),
language,
},
})
)
return hotels.filter(
(hotel): hotel is { data: HotelData; url: HotelPageUrl } =>
!!hotel.data
)
}),
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
}),
}),
}),
nearbyHotelIds: serviceProcedure
.input(nearbyHotelIdsInput)

View File

@@ -432,7 +432,6 @@ export async function getHotelIdsByCityIdentifier(
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: cityId,
onlyBasicInfo: "true",
})
const options: RequestOptionsWithOutBody = {
// needs to clear default option as only