Files
web/packages/trpc/lib/routers/hotels/utils.ts
Joakim Jäderberg 8498026189 Merged in chore/refactor-hotel-trpc-routes (pull request #2891)
Chore/refactor hotel trpc routes

* chore(SW-3519): refactor trpc hotel routers

* chore(SW-3519): refactor trpc hotel routers

* refactor

* merge

* Merge branch 'master' of bitbucket.org:scandic-swap/web into chore/refactor-hotel-trpc-routes


Approved-by: Linus Flood
2025-10-01 12:55:45 +00:00

346 lines
10 KiB
TypeScript

import deepmerge from "deepmerge"
import { selectRate } from "@scandic-hotels/common/constants/routes/hotelReservation"
import { getCacheClient } from "@scandic-hotels/common/dataCache"
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
import { chunk } from "@scandic-hotels/common/utils/chunk"
import * as api from "../../api"
import { BookingErrorCodeEnum } from "../../enums/bookingErrorCode"
import { AvailabilityEnum } from "../../enums/selectHotel"
import { toApiLang } from "../../utils"
import { sortRoomConfigs } from "../../utils/sortRoomConfigs"
import { getCity } from "./services/getCity"
import { locationsSchema } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { z } from "zod"
import type { BedTypeSelection } from "../../types/bedTypeSelection"
import type { Room as RoomCategory } from "../../types/hotel"
import type { CitiesGroupedByCountry } from "../../types/locations"
import type {
Product,
Products,
RateDefinition,
RedemptionsProduct,
RoomConfiguration,
} from "../../types/roomAvailability"
import type { RoomsAvailabilityExtendedInputSchema } from "./availability/enterDetails"
export const locationsAffix = "locations"
const hotelUtilsLogger = createLogger("hotelUtils")
export async function getLocations({
lang,
citiesByCountry,
serviceToken,
}: {
lang: Lang
citiesByCountry: CitiesGroupedByCountry | null
serviceToken: string
}) {
const cacheClient = await getCacheClient()
const countryKeys = Object.keys(citiesByCountry ?? {})
let cacheKey = `${lang}:locations`
if (countryKeys.length) {
cacheKey += `:${countryKeys.join(",")}`
}
return await cacheClient.cacheOrGet(
cacheKey.toLowerCase(),
async () => {
const params = new URLSearchParams({
language: toApiLang(lang),
})
const apiResponse = await api.get(
api.endpoints.v1.Hotel.locations,
{
headers: {
Authorization: `Bearer ${serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
if (apiResponse.status === 401) {
throw new Error("unauthorized")
} else if (apiResponse.status === 403) {
throw new Error("forbidden")
}
throw new Error("downstream error")
}
const apiJson = await apiResponse.json()
const verifiedLocations = locationsSchema.safeParse(apiJson)
if (!verifiedLocations.success) {
hotelUtilsLogger.error(
`Locations Verification Failed`,
verifiedLocations.error
)
throw new Error("Unable to parse locations")
}
const chunkedLocations = chunk(verifiedLocations.data.data, 10)
let locations: z.infer<typeof locationsSchema>["data"] = []
for (const chunk of chunkedLocations) {
const chunkLocations = await Promise.all(
chunk.map(async (location) => {
if (location.type === "cities") {
if (citiesByCountry) {
const country = Object.keys(citiesByCountry).find((country) =>
citiesByCountry[country].find(
(loc) => loc.name === location.name
)
)
if (country) {
return {
...location,
country,
}
} else {
hotelUtilsLogger.error(
`Location cannot be found in any of the countries cities`,
location
)
}
}
} else if (location.type === "hotels") {
if (location.relationships.city?.url) {
const city = await getCity({
cityUrl: location.relationships.city.url,
serviceToken,
})
if (city) {
return deepmerge(location, {
relationships: {
city,
},
})
}
}
}
return location
})
)
locations.push(...chunkLocations)
}
return locations
},
"1d"
)
}
export const TWENTYFOUR_HOURS = 60 * 60 * 24
function findProduct(product: Products, rateDefinition: RateDefinition) {
if ("corporateCheque" in product) {
return product.corporateCheque.rateCode === rateDefinition.rateCode
}
if (("member" in product && product.member) || "public" in product) {
let isMemberRate = false
if (product.member) {
isMemberRate = product.member.rateCode === rateDefinition.rateCode
}
let isPublicRate = false
if (product.public) {
isPublicRate = product.public.rateCode === rateDefinition.rateCode
}
return isMemberRate || isPublicRate
}
if ("voucher" in product) {
return product.voucher.rateCode === rateDefinition.rateCode
}
if (Array.isArray(product)) {
return product.find(
(r) => r.redemption.rateCode === rateDefinition.rateCode
)
}
}
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 === roomTypeCode &&
room.products.find((product) => findProduct(product, rateDefinition))
)
if (!selectedRoom) {
return null
}
let product: Product | RedemptionsProduct | undefined =
selectedRoom.products.find((product) =>
findProduct(product, rateDefinition)
)
if (!product) {
return null
}
if (Array.isArray(product)) {
const redemptionProduct = userPoints
? product.find(
(r) =>
r.redemption.rateCode === rateDefinition.rateCode &&
r.redemption.localPrice.pointsPerStay <= userPoints
)
: undefined
if (!redemptionProduct) {
return null
}
product = redemptionProduct
}
return {
rateDefinition,
rateDefinitions,
rooms: roomConfigurations,
product,
selectedRoom,
}
}
export function getBedTypes(
rooms: RoomConfiguration[],
roomType: string,
roomCategories?: RoomCategory[]
) {
if (!roomCategories) {
return []
}
return rooms
.filter(
(room) => room.status === AvailabilityEnum.Available || room.roomsLeft > 0
)
.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,
roomsLeft: availRoom.roomsLeft,
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())
}
export function selectRateRedirectURL(
input: RoomsAvailabilityExtendedInputSchema,
selectedRooms: boolean[]
) {
const searchParams = new URLSearchParams({
errorCode: BookingErrorCodeEnum.AvailabilityError,
fromdate: input.booking.fromDate,
hotel: input.booking.hotelId,
todate: input.booking.toDate,
})
if (input.booking.searchType) {
searchParams.set("searchtype", input.booking.searchType)
}
for (const [idx, room] of input.booking.rooms.entries()) {
searchParams.set(`room[${idx}].adults`, room.adults.toString())
if (selectedRooms[idx]) {
if (room.counterRateCode) {
searchParams.set(`room[${idx}].counterratecode`, room.counterRateCode)
}
searchParams.set(`room[${idx}].ratecode`, room.rateCode)
searchParams.set(`room[${idx}].roomtype`, room.roomTypeCode)
} else {
if (!searchParams.has("activeRoomIndex")) {
searchParams.set("activeRoomIndex", idx.toString())
}
}
if (room.bookingCode) {
searchParams.set(`room[${idx}].bookingCode`, room.bookingCode)
}
if (room.packages) {
searchParams.set(`room[${idx}].packages`, room.packages.join(","))
}
if (room.childrenInRoom?.length) {
for (const [i, kid] of room.childrenInRoom.entries()) {
searchParams.set(`room[${idx}].child[${i}].age`, kid.age.toString())
searchParams.set(`room[${idx}].child[${i}].bed`, kid.bed.toString())
}
}
}
return `${selectRate(input.lang)}?${searchParams.toString()}`
}