This creates the alternative hotels page. It is mostly a copy of the select hotel page, and most of the contents of the pages lives under the same component in /components.

Merged in feat/sw-397-alternative-hotels (pull request #1211)

Feat/sw 397 alternative hotels

* fix(SW-397): create alternative hotels page

* update types

* Adapt to new changes for fetching data

* Make bookingcode optional

* Code review fixes


Approved-by: Simon.Emanuelsson
This commit is contained in:
Niclas Edenvin
2025-01-28 12:08:40 +00:00
parent 4247e37667
commit ef22fc4627
28 changed files with 693 additions and 105 deletions

View File

@@ -12,6 +12,15 @@ export const getHotelsAvailabilityInputSchema = z.object({
bookingCode: z.string().optional().default(""),
})
export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
hotelIds: z.array(z.number()),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
adults: z.number(),
children: z.string().optional(),
bookingCode: z.string().optional().default(""),
})
export const getRoomsAvailabilityInputSchema = z.object({
hotelId: z.number(),
roomStayStartDate: z.string(),
@@ -66,6 +75,10 @@ export const getHotelsInput = z.object({
})
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> {}
export const nearbyHotelIdsInput = z.object({
hotelId: z.string(),
})
export const getBreakfastPackageInputSchema = z.object({
adults: z.number().min(1, { message: "at least one adult is required" }),
fromDate: z

View File

@@ -542,6 +542,8 @@ export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
export type ProductType =
HotelsAvailability["data"][number]["attributes"]["productType"]
export type ProductTypePrices = z.infer<typeof productTypePriceSchema>
export type HotelsAvailabilityItem =
HotelsAvailability["data"][number]["attributes"]
const roomConfigurationSchema = z.object({
status: z.string(),
@@ -889,6 +891,17 @@ export const getHotelIdsByCityIdSchema = z
})
.transform((data) => data.data.map((hotel) => hotel.id))
export const getNearbyHotelIdsSchema = z
.object({
data: z.array(
z.object({
// We only care about the hotel id
id: z.string(),
})
),
})
.transform((data) => data.data.map((hotel) => hotel.id))
export const getMeetingRoomsSchema = z.object({
data: z.array(
z.object({

View File

@@ -23,6 +23,7 @@ import {
getCityCoordinatesInputSchema,
getHotelDataInputSchema,
getHotelsAvailabilityInputSchema,
getHotelsByHotelIdsAvailabilityInputSchema,
getHotelsInput,
getMeetingRoomsInputSchema,
getRatesInputSchema,
@@ -30,12 +31,14 @@ import {
getRoomsAvailabilityInputSchema,
getSelectedRoomAvailabilityInputSchema,
type HotelDataInput,
nearbyHotelIdsInput,
} from "./input"
import {
breakfastPackagesSchema,
getHotelDataSchema,
getHotelsAvailabilitySchema,
getMeetingRoomsSchema,
getNearbyHotelIdsSchema,
getRatesSchema,
getRoomPackagesSchema,
getRoomsAvailabilitySchema,
@@ -59,9 +62,15 @@ import {
hotelsAvailabilityCounter,
hotelsAvailabilityFailCounter,
hotelsAvailabilitySuccessCounter,
hotelsByHotelIdAvailabilityCounter,
hotelsByHotelIdAvailabilityFailCounter,
hotelsByHotelIdAvailabilitySuccessCounter,
meetingRoomsCounter,
meetingRoomsFailCounter,
meetingRoomsSuccessCounter,
nearbyHotelIdsCounter,
nearbyHotelIdsFailCounter,
nearbyHotelIdsSuccessCounter,
roomsAvailabilityCounter,
roomsAvailabilityFailCounter,
roomsAvailabilitySuccessCounter,
@@ -204,7 +213,7 @@ export const getHotelData = cache(
export const hotelQueryRouter = router({
availability: router({
hotels: serviceProcedure
hotelsByCity: serviceProcedure
.input(getHotelsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
@@ -319,6 +328,122 @@ export const hotelQueryRouter = router({
),
}
}),
hotelsByHotelIds: serviceProcedure
.input(getHotelsByHotelIdsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const { lang } = ctx
const apiLang = toApiLang(lang)
const {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
} = input
const params: Record<string, string | number | number[]> = {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
...(bookingCode && { bookingCode }),
language: apiLang,
}
hotelsByHotelIdAvailabilityCounter.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.hotelsByHotelIdAvailability start",
JSON.stringify({ query: { params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotels(),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
hotelsByHotelIdAvailabilityFailCounter.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelsByHotelIdAvailability error",
JSON.stringify({
query: { params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateAvailabilityData =
getHotelsAvailabilitySchema.safeParse(apiJson)
if (!validateAvailabilityData.success) {
hotelsByHotelIdAvailabilityFailCounter.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.hotelsByHotelIdAvailability validation error",
JSON.stringify({
query: { params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
hotelsByHotelIdAvailabilitySuccessCounter.add(1, {
hotelIds,
roomStayStartDate,
roomStayEndDate,
adults,
children,
bookingCode,
})
console.info(
"api.hotels.hotelsByHotelIdAvailability success",
JSON.stringify({
query: { params },
})
)
return {
availability: validateAvailabilityData.data.data.flatMap(
(hotels) => hotels.attributes
),
}
}),
rooms: serviceProcedure
.input(getRoomsAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
@@ -907,6 +1032,85 @@ export const hotelQueryRouter = router({
)
}),
}),
nearbyHotelIds: serviceProcedure
.input(nearbyHotelIdsInput)
.query(async function ({ ctx, input }) {
const { lang } = ctx
const apiLang = toApiLang(lang)
const { hotelId } = input
const params: Record<string, string | number> = {
language: apiLang,
}
nearbyHotelIdsCounter.add(1, {
hotelId,
})
console.info(
"api.hotels.nearbyHotelIds start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.nearbyHotels(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
nearbyHotelIdsFailCounter.add(1, {
hotelId,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.nearbyHotelIds error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson)
if (!validateHotelData.success) {
nearbyHotelIdsFailCounter.add(1, {
hotelId,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.nearbyHotelIds validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
nearbyHotelIdsSuccessCounter.add(1, {
hotelId,
})
console.info(
"api.hotels.nearbyHotelIds success",
JSON.stringify({
query: { hotelId, params },
})
)
return validateHotelData.data.map((id: string) => parseInt(id, 10))
}),
locations: router({
get: serviceProcedure.query(async function ({ ctx }) {
const searchParams = new URLSearchParams()

View File

@@ -25,6 +25,16 @@ export const hotelsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.hotels-fail"
)
export const hotelsByHotelIdAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.hotels-by-hotel-id"
)
export const hotelsByHotelIdAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.hotels-by-hotel-id-success"
)
export const hotelsByHotelIdAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.hotels-by-hotel-id-fail"
)
export const roomsAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.rooms"
)
@@ -73,6 +83,16 @@ export const getHotelIdsFailCounter = meter.createCounter(
"trpc.hotel.hotel-ids.get-fail"
)
export const nearbyHotelIdsCounter = meter.createCounter(
"trpc.hotel.nearby-hotel-ids.get"
)
export const nearbyHotelIdsSuccessCounter = meter.createCounter(
"trpc.hotel.nearby-hotel-ids.get-success"
)
export const nearbyHotelIdsFailCounter = meter.createCounter(
"trpc.hotel.nearby-hotel-ids.get-fail"
)
export const meetingRoomsCounter = meter.createCounter(
"trpc.hotels.meetingRooms"
)