import { z } from "zod" import { Lang } from "@scandic-hotels/common/constants/language" import { RateEnum } from "@scandic-hotels/common/constants/rate" import { RateTypeEnum } from "@scandic-hotels/common/constants/rateType" import { createLogger } from "@scandic-hotels/common/logger/createLogger" import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking" import { AvailabilityEnum } from "../../../enums/selectHotel" import { unauthorizedError } from "../../../errors" import { safeProtectedServiceProcedure } from "../../../procedures" import { isValidSession } from "../../../utils/session" import { baseBookingSchema, baseRoomSchema, selectedRoomSchema } from "../input" import { getHotel } from "../services/getHotel" import { getRoomsAvailability } from "../services/getRoomsAvailability" import { getBedTypes, getSelectedRoomAvailability, selectRateRedirectURL, } from "../utils" import type { Room } from "../../../types/room" export type RoomsAvailabilityExtendedInputSchema = z.input< typeof enterDetailsRoomsAvailabilityInputSchema > export const enterDetailsRoomsAvailabilityInputSchema = z.object({ booking: baseBookingSchema.extend({ rooms: z.array(baseRoomSchema.merge(selectedRoomSchema)), }), lang: z.nativeEnum(Lang), }) type Context = { userToken: string | null userPoints?: number } const logger = createLogger("trpc:availability:enterDetails") export const enterDetails = safeProtectedServiceProcedure .input(enterDetailsRoomsAvailabilityInputSchema) .use(async ({ ctx, input, next }) => { const userToken = await ctx.getScandicUserToken() if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) { if (isValidSession(ctx.session)) { const pointsValue = await ctx.getUserPointsBalance() if (pointsValue) { return next({ ctx: { userToken, userPoints: pointsValue, }, input, }) } } throw unauthorizedError() } return next({ ctx: { userToken, }, }) }) .query(async function ({ ctx, input }) { const availability = await getRoomsAvailability( input, ctx.userToken, ctx.serviceToken, ctx.userPoints ) const hotelData = await getHotel( { hotelId: input.booking.hotelId, isCardOnlyPayment: false, language: input.lang || ctx.lang, }, ctx.serviceToken ) const selectedRooms = [] for (const [idx, room] of availability.entries()) { if (!room || "error" in room) { logger.error(`Availability failed: ${room.error}`, room.details) selectedRooms.push(null) continue } const bookingRoom = input.booking.rooms[idx] const selected = getSelectedRoomAvailability( bookingRoom.rateCode, room.rateDefinitions, room.roomConfigurations, bookingRoom.roomTypeCode, ctx.userPoints ) if (!selected) { logger.error("Unable to find selected room") selectedRooms.push(null) continue } const { rateDefinition, rateDefinitions, product, rooms, selectedRoom } = selected const bedTypes = getBedTypes( rooms, selectedRoom.roomType, hotelData?.roomCategories ) const counterRateCode = input.booking.rooms[idx].counterRateCode const rateCode = input.booking.rooms[idx].rateCode let memberRateDefinition = undefined if ("member" in product && product.member && counterRateCode) { memberRateDefinition = rateDefinitions.find( (rate) => (rate.rateCode.toLowerCase() === counterRateCode.toLowerCase() || rate.rateCode.toLowerCase() === rateCode.toLowerCase()) && rate.isMemberRate ) } const selectedPackages = input.booking.rooms[idx].packages selectedRooms.push({ bedTypes, breakfastIncluded: rateDefinition.breakfastIncluded, cancellationText: rateDefinition.cancellationText, cancellationRule: rateDefinition.cancellationRule, isAvailable: selectedRoom.status === AvailabilityEnum.Available, isFlexRate: product.rate === RateEnum.flex, memberMustBeGuaranteed: memberRateDefinition?.mustBeGuaranteed, mustBeGuaranteed: rateDefinition.mustBeGuaranteed, packages: room.packages.filter((pkg) => selectedPackages?.includes(pkg.code) ), rate: product.rate, rateDefinitionTitle: rateDefinition.title, rateDetails: rateDefinition.generalTerms, memberRateDetails: memberRateDefinition?.generalTerms, // Send rate Title when it is a booking code rate rateTitle: rateDefinition.rateType !== RateTypeEnum.Regular ? rateDefinition.title : undefined, rateType: rateDefinition.rateType, roomRate: product, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, }) } const totalBedsAvailableForRoomTypeCode: Record = {} for (const selectedRoom of selectedRooms) { if (selectedRoom) { if (!totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode]) { totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] = selectedRoom.bedTypes.reduce( (total, bedType) => total + bedType.roomsLeft, 0 ) } } } for (const [idx, selectedRoom] of selectedRooms.entries()) { if (selectedRoom) { const totalBedsLeft = totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] if (totalBedsLeft <= 0) { selectedRooms[idx] = null continue } totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] = totalBedsAvailableForRoomTypeCode[selectedRoom.roomTypeCode] - 1 } } if (selectedRooms.some((sr) => !sr)) { logger.debug("DEBUG: REDIRECTING TO SELECT RATE", selectedRooms) return selectRateRedirectURL(input, selectedRooms.map(Boolean)) } const rooms: Room[] = selectedRooms.filter((sr) => !!sr) return rooms })