feat(SW-664): Hotel listing component and queries for content pages

This commit is contained in:
Erik Tiekstra
2024-12-11 14:46:38 +01:00
parent 118f1afafa
commit 3939bf7cdc
32 changed files with 989 additions and 140 deletions

View File

@@ -1,9 +1,8 @@
import { metrics } from "@opentelemetry/api"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { badRequestError } from "@/server/errors/trpc"
import {
contentStackBaseWithServiceProcedure,
publicProcedure,
router,
safeProtectedServiceProcedure,
@@ -13,12 +12,14 @@ import { toApiLang } from "@/server/utils"
import { cache } from "@/utils/cache"
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
import { getVerifiedUser, parsedUser } from "../user/query"
import {
getBreakfastPackageInputSchema,
getCityCoordinatesInputSchema,
getHotelDataInputSchema,
getHotelsAvailabilityInputSchema,
getHotelsInput,
getRatesInputSchema,
getRoomPackagesInputSchema,
getRoomsAvailabilityInputSchema,
@@ -33,10 +34,35 @@ import {
getRoomPackagesSchema,
getRoomsAvailabilitySchema,
} from "./output"
import {
breakfastPackagesCounter,
breakfastPackagesFailCounter,
breakfastPackagesSuccessCounter,
getHotelCounter,
getHotelFailCounter,
getHotelsCounter,
getHotelsFailCounter,
getHotelsSuccessCounter,
getHotelSuccessCounter,
getPackagesCounter,
getPackagesFailCounter,
getPackagesSuccessCounter,
hotelsAvailabilityCounter,
hotelsAvailabilityFailCounter,
hotelsAvailabilitySuccessCounter,
roomsAvailabilityCounter,
roomsAvailabilityFailCounter,
roomsAvailabilitySuccessCounter,
selectedRoomAvailabilityCounter,
selectedRoomAvailabilityFailCounter,
selectedRoomAvailabilitySuccessCounter,
} from "./telemetry"
import tempRatesData from "./tempRatesData.json"
import {
getCitiesByCountry,
getCountries,
getHotelIdsByCityId,
getHotelIdsByCountry,
getLocations,
TWENTYFOUR_HOURS,
} from "./utils"
@@ -45,57 +71,9 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
const meter = metrics.getMeter("trpc.hotels")
const getHotelCounter = meter.createCounter("trpc.hotel.get")
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get")
const getPackagesSuccessCounter = meter.createCounter(
"trpc.hotel.packages.get-success"
)
const getPackagesFailCounter = meter.createCounter(
"trpc.hotel.packages.get-fail"
)
const hotelsAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.hotels"
)
const hotelsAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.hotels-success"
)
const hotelsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.hotels-fail"
)
const roomsAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.rooms"
)
const roomsAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.rooms-success"
)
const roomsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.rooms-fail"
)
const selectedRoomAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.room"
)
const selectedRoomAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.room-success"
)
const selectedRoomAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.room-fail"
)
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
const breakfastPackagesSuccessCounter = meter.createCounter(
"trpc.package.breakfast-success"
)
const breakfastPackagesFailCounter = meter.createCounter(
"trpc.package.breakfast-fail"
)
import type { Hotel } from "@/types/hotel"
import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
export const getHotelData = cache(
async (input: HotelDataInput, serviceToken: string) => {
@@ -695,6 +673,199 @@ export const hotelQueryRouter = router({
return getHotelData(input, ctx.serviceToken)
}),
}),
hotels: router({
get: contentStackBaseWithServiceProcedure
.input(getHotelsInput)
.query(async function ({ ctx, input }) {
const { locationFilter, hotelsToInclude } = input
const language = ctx.lang
const options: RequestOptionsWithOutBody = {
cache: "force-cache",
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
next: {
revalidate: TWENTYFOUR_HOURS,
},
}
let hotelsToFetch: string[] = []
getHotelsCounter.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: toApiLang(ctx.lang),
})
const locations = await getLocations(
ctx.lang,
options,
locationsParams,
null
)
if (!locations || "error" in locations) {
return []
}
const cityId = locations
.filter((loc): loc is CityLocation => loc.type === "cities")
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
if (!cityId) {
getHotelsFailCounter.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: ctx.lang,
city: cityId,
onlyBasicInfo: "true",
})
const hotelIds = await getHotelIdsByCityId(
cityId,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
getHotelsFailCounter.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: ctx.lang,
country: locationFilter.country,
onlyBasicInfo: "true",
})
const hotelIds = await getHotelIdsByCountry(
locationFilter.country,
options,
hotelIdsParams
)
if (!hotelIds?.length) {
getHotelsFailCounter.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) {
getHotelsFailCounter.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 []
}
const hotels = await Promise.all(
hotelsToFetch.map(async (hotelId) => {
const [hotelData, url] = await Promise.all([
getHotelData({ hotelId, language }, ctx.serviceToken),
getHotelPageUrl(language, hotelId),
])
return {
data: hotelData?.data.attributes,
url,
}
})
)
getHotelsSuccessCounter.add(1, {
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: Hotel; url: HotelPageUrl } => !!hotel.data
)
}),
}),
locations: router({
get: serviceProcedure.query(async function ({ ctx }) {
const searchParams = new URLSearchParams()