feat: bedtypes is selectable again
This commit is contained in:
committed by
Michael Zetterberg
parent
f62723c6e5
commit
afb37d0cc5
@@ -4,6 +4,7 @@ import { Lang } from "@/constants/languages"
|
||||
|
||||
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { Country } from "@/types/enums/country"
|
||||
|
||||
export const hotelsAvailabilityInputSchema = z.object({
|
||||
@@ -25,55 +26,88 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
|
||||
bookingCode: z.string().optional().default(""),
|
||||
})
|
||||
|
||||
export const roomsCombinedAvailabilityInputSchema = z.object({
|
||||
adultsCount: z.array(z.number()),
|
||||
const childrenInRoomSchema = z
|
||||
.array(
|
||||
z.object({
|
||||
age: z.number(),
|
||||
bed: z.nativeEnum(ChildBedMapEnum),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
|
||||
const baseRoomSchema = z.object({
|
||||
adults: z.number().int().min(1),
|
||||
bookingCode: z.string().optional(),
|
||||
childArray: z
|
||||
.array(
|
||||
z
|
||||
.array(
|
||||
z.object({
|
||||
age: z.number(),
|
||||
bed: z.nativeEnum(ChildBedMapEnum),
|
||||
})
|
||||
)
|
||||
.nullable()
|
||||
)
|
||||
.nullish(),
|
||||
hotelId: z.string(),
|
||||
lang: z.nativeEnum(Lang),
|
||||
rateCode: z.string().optional(),
|
||||
roomStayEndDate: z.string(),
|
||||
roomStayStartDate: z.string(),
|
||||
redemption: z.boolean().optional().default(false),
|
||||
roomFeatureCodesArray: z
|
||||
.array(z.array(z.nativeEnum(RoomPackageCodeEnum)).nullable())
|
||||
childrenInRoom: childrenInRoomSchema,
|
||||
packages: z
|
||||
.array(z.nativeEnum({ ...BreakfastPackageEnum, ...RoomPackageCodeEnum }))
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const selectedRoomAvailabilityInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
roomStayStartDate: z.string(),
|
||||
roomStayEndDate: z.string(),
|
||||
adults: z.number(),
|
||||
children: z.string().optional(),
|
||||
bookingCode: z.string().optional(),
|
||||
const selectedRoomSchema = z.object({
|
||||
counterRateCode: z.string().optional(),
|
||||
rateCode: z.string(),
|
||||
roomTypeCode: z.string(),
|
||||
counterRateCode: z.string().optional(),
|
||||
packageCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
redemption: z.boolean().optional(),
|
||||
})
|
||||
|
||||
export type GetSelectedRoomAvailabilityInput = z.input<
|
||||
typeof selectedRoomAvailabilityInputSchema
|
||||
>
|
||||
|
||||
export const ratesInputSchema = z.object({
|
||||
const baseBookingSchema = z.object({
|
||||
bookingCode: z.string().optional(),
|
||||
fromDate: z.string(),
|
||||
hotelId: z.string(),
|
||||
searchType: z.string().optional(),
|
||||
toDate: z.string(),
|
||||
})
|
||||
|
||||
export const selectRateRoomsAvailabilityInputSchema = z.object({
|
||||
booking: baseBookingSchema.extend({
|
||||
rooms: z.array(
|
||||
baseRoomSchema.extend({
|
||||
rateCode: z.string().optional(),
|
||||
roomTypeCode: z.string().optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
export const selectRateRoomAvailabilityInputSchema = z.object({
|
||||
booking: baseBookingSchema.extend({
|
||||
room: baseRoomSchema.extend({
|
||||
rateCode: z.string().optional(),
|
||||
roomTypeCode: z.string().optional(),
|
||||
}),
|
||||
}),
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
export const enterDetailsRoomsAvailabilityInputSchema = z.object({
|
||||
booking: baseBookingSchema.extend({
|
||||
rooms: z.array(baseRoomSchema.merge(selectedRoomSchema)),
|
||||
}),
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
export const myStayRoomAvailabilityInputSchema = z.object({
|
||||
booking: baseBookingSchema.extend({
|
||||
room: baseRoomSchema.merge(selectedRoomSchema),
|
||||
}),
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
export const roomFeaturesInputSchema = z.object({
|
||||
adults: z.number(),
|
||||
childrenInRoom: childrenInRoomSchema,
|
||||
endDate: z.string(),
|
||||
hotelId: z.string(),
|
||||
lang: z.nativeEnum(Lang),
|
||||
roomFeatureCodes: z
|
||||
.array(z.nativeEnum({ ...BreakfastPackageEnum, ...RoomPackageCodeEnum }))
|
||||
.optional(),
|
||||
startDate: z.string(),
|
||||
})
|
||||
|
||||
export type RoomFeaturesInput = z.input<typeof roomFeaturesInputSchema>
|
||||
|
||||
export const hotelInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
isCardOnlyPayment: z.boolean(),
|
||||
@@ -91,7 +125,7 @@ export const getHotelsByCSFilterInput = z.object({
|
||||
hotelsToInclude: z.array(z.string()),
|
||||
})
|
||||
export interface GetHotelsByCSFilterInput
|
||||
extends z.infer<typeof getHotelsByCSFilterInput> {}
|
||||
extends z.infer<typeof getHotelsByCSFilterInput> { }
|
||||
|
||||
export const nearbyHotelIdsInput = z.object({
|
||||
hotelId: z.string(),
|
||||
@@ -127,13 +161,13 @@ export const ancillaryPackageInputSchema = z.object({
|
||||
})
|
||||
|
||||
export const roomPackagesInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
adults: z.number(),
|
||||
children: z.number().optional().default(0),
|
||||
packageCodes: z.array(z.string()).optional().default([]),
|
||||
endDate: z.string(),
|
||||
hotelId: z.string(),
|
||||
lang: z.nativeEnum(Lang),
|
||||
packageCodes: z.array(z.string()).optional().default([]),
|
||||
startDate: z.string(),
|
||||
})
|
||||
export const cityCoordinatesInputSchema = z.object({
|
||||
city: z.string(),
|
||||
@@ -167,22 +201,3 @@ export const getLocationsInput = z.object({
|
||||
export const getLocationsUrlsInput = z.object({
|
||||
lang: z.nativeEnum(Lang),
|
||||
})
|
||||
|
||||
export const roomFeaturesInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
startDate: z.string(),
|
||||
endDate: z.string(),
|
||||
adults: z.number(),
|
||||
childrenInRoom: z
|
||||
.array(
|
||||
z.object({
|
||||
age: z.number(),
|
||||
bed: z.nativeEnum(ChildBedMapEnum),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
roomFeatureCodes: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(),
|
||||
roomIndex: z.number().optional(),
|
||||
})
|
||||
|
||||
export type RoomFeaturesInput = z.input<typeof roomFeaturesInputSchema>
|
||||
|
||||
@@ -70,14 +70,10 @@ export const metrics = {
|
||||
fail: meter.createCounter("trpc.hotel.packages.get-fail"),
|
||||
success: meter.createCounter("trpc.hotel.packages.get-success"),
|
||||
},
|
||||
roomsCombinedAvailability: {
|
||||
counter: meter.createCounter("trpc.hotel.roomsCombinedAvailability.rooms"),
|
||||
fail: meter.createCounter(
|
||||
"trpc.hotel.roomsCombinedAvailability.rooms-fail"
|
||||
),
|
||||
success: meter.createCounter(
|
||||
"trpc.hotel.roomsCombinedAvailability.rooms-success"
|
||||
),
|
||||
roomsAvailability: {
|
||||
counter: meter.createCounter("trpc.hotel.roomsAvailability.rooms"),
|
||||
fail: meter.createCounter("trpc.hotel.roomsAvailability.rooms-fail"),
|
||||
success: meter.createCounter("trpc.hotel.roomsAvailability.rooms-success"),
|
||||
},
|
||||
selectedRoomAvailability: {
|
||||
counter: meter.createCounter("trpc.hotel.availability.room"),
|
||||
|
||||
@@ -23,10 +23,10 @@ import {
|
||||
breakfastPackageSchema,
|
||||
packageSchema,
|
||||
} from "./schemas/packages"
|
||||
import { rateSchema } from "./schemas/rate"
|
||||
import { relationshipsSchema } from "./schemas/relationships"
|
||||
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
|
||||
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
|
||||
import { sortRoomConfigs } from "./utils"
|
||||
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
@@ -42,7 +42,6 @@ import type {
|
||||
import type {
|
||||
Product,
|
||||
RateDefinition,
|
||||
RoomConfiguration,
|
||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
|
||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||
@@ -136,18 +135,6 @@ const cancellationRules = {
|
||||
NotCancellable: 0,
|
||||
} as const
|
||||
|
||||
// Used to ensure `Available` rooms
|
||||
// are shown before all `NotAvailable`
|
||||
const statusLookup = {
|
||||
[AvailabilityEnum.Available]: 1,
|
||||
[AvailabilityEnum.NotAvailable]: 2,
|
||||
}
|
||||
|
||||
function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
||||
// @ts-expect-error - array indexing
|
||||
return statusLookup[a.status] - statusLookup[b.status]
|
||||
}
|
||||
|
||||
export const roomsAvailabilitySchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
@@ -158,48 +145,10 @@ export const roomsAvailabilitySchema = z
|
||||
hotelId: z.number(),
|
||||
mustBeGuaranteed: z.boolean().optional(),
|
||||
occupancy: occupancySchema.optional(),
|
||||
packages: z.array(packageSchema).optional().default([]),
|
||||
rateDefinitions: z.array(rateDefinitionSchema),
|
||||
roomConfigurations: z
|
||||
.array(roomConfigurationSchema)
|
||||
.transform((data) => {
|
||||
// Initial sort to guarantee if one bed is NotAvailable and whereas
|
||||
// the other is Available to make sure data is added to the correct
|
||||
// roomConfig
|
||||
const configs = data.sort(sortRoomConfigs)
|
||||
const roomConfigs = new Map<string, RoomConfiguration>()
|
||||
for (const roomConfig of configs) {
|
||||
if (roomConfigs.has(roomConfig.roomType)) {
|
||||
const currentRoomConf = roomConfigs.get(roomConfig.roomType)
|
||||
if (currentRoomConf) {
|
||||
currentRoomConf.features = roomConfig.features.reduce(
|
||||
(feats, feature) => {
|
||||
const currentFeatureIndex = feats.findIndex(
|
||||
(f) => f.code === feature.code
|
||||
)
|
||||
if (currentFeatureIndex !== -1) {
|
||||
feats[currentFeatureIndex].inventory =
|
||||
feats[currentFeatureIndex].inventory +
|
||||
feature.inventory
|
||||
} else {
|
||||
feats.push(feature)
|
||||
}
|
||||
return feats
|
||||
},
|
||||
currentRoomConf.features
|
||||
)
|
||||
currentRoomConf.roomsLeft =
|
||||
currentRoomConf.roomsLeft + roomConfig.roomsLeft
|
||||
roomConfigs.set(currentRoomConf.roomType, currentRoomConf)
|
||||
}
|
||||
} else {
|
||||
roomConfigs.set(roomConfig.roomType, roomConfig)
|
||||
}
|
||||
}
|
||||
return Array.from(roomConfigs.values())
|
||||
}),
|
||||
roomConfigurations: z.array(roomConfigurationSchema),
|
||||
}),
|
||||
relationships: relationshipsSchema.optional(),
|
||||
type: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
.transform(({ data: { attributes } }) => {
|
||||
@@ -425,8 +374,6 @@ export const roomsAvailabilitySchema = z
|
||||
}
|
||||
})
|
||||
|
||||
export const ratesSchema = z.array(rateSchema)
|
||||
|
||||
export const citiesByCountrySchema = z.object({
|
||||
data: z.array(
|
||||
citySchema.transform((data) => {
|
||||
@@ -597,17 +544,6 @@ export const packagesSchema = z
|
||||
hotelId: z.number(),
|
||||
packages: z.array(packageSchema).default([]),
|
||||
}),
|
||||
relationships: z
|
||||
.object({
|
||||
links: z.array(
|
||||
z.object({
|
||||
type: z.string(),
|
||||
url: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
type: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const flexibilityPrice = z.object({
|
||||
member: z.number(),
|
||||
standard: z.number(),
|
||||
})
|
||||
|
||||
export const rateSchema = z.object({
|
||||
breakfastIncluded: z.boolean(),
|
||||
description: z.string(),
|
||||
id: z.number(),
|
||||
imageSrc: z.string(),
|
||||
name: z.string(),
|
||||
prices: z.object({
|
||||
currency: z.string(),
|
||||
freeCancellation: flexibilityPrice,
|
||||
freeRebooking: flexibilityPrice,
|
||||
nonRefundable: flexibilityPrice,
|
||||
}),
|
||||
size: z.string(),
|
||||
})
|
||||
@@ -1,104 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Cabin",
|
||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
||||
"size": "17 - 24 m² (1 - 2 persons)",
|
||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
||||
"breakfastIncluded": false,
|
||||
"prices": {
|
||||
"currency": "SEK",
|
||||
"nonRefundable": {
|
||||
"standard": 2315,
|
||||
"member": 2247
|
||||
},
|
||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "Standard",
|
||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
||||
"size": "19 - 30 m² (1 - 2 persons)",
|
||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
||||
"breakfastIncluded": false,
|
||||
"prices": {
|
||||
"currency": "SEK",
|
||||
"nonRefundable": {
|
||||
"standard": 2315,
|
||||
"member": 2247
|
||||
},
|
||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "Superior",
|
||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
||||
"size": "22 - 40 m² (1 - 3 persons)",
|
||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
||||
"breakfastIncluded": false,
|
||||
"prices": {
|
||||
"currency": "SEK",
|
||||
"nonRefundable": {
|
||||
"standard": 2315,
|
||||
"member": 2247
|
||||
},
|
||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "Superior Family",
|
||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
||||
"size": "29 - 49 m² (3 - 4 persons)",
|
||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
||||
"breakfastIncluded": false,
|
||||
"prices": {
|
||||
"currency": "SEK",
|
||||
"nonRefundable": {
|
||||
"standard": 2315,
|
||||
"member": 2247
|
||||
},
|
||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "Superior PLUS",
|
||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
||||
"size": "21 - 28 m² (2 - 3 persons)",
|
||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
||||
"breakfastIncluded": false,
|
||||
"prices": {
|
||||
"currency": "SEK",
|
||||
"nonRefundable": {
|
||||
"standard": 2315,
|
||||
"member": 2247
|
||||
},
|
||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"name": "Junior Suite",
|
||||
"description": "Stylish, peaceful and air-conditioned room. The rooms have small clerestory windows.",
|
||||
"size": "35 - 43 m² (2 - 4 persons)",
|
||||
"imageSrc": "https://www.scandichotels.se/imageVault/publishedmedia/xnmqnmz6mz0uhuat0917/scandic-helsinki-hub-room-standard-KR-7.jpg",
|
||||
"breakfastIncluded": false,
|
||||
"prices": {
|
||||
"currency": "SEK",
|
||||
"nonRefundable": {
|
||||
"standard": 2315,
|
||||
"member": 2247
|
||||
},
|
||||
"freeRebooking": { "standard": 2437, "member": 2365 },
|
||||
"freeCancellation": { "standard": 2620, "member": 2542 }
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -1,14 +1,19 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import stringify from "json-stable-stringify-without-jsonify"
|
||||
|
||||
import { REDEMPTION } from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError } from "@/server/errors/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { generateChildrenString } from "@/components/HotelReservation/utils"
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { cache } from "@/utils/cache"
|
||||
|
||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||
import { type RoomFeaturesInput, roomPackagesInputSchema } from "./input"
|
||||
import { metrics } from "./metrics"
|
||||
import {
|
||||
type Cities,
|
||||
@@ -16,15 +21,31 @@ import {
|
||||
citiesSchema,
|
||||
countriesSchema,
|
||||
getHotelIdsSchema,
|
||||
hotelsAvailabilitySchema,
|
||||
hotelSchema,
|
||||
locationsSchema,
|
||||
packagesSchema,
|
||||
roomFeaturesSchema,
|
||||
roomsAvailabilitySchema,
|
||||
} from "./output"
|
||||
import { getHotel } from "./query"
|
||||
|
||||
import type { z } from "zod"
|
||||
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { DestinationPagesHotelData } from "@/types/hotel"
|
||||
import type {
|
||||
DestinationPagesHotelData,
|
||||
Room as RoomCategory,
|
||||
} from "@/types/hotel"
|
||||
import type { PackagesInput } from "@/types/requests/packages"
|
||||
import type {
|
||||
HotelsAvailabilityInputSchema,
|
||||
HotelsByHotelIdsAvailabilityInputSchema,
|
||||
RoomsAvailabilityInputRoom,
|
||||
RoomsAvailabilityInputSchema,
|
||||
} from "@/types/trpc/routers/hotel/availability"
|
||||
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||
import type {
|
||||
CitiesGroupedByCountry,
|
||||
CityLocation,
|
||||
@@ -34,9 +55,9 @@ import type {
|
||||
Products,
|
||||
RateDefinition,
|
||||
RedemptionsProduct,
|
||||
RoomConfiguration,
|
||||
} from "@/types/trpc/routers/hotel/roomAvailability"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
import type { selectedRoomAvailabilityInputSchema } from "./input"
|
||||
|
||||
export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||
if (!category) return PointOfInterestGroupEnum.LOCATION
|
||||
@@ -608,51 +629,184 @@ function findProduct(product: Products, rateDefinition: RateDefinition) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSelectedRoomAvailability(
|
||||
input: z.input<typeof selectedRoomAvailabilityInputSchema>,
|
||||
lang: string,
|
||||
serviceToken: string,
|
||||
userPoints?: number
|
||||
export const getHotel = cache(
|
||||
async (input: HotelInput, serviceToken: string) => {
|
||||
const callable = 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}`,
|
||||
},
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`,
|
||||
async () => {
|
||||
return callable(input.hotelId, input.language, input.isCardOnlyPayment)
|
||||
},
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export async function getHotelsAvailabilityByCity(
|
||||
input: HotelsAvailabilityInputSchema,
|
||||
apiLang: string,
|
||||
token: string, // Either service token or user access token in case of redemption search
|
||||
userPoints: number = 0
|
||||
) {
|
||||
const {
|
||||
adults,
|
||||
bookingCode,
|
||||
children,
|
||||
hotelId,
|
||||
roomStayEndDate,
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
} = input
|
||||
|
||||
const params: Record<string, string | number | undefined> = {
|
||||
const params: Record<string, string | number> = {
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
...(children && { children }),
|
||||
...(bookingCode && { bookingCode }),
|
||||
...(redemption && { isRedemption: "true" }),
|
||||
language: lang,
|
||||
...(redemption ? { isRedemption: "true" } : {}),
|
||||
language: apiLang,
|
||||
}
|
||||
|
||||
metrics.selectedRoomAvailability.counter.add(1, input)
|
||||
metrics.hotelsAvailability.counter.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.selectedRoomAvailability start",
|
||||
JSON.stringify({ query: { hotelId: input.hotelId, params } })
|
||||
"api.hotels.hotelsAvailability start",
|
||||
JSON.stringify({ query: { cityId, params } })
|
||||
)
|
||||
const apiResponseAvailability = await api.get(
|
||||
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.city(cityId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponseAvailability.ok) {
|
||||
const text = await apiResponseAvailability.text()
|
||||
metrics.selectedRoomAvailability.fail.add(1, {
|
||||
hotelId,
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.hotelsAvailability.fail.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
@@ -660,61 +814,611 @@ export async function getSelectedRoomAvailability(
|
||||
bookingCode,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponseAvailability.status,
|
||||
statusText: apiResponseAvailability.statusText,
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.selectedRoomAvailability error",
|
||||
"api.hotels.hotelsAvailability error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
query: { cityId, params },
|
||||
error: {
|
||||
status: apiResponseAvailability.status,
|
||||
statusText: apiResponseAvailability.statusText,
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw new Error("Failed to fetch selected room availability")
|
||||
throw new Error("Failed to fetch hotels availability by city")
|
||||
}
|
||||
const apiJsonAvailability = await apiResponseAvailability.json()
|
||||
const validateAvailabilityData =
|
||||
roomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||
if (!validateAvailabilityData.success) {
|
||||
metrics.selectedRoomAvailability.fail.add(1, {
|
||||
hotelId,
|
||||
metrics.hotelsAvailability.fail.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateAvailabilityData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.selectedRoomAvailability validation error",
|
||||
"api.hotels.hotelsAvailability validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
query: { cityId, params },
|
||||
error: validateAvailabilityData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const { rateDefinitions, roomConfigurations } = validateAvailabilityData.data
|
||||
|
||||
const rateDefinition = rateDefinitions.find(
|
||||
(rd) => rd.rateCode === input.rateCode
|
||||
metrics.hotelsAvailability.success.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.hotelsAvailability success",
|
||||
JSON.stringify({
|
||||
query: { cityId, params: params },
|
||||
})
|
||||
)
|
||||
if (redemption) {
|
||||
validateAvailabilityData.data.data.forEach((data) => {
|
||||
data.attributes.productType?.redemptions?.forEach((r) => {
|
||||
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
availability: validateAvailabilityData.data.data.flatMap(
|
||||
(hotels) => hotels.attributes
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export async function getHotelsAvailabilityByHotelIds(
|
||||
input: HotelsByHotelIdsAvailabilityInputSchema,
|
||||
apiLang: string,
|
||||
serviceToken: string
|
||||
) {
|
||||
const {
|
||||
hotelIds,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
} = input
|
||||
|
||||
const params = new URLSearchParams([
|
||||
["roomStayStartDate", roomStayStartDate],
|
||||
["roomStayEndDate", roomStayEndDate],
|
||||
["adults", adults.toString()],
|
||||
["children", children ?? ""],
|
||||
["bookingCode", bookingCode],
|
||||
["language", apiLang],
|
||||
])
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
`${apiLang}:hotels:availability:${hotelIds.join(",")}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||
async () => {
|
||||
/**
|
||||
* 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.)
|
||||
**/
|
||||
|
||||
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(),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
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,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw new Error("Failed to fetch hotels availability by hotelIds")
|
||||
}
|
||||
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
|
||||
),
|
||||
}
|
||||
},
|
||||
env.CACHE_TIME_CITY_SEARCH
|
||||
)
|
||||
}
|
||||
|
||||
async function getRoomFeaturesInventory(
|
||||
input: RoomFeaturesInput,
|
||||
token: string
|
||||
) {
|
||||
const {
|
||||
adults,
|
||||
childrenInRoom,
|
||||
endDate,
|
||||
hotelId,
|
||||
roomFeatureCodes,
|
||||
startDate,
|
||||
} = input
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
stringify(input),
|
||||
async function () {
|
||||
const params = {
|
||||
adults,
|
||||
hotelId,
|
||||
roomFeatureCode: roomFeatureCodes,
|
||||
roomStayEndDate: endDate,
|
||||
roomStayStartDate: startDate,
|
||||
...(childrenInRoom?.length && {
|
||||
children: generateChildrenString(childrenInRoom),
|
||||
}),
|
||||
}
|
||||
|
||||
metrics.roomFeatures.counter.add(1, params)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.roomFeatures(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = apiResponse.text()
|
||||
console.error(
|
||||
"api.availability.roomfeature error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
metrics.roomFeatures.fail.add(1, params)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data)
|
||||
if (!validatedRoomFeaturesData.success) {
|
||||
console.error(
|
||||
"api.availability.roomfeature error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedRoomFeaturesData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
metrics.roomFeatures.success.add(1, params)
|
||||
|
||||
return validatedRoomFeaturesData.data
|
||||
},
|
||||
"5m"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getPackages(
|
||||
rawInput: PackagesInput,
|
||||
serviceToken: string
|
||||
) {
|
||||
const parsedInput = roomPackagesInputSchema.safeParse(rawInput)
|
||||
if (!parsedInput.success) {
|
||||
console.info(`Failed to parse input for Get Packages`)
|
||||
console.error(parsedInput.error)
|
||||
return null
|
||||
}
|
||||
const input = parsedInput.data
|
||||
const cacheClient = await getCacheClient()
|
||||
return cacheClient.cacheOrGet(
|
||||
stringify(input),
|
||||
async function () {
|
||||
const {
|
||||
adults,
|
||||
children,
|
||||
endDate,
|
||||
hotelId,
|
||||
lang,
|
||||
packageCodes,
|
||||
startDate,
|
||||
} = input
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
adults: adults.toString(),
|
||||
children: children.toString(),
|
||||
endDate,
|
||||
language: apiLang,
|
||||
startDate,
|
||||
})
|
||||
|
||||
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 ${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
|
||||
},
|
||||
"3h"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getRoomsAvailability(
|
||||
input: RoomsAvailabilityInputSchema,
|
||||
token: string,
|
||||
serviceToken: string,
|
||||
userPoints: number | undefined
|
||||
) {
|
||||
const {
|
||||
booking: { bookingCode, fromDate, hotelId, rooms, searchType, toDate },
|
||||
lang,
|
||||
} = input
|
||||
|
||||
const redemption = searchType === REDEMPTION
|
||||
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const kids = rooms
|
||||
.map((r) => r.childrenInRoom)
|
||||
.filter(Boolean)
|
||||
.map((kid) => JSON.stringify(kid))
|
||||
const metricsData = {
|
||||
adultsCount: rooms.map((r) => r.adults),
|
||||
bookingCode,
|
||||
childArray: kids.length ? kids : undefined,
|
||||
hotelId,
|
||||
roomStayEndDate: toDate,
|
||||
roomStayStartDate: fromDate,
|
||||
}
|
||||
|
||||
metrics.roomsAvailability.counter.add(1, metricsData)
|
||||
|
||||
console.info(
|
||||
"api.hotels.roomsAvailability start",
|
||||
JSON.stringify({ query: { hotelId, params: metricsData } })
|
||||
)
|
||||
|
||||
const baseCacheKey = {
|
||||
bookingCode,
|
||||
fromDate,
|
||||
hotelId,
|
||||
lang,
|
||||
searchType,
|
||||
toDate,
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
const availabilityResponses = await Promise.allSettled(
|
||||
rooms.map(async (room: RoomsAvailabilityInputRoom) => {
|
||||
const cacheKey = {
|
||||
...baseCacheKey,
|
||||
room,
|
||||
}
|
||||
return await cacheClient.cacheOrGet(
|
||||
stringify(cacheKey),
|
||||
async function () {
|
||||
{
|
||||
const params = {
|
||||
adults: room.adults,
|
||||
language: apiLang,
|
||||
roomStayStartDate: fromDate,
|
||||
roomStayEndDate: toDate,
|
||||
...(room.childrenInRoom?.length && {
|
||||
children: generateChildrenString(room.childrenInRoom),
|
||||
}),
|
||||
...(room.bookingCode && { bookingCode: room.bookingCode }),
|
||||
...(redemption && { isRedemption: "true" }),
|
||||
}
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Availability.hotel(hotelId),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: 60,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
metrics.roomsAvailability.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.roomsAvailability.fail.add(1, metricsData)
|
||||
return {
|
||||
error: "validation_error",
|
||||
details: validateAvailabilityData.error,
|
||||
}
|
||||
}
|
||||
|
||||
if (redemption) {
|
||||
for (const roomConfig of validateAvailabilityData.data
|
||||
.roomConfigurations) {
|
||||
for (const product of roomConfig.redemptions) {
|
||||
if (userPoints) {
|
||||
product.redemption.hasEnoughPoints =
|
||||
userPoints >= product.redemption.localPrice.pointsPerStay
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roomFeatures = await getPackages(
|
||||
{
|
||||
adults: room.adults,
|
||||
children: room.childrenInRoom?.length,
|
||||
endDate: input.booking.toDate,
|
||||
hotelId: input.booking.hotelId,
|
||||
lang,
|
||||
packageCodes: [
|
||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
RoomPackageCodeEnum.PET_ROOM,
|
||||
],
|
||||
startDate: input.booking.fromDate,
|
||||
},
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (roomFeatures) {
|
||||
validateAvailabilityData.data.packages = roomFeatures
|
||||
}
|
||||
|
||||
// Fetch packages
|
||||
if (room.packages?.length) {
|
||||
const roomFeaturesInventory = await getRoomFeaturesInventory(
|
||||
{
|
||||
adults: room.adults,
|
||||
childrenInRoom: room.childrenInRoom,
|
||||
endDate: input.booking.toDate,
|
||||
hotelId: input.booking.hotelId,
|
||||
lang,
|
||||
roomFeatureCodes: room.packages,
|
||||
startDate: input.booking.fromDate,
|
||||
},
|
||||
serviceToken
|
||||
)
|
||||
|
||||
if (roomFeaturesInventory) {
|
||||
const features = roomFeaturesInventory.reduce<
|
||||
Record<string, number>
|
||||
>((fts, feat) => {
|
||||
fts[feat.roomTypeCode] = feat.features?.[0]?.inventory ?? 0
|
||||
return fts
|
||||
}, {})
|
||||
|
||||
const updatedRoomConfigurations =
|
||||
validateAvailabilityData.data.roomConfigurations
|
||||
// This filter is needed since we can get availability
|
||||
// back from roomFeatures yet the availability call
|
||||
// says there are no rooms left...
|
||||
.filter((rc) => rc.roomsLeft)
|
||||
.filter((rc) => features?.[rc.roomTypeCode])
|
||||
.map((rc) => ({
|
||||
...rc,
|
||||
roomsLeft: features[rc.roomTypeCode],
|
||||
status: AvailabilityEnum.Available,
|
||||
}))
|
||||
|
||||
validateAvailabilityData.data.roomConfigurations =
|
||||
updatedRoomConfigurations
|
||||
}
|
||||
}
|
||||
|
||||
return validateAvailabilityData.data
|
||||
}
|
||||
},
|
||||
"1m"
|
||||
)
|
||||
})
|
||||
)
|
||||
metrics.roomsAvailability.success.add(1, metricsData)
|
||||
|
||||
const data = availabilityResponses.map((availability) => {
|
||||
if (availability.status === "fulfilled") {
|
||||
return availability.value
|
||||
}
|
||||
return {
|
||||
details: availability.reason,
|
||||
error: "request_failure",
|
||||
}
|
||||
})
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
export function getSelectedRoomAvailability(
|
||||
rateCode: string,
|
||||
rateDefinitions: RateDefinition[],
|
||||
roomConfigurations: RoomConfiguration[],
|
||||
roomTypeCode: string,
|
||||
userPoints: number | undefined
|
||||
) {
|
||||
const rateDefinition = rateDefinitions.find((rd) => rd.rateCode === rateCode)
|
||||
if (!rateDefinition) {
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedRoom = roomConfigurations.find(
|
||||
(room) =>
|
||||
room.roomTypeCode === input.roomTypeCode &&
|
||||
room.roomTypeCode === roomTypeCode &&
|
||||
room.products.find((product) => findProduct(product, rateDefinition))
|
||||
)
|
||||
|
||||
@@ -753,3 +1457,90 @@ export async function getSelectedRoomAvailability(
|
||||
selectedRoom,
|
||||
}
|
||||
}
|
||||
|
||||
// Used to ensure `Available` rooms
|
||||
// are shown before all `NotAvailable`
|
||||
const statusLookup = {
|
||||
[AvailabilityEnum.Available]: 1,
|
||||
[AvailabilityEnum.NotAvailable]: 2,
|
||||
}
|
||||
|
||||
export function sortRoomConfigs(a: RoomConfiguration, b: RoomConfiguration) {
|
||||
// @ts-expect-error - array indexing
|
||||
return statusLookup[a.status] - statusLookup[b.status]
|
||||
}
|
||||
|
||||
export function getBedTypes(
|
||||
rooms: RoomConfiguration[],
|
||||
roomType: string,
|
||||
roomCategories?: RoomCategory[]
|
||||
) {
|
||||
if (!roomCategories) {
|
||||
return []
|
||||
}
|
||||
|
||||
return rooms
|
||||
.filter((room) => room.roomType === roomType)
|
||||
.map((availRoom) => {
|
||||
const matchingRoom = 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))
|
||||
}
|
||||
|
||||
export function mergeRoomTypes(roomConfigurations: RoomConfiguration[]) {
|
||||
// Initial sort to guarantee if one bed is NotAvailable and whereas
|
||||
// the other is Available to make sure data is added to the correct
|
||||
// roomConfig
|
||||
roomConfigurations.sort(sortRoomConfigs)
|
||||
|
||||
const roomConfigs = new Map<string, RoomConfiguration>()
|
||||
for (const roomConfig of roomConfigurations) {
|
||||
if (roomConfigs.has(roomConfig.roomType)) {
|
||||
const currentRoomConf = roomConfigs.get(roomConfig.roomType)
|
||||
if (currentRoomConf) {
|
||||
currentRoomConf.features = roomConfig.features.reduce(
|
||||
(feats, feature) => {
|
||||
const currentFeatureIndex = feats.findIndex(
|
||||
(f) => f.code === feature.code
|
||||
)
|
||||
if (currentFeatureIndex !== -1) {
|
||||
feats[currentFeatureIndex].inventory =
|
||||
feats[currentFeatureIndex].inventory + feature.inventory
|
||||
} else {
|
||||
feats.push(feature)
|
||||
}
|
||||
return feats
|
||||
},
|
||||
currentRoomConf.features
|
||||
)
|
||||
currentRoomConf.roomsLeft =
|
||||
currentRoomConf.roomsLeft + roomConfig.roomsLeft
|
||||
roomConfigs.set(currentRoomConf.roomType, currentRoomConf)
|
||||
}
|
||||
} else {
|
||||
roomConfigs.set(roomConfig.roomType, roomConfig)
|
||||
}
|
||||
}
|
||||
return Array.from(roomConfigs.values())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user