feat: SW-2028 Fixed review comments

This commit is contained in:
Hrishikesh Vaipurkar
2025-03-31 16:35:52 +02:00
parent 4e9ee82efa
commit 96fd0b73e4
14 changed files with 214 additions and 195 deletions

View File

@@ -9,6 +9,8 @@ import {
nullableStringValidator,
} from "@/utils/zod/stringValidator"
import { CurrencyEnum } from "@/types/enums/currency"
const guestSchema = z.object({
email: nullableStringEmailValidator,
firstName: nullableStringValidator,
@@ -97,7 +99,7 @@ export const packageSchema = z
unitPrice: z.number(),
totalPrice: z.number().nullish(),
totalUnit: z.number().int().nullish(),
currency: z.string().default(""),
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown),
points: nullableIntValidator,
}),
comment: z.string().nullish(),
@@ -218,7 +220,7 @@ export const bookingConfirmationSchema = z
computedReservationStatus: z.string().nullable().default(""),
confirmationNumber: nullableStringValidator,
createDateTime: z.date({ coerce: true }),
currencyCode: z.string(),
currencyCode: z.nativeEnum(CurrencyEnum),
guest: guestSchema,
linkedReservations: nullableArrayObjectValidator(
linkedReservationSchema

View File

@@ -208,12 +208,12 @@ export const getHotel = cache(
}
)
export const getHotelsAvailabilityByCity = async (
async function getHotelsAvailabilityByCity(
input: HotelsAvailabilityInputSchema,
apiLang: string,
token: string, // Either service token or user access token in case of redemption search
session?: Session
) => {
) {
const {
cityId,
roomStayStartDate,
@@ -223,131 +223,124 @@ export const getHotelsAvailabilityByCity = async (
bookingCode,
redemption,
} = input
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}:${redemption ? "isRedemption" : ""}`,
async () => {
const params: Record<string, string | number> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
...(redemption ? { isRedemption: "true" } : {}),
language: apiLang,
}
metrics.hotelsAvailability.counter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
})
console.info(
"api.hotels.hotelsAvailability start",
JSON.stringify({ query: { cityId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.city(cityId),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.hotelsAvailability.fail.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsAvailability error",
JSON.stringify({
query: { cityId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
throw new Error("Failed to fetch hotels availability by city")
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
hotelsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
metrics.hotelsAvailability.fail.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.hotelsAvailability validation error",
JSON.stringify({
query: { cityId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
metrics.hotelsAvailability.success.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
})
console.info(
"api.hotels.hotelsAvailability success",
JSON.stringify({
query: { cityId, params: params },
})
)
if (redemption && session) {
const verifiedUser = await getVerifiedUser({ session })
if (!verifiedUser?.error) {
const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0
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
),
}
},
env.CACHE_TIME_CITY_SEARCH
const params: Record<string, string | number> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
...(redemption ? { isRedemption: "true" } : {}),
language: apiLang,
}
metrics.hotelsAvailability.counter.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
})
console.info(
"api.hotels.hotelsAvailability start",
JSON.stringify({ query: { cityId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.city(cityId),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
metrics.hotelsAvailability.fail.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsAvailability error",
JSON.stringify({
query: { cityId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
throw new Error("Failed to fetch hotels availability by city")
}
const apiJson = await apiResponse.json()
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
metrics.hotelsAvailability.fail.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.hotelsAvailability validation error",
JSON.stringify({
query: { cityId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
metrics.hotelsAvailability.success.add(1, {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
redemption,
})
console.info(
"api.hotels.hotelsAvailability success",
JSON.stringify({
query: { cityId, params: params },
})
)
if (redemption && session) {
const verifiedUser = await getVerifiedUser({ session })
if (!verifiedUser?.error) {
const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0
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
),
}
}
export const getHotelsAvailabilityByHotelIds = async (
@@ -492,7 +485,23 @@ export const hotelQueryRouter = router({
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
return getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
const {
cityId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
} = input
const cacheClient = await getCacheClient()
return await cacheClient.cacheOrGet(
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
async () => {
return getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
},
env.CACHE_TIME_CITY_SEARCH
)
}),
hotelsByCityWithRedemption: protectedProcedure
.input(hotelsAvailabilityInputSchema)
@@ -503,7 +512,7 @@ export const hotelQueryRouter = router({
input,
apiLang,
ctx.session.token.access_token,
ctx.session,
ctx.session
)
}),
hotelsByHotelIds: serviceProcedure
@@ -519,12 +528,16 @@ export const hotelQueryRouter = router({
.use(async ({ ctx, input, next }) => {
if (input.redemption) {
if (ctx.session?.token.access_token) {
return next({
ctx: {
token: ctx.session.token.access_token,
},
input,
})
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()
}
@@ -622,16 +635,15 @@ export const hotelQueryRouter = router({
)?.mustBeGuaranteed
}
if (redemption && ctx.session) {
const verifiedUser = await getVerifiedUser({ session: ctx.session })
if (!verifiedUser?.error) {
const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0
validateAvailabilityData.data.roomConfigurations.forEach((data) => {
data.redemptions?.forEach(r => {
r.redemption.hasEnoughPoints = userPoints >= r.redemption.localPrice.pointsPerStay
if (redemption) {
validateAvailabilityData.data.roomConfigurations.forEach(
(data) => {
data.redemptions?.forEach((r) => {
r.redemption.hasEnoughPoints =
ctx.userPoints >= r.redemption.localPrice.pointsPerStay
})
})
}
}
)
}
return validateAvailabilityData.data

View File

@@ -4,18 +4,19 @@ import { imageSizesSchema } from "./image"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { CurrencyEnum } from "@/types/enums/currency"
import { PackageTypeEnum } from "@/types/enums/packages"
// TODO: Remove optional and default when the API change has been deployed
export const packagePriceSchema = z
.object({
currency: z.string().default("N/A"),
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown),
price: z.number(),
totalPrice: z.number(),
})
.optional()
.default({
currency: "N/A",
currency: CurrencyEnum.Unknown,
price: 0,
totalPrice: 0,
})