Merged in feature/SW-3572-hotel-data-endpoint (pull request #3051)

SW-3572 API route for listing hotels per city or country

* wip hotel data endpoint

* Correct route params type

* wip

* skip static paths call

* timeout when getting destinations take too long

* call noStore when we get a timeout

* add cache-control headers

* .

* .

* .

* wip

* wip

* wip

* wip

* add route for getting hotels per country

* include city when listing by country

* fix distance SI unit

* fix sorting

* Merge branch 'master' of bitbucket.org:scandic-swap/web into feature/SW-3572-hotel-data-endpoint

* packages/tracking passWithNoTests

* revalidate must be static value

* remove oxc reference

* cleanup

* cleanup hotel api route

* feat(SW-3572): cleanup error handling


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-11-03 12:10:22 +00:00
parent e8626d56af
commit 15a2da333d
25 changed files with 1227 additions and 249 deletions

View File

@@ -11,11 +11,9 @@ import { BreakfastPackageEnum } from "../../enums/breakfast"
import { badRequestError } from "../../errors"
import {
contentStackBaseWithServiceProcedure,
publicProcedure,
safeProtectedServiceProcedure,
serviceProcedure,
} from "../../procedures"
import { getCityPageUrls } from "../../routers/contentstack/destinationCityPage/utils"
import { getHotelPageUrls } from "../../routers/contentstack/hotelPage/utils"
import {
ancillaryPackageInputSchema,
@@ -26,8 +24,6 @@ import {
getHotelsByCityIdentifierInput,
getHotelsByCountryInput,
getHotelsByCSFilterInput,
getLocationsInput,
getLocationsUrlsInput,
getMeetingRoomsInputSchema,
hotelInputSchema,
nearbyHotelIdsInput,
@@ -38,22 +34,22 @@ import {
breakfastPackagesSchema,
getNearbyHotelIdsSchema,
} from "../../routers/hotels/output"
import { isCityLocation } from "../../types/locations"
import { toApiLang } from "../../utils"
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
import { meetingRoomsSchema } from "./schemas/meetingRoom"
import { getCitiesByCountry } from "./services/getCitiesByCountry"
import { getHotelIdsByCityIdentifier } from "./services/getCityByCityIdentifier"
import { getCountries } from "./services/getCountries"
import { getHotel } from "./services/getHotel"
import { getHotelIdsByCityId } from "./services/getHotelIdsByCityId"
import { getHotelIdsByCountry } from "./services/getHotelIdsByCountry"
import { getHotelsByHotelIds } from "./services/getHotelsByHotelIds"
import { getLocationsByCountries } from "./services/getLocationsByCountries"
import { getPackages } from "./services/getPackages"
import { availability } from "./availability"
import { getLocations } from "./utils"
import { locationsRouter } from "./locations"
import type { HotelListingHotelData } from "../../types/hotel"
import type { CityLocation } from "../../types/locations"
const hotelQueryLogger = createLogger("hotelQueryRouter")
@@ -82,9 +78,11 @@ export const hotelQueryRouter = router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsByCountryInput)
.query(async ({ ctx, input }) => {
const { lang, serviceToken } = ctx
const { serviceToken } = ctx
const { country } = input
const lang = input.lang ?? ctx.lang
const hotelIds = await getHotelIdsByCountry({
country,
serviceToken: ctx.serviceToken,
@@ -133,20 +131,18 @@ export const hotelQueryRouter = router({
hotelsToFetch = hotelsToInclude
shouldSortByDistance = false
} else if (locationFilter?.city) {
const locations = await getLocations({
const locations = await getLocationsByCountries({
lang: language,
serviceToken: ctx.serviceToken,
citiesByCountry: null,
})
if (!locations || "error" in locations) {
if (!locations || locations.length === 0) {
return []
}
const cityId = locations
.filter(
(loc): loc is CityLocation =>
"type" in loc && loc.type === "cities"
)
.filter(isCityLocation)
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
if (!cityId) {
@@ -339,87 +335,7 @@ export const hotelQueryRouter = router({
env.CACHE_TIME_HOTELS
)
}),
locations: router({
get: serviceProcedure.input(getLocationsInput).query(async function ({
ctx,
input,
}) {
const lang = input.lang ?? ctx.lang
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`${lang}:getLocations`,
async () => {
const countries = await getCountries({
lang: lang,
serviceToken: ctx.serviceToken,
})
if (!countries) {
throw new Error("Unable to fetch countries")
}
const countryNames = countries.data.map((country) => country.name)
const citiesByCountry = await getCitiesByCountry({
countries: countryNames,
serviceToken: ctx.serviceToken,
lang,
})
const locations = await getLocations({
lang,
serviceToken: ctx.serviceToken,
citiesByCountry,
})
if (!locations || "error" in locations) {
throw new Error("Unable to fetch locations")
}
return locations
},
"max"
)
}),
urls: publicProcedure
.input(getLocationsUrlsInput)
.query(async ({ input }) => {
const { lang } = input
const locationsUrlsCounter = createCounter(
"trpc.hotel.locations",
"urls"
)
const metricsLocationsUrls = locationsUrlsCounter.init({
lang,
})
metricsLocationsUrls.start()
const [hotelPageUrlsResult, cityPageUrlsResult] =
await Promise.allSettled([
getHotelPageUrls(lang),
getCityPageUrls(lang),
])
if (
hotelPageUrlsResult.status === "rejected" ||
cityPageUrlsResult.status === "rejected"
) {
metricsLocationsUrls.dataError(`Failed to get data for page URLs`, {
hotelPageUrlsResult,
cityPageUrlsResult,
})
return null
}
metricsLocationsUrls.success()
return {
hotels: hotelPageUrlsResult.value,
cities: cityPageUrlsResult.value,
}
}),
}),
locations: locationsRouter,
map: router({
city: serviceProcedure
.input(cityCoordinatesInputSchema)