Fix/SW-902 update response city availability * fix(SW-902): update response for API changes * fix(SW-902): add total row for pricePerStay * fix(SW-902): fix optional requestedPrice * fix(SW-902): fix bookingCode output * feat(SW-903): fix sorting Approved-by: Pontus Dreij Approved-by: Niclas Edenvin
1082 lines
31 KiB
TypeScript
1082 lines
31 KiB
TypeScript
import { metrics } from "@opentelemetry/api"
|
|
import { cache } from "react"
|
|
|
|
import { Lang } from "@/constants/languages"
|
|
import * as api from "@/lib/api"
|
|
import { dt } from "@/lib/dt"
|
|
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
|
import { request } from "@/lib/graphql/request"
|
|
import {
|
|
badRequestError,
|
|
notFound,
|
|
serverErrorByStatus,
|
|
} from "@/server/errors/trpc"
|
|
import {
|
|
contentStackUidWithServiceProcedure,
|
|
publicProcedure,
|
|
router,
|
|
safeProtectedServiceProcedure,
|
|
serviceProcedure,
|
|
} from "@/server/trpc"
|
|
import { toApiLang } from "@/server/utils"
|
|
|
|
import { hotelPageSchema } from "../contentstack/hotelPage/output"
|
|
import {
|
|
fetchHotelPageRefs,
|
|
generatePageTags,
|
|
getHotelPageCounter,
|
|
validateHotelPageRefs,
|
|
} from "../contentstack/hotelPage/utils"
|
|
import { getVerifiedUser, parsedUser } from "../user/query"
|
|
import {
|
|
getBreakfastPackageInputSchema,
|
|
getHotelDataInputSchema,
|
|
getHotelsAvailabilityInputSchema,
|
|
getRatesInputSchema,
|
|
getRoomPackagesInputSchema,
|
|
getRoomsAvailabilityInputSchema,
|
|
getSelectedRoomAvailabilityInputSchema,
|
|
type HotelDataInput,
|
|
} from "./input"
|
|
import {
|
|
breakfastPackagesSchema,
|
|
getHotelDataSchema,
|
|
getHotelsAvailabilitySchema,
|
|
getRatesSchema,
|
|
getRoomPackagesSchema,
|
|
getRoomsAvailabilitySchema,
|
|
} from "./output"
|
|
import tempRatesData from "./tempRatesData.json"
|
|
import {
|
|
getCitiesByCountry,
|
|
getCountries,
|
|
getLocations,
|
|
TWENTYFOUR_HOURS,
|
|
} from "./utils"
|
|
|
|
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
|
|
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
|
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
|
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
|
import type { Facility } from "@/types/hotel"
|
|
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 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"
|
|
)
|
|
|
|
async function getContentstackData(lang: Lang, uid?: string | null) {
|
|
if (!uid) {
|
|
return null
|
|
}
|
|
const contentPageRefsData = await fetchHotelPageRefs(lang, uid)
|
|
const contentPageRefs = validateHotelPageRefs(contentPageRefsData, lang, uid)
|
|
if (!contentPageRefs) {
|
|
return null
|
|
}
|
|
|
|
const tags = generatePageTags(contentPageRefs, lang)
|
|
|
|
getHotelPageCounter.add(1, { lang, uid })
|
|
console.info(
|
|
"contentstack.hotelPage start",
|
|
JSON.stringify({
|
|
query: { lang, uid },
|
|
})
|
|
)
|
|
const response = await request<GetHotelPageData>(
|
|
GetHotelPage,
|
|
{
|
|
locale: lang,
|
|
uid,
|
|
},
|
|
{
|
|
cache: "force-cache",
|
|
next: {
|
|
tags,
|
|
},
|
|
}
|
|
)
|
|
|
|
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: ${lang})`
|
|
)
|
|
console.error(hotelPageData.error)
|
|
return null
|
|
}
|
|
|
|
return hotelPageData.data.hotel_page
|
|
}
|
|
|
|
export const getHotelData = cache(
|
|
async (input: HotelDataInput, serviceToken: string) => {
|
|
const { hotelId, language, isCardOnlyPayment } = input
|
|
|
|
const params: Record<string, string> = {
|
|
hotelId,
|
|
language,
|
|
}
|
|
|
|
params.include = "RoomCategories" // "RoomCategories","NearbyHotels","Restaurants","City",
|
|
|
|
getHotelCounter.add(1, {
|
|
hotelId,
|
|
language,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelData start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
// needs to clear default option as only
|
|
// cache or next.revalidate is permitted
|
|
cache: undefined,
|
|
next: {
|
|
revalidate: 60 * 30, // 30 minutes
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getHotelFailCounter.add(1, {
|
|
hotelId,
|
|
language,
|
|
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,
|
|
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,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelData success",
|
|
JSON.stringify({
|
|
query: { hotelId, params: params },
|
|
})
|
|
)
|
|
const hotelData = validateHotelData.data
|
|
|
|
if (isCardOnlyPayment) {
|
|
hotelData.data.attributes.merchantInformationData.alternatePaymentOptions =
|
|
[]
|
|
}
|
|
if (hotelData.data.attributes.gallery) {
|
|
const smallerImages = hotelData.data.attributes.gallery.smallerImages
|
|
const hotelGalleryImages =
|
|
hotelData.data.attributes.hotelType === HotelTypeEnum.Signature
|
|
? smallerImages.slice(0, 10)
|
|
: smallerImages.slice(0, 6)
|
|
hotelData.data.attributes.galleryImages = hotelGalleryImages
|
|
}
|
|
|
|
return hotelData
|
|
}
|
|
)
|
|
|
|
export const hotelQueryRouter = router({
|
|
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
|
|
const { lang, uid } = ctx
|
|
|
|
const contentstackData = await getContentstackData(lang, uid)
|
|
const hotelId = contentstackData?.hotel_page_id
|
|
|
|
if (!hotelId) {
|
|
throw notFound(`Hotel not found for uid: ${uid}`)
|
|
}
|
|
|
|
const hotelData = await getHotelData(
|
|
{
|
|
hotelId,
|
|
language: ctx.lang,
|
|
},
|
|
ctx.serviceToken
|
|
)
|
|
|
|
if (!hotelData) {
|
|
throw notFound()
|
|
}
|
|
|
|
const included = hotelData.included || []
|
|
|
|
const hotelAttributes = hotelData.data.attributes
|
|
const images = hotelAttributes.gallery?.smallerImages
|
|
const hotelAlerts = hotelAttributes.specialAlerts
|
|
|
|
const roomCategories = included
|
|
? included.filter((item) => item.type === "roomcategories")
|
|
: []
|
|
|
|
const activities = contentstackData?.content
|
|
? contentstackData?.content[0]
|
|
: null
|
|
|
|
const facilities: Facility[] = [
|
|
{
|
|
...hotelData.data.attributes.restaurantImages,
|
|
id: FacilityCardTypeEnum.restaurant,
|
|
headingText:
|
|
hotelData?.data.attributes.restaurantImages?.headingText ?? "",
|
|
heroImages:
|
|
hotelData?.data.attributes.restaurantImages?.heroImages ?? [],
|
|
},
|
|
{
|
|
...hotelData.data.attributes.conferencesAndMeetings,
|
|
id: FacilityCardTypeEnum.conference,
|
|
headingText:
|
|
hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "",
|
|
heroImages:
|
|
hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [],
|
|
},
|
|
{
|
|
...hotelData.data.attributes.healthAndWellness,
|
|
id: FacilityCardTypeEnum.wellness,
|
|
headingText:
|
|
hotelData?.data.attributes.healthAndWellness?.headingText ?? "",
|
|
heroImages:
|
|
hotelData?.data.attributes.healthAndWellness?.heroImages ?? [],
|
|
},
|
|
]
|
|
|
|
return {
|
|
hotelId,
|
|
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,
|
|
facilities,
|
|
alerts: hotelAlerts,
|
|
faq: contentstackData?.faq,
|
|
healthFacilities: hotelAttributes.healthFacilities,
|
|
}
|
|
}),
|
|
availability: router({
|
|
hotels: serviceProcedure
|
|
.input(getHotelsAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { lang } = ctx
|
|
const apiLang = toApiLang(lang)
|
|
const {
|
|
cityId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
} = input
|
|
|
|
const params: Record<string, string | number> = {
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
...(children && { children }),
|
|
bookingCode,
|
|
language: apiLang,
|
|
}
|
|
hotelsAvailabilityCounter.add(1, {
|
|
cityId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelsAvailability start",
|
|
JSON.stringify({ query: { cityId, params } })
|
|
)
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Availability.city(cityId),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
hotelsAvailabilityFailCounter.add(1, {
|
|
cityId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.hotelsAvailability error",
|
|
JSON.stringify({
|
|
query: { cityId, params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
const apiJson = await apiResponse.json()
|
|
const validateAvailabilityData =
|
|
getHotelsAvailabilitySchema.safeParse(apiJson)
|
|
if (!validateAvailabilityData.success) {
|
|
hotelsAvailabilityFailCounter.add(1, {
|
|
cityId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validateAvailabilityData.error),
|
|
})
|
|
console.error(
|
|
"api.hotels.hotelsAvailability validation error",
|
|
JSON.stringify({
|
|
query: { cityId, params },
|
|
error: validateAvailabilityData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
hotelsAvailabilitySuccessCounter.add(1, {
|
|
cityId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelsAvailability success",
|
|
JSON.stringify({
|
|
query: { cityId, params: params },
|
|
})
|
|
)
|
|
return {
|
|
availability: validateAvailabilityData.data.data
|
|
.filter(
|
|
(hotels) =>
|
|
hotels.attributes.status === AvailabilityEnum.Available
|
|
)
|
|
.flatMap((hotels) => hotels.attributes),
|
|
}
|
|
}),
|
|
rooms: serviceProcedure
|
|
.input(getRoomsAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
rateCode,
|
|
} = input
|
|
|
|
const params: Record<string, string | number | undefined> = {
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
...(children && { children }),
|
|
bookingCode,
|
|
}
|
|
|
|
roomsAvailabilityCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.roomsAvailability start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
roomsAvailabilityFailCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.roomsAvailability error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
const apiJson = await apiResponse.json()
|
|
const validateAvailabilityData =
|
|
getRoomsAvailabilitySchema.safeParse(apiJson)
|
|
if (!validateAvailabilityData.success) {
|
|
roomsAvailabilityFailCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validateAvailabilityData.error),
|
|
})
|
|
console.error(
|
|
"api.hotels.roomsAvailability validation error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: validateAvailabilityData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
roomsAvailabilitySuccessCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.roomsAvailability success",
|
|
JSON.stringify({
|
|
query: { hotelId, params: params },
|
|
})
|
|
)
|
|
|
|
if (rateCode) {
|
|
validateAvailabilityData.data.mustBeGuaranteed =
|
|
validateAvailabilityData.data.rateDefinitions.filter(
|
|
(rate) => rate.rateCode === rateCode
|
|
)[0].mustBeGuaranteed
|
|
}
|
|
|
|
return validateAvailabilityData.data
|
|
}),
|
|
room: serviceProcedure
|
|
.input(getSelectedRoomAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
rateCode,
|
|
roomTypeCode,
|
|
packageCodes,
|
|
} = input
|
|
|
|
const params: Record<string, string | number | undefined> = {
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
...(children && { children }),
|
|
bookingCode,
|
|
language: toApiLang(ctx.lang),
|
|
}
|
|
|
|
selectedRoomAvailabilityCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.selectedRoomAvailability start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
const apiResponseAvailability = await api.get(
|
|
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponseAvailability.ok) {
|
|
const text = await apiResponseAvailability.text()
|
|
selectedRoomAvailabilityFailCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponseAvailability.status,
|
|
statusText: apiResponseAvailability.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.selectedRoomAvailability error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: {
|
|
status: apiResponseAvailability.status,
|
|
statusText: apiResponseAvailability.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
const apiJsonAvailability = await apiResponseAvailability.json()
|
|
const validateAvailabilityData =
|
|
getRoomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
|
if (!validateAvailabilityData.success) {
|
|
selectedRoomAvailabilityFailCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validateAvailabilityData.error),
|
|
})
|
|
console.error(
|
|
"api.hotels.selectedRoomAvailability validation error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: validateAvailabilityData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
|
|
const hotelData = await getHotelData(
|
|
{
|
|
hotelId,
|
|
language: ctx.lang,
|
|
},
|
|
ctx.serviceToken
|
|
)
|
|
|
|
const availableRooms =
|
|
validateAvailabilityData.data.roomConfigurations.filter((room) => {
|
|
if (packageCodes) {
|
|
return (
|
|
room.status === "Available" &&
|
|
room.features.some(
|
|
(feature) =>
|
|
packageCodes.includes(feature.code) && feature.inventory > 0
|
|
)
|
|
)
|
|
}
|
|
return room.status === "Available"
|
|
})
|
|
|
|
const selectedRoom = availableRooms.find(
|
|
(room) => room.roomTypeCode === roomTypeCode
|
|
)
|
|
|
|
const availableRoomsInCategory = availableRooms.filter(
|
|
(room) => room.roomType === selectedRoom?.roomType
|
|
)
|
|
if (!selectedRoom) {
|
|
console.error("No matching room found")
|
|
return null
|
|
}
|
|
|
|
const rateTypes = selectedRoom.products.find(
|
|
(rate) =>
|
|
rate.productType.public.rateCode === rateCode ||
|
|
rate.productType.member?.rateCode === rateCode
|
|
)
|
|
|
|
if (!rateTypes) {
|
|
console.error("No matching rate found")
|
|
return null
|
|
}
|
|
const rates = rateTypes.productType
|
|
|
|
const mustBeGuaranteed =
|
|
validateAvailabilityData.data.rateDefinitions.filter(
|
|
(rate) => rate.rateCode === rateCode
|
|
)[0].mustBeGuaranteed
|
|
|
|
const cancellationText =
|
|
validateAvailabilityData.data.rateDefinitions.find(
|
|
(rate) => rate.rateCode === rateCode
|
|
)?.cancellationText ?? ""
|
|
|
|
const bedTypes = availableRoomsInCategory
|
|
.map((availRoom) => {
|
|
const matchingRoom = hotelData?.included
|
|
?.find((room) => room.name === availRoom.roomType)
|
|
?.roomTypes.find(
|
|
(roomType) => roomType.code === availRoom.roomTypeCode
|
|
)
|
|
|
|
if (matchingRoom) {
|
|
return {
|
|
description: matchingRoom.mainBed.description,
|
|
size: matchingRoom.mainBed.widthRange,
|
|
value: matchingRoom.code,
|
|
}
|
|
}
|
|
})
|
|
.filter((bed): bed is BedTypeSelection => Boolean(bed))
|
|
|
|
selectedRoomAvailabilitySuccessCounter.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.selectedRoomAvailability success",
|
|
JSON.stringify({
|
|
query: { hotelId, params: params },
|
|
})
|
|
)
|
|
|
|
return {
|
|
selectedRoom,
|
|
mustBeGuaranteed,
|
|
cancellationText,
|
|
memberRate: rates?.member,
|
|
publicRate: rates.public,
|
|
bedTypes,
|
|
}
|
|
}),
|
|
}),
|
|
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(getHotelDataInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
return getHotelData(input, ctx.serviceToken)
|
|
}),
|
|
}),
|
|
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 countries = await getCountries(options, searchParams, ctx.lang)
|
|
|
|
let citiesByCountry = null
|
|
if (countries) {
|
|
citiesByCountry = await getCitiesByCountry(
|
|
countries,
|
|
options,
|
|
searchParams,
|
|
ctx.lang
|
|
)
|
|
}
|
|
|
|
const locations = await getLocations(
|
|
ctx.lang,
|
|
options,
|
|
searchParams,
|
|
citiesByCountry
|
|
)
|
|
|
|
if (Array.isArray(locations)) {
|
|
return {
|
|
data: locations,
|
|
}
|
|
}
|
|
|
|
return locations
|
|
}),
|
|
}),
|
|
packages: router({
|
|
get: serviceProcedure
|
|
.input(getRoomPackagesInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { hotelId, startDate, endDate, adults, children, packageCodes } =
|
|
input
|
|
|
|
const { lang } = ctx
|
|
|
|
const apiLang = toApiLang(lang)
|
|
|
|
const searchParams = new URLSearchParams({
|
|
startDate,
|
|
endDate,
|
|
adults: adults.toString(),
|
|
children: children.toString(),
|
|
language: apiLang,
|
|
})
|
|
|
|
packageCodes.forEach((code) => {
|
|
searchParams.append("packageCodes", code)
|
|
})
|
|
|
|
const params = searchParams.toString()
|
|
|
|
getPackagesCounter.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.packages start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Package.Packages.hotel(hotelId),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
getPackagesFailCounter.add(1, {
|
|
hotelId,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.packages error",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson)
|
|
|
|
if (!validatedPackagesData.success) {
|
|
getHotelFailCounter.add(1, {
|
|
hotelId,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validatedPackagesData.error),
|
|
})
|
|
|
|
console.error(
|
|
"api.hotels.packages validation error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: validatedPackagesData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
|
|
getPackagesSuccessCounter.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.packages success",
|
|
JSON.stringify({ query: { hotelId, params: params } })
|
|
)
|
|
|
|
return validatedPackagesData.data
|
|
}),
|
|
breakfast: safeProtectedServiceProcedure
|
|
.input(getBreakfastPackageInputSchema)
|
|
.query(async function ({ ctx, input }) {
|
|
const { lang } = ctx
|
|
|
|
const apiLang = toApiLang(lang)
|
|
const params = {
|
|
Adults: input.adults,
|
|
EndDate: dt(input.toDate).format("YYYY-MM-DD"),
|
|
StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
|
|
language: apiLang,
|
|
}
|
|
|
|
const metricsData = { ...params, hotelId: input.hotelId }
|
|
breakfastPackagesCounter.add(1, metricsData)
|
|
console.info(
|
|
"api.package.breakfast start",
|
|
JSON.stringify({ query: metricsData })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Package.Breakfast.hotel(input.hotelId),
|
|
{
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: 60,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
breakfastPackagesFailCounter.add(1, {
|
|
...metricsData,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.hotelsAvailability error",
|
|
JSON.stringify({
|
|
query: metricsData,
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson)
|
|
if (!breakfastPackages.success) {
|
|
hotelsAvailabilityFailCounter.add(1, {
|
|
...metricsData,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(breakfastPackages.error),
|
|
})
|
|
console.error(
|
|
"api.package.breakfast validation error",
|
|
JSON.stringify({
|
|
query: metricsData,
|
|
error: breakfastPackages.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
breakfastPackagesSuccessCounter.add(1, metricsData)
|
|
console.info(
|
|
"api.package.breakfast success",
|
|
JSON.stringify({
|
|
query: metricsData,
|
|
})
|
|
)
|
|
|
|
if (ctx.session?.token) {
|
|
const apiUser = await getVerifiedUser({ session: ctx.session })
|
|
if (apiUser && !("error" in apiUser)) {
|
|
const user = parsedUser(apiUser.data, false)
|
|
if (
|
|
user.membership &&
|
|
["L6", "L7"].includes(user.membership.membershipLevel)
|
|
) {
|
|
const freeBreakfastPackage = breakfastPackages.data.find(
|
|
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
|
)
|
|
if (freeBreakfastPackage?.localPrice) {
|
|
return [freeBreakfastPackage]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return breakfastPackages.data.filter(
|
|
(pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
|
)
|
|
}),
|
|
}),
|
|
})
|