Files
web/packages/trpc/lib/routers/hotels/availability/enterDetails.ts
Joakim Jäderberg 3b3e7308cc Merged in feat/SW-3549-pass-scandic-token (pull request #2989)
Feat/SW-3549 pass scandic token

* WIP pass scandic token

* pass scandic token when booking

* Merge branch 'master' of bitbucket.org:scandic-swap/web into feat/SW-3549-pass-scandic-token

* pass user token when doing availability search

* undo changes

* merge

* Merged in fix/sw-3551-rsc-bookingflowconfig (pull request #2988)

fix(SW-3551): Fix issue with BookingConfigProvider in RSC

* wip move config to pages

* Move config providing to pages

* Merged in fix/update-promo-error-modal-text (pull request #2990)

fix: update promo error modal text

* fix: update promo error modal text


Approved-by: Emma Zettervall

* Merged in fix/sw-3514-missing-membership-input-for-multiroom (pull request #2991)

fix(SW-3514): Show join Scandic Friends card for SAS multiroom

* Show join card for room 2+


Approved-by: Hrishikesh Vaipurkar

* Merged in feat/lokalise-rebuild (pull request #2993)

Feat/lokalise rebuild

* chore(lokalise): update translation ids

* chore(lokalise): easier to switch between projects

* chore(lokalise): update translation ids

* .

* .

* .

* .

* .

* .

* chore(lokalise): update translation ids

* chore(lokalise): update translation ids

* .

* .

* .

* chore(lokalise): update translation ids

* chore(lokalise): update translation ids

* .

* .

* chore(lokalise): update translation ids

* chore(lokalise): update translation ids

* chore(lokalise): new translations

* merge

* switch to errors for missing id's

* merge

* sync translations


Approved-by: Linus Flood

* Merged in feat/SW-3552-logout-from-social-session-when- (pull request #2994)

feat(SW-3552): Removed scandic session on logout

Approved-by: Joakim Jäderberg

* merge

* replace getRedemptionTokenSafely() with context based instead

* Refactor user verification and error handling across multiple components; implement safeTry utility for safer async calls

* Refactor user verification and error handling across multiple components; implement safeTry utility for safer async calls

* merge

* Merge branch 'master' of bitbucket.org:scandic-swap/web into feat/SW-3549-pass-scandic-token

* add booking scope

remove unused getMembershipNumber()


Approved-by: Anton Gunnarsson
Approved-by: Hrishikesh Vaipurkar
2025-10-24 13:17:02 +00:00

187 lines
6.1 KiB
TypeScript

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 && userToken) {
return next<Context>({
ctx: {
userToken,
userPoints: pointsValue,
},
input,
})
}
}
throw unauthorizedError()
}
return next<Context>({
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<string, number> = {}
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)) {
console.log("DEBUG: REDIRECTING TO SELECT RATE", selectedRooms)
return selectRateRedirectURL(input, selectedRooms.map(Boolean))
}
const rooms: Room[] = selectedRooms.filter((sr) => !!sr)
return rooms
})