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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user