import stringify from "json-stable-stringify-without-jsonify" import { getCacheClient } from "@scandic-hotels/common/dataCache" import { createCounter } from "@scandic-hotels/common/telemetry" import * as api from "../../../api" import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking" import { RoomPackageCodeEnum } from "../../../enums/roomFilter" import { AvailabilityEnum } from "../../../enums/selectHotel" import { toApiLang } from "../../../utils" import { generateChildrenString } from "../helpers" import { roomsAvailabilitySchema } from "../output" import { getPackages } from "./getPackages" import { getRoomFeaturesInventory } from "./getRoomFeaturesInventory" import type { RoomsAvailabilityOutputSchema } from "../availability/selectRate/rooms/schema" export async function getRoomsAvailability( input: RoomsAvailabilityOutputSchema, userToken: string | null | undefined, serviceToken: string, userPoints: number | undefined ) { const { booking: { bookingCode, fromDate, hotelId, rooms, searchType, toDate }, lang, } = input const redemption = searchType === SEARCH_TYPE_REDEMPTION const getRoomsAvailabilityCounter = createCounter( "hotel.getRoomsAvailability" ) const metricsGetRoomsAvailability = getRoomsAvailabilityCounter.init({ input, redemption, }) metricsGetRoomsAvailability.start() const apiLang = toApiLang(lang) const baseCacheKey = { lang, hotelId, fromDate, toDate, searchType, bookingCode, } const cacheClient = await getCacheClient() const availabilityResponses = await Promise.allSettled( rooms.map((room) => { const cacheKey = { ...baseCacheKey, room, } const token = userToken ? ({ type: "userToken", token: userToken } as const) : ({ type: "serviceToken", token: serviceToken } as const) const result = cacheClient.cacheOrGet( stringify(cacheKey), async function () { { const params = { adults: room.adults, language: apiLang, roomStayStartDate: fromDate, roomStayEndDate: toDate, ...(room.childrenInRoom?.length && { children: generateChildrenString(room.childrenInRoom), }), ...(bookingCode && { bookingCode: bookingCode }), ...(redemption && { isRedemption: "true" }), } const apiResponse = await api.get( api.endpoints.v1.Availability.hotel(hotelId), { cache: undefined, // overwrite default headers: { Authorization: `Bearer ${token.token}`, }, }, params ) if (!apiResponse.ok) { await metricsGetRoomsAvailability.httpError(apiResponse) const text = await apiResponse.text() return { error: "http_error", details: text } } const apiJson = await apiResponse.json() const validateAvailabilityData = roomsAvailabilitySchema.safeParse(apiJson) if (!validateAvailabilityData.success) { metricsGetRoomsAvailability.validationError( validateAvailabilityData.error ) 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 || 0, 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 >((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 } }, token.type === "userToken" ? "no cache" : "1m" ) return result }) ) const data = availabilityResponses.map((availability) => { if (availability.status === "fulfilled") { return availability.value } return { details: availability.reason, error: "request_failure", } }) metricsGetRoomsAvailability.success() return data }