519 lines
15 KiB
TypeScript
519 lines
15 KiB
TypeScript
import { metrics } from "@opentelemetry/api"
|
|
|
|
import * as api from "@/lib/api"
|
|
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage.graphql"
|
|
import { request } from "@/lib/graphql/request"
|
|
import {
|
|
badRequestError,
|
|
notFound,
|
|
serverErrorByStatus,
|
|
} from "@/server/errors/trpc"
|
|
import { extractHotelImages } from "@/server/routers/utils/hotels"
|
|
import {
|
|
contentStackUidWithServiceProcedure,
|
|
publicProcedure,
|
|
router,
|
|
serviceProcedure,
|
|
} from "@/server/trpc"
|
|
import { toApiLang } from "@/server/utils"
|
|
|
|
import { makeImageVaultImage } from "@/utils/imageVault"
|
|
import { removeMultipleSlashes } from "@/utils/url"
|
|
|
|
import {
|
|
type ContentBlockItem,
|
|
type HotelPageDataRaw,
|
|
validateHotelPageSchema,
|
|
} from "../contentstack/hotelPage/output"
|
|
import {
|
|
getAvailabilityInputSchema,
|
|
getHotelInputSchema,
|
|
getlHotelDataInputSchema,
|
|
getRatesInputSchema,
|
|
} from "./input"
|
|
import {
|
|
getAvailabilitySchema,
|
|
getHotelDataSchema,
|
|
getRatesSchema,
|
|
roomSchema,
|
|
} from "./output"
|
|
import tempRatesData from "./tempRatesData.json"
|
|
|
|
import { HotelBlocksTypenameEnum } from "@/types/components/hotelPage/enums"
|
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
|
|
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 rawContentStackData = await request<HotelPageDataRaw>(GetHotelPage, {
|
|
locale,
|
|
uid,
|
|
})
|
|
|
|
if (!rawContentStackData.data) {
|
|
throw notFound(rawContentStackData)
|
|
}
|
|
|
|
const hotelPageData = validateHotelPageSchema.safeParse(
|
|
rawContentStackData.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: contentStackUidWithServiceProcedure
|
|
.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}`,
|
|
{
|
|
cache: "no-store",
|
|
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.map((block: ContentBlockItem) => {
|
|
switch (block.__typename) {
|
|
case HotelBlocksTypenameEnum.HotelPageContentUpcomingActivitiesCard:
|
|
return {
|
|
...block.upcoming_activities_card,
|
|
background_image: makeImageVaultImage(
|
|
block.upcoming_activities_card?.background_image
|
|
),
|
|
contentPage:
|
|
block.upcoming_activities_card?.hotel_page_activities_content_pageConnection?.edges.map(
|
|
({ node: contentPage }: { node: any }) => {
|
|
return {
|
|
href:
|
|
contentPage.web?.original_url ||
|
|
removeMultipleSlashes(
|
|
`/${contentPage.system.locale}/${contentPage.url}`
|
|
),
|
|
}
|
|
}
|
|
),
|
|
}
|
|
}
|
|
})[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,
|
|
roomCategories,
|
|
activitiesCard: activities,
|
|
}
|
|
}),
|
|
availability: router({
|
|
get: serviceProcedure
|
|
.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.v0.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: serviceProcedure
|
|
.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
|
|
}),
|
|
}),
|
|
})
|