Files
web/server/routers/hotels/query.ts
2024-09-27 14:23:49 +02:00

558 lines
16 KiB
TypeScript

import { metrics } from "@opentelemetry/api"
import { unstable_cache } from "next/cache"
import * as api from "@/lib/api"
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
import { request } from "@/lib/graphql/request"
import {
badRequestError,
notFound,
serverErrorByStatus,
} from "@/server/errors/trpc"
import { extractHotelImages } from "@/server/routers/utils/hotels"
import {
contentStackUidWithHotelServiceProcedure,
hotelServiceProcedure,
publicProcedure,
router,
} from "@/server/trpc"
import { toApiLang } from "@/server/utils"
import { hotelPageSchema } from "../contentstack/hotelPage/output"
import {
getAvailabilityInputSchema,
getHotelInputSchema,
getlHotelDataInputSchema,
getRatesInputSchema,
} from "./input"
import {
getAvailabilitySchema,
getHotelDataSchema,
getRatesSchema,
roomSchema,
} from "./output"
import tempRatesData from "./tempRatesData.json"
import {
getCitiesByCountry,
getCountries,
getLocations,
locationsAffix,
TWENTYFOUR_HOURS,
} from "./utils"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
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 availabilityCounter = meter.createCounter("trpc.hotel.availability")
const availabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability-success"
)
const availabilityFailCounter = meter.createCounter(
"trpc.hotel.availability-fail"
)
async function getContentstackData(
locale: string,
uid: string | null | undefined
) {
const response = await request<GetHotelPageData>(GetHotelPage, {
locale,
uid,
})
if (!response.data) {
throw notFound(response)
}
const hotelPageData = hotelPageSchema.safeParse(response.data)
if (!hotelPageData.success) {
console.error(
`Failed to validate Hotel Page - (uid: ${uid}, lang: ${locale})`
)
console.error(hotelPageData.error)
return null
}
return hotelPageData.data.hotel_page
}
export const hotelQueryRouter = router({
get: contentStackUidWithHotelServiceProcedure
.input(getHotelInputSchema)
.query(async ({ ctx, input }) => {
const { lang, uid } = ctx
const { include } = input
const contentstackData = await getContentstackData(lang, uid)
const hotelId = contentstackData?.hotel_page_id
if (!hotelId) {
throw notFound(`Hotel not found for uid: ${uid}`)
}
const apiLang = toApiLang(lang)
const params: Record<string, string> = {
hotelId,
language: apiLang,
}
if (include) {
params.include = include.join(",")
}
getHotelCounter.add(1, { hotelId, lang, include })
console.info(
"api.hotels.hotel start",
JSON.stringify({
query: { hotelId, params },
})
)
const apiResponse = await api.get(
`${api.endpoints.v1.hotels}/${hotelId}`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
lang,
include,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotel error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const validatedHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validatedHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
lang,
include,
error_type: "validation_error",
error: JSON.stringify(validatedHotelData.error),
})
console.error(
"api.hotels.hotel validation error",
JSON.stringify({
query: { hotelId, params },
error: validatedHotelData.error,
})
)
throw badRequestError()
}
const included = validatedHotelData.data.included || []
const hotelAttributes = validatedHotelData.data.data.attributes
const images = extractHotelImages(hotelAttributes)
const roomCategories = included
? included
.filter((item) => item.type === "roomcategories")
.map((roomCategory) => {
const validatedRoom = roomSchema.safeParse(roomCategory)
if (!validatedRoom.success) {
getHotelFailCounter.add(1, {
hotelId,
lang,
include,
error_type: "validation_error",
error: JSON.stringify(
validatedRoom.error.issues.map(({ code, message }) => ({
code,
message,
}))
),
})
console.error(
"api.hotels.hotel validation error",
JSON.stringify({
query: { hotelId, params },
error: validatedRoom.error,
})
)
throw badRequestError()
}
return validatedRoom.data
})
: []
const activities = contentstackData?.content
? contentstackData?.content[0]
: null
getHotelSuccessCounter.add(1, { hotelId, lang, include })
console.info(
"api.hotels.hotel success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
hotelName: hotelAttributes.name,
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
hotelLocation: hotelAttributes.location,
hotelAddress: hotelAttributes.address,
hotelRatings: hotelAttributes.ratings,
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
hotelImages: images,
pointsOfInterest: hotelAttributes.pointsOfInterest,
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
}
}),
availability: router({
get: hotelServiceProcedure
.input(getAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
attachedProfileId,
} = input
const params: Record<string, string | number> = {
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
attachedProfileId,
}
availabilityCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
})
console.info(
"api.hotels.availability start",
JSON.stringify({ query: { cityId, params } })
)
const apiResponse = await api.get(
`${api.endpoints.v1.availability}/${cityId}`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
availabilityFailCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.availability error",
JSON.stringify({
query: { cityId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
getAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
availabilityFailCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.availability validation error",
JSON.stringify({
query: { cityId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
availabilitySuccessCounter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
})
console.info(
"api.hotels.availability success",
JSON.stringify({
query: { cityId, params: params },
})
)
return {
availability: validateAvailabilityData.data.data
.filter(
(hotels) =>
hotels.attributes.status === AvailabilityEnum.Available
)
.flatMap((hotels) => hotels.attributes),
}
}),
}),
rates: router({
get: publicProcedure
.input(getRatesInputSchema)
.query(async ({ input, ctx }) => {
// TODO: Do a real API call when the endpoint is ready
// const { hotelId } = input
// const params = new URLSearchParams()
// const apiLang = toApiLang(language)
// params.set("hotelId", hotelId.toString())
// params.set("language", apiLang)
console.info("api.hotels.rates start", JSON.stringify({}))
const validatedHotelData = getRatesSchema.safeParse(tempRatesData)
if (!tempRatesData) {
console.error(
"api.hotels.rates error",
JSON.stringify({ error: null })
)
//Can't return null here since consuming component does not handle null yet
// return null
}
if (!validatedHotelData.success) {
console.error(
"api.hotels.rates validation error",
JSON.stringify({
error: validatedHotelData.error,
})
)
throw badRequestError()
}
console.info("api.hotels.rates success", JSON.stringify({}))
return validatedHotelData.data
}),
}),
hotelData: router({
get: hotelServiceProcedure
.input(getlHotelDataInputSchema)
.query(async ({ ctx, input }) => {
const { hotelId, language, include } = input
const params: Record<string, string> = {
hotelId,
language,
}
if (include) {
params.include = include.join(",")
}
getHotelCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
`${api.endpoints.v1.hotels}/${hotelId}`,
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelData error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validateHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.hotelData validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
getHotelSuccessCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return validateHotelData.data
}),
}),
locations: router({
get: hotelServiceProcedure.query(async function ({ ctx }) {
const searchParams = new URLSearchParams()
searchParams.set("language", toApiLang(ctx.lang))
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: TWENTYFOUR_HOURS,
},
}
const getCachedCountries = unstable_cache(
getCountries,
[`${ctx.lang}:${locationsAffix}:countries`],
{ revalidate: TWENTYFOUR_HOURS }
)
const countries = await getCachedCountries(options, searchParams)
const getCachedCitiesByCountry = unstable_cache(
getCitiesByCountry,
[`${ctx.lang}:${locationsAffix}:cities-by-country`],
{ revalidate: TWENTYFOUR_HOURS }
)
let citiesByCountry = null
if (countries) {
citiesByCountry = await getCachedCitiesByCountry(
countries,
options,
searchParams
)
}
const getCachedLocations = unstable_cache(
getLocations,
[`${ctx.lang}:${locationsAffix}`],
{ revalidate: TWENTYFOUR_HOURS }
)
const locations = await getCachedLocations(
ctx.lang,
options,
searchParams,
citiesByCountry
)
if (Array.isArray(locations)) {
return {
data: locations,
}
}
return locations
}),
}),
})