Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
1788 lines
52 KiB
TypeScript
1788 lines
52 KiB
TypeScript
import { unstable_cache } from "next/cache"
|
|
|
|
import { ApiLang } from "@/constants/languages"
|
|
import { env } from "@/env/server"
|
|
import * as api from "@/lib/api"
|
|
import { dt } from "@/lib/dt"
|
|
import { badRequestError } from "@/server/errors/trpc"
|
|
import {
|
|
contentStackBaseWithServiceProcedure,
|
|
publicProcedure,
|
|
router,
|
|
safeProtectedServiceProcedure,
|
|
serviceProcedure,
|
|
} from "@/server/trpc"
|
|
import { toApiLang } from "@/server/utils"
|
|
|
|
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
|
import { cache } from "@/utils/cache"
|
|
|
|
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
|
import { getVerifiedUser, parsedUser } from "../user/query"
|
|
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
|
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
|
import {
|
|
ancillaryPackageInputSchema,
|
|
breakfastPackageInputSchema,
|
|
cityCoordinatesInputSchema,
|
|
getAdditionalDataInputSchema,
|
|
getHotelsByCityIdentifierInput,
|
|
getHotelsByCountryInput,
|
|
getHotelsByCSFilterInput,
|
|
getHotelsByHotelIdsAvailabilityInputSchema,
|
|
getMeetingRoomsInputSchema,
|
|
hotelInputSchema,
|
|
hotelsAvailabilityInputSchema,
|
|
nearbyHotelIdsInput,
|
|
ratesInputSchema,
|
|
roomPackagesInputSchema,
|
|
roomsCombinedAvailabilityInputSchema,
|
|
selectedRoomAvailabilityInputSchema,
|
|
} from "./input"
|
|
import { metrics } from "./metrics"
|
|
import {
|
|
ancillaryPackagesSchema,
|
|
breakfastPackagesSchema,
|
|
getNearbyHotelIdsSchema,
|
|
hotelsAvailabilitySchema,
|
|
hotelSchema,
|
|
packagesSchema,
|
|
ratesSchema,
|
|
roomsAvailabilitySchema,
|
|
} from "./output"
|
|
import tempRatesData from "./tempRatesData.json"
|
|
import {
|
|
getCitiesByCountry,
|
|
getCountries,
|
|
getHotelIdsByCityId,
|
|
getHotelIdsByCityIdentifier,
|
|
getHotelIdsByCountry,
|
|
getHotelsByHotelIds,
|
|
getLocations,
|
|
} from "./utils"
|
|
|
|
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 { HotelDataWithUrl } from "@/types/hotel"
|
|
import type {
|
|
HotelsAvailabilityInputSchema,
|
|
HotelsByHotelIdsAvailabilityInputSchema,
|
|
} from "@/types/trpc/routers/hotel/availability"
|
|
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
|
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
|
|
|
export const getHotel = cache(
|
|
async (input: HotelInput, serviceToken: string) => {
|
|
const callable = unstable_cache(
|
|
async function (
|
|
hotelId: HotelInput["hotelId"],
|
|
language: HotelInput["language"],
|
|
isCardOnlyPayment?: HotelInput["isCardOnlyPayment"]
|
|
) {
|
|
/**
|
|
* Since API expects the params appended and not just
|
|
* a comma separated string we need to initialize the
|
|
* SearchParams with a sequence of pairs
|
|
* (include=City&include=NearbyHotels&include=Restaurants etc.)
|
|
**/
|
|
const params = new URLSearchParams([
|
|
["include", "AdditionalData"],
|
|
["include", "City"],
|
|
["include", "NearbyHotels"],
|
|
["include", "Restaurants"],
|
|
["include", "RoomCategories"],
|
|
["language", toApiLang(language)],
|
|
])
|
|
metrics.hotel.counter.add(1, {
|
|
hotelId,
|
|
language,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelData start",
|
|
JSON.stringify({ query: { hotelId, params: params.toString() } })
|
|
)
|
|
|
|
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: env.CACHE_TIME_HOTELS,
|
|
tags: [`${language}:hotel:${hotelId}`],
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.hotel.fail.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: params.toString() },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validateHotelData = hotelSchema.safeParse(apiJson)
|
|
|
|
if (!validateHotelData.success) {
|
|
metrics.hotel.fail.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: params.toString() },
|
|
error: validateHotelData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
|
|
metrics.hotel.success.add(1, {
|
|
hotelId,
|
|
language,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelData success",
|
|
JSON.stringify({
|
|
query: { hotelId, params: params.toString() },
|
|
})
|
|
)
|
|
const hotelData = validateHotelData.data
|
|
|
|
if (isCardOnlyPayment) {
|
|
hotelData.hotel.merchantInformationData.alternatePaymentOptions = []
|
|
}
|
|
|
|
const gallery = hotelData.additionalData?.gallery
|
|
if (gallery) {
|
|
const smallerImages = gallery.smallerImages
|
|
const hotelGalleryImages =
|
|
hotelData.hotel.hotelType === HotelTypeEnum.Signature
|
|
? smallerImages.slice(0, 10)
|
|
: smallerImages.slice(0, 6)
|
|
hotelData.hotel.galleryImages = hotelGalleryImages
|
|
}
|
|
|
|
return hotelData
|
|
},
|
|
[`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`],
|
|
{
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
tags: [
|
|
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
|
|
],
|
|
}
|
|
)
|
|
|
|
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
|
|
}
|
|
)
|
|
|
|
export const getHotelsAvailabilityByCity = async (
|
|
input: HotelsAvailabilityInputSchema,
|
|
apiLang: string,
|
|
serviceToken: string
|
|
) => {
|
|
const {
|
|
cityId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
} = input
|
|
|
|
const params: Record<string, string | number> = {
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
...(children && { children }),
|
|
...(bookingCode && { bookingCode }),
|
|
language: apiLang,
|
|
}
|
|
metrics.hotelsAvailability.counter.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),
|
|
{
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.hotelsAvailability.fail.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 = hotelsAvailabilitySchema.safeParse(apiJson)
|
|
if (!validateAvailabilityData.success) {
|
|
metrics.hotelsAvailability.fail.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()
|
|
}
|
|
metrics.hotelsAvailability.success.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.flatMap(
|
|
(hotels) => hotels.attributes
|
|
),
|
|
}
|
|
}
|
|
|
|
export const getHotelsAvailabilityByHotelIds = async (
|
|
input: HotelsByHotelIdsAvailabilityInputSchema,
|
|
apiLang: string,
|
|
serviceToken: string
|
|
) => {
|
|
const {
|
|
hotelIds,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
} = input
|
|
|
|
/**
|
|
* Since API expects the params appended and not just
|
|
* a comma separated string we need to initialize the
|
|
* SearchParams with a sequence of pairs
|
|
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
|
**/
|
|
const params = new URLSearchParams([
|
|
["roomStayStartDate", roomStayStartDate],
|
|
["roomStayEndDate", roomStayEndDate],
|
|
["adults", adults.toString()],
|
|
["children", children ?? ""],
|
|
["bookingCode", bookingCode],
|
|
["language", apiLang],
|
|
])
|
|
hotelIds.forEach((hotelId) => params.append("hotelIds", hotelId.toString()))
|
|
metrics.hotelsByHotelIdAvailability.counter.add(1, {
|
|
hotelIds,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelsByHotelIdAvailability start",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Availability.hotels(),
|
|
{
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_CITY_SEARCH,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
|
hotelIds,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.hotelsByHotelIdAvailability error",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
const apiJson = await apiResponse.json()
|
|
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
|
if (!validateAvailabilityData.success) {
|
|
metrics.hotelsByHotelIdAvailability.fail.add(1, {
|
|
hotelIds,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validateAvailabilityData.error),
|
|
})
|
|
console.error(
|
|
"api.hotels.hotelsByHotelIdAvailability validation error",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: validateAvailabilityData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
metrics.hotelsByHotelIdAvailability.success.add(1, {
|
|
hotelIds,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.hotelsByHotelIdAvailability success",
|
|
JSON.stringify({
|
|
query: { params },
|
|
})
|
|
)
|
|
return {
|
|
availability: validateAvailabilityData.data.data.flatMap(
|
|
(hotels) => hotels.attributes
|
|
),
|
|
}
|
|
}
|
|
|
|
export const hotelQueryRouter = router({
|
|
availability: router({
|
|
hotelsByCity: serviceProcedure
|
|
.input(hotelsAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { lang } = ctx
|
|
const apiLang = toApiLang(lang)
|
|
return getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
|
}),
|
|
hotelsByHotelIds: serviceProcedure
|
|
.input(getHotelsByHotelIdsAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { lang } = ctx
|
|
const apiLang = toApiLang(lang)
|
|
return getHotelsAvailabilityByHotelIds(input, apiLang, ctx.serviceToken)
|
|
}),
|
|
|
|
roomsCombinedAvailability: serviceProcedure
|
|
.input(roomsCombinedAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { lang } = input
|
|
const apiLang = toApiLang(lang)
|
|
const {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
uniqueAdultsCount,
|
|
childArray,
|
|
bookingCode,
|
|
rateCode,
|
|
} = input
|
|
|
|
const metricsData = {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
uniqueAdultsCount,
|
|
childArray: childArray ? JSON.stringify(childArray) : undefined,
|
|
bookingCode,
|
|
}
|
|
|
|
metrics.roomsCombinedAvailability.counter.add(1, metricsData)
|
|
|
|
console.info(
|
|
"api.hotels.roomsCombinedAvailability start",
|
|
JSON.stringify({ query: { hotelId, params: metricsData } })
|
|
)
|
|
|
|
const availabilityResponses = await Promise.allSettled(
|
|
uniqueAdultsCount.map(async (adultCount: number) => {
|
|
const params: Record<string, string | number | undefined> = {
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults: adultCount,
|
|
...(childArray &&
|
|
childArray.length > 0 && {
|
|
children: generateChildrenString(childArray),
|
|
}),
|
|
...(bookingCode && { bookingCode }),
|
|
language: apiLang,
|
|
}
|
|
|
|
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()
|
|
metrics.roomsCombinedAvailability.fail.add(1, metricsData)
|
|
console.error("Failed API call", { params, text })
|
|
return { error: "http_error", details: text }
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
|
|
const validateAvailabilityData =
|
|
roomsAvailabilitySchema.safeParse(apiJson)
|
|
|
|
if (!validateAvailabilityData.success) {
|
|
console.error("Validation error", {
|
|
params,
|
|
error: validateAvailabilityData.error,
|
|
})
|
|
metrics.roomsCombinedAvailability.fail.add(1, metricsData)
|
|
return {
|
|
error: "validation_error",
|
|
details: validateAvailabilityData.error,
|
|
}
|
|
}
|
|
|
|
if (rateCode) {
|
|
validateAvailabilityData.data.mustBeGuaranteed =
|
|
validateAvailabilityData.data.rateDefinitions.find(
|
|
(rate) => rate.rateCode === rateCode
|
|
)?.mustBeGuaranteed
|
|
}
|
|
|
|
return validateAvailabilityData.data
|
|
})
|
|
)
|
|
metrics.roomsCombinedAvailability.success.add(1, metricsData)
|
|
return availabilityResponses
|
|
}),
|
|
room: serviceProcedure
|
|
.input(selectedRoomAvailabilityInputSchema)
|
|
.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 && { bookingCode }),
|
|
language: toApiLang(ctx.lang),
|
|
}
|
|
|
|
metrics.selectedRoomAvailability.counter.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()
|
|
metrics.selectedRoomAvailability.fail.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 =
|
|
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
|
if (!validateAvailabilityData.success) {
|
|
metrics.selectedRoomAvailability.fail.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 getHotel(
|
|
{
|
|
hotelId,
|
|
isCardOnlyPayment: false,
|
|
language: ctx.lang,
|
|
},
|
|
ctx.serviceToken
|
|
)
|
|
|
|
const availableRooms =
|
|
validateAvailabilityData.data.roomConfigurations.filter((room) => {
|
|
if (packageCodes) {
|
|
return (
|
|
room.status === AvailabilityEnum.Available &&
|
|
room.features.some(
|
|
(feature) =>
|
|
packageCodes.includes(feature.code) && feature.inventory > 0
|
|
)
|
|
)
|
|
}
|
|
return room.status === AvailabilityEnum.Available
|
|
})
|
|
|
|
const selectedRoom = availableRooms.find(
|
|
(room) => room.roomTypeCode === roomTypeCode
|
|
)
|
|
|
|
const availableRoomsInCategory = availableRooms.filter(
|
|
(room) => room.roomType === selectedRoom?.roomType
|
|
)
|
|
if (!selectedRoom) {
|
|
metrics.selectedRoomAvailability.fail.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "not_found",
|
|
error: `Couldn't find selected room with input: ${roomTypeCode}`,
|
|
})
|
|
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) {
|
|
metrics.selectedRoomAvailability.fail.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
error_type: "not_found",
|
|
error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`,
|
|
})
|
|
console.error("No matching rate found")
|
|
return null
|
|
}
|
|
const rates = rateTypes.productType
|
|
|
|
const rateDefinition =
|
|
validateAvailabilityData.data.rateDefinitions.find(
|
|
(rate) => rate.rateCode === rateCode
|
|
)
|
|
|
|
const bedTypes = availableRoomsInCategory
|
|
.map((availRoom) => {
|
|
const matchingRoom = hotelData?.roomCategories
|
|
?.find((room) =>
|
|
room.roomTypes
|
|
.map((roomType) => roomType.code)
|
|
.includes(availRoom.roomTypeCode)
|
|
)
|
|
?.roomTypes.find(
|
|
(roomType) => roomType.code === availRoom.roomTypeCode
|
|
)
|
|
|
|
if (matchingRoom) {
|
|
return {
|
|
description: matchingRoom.description,
|
|
size: matchingRoom.mainBed.widthRange,
|
|
value: matchingRoom.code,
|
|
type: matchingRoom.mainBed.type,
|
|
extraBed: matchingRoom.fixedExtraBed
|
|
? {
|
|
type: matchingRoom.fixedExtraBed.type,
|
|
description: matchingRoom.fixedExtraBed.description,
|
|
}
|
|
: undefined,
|
|
}
|
|
}
|
|
})
|
|
.filter((bed): bed is BedTypeSelection => Boolean(bed))
|
|
|
|
metrics.selectedRoomAvailability.success.add(1, {
|
|
hotelId,
|
|
roomStayStartDate,
|
|
roomStayEndDate,
|
|
adults,
|
|
children,
|
|
bookingCode,
|
|
})
|
|
console.info(
|
|
"api.hotels.selectedRoomAvailability success",
|
|
JSON.stringify({
|
|
query: { hotelId, params: params },
|
|
})
|
|
)
|
|
|
|
return {
|
|
selectedRoom,
|
|
rateDetails: rateDefinition?.generalTerms,
|
|
cancellationText: rateDefinition?.cancellationText ?? "",
|
|
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
|
|
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
|
|
memberRate: rates?.member,
|
|
publicRate: rates.public,
|
|
bedTypes,
|
|
}
|
|
}),
|
|
hotelsByCityWithBookingCode: serviceProcedure
|
|
.input(hotelsAvailabilityInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { lang } = ctx
|
|
const apiLang = toApiLang(lang)
|
|
|
|
metrics.hotelsAvailabilityBookingCode.counter.add(1, {
|
|
...input,
|
|
})
|
|
const bookingCodeAvailabilityResponse =
|
|
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
|
if (!bookingCodeAvailabilityResponse) {
|
|
metrics.hotelsAvailabilityBookingCode.fail.add(1, {
|
|
...input,
|
|
error_type: "unknown",
|
|
})
|
|
return null
|
|
}
|
|
|
|
// Get regular availability of hotels which don't have availability with booking code.
|
|
const unavailableHotelIds =
|
|
bookingCodeAvailabilityResponse?.availability
|
|
.filter((hotel) => {
|
|
return hotel.status === "NotAvailable"
|
|
})
|
|
.flatMap((hotel) => {
|
|
return hotel.hotelId
|
|
})
|
|
|
|
// All hotels have availability with booking code no need to fetch regular prices.
|
|
// return response as is without any filtering as below.
|
|
if (!unavailableHotelIds || !unavailableHotelIds.length) {
|
|
return bookingCodeAvailabilityResponse
|
|
}
|
|
const unavailableHotelsInput = {
|
|
...input,
|
|
bookingCode: "",
|
|
hotelIds: unavailableHotelIds,
|
|
}
|
|
const unavailableHotels = await getHotelsAvailabilityByHotelIds(
|
|
unavailableHotelsInput,
|
|
apiLang,
|
|
ctx.serviceToken
|
|
)
|
|
|
|
metrics.hotelsAvailabilityBookingCode.success.add(1, {
|
|
...input,
|
|
})
|
|
console.info("api.hotels.hotelsAvailabilityBookingCode success")
|
|
|
|
// No regular rates available due to network or API failure (no need to filter & merge).
|
|
if (!unavailableHotels) {
|
|
return bookingCodeAvailabilityResponse
|
|
}
|
|
|
|
// Filtering the response hotels to merge bookingCode rates and regular rates in single response.
|
|
return {
|
|
availability: bookingCodeAvailabilityResponse.availability
|
|
.filter((hotel) => {
|
|
return hotel.status === "Available"
|
|
})
|
|
.concat(unavailableHotels.availability),
|
|
}
|
|
}),
|
|
}),
|
|
rates: router({
|
|
get: publicProcedure.input(ratesInputSchema).query(async () => {
|
|
// 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 = ratesSchema.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
|
|
}),
|
|
}),
|
|
get: serviceProcedure
|
|
.input(hotelInputSchema)
|
|
.query(async ({ ctx, input }) => {
|
|
return getHotel(input, ctx.serviceToken)
|
|
}),
|
|
hotels: router({
|
|
byCountry: router({
|
|
get: contentStackBaseWithServiceProcedure
|
|
.input(getHotelsByCountryInput)
|
|
.query(async ({ ctx, input }) => {
|
|
const { lang, serviceToken } = ctx
|
|
const { country } = input
|
|
|
|
const options: RequestOptionsWithOutBody = {
|
|
// needs to clear default option as only
|
|
// cache or next.revalidate is permitted
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
const hotelIdsParams = new URLSearchParams({
|
|
language: ApiLang.En,
|
|
country,
|
|
})
|
|
const hotelIds = await getHotelIdsByCountry(
|
|
country,
|
|
options,
|
|
hotelIdsParams
|
|
)
|
|
|
|
return await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
|
}),
|
|
}),
|
|
byCityIdentifier: router({
|
|
get: contentStackBaseWithServiceProcedure
|
|
.input(getHotelsByCityIdentifierInput)
|
|
.query(async ({ ctx, input }) => {
|
|
const { lang, serviceToken } = ctx
|
|
const { cityIdentifier } = input
|
|
|
|
const hotelIds = await getHotelIdsByCityIdentifier(
|
|
cityIdentifier,
|
|
serviceToken
|
|
)
|
|
|
|
return await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
|
}),
|
|
}),
|
|
byCSFilter: router({
|
|
get: contentStackBaseWithServiceProcedure
|
|
.input(getHotelsByCSFilterInput)
|
|
.query(async function ({ ctx, input }) {
|
|
const { locationFilter, hotelsToInclude } = input
|
|
|
|
const language = ctx.lang
|
|
const apiLang = toApiLang(language)
|
|
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: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
|
|
let hotelsToFetch: string[] = []
|
|
|
|
metrics.hotels.counter.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: apiLang,
|
|
})
|
|
const locations = await getLocations(
|
|
language,
|
|
options,
|
|
locationsParams,
|
|
null
|
|
)
|
|
if (!locations || "error" in locations) {
|
|
return []
|
|
}
|
|
|
|
const cityId = locations
|
|
.filter(
|
|
(loc): loc is CityLocation =>
|
|
"type" in loc && loc.type === "cities"
|
|
)
|
|
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
|
|
|
|
if (!cityId) {
|
|
metrics.hotels.fail.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: apiLang,
|
|
city: cityId,
|
|
})
|
|
const hotelIds = await getHotelIdsByCityId(
|
|
cityId,
|
|
options,
|
|
hotelIdsParams
|
|
)
|
|
|
|
if (!hotelIds?.length) {
|
|
metrics.hotels.fail.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: ApiLang.En,
|
|
country: locationFilter.country,
|
|
})
|
|
const hotelIds = await getHotelIdsByCountry(
|
|
locationFilter.country,
|
|
options,
|
|
hotelIdsParams
|
|
)
|
|
|
|
if (!hotelIds?.length) {
|
|
metrics.hotels.fail.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) {
|
|
metrics.hotels.fail.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 hotelPages = await getHotelPageUrls(language)
|
|
const hotels = await Promise.all(
|
|
hotelsToFetch.map(async (hotelId) => {
|
|
const hotelData = await getHotel(
|
|
{ hotelId, isCardOnlyPayment: false, language },
|
|
ctx.serviceToken
|
|
)
|
|
const hotelPage = hotelPages.find(
|
|
(page) => page.hotelId === hotelId
|
|
)
|
|
|
|
return hotelData
|
|
? {
|
|
...hotelData,
|
|
url: hotelPage?.url ?? null,
|
|
}
|
|
: null
|
|
})
|
|
)
|
|
|
|
metrics.hotels.success.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 HotelDataWithUrl => !!hotel)
|
|
}),
|
|
}),
|
|
getAllHotels: router({
|
|
get: serviceProcedure.query(async function ({ ctx }) {
|
|
const apiLang = toApiLang(ctx.lang)
|
|
const params = new URLSearchParams({
|
|
language: apiLang,
|
|
})
|
|
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: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
const countries = await getCountries(options, params, ctx.lang)
|
|
if (!countries) {
|
|
return null
|
|
}
|
|
const countryNames = countries.data.map((country) => country.name)
|
|
const hotelData: HotelDataWithUrl[] = (
|
|
await Promise.all(
|
|
countryNames.map(async (country) => {
|
|
const countryParams = new URLSearchParams({
|
|
country: country,
|
|
})
|
|
const hotelIds = await getHotelIdsByCountry(
|
|
country,
|
|
options,
|
|
countryParams
|
|
)
|
|
|
|
const hotels = await getHotelsByHotelIds(
|
|
hotelIds,
|
|
ctx.lang,
|
|
ctx.serviceToken
|
|
)
|
|
return hotels
|
|
})
|
|
)
|
|
).flat()
|
|
return hotelData
|
|
}),
|
|
}),
|
|
}),
|
|
nearbyHotelIds: serviceProcedure
|
|
.input(nearbyHotelIdsInput)
|
|
.query(async function ({ ctx, input }) {
|
|
const { lang } = ctx
|
|
const apiLang = toApiLang(lang)
|
|
|
|
const { hotelId } = input
|
|
const params: Record<string, string | number> = {
|
|
language: apiLang,
|
|
}
|
|
metrics.nearbyHotelIds.counter.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.nearbyHotelIds start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.nearbyHotelIds.fail.add(1, {
|
|
hotelId,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.nearbyHotelIds error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
const apiJson = await apiResponse.json()
|
|
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
|
|
if (!validateHotelData.success) {
|
|
metrics.nearbyHotelIds.fail.add(1, {
|
|
hotelId,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validateHotelData.error),
|
|
})
|
|
console.error(
|
|
"api.hotels.nearbyHotelIds validation error",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
error: validateHotelData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
metrics.nearbyHotelIds.success.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.nearbyHotelIds success",
|
|
JSON.stringify({
|
|
query: { hotelId, params },
|
|
})
|
|
)
|
|
|
|
return validateHotelData.data.map((id: string) => parseInt(id, 10))
|
|
}),
|
|
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: env.CACHE_TIME_HOTELS,
|
|
},
|
|
}
|
|
|
|
const countries = await getCountries(options, searchParams, ctx.lang)
|
|
if (!countries) {
|
|
return null
|
|
}
|
|
const countryNames = countries.data.map((country) => country.name)
|
|
const citiesByCountry = await getCitiesByCountry(
|
|
countryNames,
|
|
options,
|
|
searchParams,
|
|
ctx.lang
|
|
)
|
|
|
|
const locations = await getLocations(
|
|
ctx.lang,
|
|
options,
|
|
searchParams,
|
|
citiesByCountry
|
|
)
|
|
|
|
if (Array.isArray(locations)) {
|
|
return {
|
|
data: locations,
|
|
}
|
|
}
|
|
|
|
return locations
|
|
}),
|
|
}),
|
|
map: router({
|
|
city: serviceProcedure
|
|
.input(cityCoordinatesInputSchema)
|
|
.query(async function ({ input }) {
|
|
const apiKey = process.env.GOOGLE_STATIC_MAP_KEY
|
|
const { city, hotel } = input
|
|
|
|
async function fetchCoordinates(address: string) {
|
|
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
|
|
const response = await fetch(url)
|
|
const data = await response.json()
|
|
|
|
if (data.status !== "OK") {
|
|
console.error(`Geocode error: ${data.status}`)
|
|
return null
|
|
}
|
|
|
|
const location = data.results[0]?.geometry?.location
|
|
if (!location) {
|
|
console.error("No location found in geocode response")
|
|
return null
|
|
}
|
|
|
|
return location
|
|
}
|
|
|
|
let location = await fetchCoordinates(city)
|
|
if (!location) {
|
|
location = await fetchCoordinates(`${city}, ${hotel.address}`)
|
|
}
|
|
|
|
if (!location) {
|
|
throw new Error("Unable to fetch coordinates")
|
|
}
|
|
|
|
return location
|
|
}),
|
|
}),
|
|
meetingRooms: safeProtectedServiceProcedure
|
|
.input(getMeetingRoomsInputSchema)
|
|
.query(async function ({ ctx, input }) {
|
|
const { hotelId, language } = input
|
|
|
|
const params: Record<string, string | string[]> = {
|
|
hotelId,
|
|
language,
|
|
}
|
|
const metricsData = { ...params, hotelId: input.hotelId }
|
|
metrics.meetingRooms.counter.add(1, metricsData)
|
|
console.info(
|
|
"api.hotels.meetingRooms start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Hotel.Hotels.meetingRooms(input.hotelId),
|
|
{
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.meetingRooms.fail.add(1, {
|
|
...metricsData,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.meetingRooms error",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return []
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validatedMeetingRooms = meetingRoomsSchema.safeParse(apiJson)
|
|
|
|
if (!validatedMeetingRooms.success) {
|
|
console.error(
|
|
"api.hotels.meetingRooms validation error",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: validatedMeetingRooms.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
metrics.meetingRooms.success.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.meetingRooms success",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
|
|
return validatedMeetingRooms.data.data
|
|
}),
|
|
additionalData: safeProtectedServiceProcedure
|
|
.input(getAdditionalDataInputSchema)
|
|
.query(async function ({ ctx, input }) {
|
|
const { hotelId, language } = input
|
|
|
|
const params: Record<string, string | string[]> = {
|
|
hotelId,
|
|
language,
|
|
}
|
|
const metricsData = { ...params, hotelId: input.hotelId }
|
|
metrics.additionalData.counter.add(1, metricsData)
|
|
console.info(
|
|
"api.hotels.additionalData start",
|
|
JSON.stringify({ query: { hotelId, params } })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId),
|
|
{
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: env.CACHE_TIME_HOTELS,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.additionalData.fail.add(1, {
|
|
...metricsData,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.hotels.additionalData error",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validatedAdditionalData = additionalDataSchema.safeParse(apiJson)
|
|
|
|
if (!validatedAdditionalData.success) {
|
|
console.error(
|
|
"api.hotels.additionalData validation error",
|
|
JSON.stringify({
|
|
query: { params },
|
|
error: validatedAdditionalData.error,
|
|
})
|
|
)
|
|
throw badRequestError()
|
|
}
|
|
metrics.additionalData.success.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.additionalData success",
|
|
JSON.stringify({ query: { params } })
|
|
)
|
|
|
|
return validatedAdditionalData.data
|
|
}),
|
|
packages: router({
|
|
get: serviceProcedure
|
|
.input(roomPackagesInputSchema)
|
|
.query(async ({ input, ctx }) => {
|
|
const { hotelId, startDate, endDate, adults, children, packageCodes } =
|
|
input
|
|
|
|
const { lang } = input
|
|
|
|
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()
|
|
|
|
metrics.packages.counter.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}`,
|
|
},
|
|
},
|
|
searchParams
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
metrics.packages.fail.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 } })
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const validatedPackagesData = packagesSchema.safeParse(apiJson)
|
|
if (!validatedPackagesData.success) {
|
|
metrics.packages.fail.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,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
metrics.packages.success.add(1, {
|
|
hotelId,
|
|
})
|
|
console.info(
|
|
"api.hotels.packages success",
|
|
JSON.stringify({ query: { hotelId, params: params } })
|
|
)
|
|
|
|
return validatedPackagesData.data
|
|
}),
|
|
breakfast: safeProtectedServiceProcedure
|
|
.input(breakfastPackageInputSchema)
|
|
.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 }
|
|
metrics.breakfastPackage.counter.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()
|
|
metrics.breakfastPackage.fail.add(1, {
|
|
...metricsData,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.package.breakfast 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) {
|
|
metrics.breakfastPackage.fail.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
|
|
}
|
|
|
|
metrics.breakfastPackage.success.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
|
|
)
|
|
}),
|
|
ancillary: safeProtectedServiceProcedure
|
|
.input(ancillaryPackageInputSchema)
|
|
.query(async function ({ ctx, input }) {
|
|
const { lang } = ctx
|
|
|
|
const apiLang = toApiLang(lang)
|
|
const params = {
|
|
EndDate: dt(input.toDate).format("YYYY-MM-DD"),
|
|
StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
|
|
language: apiLang,
|
|
}
|
|
|
|
const metricsData = { ...params, hotelId: input.hotelId }
|
|
metrics.ancillaryPackage.counter.add(1, metricsData)
|
|
console.info(
|
|
"api.package.ancillary start",
|
|
JSON.stringify({ query: metricsData })
|
|
)
|
|
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Package.Ancillary.hotel(input.hotelId),
|
|
{
|
|
cache: undefined,
|
|
headers: {
|
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
|
},
|
|
next: {
|
|
revalidate: 60,
|
|
},
|
|
},
|
|
params
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
metrics.ancillaryPackage.fail.add(1, {
|
|
...metricsData,
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.package.ancillary start error",
|
|
JSON.stringify({
|
|
query: metricsData,
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
const apiJson = await apiResponse.json()
|
|
const ancillaryPackages = ancillaryPackagesSchema.safeParse(apiJson)
|
|
if (!ancillaryPackages.success) {
|
|
metrics.ancillaryPackage.fail.add(1, {
|
|
...metricsData,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(ancillaryPackages.error),
|
|
})
|
|
console.error(
|
|
"api.package.ancillary validation error",
|
|
JSON.stringify({
|
|
query: metricsData,
|
|
error: ancillaryPackages.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
metrics.ancillaryPackage.success.add(1, metricsData)
|
|
console.info(
|
|
"api.package.ancillary success",
|
|
JSON.stringify({
|
|
query: metricsData,
|
|
})
|
|
)
|
|
return ancillaryPackages.data
|
|
}),
|
|
}),
|
|
})
|