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
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
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 { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||
@@ -9,10 +7,8 @@ import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||
import { env } from "../../../env/server"
|
||||
import { router } from "../.."
|
||||
import * as api from "../../api"
|
||||
import { SEARCH_TYPE_REDEMPTION } from "../../constants/booking"
|
||||
import { BreakfastPackageEnum } from "../../enums/breakfast"
|
||||
import { AvailabilityEnum } from "../../enums/selectHotel"
|
||||
import { badRequestError, unauthorizedError } from "../../errors"
|
||||
import { badRequestError } from "../../errors"
|
||||
import {
|
||||
contentStackBaseWithServiceProcedure,
|
||||
publicProcedure,
|
||||
@@ -25,23 +21,17 @@ import {
|
||||
ancillaryPackageInputSchema,
|
||||
breakfastPackageInputSchema,
|
||||
cityCoordinatesInputSchema,
|
||||
enterDetailsRoomsAvailabilityInputSchema,
|
||||
getAdditionalDataInputSchema,
|
||||
getDestinationsMapDataInput,
|
||||
getHotelsByCityIdentifierInput,
|
||||
getHotelsByCountryInput,
|
||||
getHotelsByCSFilterInput,
|
||||
getHotelsByHotelIdsAvailabilityInputSchema,
|
||||
getLocationsInput,
|
||||
getLocationsUrlsInput,
|
||||
getMeetingRoomsInputSchema,
|
||||
hotelInputSchema,
|
||||
hotelsAvailabilityInputSchema,
|
||||
myStayRoomAvailabilityInputSchema,
|
||||
nearbyHotelIdsInput,
|
||||
roomPackagesInputSchema,
|
||||
selectRateRoomAvailabilityInputSchema,
|
||||
selectRateRoomsAvailabilityInputSchema,
|
||||
} from "../../routers/hotels/input"
|
||||
import {
|
||||
ancillaryPackagesSchema,
|
||||
@@ -49,498 +39,26 @@ import {
|
||||
getNearbyHotelIdsSchema,
|
||||
} from "../../routers/hotels/output"
|
||||
import { toApiLang } from "../../utils"
|
||||
import { getVerifiedUser } from "../user/utils"
|
||||
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
||||
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
||||
import {
|
||||
getBedTypes,
|
||||
getCitiesByCountry,
|
||||
getCountries,
|
||||
getHotel,
|
||||
getHotelIdsByCityId,
|
||||
getHotelIdsByCityIdentifier,
|
||||
getHotelIdsByCountry,
|
||||
getHotelsAvailabilityByCity,
|
||||
getHotelsAvailabilityByHotelIds,
|
||||
getHotelsByHotelIds,
|
||||
getLocations,
|
||||
getPackages,
|
||||
getRoomsAvailability,
|
||||
getSelectedRoomAvailability,
|
||||
mergeRoomTypes,
|
||||
selectRateRedirectURL,
|
||||
} from "./utils"
|
||||
import { getCitiesByCountry } from "./services/getCitiesByCountry"
|
||||
import { getHotelIdsByCityIdentifier } from "./services/getCityByCityIdentifier"
|
||||
import { getCountries } from "./services/getCountries"
|
||||
import { getHotel } from "./services/getHotel"
|
||||
import { getHotelIdsByCityId } from "./services/getHotelIdsByCityId"
|
||||
import { getHotelIdsByCountry } from "./services/getHotelIdsByCountry"
|
||||
import { getHotelsByHotelIds } from "./services/getHotelsByHotelIds"
|
||||
import { getPackages } from "./services/getPackages"
|
||||
import { availability } from "./availability"
|
||||
import { getLocations } from "./utils"
|
||||
|
||||
import type { HotelListingHotelData } from "../../types/hotel"
|
||||
import type { CityLocation } from "../../types/locations"
|
||||
import type { Room } from "../../types/room"
|
||||
|
||||
const hotelQueryLogger = createLogger("hotelQueryRouter")
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
availability: router({
|
||||
hotelsByCity: safeProtectedServiceProcedure
|
||||
.input(hotelsAvailabilityInputSchema)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
if (input.redemption) {
|
||||
if (ctx.session?.token.access_token) {
|
||||
const verifiedUser = await getVerifiedUser({ session: ctx.session })
|
||||
if (!verifiedUser?.error) {
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.session.token.access_token,
|
||||
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||
},
|
||||
input,
|
||||
})
|
||||
}
|
||||
}
|
||||
throw unauthorizedError()
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.serviceToken,
|
||||
},
|
||||
input,
|
||||
})
|
||||
})
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { lang } = ctx
|
||||
const apiLang = toApiLang(lang)
|
||||
const {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
bookingCode,
|
||||
redemption,
|
||||
} = input
|
||||
|
||||
// In case of redemption do not cache result
|
||||
if (redemption) {
|
||||
return getHotelsAvailabilityByCity(
|
||||
input,
|
||||
apiLang,
|
||||
ctx.token,
|
||||
ctx.userPoints
|
||||
)
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||
async () => {
|
||||
return getHotelsAvailabilityByCity(input, apiLang, ctx.token)
|
||||
},
|
||||
env.CACHE_TIME_CITY_SEARCH
|
||||
)
|
||||
}),
|
||||
hotelsByHotelIds: safeProtectedServiceProcedure
|
||||
.input(getHotelsByHotelIdsAvailabilityInputSchema)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
if (input.redemption) {
|
||||
if (ctx.session?.token.access_token) {
|
||||
const verifiedUser = await getVerifiedUser({ session: ctx.session })
|
||||
if (!verifiedUser?.error) {
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.session.token.access_token,
|
||||
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||
},
|
||||
input,
|
||||
})
|
||||
}
|
||||
}
|
||||
throw unauthorizedError()
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.serviceToken,
|
||||
},
|
||||
input,
|
||||
})
|
||||
})
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { lang } = ctx
|
||||
const apiLang = toApiLang(lang)
|
||||
return getHotelsAvailabilityByHotelIds(
|
||||
input,
|
||||
apiLang,
|
||||
ctx.token,
|
||||
ctx.userPoints
|
||||
)
|
||||
}),
|
||||
|
||||
enterDetails: safeProtectedServiceProcedure
|
||||
.input(enterDetailsRoomsAvailabilityInputSchema)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
|
||||
if (ctx.session?.token.access_token) {
|
||||
const verifiedUser = await getVerifiedUser({ session: ctx.session })
|
||||
if (!verifiedUser?.error) {
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.session.token.access_token,
|
||||
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
throw unauthorizedError()
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.serviceToken,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx, input }) {
|
||||
const availability = await getRoomsAvailability(
|
||||
input,
|
||||
ctx.token,
|
||||
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) {
|
||||
hotelQueryLogger.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) {
|
||||
hotelQueryLogger.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))
|
||||
}
|
||||
|
||||
// Make TS show appropriate type
|
||||
return selectedRooms.filter((sr): sr is Room => !!sr)
|
||||
}),
|
||||
myStay: safeProtectedServiceProcedure
|
||||
.input(myStayRoomAvailabilityInputSchema)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
|
||||
if (ctx.session?.token.access_token) {
|
||||
const verifiedUser = await getVerifiedUser({ session: ctx.session })
|
||||
if (!verifiedUser?.error) {
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.session.token.access_token,
|
||||
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
throw unauthorizedError()
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.serviceToken,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx, input }) {
|
||||
const [availability] = await getRoomsAvailability(
|
||||
{
|
||||
booking: {
|
||||
...input.booking,
|
||||
rooms: [input.booking.room],
|
||||
},
|
||||
lang: input.lang,
|
||||
},
|
||||
ctx.token,
|
||||
ctx.serviceToken,
|
||||
ctx.userPoints
|
||||
)
|
||||
|
||||
if (!availability || "error" in availability) {
|
||||
return null
|
||||
}
|
||||
|
||||
const bookingRoom = input.booking.room
|
||||
const selected = getSelectedRoomAvailability(
|
||||
bookingRoom.rateCode,
|
||||
availability.rateDefinitions,
|
||||
availability.roomConfigurations,
|
||||
bookingRoom.roomTypeCode,
|
||||
ctx.userPoints
|
||||
)
|
||||
|
||||
if (!selected) {
|
||||
hotelQueryLogger.error("Unable to find selected room")
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
product: selected.product,
|
||||
selectedRoom: selected.selectedRoom,
|
||||
}
|
||||
}),
|
||||
selectRate: router({
|
||||
room: safeProtectedServiceProcedure
|
||||
.input(selectRateRoomAvailabilityInputSchema)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
|
||||
if (ctx.session?.token.access_token) {
|
||||
const verifiedUser = await getVerifiedUser({
|
||||
session: ctx.session,
|
||||
})
|
||||
if (!verifiedUser?.error) {
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.session.token.access_token,
|
||||
userPoints:
|
||||
verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
throw unauthorizedError()
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.serviceToken,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx, input }) {
|
||||
const [availability] = await getRoomsAvailability(
|
||||
{
|
||||
booking: {
|
||||
...input.booking,
|
||||
rooms: [input.booking.room],
|
||||
},
|
||||
lang: input.lang,
|
||||
},
|
||||
ctx.token,
|
||||
ctx.serviceToken,
|
||||
ctx.userPoints
|
||||
)
|
||||
|
||||
if (!availability || "error" in availability) {
|
||||
return null
|
||||
}
|
||||
|
||||
const roomConfigurations = mergeRoomTypes(
|
||||
availability.roomConfigurations
|
||||
)
|
||||
|
||||
return {
|
||||
...availability,
|
||||
roomConfigurations,
|
||||
}
|
||||
}),
|
||||
|
||||
rooms: safeProtectedServiceProcedure
|
||||
.input(selectRateRoomsAvailabilityInputSchema)
|
||||
.use(async ({ ctx, input, next }) => {
|
||||
if (input.booking.searchType === SEARCH_TYPE_REDEMPTION) {
|
||||
if (ctx.session?.token.access_token) {
|
||||
const verifiedUser = await getVerifiedUser({
|
||||
session: ctx.session,
|
||||
})
|
||||
if (!verifiedUser?.error) {
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.session.token.access_token,
|
||||
userPoints:
|
||||
verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
throw unauthorizedError()
|
||||
}
|
||||
return next({
|
||||
ctx: {
|
||||
token: ctx.serviceToken,
|
||||
},
|
||||
})
|
||||
})
|
||||
.query(async function ({ ctx, input }) {
|
||||
input.booking.rooms = input.booking.rooms.map((room) => ({
|
||||
...room,
|
||||
bookingCode: room.bookingCode || input.booking.bookingCode,
|
||||
}))
|
||||
|
||||
const availability = await getRoomsAvailability(
|
||||
input,
|
||||
ctx.token,
|
||||
ctx.serviceToken,
|
||||
ctx.userPoints
|
||||
)
|
||||
|
||||
for (const room of availability) {
|
||||
if (!room || "error" in room) {
|
||||
continue
|
||||
}
|
||||
|
||||
room.roomConfigurations = mergeRoomTypes(room.roomConfigurations)
|
||||
}
|
||||
|
||||
return availability
|
||||
}),
|
||||
}),
|
||||
|
||||
hotelsByCityWithBookingCode: serviceProcedure
|
||||
.input(hotelsAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const { lang } = ctx
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const bookingCodeAvailabilityResponse =
|
||||
await getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
||||
|
||||
// Get regular availability of hotels which don't have availability with booking code.
|
||||
const unavailableHotelIds = bookingCodeAvailabilityResponse.availability
|
||||
.filter((hotel) => {
|
||||
return hotel.status === "NotAvailable"
|
||||
})
|
||||
.flatMap((hotel) => {
|
||||
return hotel.hotelId
|
||||
})
|
||||
|
||||
// All hotels have availability with booking code no need to fetch regular prices.
|
||||
// return response as is without any filtering as below.
|
||||
if (!unavailableHotelIds || !unavailableHotelIds.length) {
|
||||
return bookingCodeAvailabilityResponse
|
||||
}
|
||||
|
||||
const unavailableHotelsInput = {
|
||||
...input,
|
||||
bookingCode: "",
|
||||
hotelIds: unavailableHotelIds,
|
||||
}
|
||||
const unavailableHotels = await getHotelsAvailabilityByHotelIds(
|
||||
unavailableHotelsInput,
|
||||
apiLang,
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
// No regular rates available due to network or API failure (no need to filter & merge).
|
||||
if (!unavailableHotels) {
|
||||
return bookingCodeAvailabilityResponse
|
||||
}
|
||||
|
||||
// Filtering the response hotels to merge bookingCode rates and regular rates in single response.
|
||||
return {
|
||||
availability: bookingCodeAvailabilityResponse.availability
|
||||
.filter((hotel) => {
|
||||
return hotel.status === "Available"
|
||||
})
|
||||
.concat(unavailableHotels.availability),
|
||||
}
|
||||
}),
|
||||
}),
|
||||
availability,
|
||||
get: serviceProcedure
|
||||
.input(hotelInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
|
||||
Reference in New Issue
Block a user