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 { contentStackUidWithServiceProcedure, publicProcedure, router, serviceProcedure, } 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(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: 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 = { 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: serviceProcedure .input(getAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { cityId, roomStayStartDate, roomStayEndDate, adults, children, promotionCode, reservationProfileType, attachedProfileId, } = input const params: Record = { 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 = { 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: serviceProcedure.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 }), }), })