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,8 +1,6 @@
|
|||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
import { logger } from "@scandic-hotels/common/logger"
|
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
@@ -13,6 +11,12 @@ export const GET = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await auth()
|
const user = await auth()
|
||||||
logger.debug("[DEBUG] access-token", user?.token)
|
const sortedEnv = Object.keys(env)
|
||||||
return NextResponse.json(user)
|
.sort()
|
||||||
|
.reduce<Record<string, unknown>>((acc, key) => {
|
||||||
|
acc[key] = env[key as keyof typeof env]
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return NextResponse.json({ user, env: sortedEnv })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { getServiceToken } from "@scandic-hotels/common/tokenManager"
|
import { getServiceToken } from "@scandic-hotels/common/tokenManager"
|
||||||
import { getCountries } from "@scandic-hotels/trpc/routers/hotels/utils"
|
import { getCountries } from "@scandic-hotels/trpc/routers/hotels/services/getCountries"
|
||||||
|
|
||||||
import type { Lang } from "@scandic-hotels/common/constants/language"
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ import { Lang } from "@scandic-hotels/common/constants/language"
|
|||||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
import { getServiceToken } from "@scandic-hotels/common/tokenManager"
|
import { getServiceToken } from "@scandic-hotels/common/tokenManager"
|
||||||
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
import { safeTry } from "@scandic-hotels/common/utils/safeTry"
|
||||||
import {
|
import { getCountries } from "@scandic-hotels/trpc/routers/hotels/services/getCountries"
|
||||||
getCountries,
|
import { getHotelIdsByCountry } from "@scandic-hotels/trpc/routers/hotels/services/getHotelIdsByCountry"
|
||||||
getHotelIdsByCountry,
|
|
||||||
} from "@scandic-hotels/trpc/routers/hotels/utils"
|
|
||||||
|
|
||||||
import type { WarmupFunction, WarmupResult } from "."
|
import type { WarmupFunction, WarmupResult } from "."
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { logger } from "@scandic-hotels/common/logger"
|
|||||||
import { type RouterOutput, trpc } from "@scandic-hotels/trpc/client"
|
import { type RouterOutput, trpc } from "@scandic-hotels/trpc/client"
|
||||||
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||||
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
|
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
|
||||||
import { selectRateRoomsAvailabilityInputSchema } from "@scandic-hotels/trpc/routers/hotels/input"
|
import { selectRateRoomsAvailabilityInputSchema } from "@scandic-hotels/trpc/routers/hotels/availability/selectRate/rooms/schema"
|
||||||
|
|
||||||
import { useIsLoggedIn } from "../../hooks/useIsLoggedIn"
|
import { useIsLoggedIn } from "../../hooks/useIsLoggedIn"
|
||||||
import useLang from "../../hooks/useLang"
|
import useLang from "../../hooks/useLang"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { type RouterOutput } from "@scandic-hotels/trpc/client"
|
import { type RouterOutput } from "@scandic-hotels/trpc/client"
|
||||||
|
|
||||||
import type { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
import type { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
|
||||||
import type { RoomsAvailabilityOutputSchema } from "@scandic-hotels/trpc/types/availability"
|
import type { RoomsAvailabilityOutputSchema } from "@scandic-hotels/trpc/routers/hotels/availability/selectRate/rooms/schema"
|
||||||
import type { PackageEnum } from "@scandic-hotels/trpc/types/packages"
|
import type { PackageEnum } from "@scandic-hotels/trpc/types/packages"
|
||||||
import type { RoomConfiguration } from "@scandic-hotels/trpc/types/roomAvailability"
|
import type { RoomConfiguration } from "@scandic-hotels/trpc/types/roomAvailability"
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { cache } from "react"
|
|||||||
|
|
||||||
import { serverClient } from "../../trpc"
|
import { serverClient } from "../../trpc"
|
||||||
|
|
||||||
import type { RoomsAvailabilityExtendedInputSchema } from "@scandic-hotels/trpc/types/availability"
|
import type { RoomsAvailabilityExtendedInputSchema } from "@scandic-hotels/trpc/routers/hotels/availability/enterDetails"
|
||||||
|
|
||||||
export const getSelectedRoomsAvailabilityEnterDetails = cache(
|
export const getSelectedRoomsAvailabilityEnterDetails = cache(
|
||||||
async function getMemoizedSelectedRoomsAvailability(
|
async function getMemoizedSelectedRoomsAvailability(
|
||||||
|
|||||||
@@ -10,12 +10,10 @@ import { safeProtectedServiceProcedure } from "../../procedures"
|
|||||||
import { getCityPageUrls } from "../../routers/contentstack/destinationCityPage/utils"
|
import { getCityPageUrls } from "../../routers/contentstack/destinationCityPage/utils"
|
||||||
import { getCountryPageUrls } from "../../routers/contentstack/destinationCountryPage/utils"
|
import { getCountryPageUrls } from "../../routers/contentstack/destinationCountryPage/utils"
|
||||||
import { getHotelPageUrls } from "../../routers/contentstack/hotelPage/utils"
|
import { getHotelPageUrls } from "../../routers/contentstack/hotelPage/utils"
|
||||||
import {
|
import { getLocations } from "../../routers/hotels/utils"
|
||||||
getCitiesByCountry,
|
|
||||||
getCountries,
|
|
||||||
getLocations,
|
|
||||||
} from "../../routers/hotels/utils"
|
|
||||||
import { ApiCountry, type Country } from "../../types/country"
|
import { ApiCountry, type Country } from "../../types/country"
|
||||||
|
import { getCitiesByCountry } from "../hotels/services/getCitiesByCountry"
|
||||||
|
import { getCountries } from "../hotels/services/getCountries"
|
||||||
import { filterAndCategorizeAutoComplete } from "./util/filterAndCategorizeAutoComplete"
|
import { filterAndCategorizeAutoComplete } from "./util/filterAndCategorizeAutoComplete"
|
||||||
import { mapLocationToAutoCompleteLocation } from "./util/mapLocationToAutoCompleteLocation"
|
import { mapLocationToAutoCompleteLocation } from "./util/mapLocationToAutoCompleteLocation"
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { createRefIdPlugin } from "../../plugins/refIdToConfirmationNumber"
|
|||||||
import { safeProtectedServiceProcedure } from "../../procedures"
|
import { safeProtectedServiceProcedure } from "../../procedures"
|
||||||
import { encrypt } from "../../utils/encryption"
|
import { encrypt } from "../../utils/encryption"
|
||||||
import { isValidSession } from "../../utils/session"
|
import { isValidSession } from "../../utils/session"
|
||||||
import { getMembershipNumber } from "../user/utils"
|
import { getMembershipNumber } from "../user/utils/getMemberShipNumber"
|
||||||
import {
|
import {
|
||||||
addPackageInput,
|
addPackageInput,
|
||||||
cancelBookingsInput,
|
cancelBookingsInput,
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
safeProtectedServiceProcedure,
|
safeProtectedServiceProcedure,
|
||||||
serviceProcedure,
|
serviceProcedure,
|
||||||
} from "../../procedures"
|
} from "../../procedures"
|
||||||
import { getHotel } from "../../routers/hotels/utils"
|
|
||||||
import { toApiLang } from "../../utils"
|
import { toApiLang } from "../../utils"
|
||||||
import { encrypt } from "../../utils/encryption"
|
import { encrypt } from "../../utils/encryption"
|
||||||
import { isValidSession } from "../../utils/session"
|
import { isValidSession } from "../../utils/session"
|
||||||
|
import { getHotel } from "../hotels/services/getHotel"
|
||||||
import { getHotelRoom } from "./helpers"
|
import { getHotelRoom } from "./helpers"
|
||||||
import {
|
import {
|
||||||
createRefIdInput,
|
createRefIdInput,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { request } from "../../../graphql/request"
|
import { request } from "../../../graphql/request"
|
||||||
import { contentStackUidWithServiceProcedure } from "../../../procedures"
|
import { contentStackUidWithServiceProcedure } from "../../../procedures"
|
||||||
import { generateRefsResponseTag } from "../../../utils/generateTag"
|
import { generateRefsResponseTag } from "../../../utils/generateTag"
|
||||||
import { getCityByCityIdentifier } from "../../hotels/utils"
|
import { getCityByCityIdentifier } from "../../hotels/services/getCityByCityIdentifier"
|
||||||
import {
|
import {
|
||||||
destinationCityPageRefsSchema,
|
destinationCityPageRefsSchema,
|
||||||
destinationCityPageSchema,
|
destinationCityPageSchema,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { request } from "../../../graphql/request"
|
|||||||
import { ApiCountry, type Country } from "../../../types/country"
|
import { ApiCountry, type Country } from "../../../types/country"
|
||||||
import { DestinationCountryPageEnum } from "../../../types/destinationCountryPage"
|
import { DestinationCountryPageEnum } from "../../../types/destinationCountryPage"
|
||||||
import { generateTag, generateTagsFromSystem } from "../../../utils/generateTag"
|
import { generateTag, generateTagsFromSystem } from "../../../utils/generateTag"
|
||||||
import { getCitiesByCountry } from "../../hotels/utils"
|
import { getCitiesByCountry } from "../../hotels/services/getCitiesByCountry"
|
||||||
import { destinationCityListDataSchema } from "../destinationCityPage/output"
|
import { destinationCityListDataSchema } from "../destinationCityPage/output"
|
||||||
import { countryPageUrlsSchema } from "./output"
|
import { countryPageUrlsSchema } from "./output"
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,9 @@ import {
|
|||||||
generateRefsResponseTag,
|
generateRefsResponseTag,
|
||||||
generateTag,
|
generateTag,
|
||||||
} from "../../../utils/generateTag"
|
} from "../../../utils/generateTag"
|
||||||
import {
|
import { getCitiesByCountry } from "../../hotels/services/getCitiesByCountry"
|
||||||
getCitiesByCountry,
|
import { getCountries } from "../../hotels/services/getCountries"
|
||||||
getCountries,
|
import { getHotelIdsByCityId } from "../../hotels/services/getHotelIdsByCityId"
|
||||||
getHotelIdsByCityId,
|
|
||||||
} from "../../hotels/utils"
|
|
||||||
import { getCityPageUrls } from "../destinationCityPage/utils"
|
import { getCityPageUrls } from "../destinationCityPage/utils"
|
||||||
import { getCountryPageUrls } from "../destinationCountryPage/utils"
|
import { getCountryPageUrls } from "../destinationCountryPage/utils"
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { GetStartPageMetadata } from "../../../graphql/Query/StartPage/Metadata.
|
|||||||
import { request } from "../../../graphql/request"
|
import { request } from "../../../graphql/request"
|
||||||
import { contentStackUidWithServiceProcedure } from "../../../procedures"
|
import { contentStackUidWithServiceProcedure } from "../../../procedures"
|
||||||
import { generateTag } from "../../../utils/generateTag"
|
import { generateTag } from "../../../utils/generateTag"
|
||||||
import { getHotel } from "../../hotels/utils"
|
import { getHotel } from "../../hotels/services/getHotel"
|
||||||
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
|
import { getUrlsOfAllLanguages } from "../languageSwitcher/utils"
|
||||||
import { getMetadataInput } from "./input"
|
import { getMetadataInput } from "./input"
|
||||||
import { getNonContentstackUrls, rawMetadataSchema } from "./output"
|
import { getNonContentstackUrls, rawMetadataSchema } from "./output"
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import { getSortedCities } from "../../../utils/getSortedCities"
|
|||||||
import {
|
import {
|
||||||
getCityByCityIdentifier,
|
getCityByCityIdentifier,
|
||||||
getHotelIdsByCityIdentifier,
|
getHotelIdsByCityIdentifier,
|
||||||
getHotelIdsByCountry,
|
} from "../../hotels/services/getCityByCityIdentifier"
|
||||||
getHotelsByHotelIds,
|
import { getHotelIdsByCountry } from "../../hotels/services/getHotelIdsByCountry"
|
||||||
} from "../../hotels/utils"
|
import { getHotelsByHotelIds } from "../../hotels/services/getHotelsByHotelIds"
|
||||||
import { getCityPages } from "../destinationCountryPage/utils"
|
import { getCityPages } from "../destinationCountryPage/utils"
|
||||||
import { transformDestinationFiltersResponse } from "../schemas/destinationFilters"
|
import { transformDestinationFiltersResponse } from "../schemas/destinationFilters"
|
||||||
|
|
||||||
|
|||||||
178
packages/trpc/lib/routers/hotels/availability/enterDetails.ts
Normal file
178
packages/trpc/lib/routers/hotels/availability/enterDetails.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
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 { getVerifiedUser } from "../../user/utils/getVerifiedUser"
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
|
||||||
|
const logger = createLogger("trpc:availability:enterDetails")
|
||||||
|
export const 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) {
|
||||||
|
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
|
||||||
|
})
|
||||||
122
packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts
Normal file
122
packages/trpc/lib/routers/hotels/availability/hotelsByCity.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import dayjs from "dayjs"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
|
||||||
|
import { env } from "../../../../env/server"
|
||||||
|
import { unauthorizedError } from "../../../errors"
|
||||||
|
import { safeProtectedServiceProcedure } from "../../../procedures"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { getVerifiedUser } from "../../user/utils/getVerifiedUser"
|
||||||
|
import { getHotelsAvailabilityByCity } from "../services/getHotelsAvailabilityByCity"
|
||||||
|
|
||||||
|
export type HotelsAvailabilityInputSchema = z.output<
|
||||||
|
typeof hotelsAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
export const hotelsAvailabilityInputSchema = z
|
||||||
|
.object({
|
||||||
|
cityId: z.string(),
|
||||||
|
roomStayStartDate: z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
const fromDate = dayjs(val)
|
||||||
|
|
||||||
|
return fromDate.isValid()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_INVALID",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
roomStayEndDate: z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
const fromDate = dayjs(val)
|
||||||
|
return fromDate.isValid()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "TODATE_INVALID",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
adults: z.number(),
|
||||||
|
children: z.string().optional(),
|
||||||
|
bookingCode: z.string().optional().default(""),
|
||||||
|
redemption: z.boolean().optional().default(false),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.roomStayStartDate).startOf("day")
|
||||||
|
const toDate = dayjs(data.roomStayEndDate).startOf("day")
|
||||||
|
|
||||||
|
return fromDate.isBefore(toDate)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_BEFORE_TODATE",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.roomStayStartDate)
|
||||||
|
const today = dayjs().startOf("day")
|
||||||
|
|
||||||
|
return fromDate.isSameOrAfter(today)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_CANNOT_BE_IN_THE_PAST",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const 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
|
||||||
|
)
|
||||||
|
})
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { serviceProcedure } from "../../../procedures"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { getHotelsAvailabilityByCity } from "../services/getHotelsAvailabilityByCity"
|
||||||
|
import { getHotelsAvailabilityByHotelIds } from "../services/getHotelsAvailabilityByHotelIds"
|
||||||
|
import { hotelsAvailabilityInputSchema } from "./hotelsByCity"
|
||||||
|
|
||||||
|
export const 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),
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import dayjs from "dayjs"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { unauthorizedError } from "../../../errors"
|
||||||
|
import { safeProtectedServiceProcedure } from "../../../procedures"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { getVerifiedUser } from "../../user/utils/getVerifiedUser"
|
||||||
|
import { getHotelsAvailabilityByHotelIds } from "../services/getHotelsAvailabilityByHotelIds"
|
||||||
|
|
||||||
|
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
||||||
|
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
export const getHotelsByHotelIdsAvailabilityInputSchema = z
|
||||||
|
.object({
|
||||||
|
hotelIds: z.array(z.number()),
|
||||||
|
roomStayStartDate: z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
const fromDate = dayjs(val)
|
||||||
|
return fromDate.isValid()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_INVALID",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
roomStayEndDate: z.string().refine(
|
||||||
|
(val) => {
|
||||||
|
const toDate = dayjs(val)
|
||||||
|
|
||||||
|
return toDate.isValid()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "TODATE_INVALID",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
adults: z.number(),
|
||||||
|
children: z.string().optional(),
|
||||||
|
bookingCode: z.string().optional().default(""),
|
||||||
|
redemption: z.boolean().optional().default(false),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.roomStayStartDate).startOf("day")
|
||||||
|
const toDate = dayjs(data.roomStayEndDate).startOf("day")
|
||||||
|
|
||||||
|
return fromDate.isBefore(toDate)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_BEFORE_TODATE",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.roomStayStartDate)
|
||||||
|
const today = dayjs().startOf("day")
|
||||||
|
|
||||||
|
return fromDate.isSameOrAfter(today)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_CANNOT_BE_IN_THE_PAST",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export const 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
|
||||||
|
)
|
||||||
|
})
|
||||||
16
packages/trpc/lib/routers/hotels/availability/index.ts
Normal file
16
packages/trpc/lib/routers/hotels/availability/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { router } from "../../.."
|
||||||
|
import { enterDetails } from "./enterDetails"
|
||||||
|
import { hotelsByCity } from "./hotelsByCity"
|
||||||
|
import { hotelsByCityWithBookingCode } from "./hotelsByCityWithBookingCode"
|
||||||
|
import { hotelsByHotelIds } from "./hotelsByHotelIds"
|
||||||
|
import { myStay } from "./myStay"
|
||||||
|
import { selectRate } from "./selectRate"
|
||||||
|
|
||||||
|
export const availability = router({
|
||||||
|
hotelsByCity,
|
||||||
|
hotelsByHotelIds,
|
||||||
|
enterDetails,
|
||||||
|
myStay,
|
||||||
|
selectRate,
|
||||||
|
hotelsByCityWithBookingCode,
|
||||||
|
})
|
||||||
81
packages/trpc/lib/routers/hotels/availability/myStay.ts
Normal file
81
packages/trpc/lib/routers/hotels/availability/myStay.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
|
|
||||||
|
import { SEARCH_TYPE_REDEMPTION } from "../../../constants/booking"
|
||||||
|
import { unauthorizedError } from "../../../errors"
|
||||||
|
import { safeProtectedServiceProcedure } from "../../../procedures"
|
||||||
|
import { getVerifiedUser } from "../../user/utils/getVerifiedUser"
|
||||||
|
import { baseBookingSchema, baseRoomSchema, selectedRoomSchema } from "../input"
|
||||||
|
import { getRoomsAvailability } from "../services/getRoomsAvailability"
|
||||||
|
import { getSelectedRoomAvailability } from "../utils"
|
||||||
|
|
||||||
|
export const myStayRoomAvailabilityInputSchema = z.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
room: baseRoomSchema.merge(selectedRoomSchema),
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
const logger = createLogger("trpc:availability:myStay")
|
||||||
|
export const 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) {
|
||||||
|
logger.error("Unable to find selected room")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
product: selected.product,
|
||||||
|
selectedRoom: selected.selectedRoom,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { router } from "../../../.."
|
||||||
|
import { room } from "./room"
|
||||||
|
import { rooms } from "./rooms"
|
||||||
|
|
||||||
|
export const selectRate = router({
|
||||||
|
room,
|
||||||
|
rooms,
|
||||||
|
})
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
import { SEARCH_TYPE_REDEMPTION } from "../../../../constants/booking"
|
||||||
|
import { unauthorizedError } from "../../../../errors"
|
||||||
|
import { safeProtectedServiceProcedure } from "../../../../procedures"
|
||||||
|
import { getVerifiedUser } from "../../../user/utils/getVerifiedUser"
|
||||||
|
import { baseBookingSchema, baseRoomSchema } from "../../input"
|
||||||
|
import { getRoomsAvailability } from "../../services/getRoomsAvailability"
|
||||||
|
import { mergeRoomTypes } from "../../utils"
|
||||||
|
|
||||||
|
export const selectRateRoomAvailabilityInputSchema = z.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
room: baseRoomSchema,
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const 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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import "server-only"
|
||||||
|
|
||||||
|
import { SEARCH_TYPE_REDEMPTION } from "../../../../../constants/booking"
|
||||||
|
import { unauthorizedError } from "../../../../../errors"
|
||||||
|
import { safeProtectedServiceProcedure } from "../../../../../procedures"
|
||||||
|
import { getVerifiedUser } from "../../../../user/utils/getVerifiedUser"
|
||||||
|
import { getRoomsAvailability } from "../../../services/getRoomsAvailability"
|
||||||
|
import { mergeRoomTypes } from "../../../utils"
|
||||||
|
import { selectRateRoomsAvailabilityInputSchema } from "./schema"
|
||||||
|
|
||||||
|
export const 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
|
||||||
|
})
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import dayjs from "dayjs"
|
||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
import { baseBookingSchema, baseRoomSchema } from "../../../input"
|
||||||
|
|
||||||
|
export type RoomsAvailabilityInputRoom =
|
||||||
|
RoomsAvailabilityInputSchema["booking"]["rooms"][number]
|
||||||
|
export type RoomsAvailabilityOutputSchema = z.output<
|
||||||
|
typeof selectRateRoomsAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
export type RoomsAvailabilityInputSchema = z.input<
|
||||||
|
typeof selectRateRoomsAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
export const selectRateRoomsAvailabilityInputSchema = z
|
||||||
|
.object({
|
||||||
|
booking: baseBookingSchema.extend({
|
||||||
|
rooms: z.array(baseRoomSchema),
|
||||||
|
}),
|
||||||
|
lang: z.nativeEnum(Lang),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.booking.fromDate)
|
||||||
|
|
||||||
|
return fromDate.isValid()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_INVALID",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const toDate = dayjs(data.booking.toDate)
|
||||||
|
|
||||||
|
return toDate.isValid()
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "TODATE_INVALID",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.booking.fromDate).startOf("day")
|
||||||
|
const toDate = dayjs(data.booking.toDate).startOf("day")
|
||||||
|
|
||||||
|
return fromDate.isBefore(toDate)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "TODATE_MUST_BE_AFTER_FROMDATE",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.refine(
|
||||||
|
(data) => {
|
||||||
|
const fromDate = dayjs(data.booking.fromDate)
|
||||||
|
const today = dayjs().startOf("day")
|
||||||
|
|
||||||
|
return fromDate.isSameOrAfter(today)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: "FROMDATE_CANNOT_BE_IN_THE_PAST",
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import dayjs from "dayjs"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
@@ -8,106 +7,6 @@ import { ChildBedMapEnum } from "../../enums/childBedMapEnum"
|
|||||||
import { RoomPackageCodeEnum } from "../../enums/roomFilter"
|
import { RoomPackageCodeEnum } from "../../enums/roomFilter"
|
||||||
import { Country } from "../../types/country"
|
import { Country } from "../../types/country"
|
||||||
|
|
||||||
export const hotelsAvailabilityInputSchema = z
|
|
||||||
.object({
|
|
||||||
cityId: z.string(),
|
|
||||||
roomStayStartDate: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
const fromDate = dayjs(val)
|
|
||||||
|
|
||||||
return fromDate.isValid()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_INVALID",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
roomStayEndDate: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
const fromDate = dayjs(val)
|
|
||||||
return fromDate.isValid()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "TODATE_INVALID",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
adults: z.number(),
|
|
||||||
children: z.string().optional(),
|
|
||||||
bookingCode: z.string().optional().default(""),
|
|
||||||
redemption: z.boolean().optional().default(false),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.roomStayStartDate).startOf("day")
|
|
||||||
const toDate = dayjs(data.roomStayEndDate).startOf("day")
|
|
||||||
|
|
||||||
return fromDate.isBefore(toDate)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_BEFORE_TODATE",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.roomStayStartDate)
|
|
||||||
const today = dayjs().startOf("day")
|
|
||||||
|
|
||||||
return fromDate.isSameOrAfter(today)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_CANNOT_BE_IN_THE_PAST",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const getHotelsByHotelIdsAvailabilityInputSchema = z
|
|
||||||
.object({
|
|
||||||
hotelIds: z.array(z.number()),
|
|
||||||
roomStayStartDate: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
const fromDate = dayjs(val)
|
|
||||||
return fromDate.isValid()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_INVALID",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
roomStayEndDate: z.string().refine(
|
|
||||||
(val) => {
|
|
||||||
const toDate = dayjs(val)
|
|
||||||
|
|
||||||
return toDate.isValid()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "TODATE_INVALID",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
adults: z.number(),
|
|
||||||
children: z.string().optional(),
|
|
||||||
bookingCode: z.string().optional().default(""),
|
|
||||||
redemption: z.boolean().optional().default(false),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.roomStayStartDate).startOf("day")
|
|
||||||
const toDate = dayjs(data.roomStayEndDate).startOf("day")
|
|
||||||
|
|
||||||
return fromDate.isBefore(toDate)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_BEFORE_TODATE",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.roomStayStartDate)
|
|
||||||
const today = dayjs().startOf("day")
|
|
||||||
|
|
||||||
return fromDate.isSameOrAfter(today)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_CANNOT_BE_IN_THE_PAST",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const childrenInRoomSchema = z
|
const childrenInRoomSchema = z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -117,7 +16,7 @@ const childrenInRoomSchema = z
|
|||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
|
|
||||||
const baseRoomSchema = z.object({
|
export const baseRoomSchema = z.object({
|
||||||
adults: z.number().int().min(1),
|
adults: z.number().int().min(1),
|
||||||
bookingCode: z.string().optional(),
|
bookingCode: z.string().optional(),
|
||||||
childrenInRoom: childrenInRoomSchema,
|
childrenInRoom: childrenInRoomSchema,
|
||||||
@@ -126,13 +25,13 @@ const baseRoomSchema = z.object({
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedRoomSchema = z.object({
|
export const selectedRoomSchema = z.object({
|
||||||
counterRateCode: z.string().optional(),
|
counterRateCode: z.string().optional(),
|
||||||
rateCode: z.string(),
|
rateCode: z.string(),
|
||||||
roomTypeCode: z.string(),
|
roomTypeCode: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const baseBookingSchema = z.object({
|
export const baseBookingSchema = z.object({
|
||||||
bookingCode: z.string().optional(),
|
bookingCode: z.string().optional(),
|
||||||
fromDate: z.string(),
|
fromDate: z.string(),
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
@@ -140,77 +39,6 @@ const baseBookingSchema = z.object({
|
|||||||
toDate: z.string(),
|
toDate: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const selectRateRoomsAvailabilityInputSchema = z
|
|
||||||
.object({
|
|
||||||
booking: baseBookingSchema.extend({
|
|
||||||
rooms: z.array(baseRoomSchema),
|
|
||||||
}),
|
|
||||||
lang: z.nativeEnum(Lang),
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.booking.fromDate)
|
|
||||||
|
|
||||||
return fromDate.isValid()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_INVALID",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const toDate = dayjs(data.booking.toDate)
|
|
||||||
|
|
||||||
return toDate.isValid()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "TODATE_INVALID",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.booking.fromDate).startOf("day")
|
|
||||||
const toDate = dayjs(data.booking.toDate).startOf("day")
|
|
||||||
|
|
||||||
return fromDate.isBefore(toDate)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "TODATE_MUST_BE_AFTER_FROMDATE",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
const fromDate = dayjs(data.booking.fromDate)
|
|
||||||
const today = dayjs().startOf("day")
|
|
||||||
|
|
||||||
return fromDate.isSameOrAfter(today)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "FROMDATE_CANNOT_BE_IN_THE_PAST",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
export const selectRateRoomAvailabilityInputSchema = z.object({
|
|
||||||
booking: baseBookingSchema.extend({
|
|
||||||
room: baseRoomSchema,
|
|
||||||
}),
|
|
||||||
lang: z.nativeEnum(Lang),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const enterDetailsRoomsAvailabilityInputSchema = z.object({
|
|
||||||
booking: baseBookingSchema.extend({
|
|
||||||
rooms: z.array(baseRoomSchema.merge(selectedRoomSchema)),
|
|
||||||
}),
|
|
||||||
lang: z.nativeEnum(Lang),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const myStayRoomAvailabilityInputSchema = z.object({
|
|
||||||
booking: baseBookingSchema.extend({
|
|
||||||
room: baseRoomSchema.merge(selectedRoomSchema),
|
|
||||||
}),
|
|
||||||
lang: z.nativeEnum(Lang),
|
|
||||||
})
|
|
||||||
|
|
||||||
export const roomFeaturesInputSchema = z.object({
|
export const roomFeaturesInputSchema = z.object({
|
||||||
adults: z.number(),
|
adults: z.number(),
|
||||||
childrenInRoom: childrenInRoomSchema,
|
childrenInRoom: childrenInRoomSchema,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
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 { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
import { dt } from "@scandic-hotels/common/dt"
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
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 { env } from "../../../env/server"
|
||||||
import { router } from "../.."
|
import { router } from "../.."
|
||||||
import * as api from "../../api"
|
import * as api from "../../api"
|
||||||
import { SEARCH_TYPE_REDEMPTION } from "../../constants/booking"
|
|
||||||
import { BreakfastPackageEnum } from "../../enums/breakfast"
|
import { BreakfastPackageEnum } from "../../enums/breakfast"
|
||||||
import { AvailabilityEnum } from "../../enums/selectHotel"
|
import { badRequestError } from "../../errors"
|
||||||
import { badRequestError, unauthorizedError } from "../../errors"
|
|
||||||
import {
|
import {
|
||||||
contentStackBaseWithServiceProcedure,
|
contentStackBaseWithServiceProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
@@ -25,23 +21,17 @@ import {
|
|||||||
ancillaryPackageInputSchema,
|
ancillaryPackageInputSchema,
|
||||||
breakfastPackageInputSchema,
|
breakfastPackageInputSchema,
|
||||||
cityCoordinatesInputSchema,
|
cityCoordinatesInputSchema,
|
||||||
enterDetailsRoomsAvailabilityInputSchema,
|
|
||||||
getAdditionalDataInputSchema,
|
getAdditionalDataInputSchema,
|
||||||
getDestinationsMapDataInput,
|
getDestinationsMapDataInput,
|
||||||
getHotelsByCityIdentifierInput,
|
getHotelsByCityIdentifierInput,
|
||||||
getHotelsByCountryInput,
|
getHotelsByCountryInput,
|
||||||
getHotelsByCSFilterInput,
|
getHotelsByCSFilterInput,
|
||||||
getHotelsByHotelIdsAvailabilityInputSchema,
|
|
||||||
getLocationsInput,
|
getLocationsInput,
|
||||||
getLocationsUrlsInput,
|
getLocationsUrlsInput,
|
||||||
getMeetingRoomsInputSchema,
|
getMeetingRoomsInputSchema,
|
||||||
hotelInputSchema,
|
hotelInputSchema,
|
||||||
hotelsAvailabilityInputSchema,
|
|
||||||
myStayRoomAvailabilityInputSchema,
|
|
||||||
nearbyHotelIdsInput,
|
nearbyHotelIdsInput,
|
||||||
roomPackagesInputSchema,
|
roomPackagesInputSchema,
|
||||||
selectRateRoomAvailabilityInputSchema,
|
|
||||||
selectRateRoomsAvailabilityInputSchema,
|
|
||||||
} from "../../routers/hotels/input"
|
} from "../../routers/hotels/input"
|
||||||
import {
|
import {
|
||||||
ancillaryPackagesSchema,
|
ancillaryPackagesSchema,
|
||||||
@@ -49,498 +39,26 @@ import {
|
|||||||
getNearbyHotelIdsSchema,
|
getNearbyHotelIdsSchema,
|
||||||
} from "../../routers/hotels/output"
|
} from "../../routers/hotels/output"
|
||||||
import { toApiLang } from "../../utils"
|
import { toApiLang } from "../../utils"
|
||||||
import { getVerifiedUser } from "../user/utils"
|
|
||||||
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
import { additionalDataSchema } from "./schemas/hotel/include/additionalData"
|
||||||
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
import { meetingRoomsSchema } from "./schemas/meetingRoom"
|
||||||
import {
|
import { getCitiesByCountry } from "./services/getCitiesByCountry"
|
||||||
getBedTypes,
|
import { getHotelIdsByCityIdentifier } from "./services/getCityByCityIdentifier"
|
||||||
getCitiesByCountry,
|
import { getCountries } from "./services/getCountries"
|
||||||
getCountries,
|
import { getHotel } from "./services/getHotel"
|
||||||
getHotel,
|
import { getHotelIdsByCityId } from "./services/getHotelIdsByCityId"
|
||||||
getHotelIdsByCityId,
|
import { getHotelIdsByCountry } from "./services/getHotelIdsByCountry"
|
||||||
getHotelIdsByCityIdentifier,
|
import { getHotelsByHotelIds } from "./services/getHotelsByHotelIds"
|
||||||
getHotelIdsByCountry,
|
import { getPackages } from "./services/getPackages"
|
||||||
getHotelsAvailabilityByCity,
|
import { availability } from "./availability"
|
||||||
getHotelsAvailabilityByHotelIds,
|
import { getLocations } from "./utils"
|
||||||
getHotelsByHotelIds,
|
|
||||||
getLocations,
|
|
||||||
getPackages,
|
|
||||||
getRoomsAvailability,
|
|
||||||
getSelectedRoomAvailability,
|
|
||||||
mergeRoomTypes,
|
|
||||||
selectRateRedirectURL,
|
|
||||||
} from "./utils"
|
|
||||||
|
|
||||||
import type { HotelListingHotelData } from "../../types/hotel"
|
import type { HotelListingHotelData } from "../../types/hotel"
|
||||||
import type { CityLocation } from "../../types/locations"
|
import type { CityLocation } from "../../types/locations"
|
||||||
import type { Room } from "../../types/room"
|
|
||||||
|
|
||||||
const hotelQueryLogger = createLogger("hotelQueryRouter")
|
const hotelQueryLogger = createLogger("hotelQueryRouter")
|
||||||
|
|
||||||
export const hotelQueryRouter = router({
|
export const hotelQueryRouter = router({
|
||||||
availability: router({
|
availability,
|
||||||
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),
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
get: serviceProcedure
|
get: serviceProcedure
|
||||||
.input(hotelInputSchema)
|
.input(hotelInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { citiesByCountrySchema } from "../output"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
import type { CitiesGroupedByCountry } from "../../../types/locations"
|
||||||
|
|
||||||
|
const logger = createLogger("trpc:hotels:getCitiesByCountry")
|
||||||
|
export const locationsAffix = "locations"
|
||||||
|
export async function getCitiesByCountry({
|
||||||
|
countries,
|
||||||
|
lang,
|
||||||
|
affix = locationsAffix,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
countries: string[]
|
||||||
|
lang: Lang
|
||||||
|
affix?: string
|
||||||
|
serviceToken: string
|
||||||
|
}): Promise<CitiesGroupedByCountry> {
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const allCitiesByCountries = await Promise.all(
|
||||||
|
countries.map(async (country) => {
|
||||||
|
return cacheClient.cacheOrGet(
|
||||||
|
`${lang}:${affix}:cities-by-country:${country}`,
|
||||||
|
async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
language: toApiLang(lang),
|
||||||
|
})
|
||||||
|
const countryResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.Cities.country(country),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!countryResponse.ok) {
|
||||||
|
throw new Error(`Unable to fetch cities by country ${country}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryJson = await countryResponse.json()
|
||||||
|
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
|
||||||
|
if (!citiesByCountry.success) {
|
||||||
|
logger.error(
|
||||||
|
`Unable to parse cities by country ${country}`,
|
||||||
|
citiesByCountry.error
|
||||||
|
)
|
||||||
|
|
||||||
|
throw new Error(`Unable to parse cities by country ${country}`)
|
||||||
|
}
|
||||||
|
return { ...citiesByCountry.data, country }
|
||||||
|
},
|
||||||
|
"1d"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredCitiesByCountries = allCitiesByCountries.map((country) => ({
|
||||||
|
...country,
|
||||||
|
data: country.data.filter((city) => city.isPublished),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const groupedCitiesByCountry: CitiesGroupedByCountry =
|
||||||
|
filteredCitiesByCountries.reduce((acc, { country, data }) => {
|
||||||
|
acc[country] = data
|
||||||
|
return acc
|
||||||
|
}, {} as CitiesGroupedByCountry)
|
||||||
|
|
||||||
|
return groupedCitiesByCountry
|
||||||
|
}
|
||||||
46
packages/trpc/lib/routers/hotels/services/getCity.ts
Normal file
46
packages/trpc/lib/routers/hotels/services/getCity.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { type Cities, citiesSchema } from "../output"
|
||||||
|
|
||||||
|
import type { Endpoint } from "../../../api/endpoints"
|
||||||
|
|
||||||
|
const logger = createLogger("trpc:hotels:getCity")
|
||||||
|
export async function getCity({
|
||||||
|
cityUrl,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
cityUrl: string
|
||||||
|
serviceToken: string
|
||||||
|
}): Promise<Cities> {
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
cityUrl,
|
||||||
|
async () => {
|
||||||
|
const url = new URL(cityUrl)
|
||||||
|
const cityResponse = await api.get(
|
||||||
|
url.pathname as Endpoint,
|
||||||
|
{ headers: { Authorization: `Bearer ${serviceToken}` } },
|
||||||
|
url.searchParams
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!cityResponse.ok) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const cityJson = await cityResponse.json()
|
||||||
|
const city = citiesSchema.safeParse(cityJson)
|
||||||
|
if (!city.success) {
|
||||||
|
logger.error(`Validation of city failed`, {
|
||||||
|
error: city.error,
|
||||||
|
cityUrl,
|
||||||
|
})
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return city.data
|
||||||
|
},
|
||||||
|
"1d"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
import { getLocations } from "../utils"
|
||||||
|
import { getHotelIdsByCityId } from "./getHotelIdsByCityId"
|
||||||
|
|
||||||
|
export async function getCityByCityIdentifier({
|
||||||
|
cityIdentifier,
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
cityIdentifier: string
|
||||||
|
lang: Lang
|
||||||
|
serviceToken: string
|
||||||
|
}) {
|
||||||
|
const locations = await getLocations({
|
||||||
|
lang,
|
||||||
|
citiesByCountry: null,
|
||||||
|
serviceToken,
|
||||||
|
})
|
||||||
|
if (!locations || "error" in locations) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const city = locations
|
||||||
|
.filter((loc) => loc.type === "cities")
|
||||||
|
.find((loc) => loc.cityIdentifier === cityIdentifier)
|
||||||
|
|
||||||
|
return city ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHotelIdsByCityIdentifier(
|
||||||
|
cityIdentifier: string,
|
||||||
|
serviceToken: string
|
||||||
|
) {
|
||||||
|
const city = await getCityByCityIdentifier({
|
||||||
|
cityIdentifier,
|
||||||
|
lang: Lang.en,
|
||||||
|
serviceToken,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!city) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotelIds = await getHotelIdsByCityId({
|
||||||
|
cityId: city.id,
|
||||||
|
serviceToken,
|
||||||
|
})
|
||||||
|
return hotelIds
|
||||||
|
}
|
||||||
57
packages/trpc/lib/routers/hotels/services/getCountries.ts
Normal file
57
packages/trpc/lib/routers/hotels/services/getCountries.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { countriesSchema } from "../output"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
const logger = createLogger("getCountries")
|
||||||
|
const locationsAffix = "locations"
|
||||||
|
export async function getCountries({
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
warmup = false,
|
||||||
|
}: {
|
||||||
|
lang: Lang
|
||||||
|
serviceToken: string
|
||||||
|
warmup?: boolean
|
||||||
|
}) {
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${lang}:${locationsAffix}:countries`,
|
||||||
|
async () => {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
language: toApiLang(lang),
|
||||||
|
})
|
||||||
|
|
||||||
|
const countryResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.countries,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!countryResponse.ok) {
|
||||||
|
throw new Error("Unable to fetch countries")
|
||||||
|
}
|
||||||
|
|
||||||
|
const countriesJson = await countryResponse.json()
|
||||||
|
const countries = countriesSchema.safeParse(countriesJson)
|
||||||
|
if (!countries.success) {
|
||||||
|
logger.error(`Validation for countries failed`, countries.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return countries.data
|
||||||
|
},
|
||||||
|
"1d",
|
||||||
|
{
|
||||||
|
cacheStrategy: warmup ? "fetch-then-cache" : "cache-first",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
96
packages/trpc/lib/routers/hotels/services/getHotel.ts
Normal file
96
packages/trpc/lib/routers/hotels/services/getHotel.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import { env } from "../../../../env/server"
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { cache } from "../../../DUPLICATED/cache"
|
||||||
|
import { HotelTypeEnum } from "../../../enums/hotelType"
|
||||||
|
import { badRequestError } from "../../../errors"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { hotelSchema } from "../output"
|
||||||
|
|
||||||
|
import type { HotelInput } from "../../../types/hotel"
|
||||||
|
|
||||||
|
export const getHotel = cache(
|
||||||
|
async (input: HotelInput, serviceToken: string) => {
|
||||||
|
const { language, isCardOnlyPayment } = input
|
||||||
|
const hotelId = input.hotelId.trim()
|
||||||
|
|
||||||
|
const getHotelCounter = createCounter("hotel", "getHotel")
|
||||||
|
const metricsGetHotel = getHotelCounter.init({
|
||||||
|
hotelId,
|
||||||
|
language,
|
||||||
|
isCardOnlyPayment,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetHotel.start()
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
const result = await cacheClient.cacheOrGet(
|
||||||
|
`${language}:hotel:${hotelId}:${!!isCardOnlyPayment}`,
|
||||||
|
async () => {
|
||||||
|
/**
|
||||||
|
* Since API expects the params appended and not just
|
||||||
|
* a comma separated string we need to initialize the
|
||||||
|
* SearchParams with a sequence of pairs
|
||||||
|
* (include=City&include=NearbyHotels&include=Restaurants etc.)
|
||||||
|
**/
|
||||||
|
const params = new URLSearchParams([
|
||||||
|
["include", "AdditionalData"],
|
||||||
|
["include", "City"],
|
||||||
|
["include", "NearbyHotels"],
|
||||||
|
["include", "Restaurants"],
|
||||||
|
["include", "RoomCategories"],
|
||||||
|
["language", toApiLang(language)],
|
||||||
|
])
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetHotel.httpError(apiResponse)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateHotelData = hotelSchema.safeParse(apiJson)
|
||||||
|
|
||||||
|
if (!validateHotelData.success) {
|
||||||
|
metricsGetHotel.validationError(validateHotelData.error)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotelData = validateHotelData.data
|
||||||
|
|
||||||
|
if (isCardOnlyPayment) {
|
||||||
|
hotelData.hotel.merchantInformationData.alternatePaymentOptions = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const gallery = hotelData.additionalData?.gallery
|
||||||
|
if (gallery) {
|
||||||
|
const smallerImages = gallery.smallerImages
|
||||||
|
const hotelGalleryImages =
|
||||||
|
hotelData.hotel.hotelType === HotelTypeEnum.Signature
|
||||||
|
? smallerImages.slice(0, 10)
|
||||||
|
: smallerImages.slice(0, 6)
|
||||||
|
hotelData.hotel.galleryImages = hotelGalleryImages
|
||||||
|
}
|
||||||
|
|
||||||
|
return hotelData
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_HOTELS
|
||||||
|
)
|
||||||
|
|
||||||
|
metricsGetHotel.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import { env } from "../../../../env/server"
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { getHotelIdsSchema } from "../output"
|
||||||
|
|
||||||
|
export async function getHotelIdsByCityId({
|
||||||
|
cityId,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
cityId: string
|
||||||
|
serviceToken: string
|
||||||
|
}) {
|
||||||
|
const getHotelIdsByCityIdCounter = createCounter(
|
||||||
|
"hotel",
|
||||||
|
"getHotelIdsByCityId"
|
||||||
|
)
|
||||||
|
const metricsGetHotelIdsByCityId = getHotelIdsByCityIdCounter.init({
|
||||||
|
cityId,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetHotelIdsByCityId.start()
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const result = await cacheClient.cacheOrGet(
|
||||||
|
`${cityId}:hotelsByCityId`,
|
||||||
|
async () => {
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
city: cityId,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.hotels,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchParams
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetHotelIdsByCityId.httpError(apiResponse)
|
||||||
|
throw new Error("Unable to fetch hotelIds by cityId")
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||||
|
if (!validatedHotelIds.success) {
|
||||||
|
metricsGetHotelIdsByCityId.validationError(validatedHotelIds.error)
|
||||||
|
throw new Error("Unable to parse data for hotelIds by cityId")
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedHotelIds.data
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_HOTELS
|
||||||
|
)
|
||||||
|
|
||||||
|
metricsGetHotelIdsByCityId.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import { env } from "../../../../env/server"
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { getHotelIdsSchema } from "../output"
|
||||||
|
|
||||||
|
export async function getHotelIdsByCountry({
|
||||||
|
country,
|
||||||
|
serviceToken,
|
||||||
|
}: {
|
||||||
|
country: string
|
||||||
|
serviceToken: string
|
||||||
|
}) {
|
||||||
|
const getHotelIdsByCountryCounter = createCounter(
|
||||||
|
"hotel",
|
||||||
|
"getHotelIdsByCountry"
|
||||||
|
)
|
||||||
|
|
||||||
|
const metricsGetHotelIdsByCountry = getHotelIdsByCountryCounter.init({
|
||||||
|
country,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetHotelIdsByCountry.start()
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
const result = await cacheClient.cacheOrGet(
|
||||||
|
`${country}:hotelsByCountry`,
|
||||||
|
async () => {
|
||||||
|
const hotelIdsParams = new URLSearchParams({
|
||||||
|
country,
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.hotels,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hotelIdsParams
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetHotelIdsByCountry.httpError(apiResponse)
|
||||||
|
throw new Error("Unable to fetch hotelIds by country")
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||||
|
if (!validatedHotelIds.success) {
|
||||||
|
metricsGetHotelIdsByCountry.validationError(validatedHotelIds.error)
|
||||||
|
throw new Error("Unable to parse hotelIds by country")
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedHotelIds.data
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_HOTELS
|
||||||
|
)
|
||||||
|
|
||||||
|
metricsGetHotelIdsByCountry.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { badRequestError } from "../../../errors"
|
||||||
|
import { hotelsAvailabilitySchema } from "../output"
|
||||||
|
|
||||||
|
import type { HotelsAvailabilityInputSchema } from "../availability/hotelsByCity"
|
||||||
|
|
||||||
|
export async function getHotelsAvailabilityByCity(
|
||||||
|
input: HotelsAvailabilityInputSchema,
|
||||||
|
apiLang: string,
|
||||||
|
token: string, // Either service token or user access token in case of redemption search
|
||||||
|
userPoints: number = 0
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params: Record<string, string | number> = {
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
...(children && { children }),
|
||||||
|
...(bookingCode && { bookingCode }),
|
||||||
|
...(redemption ? { isRedemption: "true" } : {}),
|
||||||
|
language: apiLang,
|
||||||
|
}
|
||||||
|
|
||||||
|
const getHotelsAvailabilityByCityCounter = createCounter(
|
||||||
|
"hotel",
|
||||||
|
"getHotelsAvailabilityByCity"
|
||||||
|
)
|
||||||
|
const metricsGetHotelsAvailabilityByCity =
|
||||||
|
getHotelsAvailabilityByCityCounter.init({
|
||||||
|
apiLang,
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetHotelsAvailabilityByCity.start()
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.city(cityId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetHotelsAvailabilityByCity.httpError(apiResponse)
|
||||||
|
throw new Error("Failed to fetch hotels availability by city")
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
metricsGetHotelsAvailabilityByCity.validationError(
|
||||||
|
validateAvailabilityData.error
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redemption) {
|
||||||
|
validateAvailabilityData.data.data.forEach((data) => {
|
||||||
|
data.attributes.productType?.redemptions?.forEach((r) => {
|
||||||
|
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
availability: validateAvailabilityData.data.data.flatMap(
|
||||||
|
(hotels) => hotels.attributes
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsGetHotelsAvailabilityByCity.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import { env } from "../../../../env/server"
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { badRequestError } from "../../../errors"
|
||||||
|
import { hotelsAvailabilitySchema } from "../output"
|
||||||
|
|
||||||
|
import type { HotelsByHotelIdsAvailabilityInputSchema } from "../availability/hotelsByHotelIds"
|
||||||
|
|
||||||
|
export async function getHotelsAvailabilityByHotelIds(
|
||||||
|
input: HotelsByHotelIdsAvailabilityInputSchema,
|
||||||
|
apiLang: string,
|
||||||
|
token: string,
|
||||||
|
userPoints: number = 0
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
hotelIds,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params = new URLSearchParams([
|
||||||
|
["roomStayStartDate", roomStayStartDate],
|
||||||
|
["roomStayEndDate", roomStayEndDate],
|
||||||
|
["adults", adults.toString()],
|
||||||
|
["children", children ?? ""],
|
||||||
|
["bookingCode", bookingCode],
|
||||||
|
["isRedemption", redemption.toString()],
|
||||||
|
["language", apiLang],
|
||||||
|
])
|
||||||
|
|
||||||
|
const getHotelsAvailabilityByHotelIdsCounter = createCounter(
|
||||||
|
"hotel",
|
||||||
|
"getHotelsAvailabilityByHotelIds"
|
||||||
|
)
|
||||||
|
const metricsGetHotelsAvailabilityByHotelIds =
|
||||||
|
getHotelsAvailabilityByHotelIdsCounter.init({
|
||||||
|
apiLang,
|
||||||
|
hotelIds,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetHotelsAvailabilityByHotelIds.start()
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
const result = cacheClient.cacheOrGet(
|
||||||
|
`${apiLang}:hotels:availability:${hotelIds.join(",")}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||||
|
async () => {
|
||||||
|
/**
|
||||||
|
* Since API expects the params appended and not just
|
||||||
|
* a comma separated string we need to initialize the
|
||||||
|
* SearchParams with a sequence of pairs
|
||||||
|
* (hotelIds=810&hotelIds=879&hotelIds=222 etc.)
|
||||||
|
**/
|
||||||
|
|
||||||
|
hotelIds.forEach((hotelId) =>
|
||||||
|
params.append("hotelIds", hotelId.toString())
|
||||||
|
)
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.hotels(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetHotelsAvailabilityByHotelIds.httpError(apiResponse)
|
||||||
|
throw new Error("Failed to fetch hotels availability by hotelIds")
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateAvailabilityData =
|
||||||
|
hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
metricsGetHotelsAvailabilityByHotelIds.validationError(
|
||||||
|
validateAvailabilityData.error
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (redemption) {
|
||||||
|
validateAvailabilityData.data.data.forEach((data) => {
|
||||||
|
data.attributes.productType?.redemptions?.forEach((r) => {
|
||||||
|
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
availability: validateAvailabilityData.data.data.flatMap(
|
||||||
|
(hotels) => hotels.attributes
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
redemption ? "no cache" : env.CACHE_TIME_CITY_SEARCH
|
||||||
|
)
|
||||||
|
|
||||||
|
metricsGetHotelsAvailabilityByHotelIds.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
112
packages/trpc/lib/routers/hotels/services/getHotelsByHotelIds.ts
Normal file
112
packages/trpc/lib/routers/hotels/services/getHotelsByHotelIds.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
import { getCacheClient } from "@scandic-hotels/common/dataCache"
|
||||||
|
import { chunk } from "@scandic-hotels/common/utils/chunk"
|
||||||
|
|
||||||
|
import { getHotelPageUrls } from "../../contentstack/hotelPage/utils"
|
||||||
|
import { getHotel } from "./getHotel"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
import type { HotelListingHotelData } from "../../../types/hotel"
|
||||||
|
|
||||||
|
export async function getHotelsByHotelIds({
|
||||||
|
hotelIds,
|
||||||
|
lang,
|
||||||
|
serviceToken,
|
||||||
|
contentType = "hotel",
|
||||||
|
}: {
|
||||||
|
hotelIds: string[]
|
||||||
|
lang: Lang
|
||||||
|
serviceToken: string
|
||||||
|
contentType?: "hotel" | "restaurant" | "meeting"
|
||||||
|
}) {
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const cacheKey = `${lang}:getHotelsByHotelIds:hotels:${contentType}:${[...hotelIds].sort().join(",")}`
|
||||||
|
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
cacheKey,
|
||||||
|
async () => {
|
||||||
|
const hotelPages = await getHotelPageUrls(lang)
|
||||||
|
const chunkedHotelIds = chunk(hotelIds, 10)
|
||||||
|
|
||||||
|
const hotels: HotelListingHotelData[] = []
|
||||||
|
for (const hotelIdChunk of chunkedHotelIds) {
|
||||||
|
const chunkedHotels = await Promise.all(
|
||||||
|
hotelIdChunk.map(async (hotelId) => {
|
||||||
|
const hotelResponse = await getHotel(
|
||||||
|
{ hotelId, language: lang, isCardOnlyPayment: false },
|
||||||
|
serviceToken
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hotelResponse) {
|
||||||
|
throw new Error(`Hotel not found: ${hotelId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const hotelPage = hotelPages.find(
|
||||||
|
(page) => page.hotelId === hotelId
|
||||||
|
)
|
||||||
|
const { hotel, cities, additionalData } = hotelResponse
|
||||||
|
|
||||||
|
const content = {
|
||||||
|
description: hotel.hotelContent?.texts.descriptions?.short,
|
||||||
|
galleryImages: hotel.galleryImages,
|
||||||
|
url: hotelPage?.url ?? "",
|
||||||
|
openInNewTab: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType === "restaurant") {
|
||||||
|
const restaurantDescription =
|
||||||
|
additionalData?.restaurantsOverviewPage
|
||||||
|
.restaurantsContentDescriptionShort
|
||||||
|
const restaurantImages =
|
||||||
|
additionalData.restaurantImages?.heroImages
|
||||||
|
if (restaurantDescription) {
|
||||||
|
content.description = restaurantDescription
|
||||||
|
}
|
||||||
|
if (restaurantImages && restaurantImages.length > 0) {
|
||||||
|
content.galleryImages = restaurantImages
|
||||||
|
}
|
||||||
|
} else if (contentType === "meeting") {
|
||||||
|
const meetingDescription =
|
||||||
|
hotel.hotelContent.texts.meetingDescription?.short
|
||||||
|
const meetingImages =
|
||||||
|
additionalData?.conferencesAndMeetings?.heroImages
|
||||||
|
if (meetingDescription) {
|
||||||
|
content.description = meetingDescription
|
||||||
|
}
|
||||||
|
if (meetingImages && meetingImages.length > 0) {
|
||||||
|
content.galleryImages = meetingImages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: HotelListingHotelData = {
|
||||||
|
hotel: {
|
||||||
|
id: hotel.id,
|
||||||
|
countryCode: hotel.countryCode,
|
||||||
|
galleryImages: content.galleryImages,
|
||||||
|
name: hotel.name,
|
||||||
|
tripadvisor: hotel.ratings?.tripAdvisor?.rating || null,
|
||||||
|
detailedFacilities: hotel.detailedFacilities.sort(
|
||||||
|
(a, b) => b.sortOrder - a.sortOrder
|
||||||
|
),
|
||||||
|
location: hotel.location,
|
||||||
|
hotelType: hotel.hotelType,
|
||||||
|
type: hotel.type,
|
||||||
|
address: hotel.address,
|
||||||
|
cityIdentifier: cities[0]?.cityIdentifier || null,
|
||||||
|
description: content.description || null,
|
||||||
|
},
|
||||||
|
url: content.url,
|
||||||
|
meetingUrl: additionalData.meetingRooms.meetingOnlineLink || null,
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
hotels.push(...chunkedHotels)
|
||||||
|
}
|
||||||
|
return hotels.filter((hotel): hotel is HotelListingHotelData => !!hotel)
|
||||||
|
},
|
||||||
|
"1d"
|
||||||
|
)
|
||||||
|
}
|
||||||
72
packages/trpc/lib/routers/hotels/services/getPackages.ts
Normal file
72
packages/trpc/lib/routers/hotels/services/getPackages.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
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 { toApiLang } from "../../../utils"
|
||||||
|
import { packagesSchema } from "../output"
|
||||||
|
|
||||||
|
import type { PackagesOutput } from "../../../types/packages"
|
||||||
|
|
||||||
|
export async function getPackages(input: PackagesOutput, serviceToken: string) {
|
||||||
|
const { adults, children, endDate, hotelId, lang, packageCodes, startDate } =
|
||||||
|
input
|
||||||
|
|
||||||
|
const getPackagesCounter = createCounter("hotel", "getPackages")
|
||||||
|
const metricsGetPackages = getPackagesCounter.init({
|
||||||
|
input,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetPackages.start()
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
const result = cacheClient.cacheOrGet(
|
||||||
|
stringify(input),
|
||||||
|
async function () {
|
||||||
|
const apiLang = toApiLang(lang)
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams({
|
||||||
|
adults: adults.toString(),
|
||||||
|
children: children.toString(),
|
||||||
|
endDate,
|
||||||
|
language: apiLang,
|
||||||
|
startDate,
|
||||||
|
})
|
||||||
|
|
||||||
|
packageCodes.forEach((code) => {
|
||||||
|
searchParams.append("packageCodes", code)
|
||||||
|
})
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Package.Packages.hotel(hotelId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
searchParams
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetPackages.httpError(apiResponse)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validatedPackagesData = packagesSchema.safeParse(apiJson)
|
||||||
|
if (!validatedPackagesData.success) {
|
||||||
|
metricsGetPackages.validationError(validatedPackagesData.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedPackagesData.data
|
||||||
|
},
|
||||||
|
"3h"
|
||||||
|
)
|
||||||
|
|
||||||
|
metricsGetPackages.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
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 { generateChildrenString } from "../helpers"
|
||||||
|
import { roomFeaturesSchema } from "../output"
|
||||||
|
|
||||||
|
import type { RoomFeaturesInput } from "../input"
|
||||||
|
|
||||||
|
export async function getRoomFeaturesInventory(
|
||||||
|
input: RoomFeaturesInput,
|
||||||
|
token: string
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
adults,
|
||||||
|
childrenInRoom,
|
||||||
|
endDate,
|
||||||
|
hotelId,
|
||||||
|
roomFeatureCodes,
|
||||||
|
startDate,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
adults,
|
||||||
|
hotelId,
|
||||||
|
roomFeatureCode: roomFeatureCodes,
|
||||||
|
roomStayEndDate: endDate,
|
||||||
|
roomStayStartDate: startDate,
|
||||||
|
...(childrenInRoom?.length && {
|
||||||
|
children: generateChildrenString(childrenInRoom),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const getRoomFeaturesInventoryCounter = createCounter(
|
||||||
|
"hotel",
|
||||||
|
"getRoomFeaturesInventory"
|
||||||
|
)
|
||||||
|
const metricsGetRoomFeaturesInventory =
|
||||||
|
getRoomFeaturesInventoryCounter.init(params)
|
||||||
|
|
||||||
|
metricsGetRoomFeaturesInventory.start()
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
const result = cacheClient.cacheOrGet(
|
||||||
|
stringify(input),
|
||||||
|
async function () {
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.roomFeatures(hotelId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetRoomFeaturesInventory.httpError(apiResponse)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await apiResponse.json()
|
||||||
|
const validatedRoomFeaturesData = roomFeaturesSchema.safeParse(data)
|
||||||
|
if (!validatedRoomFeaturesData.success) {
|
||||||
|
metricsGetRoomFeaturesInventory.validationError(
|
||||||
|
validatedRoomFeaturesData.error
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return validatedRoomFeaturesData.data
|
||||||
|
},
|
||||||
|
"5m"
|
||||||
|
)
|
||||||
|
|
||||||
|
metricsGetRoomFeaturesInventory.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
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,
|
||||||
|
token: string,
|
||||||
|
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 = {
|
||||||
|
bookingCode,
|
||||||
|
fromDate,
|
||||||
|
hotelId,
|
||||||
|
lang,
|
||||||
|
searchType,
|
||||||
|
toDate,
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
const availabilityResponses = await Promise.allSettled(
|
||||||
|
rooms.map((room) => {
|
||||||
|
const cacheKey = {
|
||||||
|
...baseCacheKey,
|
||||||
|
room,
|
||||||
|
}
|
||||||
|
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),
|
||||||
|
}),
|
||||||
|
...(room.bookingCode && { bookingCode: room.bookingCode }),
|
||||||
|
...(redemption && { isRedemption: "true" }),
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.hotel(hotelId),
|
||||||
|
{
|
||||||
|
cache: undefined, // overwrite default
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${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<string, number>
|
||||||
|
>((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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@ import { z } from "zod"
|
|||||||
import { Lang } from "@scandic-hotels/common/constants/language"
|
import { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
import { safeProtectedProcedure } from "../../../procedures"
|
import { safeProtectedProcedure } from "../../../procedures"
|
||||||
import { getVerifiedUser } from "../../../routers/user/utils"
|
|
||||||
import { isValidSession } from "../../../utils/session"
|
import { isValidSession } from "../../../utils/session"
|
||||||
|
import { getVerifiedUser } from "../../user/utils/getVerifiedUser"
|
||||||
import { getPrimaryLinks } from "./getPrimaryLinks"
|
import { getPrimaryLinks } from "./getPrimaryLinks"
|
||||||
import { getSecondaryLinks } from "./getSecondaryLinks"
|
import { getSecondaryLinks } from "./getSecondaryLinks"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { createLogger } from "@scandic-hotels/common/logger/createLogger"
|
|||||||
import * as api from "../../../api"
|
import * as api from "../../../api"
|
||||||
import { protectedProcedure } from "../../../procedures"
|
import { protectedProcedure } from "../../../procedures"
|
||||||
import { getUserSchema } from "../../user/output"
|
import { getUserSchema } from "../../user/output"
|
||||||
import { getVerifiedUser } from "../../user/utils"
|
import { getVerifiedUser } from "../../user/utils/getVerifiedUser"
|
||||||
|
|
||||||
import type { FriendsTier } from "../../../types/user"
|
import type { FriendsTier } from "../../../types/user"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
getFriendsMembership,
|
getFriendsMembership,
|
||||||
getMembershipCards,
|
getMembershipCards,
|
||||||
} from "../../../routers/user/helpers"
|
} from "../../../routers/user/helpers"
|
||||||
import { getVerifiedUser } from "../../../routers/user/utils"
|
|
||||||
import { toApiLang } from "../../../utils"
|
import { toApiLang } from "../../../utils"
|
||||||
import { isValidSession } from "../../../utils/session"
|
import { isValidSession } from "../../../utils/session"
|
||||||
import {
|
import {
|
||||||
@@ -21,13 +20,12 @@ import {
|
|||||||
staysInput,
|
staysInput,
|
||||||
} from "../input"
|
} from "../input"
|
||||||
import { getFriendTransactionsSchema } from "../output"
|
import { getFriendTransactionsSchema } from "../output"
|
||||||
import {
|
import { getCreditCards } from "../services/getCreditCards"
|
||||||
getCreditCards,
|
import { getPreviousStays } from "../services/getPreviousStays"
|
||||||
getPreviousStays,
|
import { getUpcomingStays } from "../services/getUpcomingStays"
|
||||||
getUpcomingStays,
|
import { getVerifiedUser } from "../utils/getVerifiedUser"
|
||||||
parsedUser,
|
import { parsedUser } from "../utils/parsedUser"
|
||||||
updateStaysBookingUrl,
|
import { updateStaysBookingUrl } from "../utils/updateStaysBookingUrl"
|
||||||
} from "../utils"
|
|
||||||
import { userTrackingInfo } from "./userTrackingInfo"
|
import { userTrackingInfo } from "./userTrackingInfo"
|
||||||
|
|
||||||
export const userQueryRouter = router({
|
export const userQueryRouter = router({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createCounter } from "@scandic-hotels/common/telemetry"
|
|||||||
import { safeProtectedProcedure } from "../../../procedures"
|
import { safeProtectedProcedure } from "../../../procedures"
|
||||||
import { isValidSession } from "../../../utils/session"
|
import { isValidSession } from "../../../utils/session"
|
||||||
import { getFriendsMembership } from "../helpers"
|
import { getFriendsMembership } from "../helpers"
|
||||||
import { getVerifiedUser } from "../utils"
|
import { getVerifiedUser } from "../utils/getVerifiedUser"
|
||||||
|
|
||||||
import type { LoginType } from "@scandic-hotels/common/constants/loginType"
|
import type { LoginType } from "@scandic-hotels/common/constants/loginType"
|
||||||
|
|
||||||
|
|||||||
60
packages/trpc/lib/routers/user/services/getCreditCards.ts
Normal file
60
packages/trpc/lib/routers/user/services/getCreditCards.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { dt } from "@scandic-hotels/common/dt"
|
||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { cache } from "../../../DUPLICATED/cache"
|
||||||
|
import { creditCardsSchema } from "../output"
|
||||||
|
|
||||||
|
import type { Session } from "next-auth"
|
||||||
|
|
||||||
|
export const getCreditCards = cache(
|
||||||
|
async ({
|
||||||
|
session,
|
||||||
|
onlyNonExpired,
|
||||||
|
}: {
|
||||||
|
session: Session
|
||||||
|
onlyNonExpired?: boolean
|
||||||
|
}) => {
|
||||||
|
const getCreditCardsCounter = createCounter("user", "getCreditCards")
|
||||||
|
const metricsGetCreditCards = getCreditCardsCounter.init({
|
||||||
|
onlyNonExpired,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetCreditCards.start()
|
||||||
|
|
||||||
|
const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.token.access_token}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetCreditCards.httpError(apiResponse)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const verifiedData = creditCardsSchema.safeParse(apiJson)
|
||||||
|
if (!verifiedData.success) {
|
||||||
|
metricsGetCreditCards.validationError(verifiedData.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = verifiedData.data.data.filter((card) => {
|
||||||
|
if (onlyNonExpired) {
|
||||||
|
try {
|
||||||
|
const expirationDate = dt(card.expirationDate).startOf("day")
|
||||||
|
const currentDate = dt().startOf("day")
|
||||||
|
return expirationDate > currentDate
|
||||||
|
} catch (_) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetCreditCards.success()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
)
|
||||||
59
packages/trpc/lib/routers/user/services/getPreviousStays.ts
Normal file
59
packages/trpc/lib/routers/user/services/getPreviousStays.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { getStaysSchema } from "../output"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
export async function getPreviousStays(
|
||||||
|
accessToken: string,
|
||||||
|
limit: number = 10,
|
||||||
|
language: Lang,
|
||||||
|
cursor?: string
|
||||||
|
) {
|
||||||
|
const getPreviousStaysCounter = createCounter("user", "getPreviousStays")
|
||||||
|
const metricsGetPreviousStays = getPreviousStaysCounter.init({
|
||||||
|
limit,
|
||||||
|
cursor,
|
||||||
|
language,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetPreviousStays.start()
|
||||||
|
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
limit: String(limit),
|
||||||
|
language: toApiLang(language),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
params.offset = cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Booking.Stays.past,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetPreviousStays.httpError(apiResponse)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
|
||||||
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||||
|
if (!verifiedData.success) {
|
||||||
|
metricsGetPreviousStays.validationError(verifiedData.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsGetPreviousStays.success()
|
||||||
|
|
||||||
|
return verifiedData.data
|
||||||
|
}
|
||||||
59
packages/trpc/lib/routers/user/services/getUpcomingStays.ts
Normal file
59
packages/trpc/lib/routers/user/services/getUpcomingStays.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { toApiLang } from "../../../utils"
|
||||||
|
import { getStaysSchema } from "../output"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
|
||||||
|
export async function getUpcomingStays(
|
||||||
|
accessToken: string,
|
||||||
|
limit: number = 10,
|
||||||
|
language: Lang,
|
||||||
|
cursor?: string
|
||||||
|
) {
|
||||||
|
const getUpcomingStaysCounter = createCounter("user", "getUpcomingStays")
|
||||||
|
const metricsGetUpcomingStays = getUpcomingStaysCounter.init({
|
||||||
|
limit,
|
||||||
|
cursor,
|
||||||
|
language,
|
||||||
|
})
|
||||||
|
|
||||||
|
metricsGetUpcomingStays.start()
|
||||||
|
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
limit: String(limit),
|
||||||
|
language: toApiLang(language),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cursor) {
|
||||||
|
params.offset = cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Booking.Stays.future,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetUpcomingStays.httpError(apiResponse)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
|
||||||
|
const verifiedData = getStaysSchema.safeParse(apiJson)
|
||||||
|
if (!verifiedData.success) {
|
||||||
|
metricsGetUpcomingStays.validationError(verifiedData.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsGetUpcomingStays.success()
|
||||||
|
|
||||||
|
return verifiedData.data
|
||||||
|
}
|
||||||
17
packages/trpc/lib/routers/user/utils/getMemberShipNumber.ts
Normal file
17
packages/trpc/lib/routers/user/utils/getMemberShipNumber.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { isValidSession } from "../../../utils/session"
|
||||||
|
import { getVerifiedUser } from "./getVerifiedUser"
|
||||||
|
|
||||||
|
import type { Session } from "next-auth"
|
||||||
|
|
||||||
|
export async function getMembershipNumber(
|
||||||
|
session: Session | null
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
if (!isValidSession(session)) return undefined
|
||||||
|
|
||||||
|
const verifiedUser = await getVerifiedUser({ session })
|
||||||
|
if (!verifiedUser || "error" in verifiedUser) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifiedUser.data.membershipNumber
|
||||||
|
}
|
||||||
79
packages/trpc/lib/routers/user/utils/getVerifiedUser.ts
Normal file
79
packages/trpc/lib/routers/user/utils/getVerifiedUser.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import { createCounter } from "@scandic-hotels/common/telemetry"
|
||||||
|
|
||||||
|
import * as api from "../../../api"
|
||||||
|
import { cache } from "../../../DUPLICATED/cache"
|
||||||
|
import { getUserSchema } from "../output"
|
||||||
|
|
||||||
|
import type { Session } from "next-auth"
|
||||||
|
|
||||||
|
export const getVerifiedUser = cache(
|
||||||
|
async ({
|
||||||
|
session,
|
||||||
|
includeExtendedPartnerData,
|
||||||
|
}: {
|
||||||
|
session: Session
|
||||||
|
includeExtendedPartnerData?: boolean
|
||||||
|
}) => {
|
||||||
|
const getVerifiedUserCounter = createCounter("user", "getVerifiedUser")
|
||||||
|
const metricsGetVerifiedUser = getVerifiedUserCounter.init()
|
||||||
|
|
||||||
|
metricsGetVerifiedUser.start()
|
||||||
|
|
||||||
|
const now = Date.now()
|
||||||
|
if (session.token.expires_at && session.token.expires_at < now) {
|
||||||
|
metricsGetVerifiedUser.dataError(`Token expired`)
|
||||||
|
return { error: true, cause: "token_expired" } as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v2.Profile.profile,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${session.token.access_token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
includeExtendedPartnerData
|
||||||
|
? { includes: "extendedPartnerInformation" }
|
||||||
|
: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
await metricsGetVerifiedUser.httpError(apiResponse)
|
||||||
|
|
||||||
|
if (apiResponse.status === 401) {
|
||||||
|
return { error: true, cause: "unauthorized" } as const
|
||||||
|
} else if (apiResponse.status === 403) {
|
||||||
|
return { error: true, cause: "forbidden" } as const
|
||||||
|
} else if (apiResponse.status === 404) {
|
||||||
|
return { error: true, cause: "notfound" } as const
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: true,
|
||||||
|
cause: "unknown",
|
||||||
|
status: apiResponse.status,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
if (!apiJson.data?.attributes) {
|
||||||
|
metricsGetVerifiedUser.dataError(
|
||||||
|
`Missing data attributes in API response`,
|
||||||
|
{
|
||||||
|
data: apiJson,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifiedData = getUserSchema.safeParse(apiJson)
|
||||||
|
if (!verifiedData.success) {
|
||||||
|
metricsGetVerifiedUser.validationError(verifiedData.error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
metricsGetVerifiedUser.success()
|
||||||
|
|
||||||
|
return verifiedData
|
||||||
|
}
|
||||||
|
)
|
||||||
54
packages/trpc/lib/routers/user/utils/parsedUser.ts
Normal file
54
packages/trpc/lib/routers/user/utils/parsedUser.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import * as maskValue from "@scandic-hotels/common/utils/maskValue"
|
||||||
|
|
||||||
|
import { countries } from "../../../constants/countries"
|
||||||
|
import { getFriendsMembership } from "../helpers"
|
||||||
|
|
||||||
|
import type { User } from "../../../types/user"
|
||||||
|
|
||||||
|
export function parsedUser(data: User, isMFA: boolean) {
|
||||||
|
const country = countries.find((c) => c.code === data.address?.countryCode)
|
||||||
|
|
||||||
|
const user = {
|
||||||
|
address: {
|
||||||
|
city: data.address?.city,
|
||||||
|
country: country?.name ?? "",
|
||||||
|
countryCode: data.address?.countryCode,
|
||||||
|
streetAddress: data.address?.streetAddress,
|
||||||
|
zipCode: data.address?.zipCode,
|
||||||
|
},
|
||||||
|
dateOfBirth: data.dateOfBirth,
|
||||||
|
email: data.email,
|
||||||
|
employmentDetails: data.employmentDetails,
|
||||||
|
firstName: data.firstName,
|
||||||
|
language: data.language,
|
||||||
|
lastName: data.lastName,
|
||||||
|
loyalty: data.loyalty,
|
||||||
|
membershipNumber: data.membershipNumber,
|
||||||
|
membership: data.loyalty ? getFriendsMembership(data.loyalty) : null,
|
||||||
|
name: `${data.firstName} ${data.lastName}`,
|
||||||
|
phoneNumber: data.phoneNumber,
|
||||||
|
profileId: data.profileId,
|
||||||
|
promotions: data.promotions || null,
|
||||||
|
} satisfies User
|
||||||
|
|
||||||
|
if (!isMFA) {
|
||||||
|
if (user.address.city) {
|
||||||
|
user.address.city = maskValue.text(user.address.city)
|
||||||
|
}
|
||||||
|
if (user.address.streetAddress) {
|
||||||
|
user.address.streetAddress = maskValue.text(user.address.streetAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.address.zipCode = data.address?.zipCode
|
||||||
|
? maskValue.text(data.address.zipCode)
|
||||||
|
: ""
|
||||||
|
|
||||||
|
user.dateOfBirth = maskValue.all(user.dateOfBirth)
|
||||||
|
|
||||||
|
user.email = maskValue.email(user.email)
|
||||||
|
|
||||||
|
user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import "server-only"
|
||||||
|
|
||||||
|
import { myStay } from "@scandic-hotels/common/constants/routes/myStay"
|
||||||
|
|
||||||
|
import { env } from "../../../../env/server"
|
||||||
|
import { encrypt } from "../../../utils/encryption"
|
||||||
|
import { getVerifiedUser } from "./getVerifiedUser"
|
||||||
|
|
||||||
|
import type { Lang } from "@scandic-hotels/common/constants/language"
|
||||||
|
import type { Session } from "next-auth"
|
||||||
|
|
||||||
|
import type { FriendTransaction, Stay } from "../output"
|
||||||
|
|
||||||
|
export async function updateStaysBookingUrl(
|
||||||
|
data: Stay[],
|
||||||
|
session: Session,
|
||||||
|
lang: Lang
|
||||||
|
): Promise<Stay[]>
|
||||||
|
|
||||||
|
export async function updateStaysBookingUrl(
|
||||||
|
data: FriendTransaction[],
|
||||||
|
session: Session,
|
||||||
|
lang: Lang
|
||||||
|
): Promise<FriendTransaction[]>
|
||||||
|
|
||||||
|
export async function updateStaysBookingUrl(
|
||||||
|
data: Stay[] | FriendTransaction[],
|
||||||
|
session: Session,
|
||||||
|
lang: Lang
|
||||||
|
) {
|
||||||
|
const user = await getVerifiedUser({
|
||||||
|
session,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (user && !("error" in user)) {
|
||||||
|
return data.map((d) => {
|
||||||
|
const originalString =
|
||||||
|
d.attributes.confirmationNumber.toString() + "," + user.data.lastName
|
||||||
|
const encryptedBookingValue = encrypt(originalString)
|
||||||
|
|
||||||
|
// Get base URL with fallback for ephemeral environments (like deploy previews).
|
||||||
|
const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com"
|
||||||
|
|
||||||
|
// Construct Booking URL.
|
||||||
|
const bookingUrl = new URL(myStay[lang], baseUrl)
|
||||||
|
|
||||||
|
// Add search parameters.
|
||||||
|
if (encryptedBookingValue) {
|
||||||
|
bookingUrl.searchParams.set("RefId", encryptedBookingValue)
|
||||||
|
} else {
|
||||||
|
bookingUrl.searchParams.set("lastName", user.data.lastName)
|
||||||
|
bookingUrl.searchParams.set(
|
||||||
|
"bookingId",
|
||||||
|
d.attributes.confirmationNumber.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
attributes: {
|
||||||
|
...d.attributes,
|
||||||
|
bookingUrl: bookingUrl.toString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
}
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import type {
|
|
||||||
enterDetailsRoomsAvailabilityInputSchema,
|
|
||||||
getHotelsByHotelIdsAvailabilityInputSchema,
|
|
||||||
hotelsAvailabilityInputSchema,
|
|
||||||
selectRateRoomsAvailabilityInputSchema,
|
|
||||||
} from "../routers/hotels/input"
|
|
||||||
import type { hotelsAvailabilitySchema } from "../routers/hotels/output"
|
import type { hotelsAvailabilitySchema } from "../routers/hotels/output"
|
||||||
import type { productTypeSchema } from "../routers/hotels/schemas/availability/productType"
|
import type { productTypeSchema } from "../routers/hotels/schemas/availability/productType"
|
||||||
import type {
|
import type {
|
||||||
@@ -16,23 +10,6 @@ import type {
|
|||||||
} from "../routers/hotels/schemas/productTypePrice"
|
} from "../routers/hotels/schemas/productTypePrice"
|
||||||
|
|
||||||
export type HotelsAvailability = z.output<typeof hotelsAvailabilitySchema>
|
export type HotelsAvailability = z.output<typeof hotelsAvailabilitySchema>
|
||||||
export type HotelsAvailabilityInputSchema = z.output<
|
|
||||||
typeof hotelsAvailabilityInputSchema
|
|
||||||
>
|
|
||||||
export type HotelsByHotelIdsAvailabilityInputSchema = z.output<
|
|
||||||
typeof getHotelsByHotelIdsAvailabilityInputSchema
|
|
||||||
>
|
|
||||||
export type RoomsAvailabilityInputSchema = z.input<
|
|
||||||
typeof selectRateRoomsAvailabilityInputSchema
|
|
||||||
>
|
|
||||||
export type RoomsAvailabilityOutputSchema = z.output<
|
|
||||||
typeof selectRateRoomsAvailabilityInputSchema
|
|
||||||
>
|
|
||||||
export type RoomsAvailabilityInputRoom =
|
|
||||||
RoomsAvailabilityInputSchema["booking"]["rooms"][number]
|
|
||||||
export type RoomsAvailabilityExtendedInputSchema = z.input<
|
|
||||||
typeof enterDetailsRoomsAvailabilityInputSchema
|
|
||||||
>
|
|
||||||
|
|
||||||
export type ProductType = z.output<typeof productTypeSchema>
|
export type ProductType = z.output<typeof productTypeSchema>
|
||||||
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
export type ProductTypePrices = z.output<typeof productTypePriceSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user