From 577a4ca35e008fe88f4e06917fcf773ead10935c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matilda=20Landstr=C3=B6m?= Date: Mon, 27 Jan 2025 11:39:13 +0000 Subject: [PATCH] Merged in feat/SW-1333-hotel-endpoint (pull request #1206) Feat(SW-133): Add additionalData endpoint Approved-by: Erik Tiekstra Approved-by: Fredrik Thorsson --- .../HotelListing/HotelListingItem/utils.ts | 6 +- components/ContentType/HotelPage/index.tsx | 21 +++-- .../SelectRate/Rooms/RoomsContainer.tsx | 2 +- .../HotelReservation/SidePeek/index.tsx | 3 +- components/SidePeeks/HotelSidePeek/index.tsx | 10 +- lib/api/endpoints.ts | 3 + lib/trpc/memoizedRequests/index.ts | 9 ++ server/routers/booking/query.ts | 2 +- server/routers/hotels/input.ts | 5 + server/routers/hotels/output.ts | 43 ++++----- server/routers/hotels/query.ts | 93 ++++++++++++++++++- .../routers/hotels/schemas/additionalData.ts | 67 +++++++++++++ server/routers/hotels/schemas/restaurants.ts | 1 + server/routers/hotels/telemetry.ts | 10 ++ .../hotelReservation/hotelSidePeek.ts | 5 +- types/hotel.ts | 7 +- types/trpc/routers/booking/confirmation.ts | 2 +- 17 files changed, 239 insertions(+), 50 deletions(-) create mode 100644 server/routers/hotels/schemas/additionalData.ts diff --git a/components/Blocks/HotelListing/HotelListingItem/utils.ts b/components/Blocks/HotelListing/HotelListingItem/utils.ts index 6fb4a6a40..750ed39fc 100644 --- a/components/Blocks/HotelListing/HotelListingItem/utils.ts +++ b/components/Blocks/HotelListing/HotelListingItem/utils.ts @@ -5,7 +5,7 @@ export function getTypeSpecificInformation( contentType: HotelListing["contentType"], hotel: Hotel ) { - const { restaurantsOverviewPage, images } = hotel.hotelContent + const { images } = hotel.hotelContent const { descriptions, meetingDescription } = hotel.hotelContent.texts const hotelData = { description: descriptions.short, @@ -24,8 +24,8 @@ export function getTypeSpecificInformation( const restaurantImage = hotel.restaurantImages?.heroImages[0] return { description: - restaurantsOverviewPage.restaurantsContentDescriptionShort || - hotelData.description, + hotel.hotelContent.restaurantsOverviewPage + .restaurantsContentDescriptionShort || hotelData.description, imageSrc: restaurantImage?.imageSizes.small || hotelData.imageSrc, altText: restaurantImage?.metaData.altText || hotelData.altText, } diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 32b62848b..598fdbe48 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -41,7 +41,7 @@ import styles from "./hotelPage.module.css" import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities" import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" -import type { Facility } from "@/types/hotel" +import type { AdditionalData, Facility } from "@/types/hotel" import { PageContentTypeEnum } from "@/types/requests/contentType" export default async function HotelPage({ hotelId }: HotelPageProps) { @@ -63,12 +63,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { name, address, pointsOfInterest, - gallery, specialAlerts, - healthAndWellness, - restaurantImages, - conferencesAndMeetings, - hotelRoomElevatorPitchText, hotelContent, detailedFacilities, healthFacilities, @@ -79,8 +74,18 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { ratings, parking, } = hotelData.data.attributes - const roomCategories = hotelData.included?.rooms || [] - const restaurants = hotelData.included?.restaurants || [] + const roomCategories = hotelData.included.rooms || [] + const restaurants = hotelData.included.restaurants || [] + const additionalData = + hotelData.included.additionalData || ({} as AdditionalData) + + const { + healthAndWellness, + restaurantImages, + conferencesAndMeetings, + hotelRoomElevatorPitchText, + gallery, + } = additionalData const images = gallery?.smallerImages const description = hotelContent.texts.descriptions.medium diff --git a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx index dc23921cb..0c74eb61d 100644 --- a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx +++ b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx @@ -95,7 +95,7 @@ export async function RoomsContainer({ diff --git a/components/HotelReservation/SidePeek/index.tsx b/components/HotelReservation/SidePeek/index.tsx index 6329df6ed..bf75ed04b 100644 --- a/components/HotelReservation/SidePeek/index.tsx +++ b/components/HotelReservation/SidePeek/index.tsx @@ -32,7 +32,7 @@ export default function HotelReservationSidePeek({ } ) - const selectedRoom = hotelData?.included?.rooms?.find((room) => + const selectedRoom = hotelData?.included.rooms?.find((room) => room.roomTypes.some((type) => type.code === roomTypeCode) ) @@ -42,6 +42,7 @@ export default function HotelReservationSidePeek({ {hotelData && ( {parking?.length > 0 && } - {hotel.hotelContent?.restaurantsOverviewPage + {additionalHotelData?.restaurantsOverviewPage ?.restaurantsContentDescriptionMedium && ( )} - {hotel?.accessibilityElevatorPitchText && ( + {additionalHotelData?.accessibilityElevatorPitchText && ( )} diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index f64b83396..74855587d 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -114,6 +114,9 @@ export namespace endpoints { export function roomCategories(hotelId: string) { return `${hotels}/${hotelId}/roomCategories` } + export function additionalData(hotelId: string) { + return `${hotels}/${hotelId}/additionalData` + } } export const locations = `${base.path.hotel}/${version}/${base.enitity.Locations}` diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index d9c381a11..d96364d06 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -171,6 +171,15 @@ export const getMeetingRooms = cache( } ) +export const getAdditionalData = cache( + async function getMemoizedAdditionalData(input: { + hotelId: string + language: Lang + }) { + return serverClient().hotel.additionalData(input) + } +) + export const getDestinationOverviewPage = cache( async function getMemoizedDestinationOverviewPage() { return serverClient().contentstack.destinationOverviewPage.get() diff --git a/server/routers/booking/query.ts b/server/routers/booking/query.ts index 08279eff9..678bd104b 100644 --- a/server/routers/booking/query.ts +++ b/server/routers/booking/query.ts @@ -146,7 +146,7 @@ export const bookingQueryRouter = router({ included: hotelData.included, }, room: getBookedHotelRoom( - hotelData.included?.rooms, + hotelData.included.rooms, booking.data.roomTypeCode ), } diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 938e78dc4..c3baec035 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -98,3 +98,8 @@ export const getMeetingRoomsInputSchema = z.object({ hotelId: z.string(), language: z.string(), }) + +export const getAdditionalDataInputSchema = z.object({ + hotelId: z.string(), + language: z.string(), +}) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 83e35c016..41cc6d937 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { ChildBedTypeEnum, type PaymentMethodEnum } from "@/constants/booking" import { toLang } from "@/server/utils" +import { additionalDataSchema } from "./schemas/additionalData" import { imageSchema } from "./schemas/image" import { restaurantSchema } from "./schemas/restaurants" import { roomSchema } from "./schemas/room" @@ -12,7 +13,7 @@ import { getPoiGroupByCategoryName } from "./utils" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { FacilityEnum } from "@/types/enums/facilities" import { PackageTypeEnum } from "@/types/enums/packages" -import type { RestaurantData, RoomData } from "@/types/hotel" +import type { AdditionalData, RestaurantData, RoomData } from "@/types/hotel" const ratingsSchema = z .object({ @@ -119,7 +120,7 @@ const hotelContentSchema = z.object({ restaurantsOverviewPageLink: z.string().optional(), restaurantsContentDescriptionShort: z.string().optional(), restaurantsContentDescriptionMedium: z.string().optional(), - }), + }), // TODO remove, use new /additionalData endpoint }) const detailedFacilitySchema = z.object({ @@ -134,12 +135,12 @@ const detailedFacilitySchema = z.object({ export const facilitySchema = z.object({ headingText: z.string().default(""), heroImages: z.array(imageSchema), -}) +}) // TODO remove, use new /additionalData endpoint export const gallerySchema = z.object({ heroImages: z.array(imageSchema), smallerImages: z.array(imageSchema), -}) +}) // TODO remove, use new /additionalData endpoint const healthFacilitySchema = z.object({ type: z.string(), @@ -289,16 +290,6 @@ export const parkingSchema = z.object({ pricing: parkingPricingSchema, }) -const specialNeedSchema = z.object({ - name: z.string(), - details: z.string(), -}) - -const specialNeedGroupSchema = z.object({ - name: z.string(), - specialNeeds: z.array(specialNeedSchema), -}) - const socialMediaSchema = z.object({ instagram: z.string().optional(), facebook: z.string().optional(), @@ -392,11 +383,10 @@ type DetailedFacility = { id: FacilityEnum } & z.infer< typeof detailedFacilitySchema > export const hotelAttributesSchema = z.object({ - accessibilityElevatorPitchText: z.string().optional(), address: addressSchema, cityId: z.string(), cityName: z.string(), - conferencesAndMeetings: facilitySchema.optional(), + conferencesAndMeetings: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint contactInformation: contactInformationSchema, detailedFacilities: z.array(detailedFacilitySchema).transform( (facilities) => @@ -407,13 +397,11 @@ export const hotelAttributesSchema = z.object({ ) .sort((a, b) => b.sortOrder - a.sortOrder) as DetailedFacility[] ), - gallery: gallerySchema.optional(), + gallery: gallerySchema.optional(), // TODO remove, use new /additionalData endpoint galleryImages: z.array(imageSchema).optional(), - healthAndWellness: facilitySchema.optional(), healthFacilities: z.array(healthFacilitySchema), hotelContent: hotelContentSchema, hotelFacts: hotelFactsSchema, - hotelRoomElevatorPitchText: z.string().optional(), hotelType: z.string().optional(), isActive: z.boolean(), isPublished: z.boolean(), @@ -430,23 +418,26 @@ export const hotelAttributesSchema = z.object({ ), ratings: ratingsSchema, rewardNight: rewardNightSchema, - restaurantImages: facilitySchema.optional(), + restaurantImages: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint socialMedia: socialMediaSchema, specialAlerts: specialAlertsSchema, - specialNeedGroups: z.array(specialNeedGroupSchema), vat: z.number(), }) const includedSchema = z - .array(z.union([roomSchema, restaurantSchema])) + .array(z.union([roomSchema, restaurantSchema, additionalDataSchema])) .transform((data) => { const rooms = data.filter((d) => d.type === "roomcategories") as RoomData[] const restaurants = data.filter( (d) => d.type === "restaurants" ) as RestaurantData[] + const additionalData = data.filter( + (d) => d.type === "additionalData" + ) as AdditionalData[] return { rooms, restaurants, + additionalData, } }) @@ -467,7 +458,13 @@ export const getHotelDataSchema = z.object({ }), // NOTE: We can pass an "include" param to the hotel API to retrieve // additional data for an individual hotel. - included: includedSchema.optional(), + included: includedSchema.optional().transform((incl) => { + return { + restaurants: incl?.restaurants, + rooms: incl?.rooms, + additionalData: incl?.additionalData[0], + } + }), }) export const childrenSchema = z.object({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 352441642..472da018c 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -16,7 +16,9 @@ import { cache } from "@/utils/cache" import { getHotelPageUrl } from "../contentstack/hotelPage/utils" import { getVerifiedUser, parsedUser } from "../user/query" +import { additionalDataSchema } from "./schemas/additionalData" import { + getAdditionalDataInputSchema, getBreakfastPackageInputSchema, getCityCoordinatesInputSchema, getHotelDataInputSchema, @@ -39,6 +41,9 @@ import { getRoomsAvailabilitySchema, } from "./output" import { + additionalDataCounter, + additionalDataFailCounter, + additionalDataSuccessCounter, breakfastPackagesCounter, breakfastPackagesFailCounter, breakfastPackagesSuccessCounter, @@ -86,7 +91,7 @@ export const getHotelData = cache( async (input: HotelDataInput, serviceToken: string) => { const { hotelId, language, isCardOnlyPayment } = input - const includes = ["RoomCategories", "Restaurants"] // "RoomCategories","NearbyHotels","Restaurants","City", + const includes = ["RoomCategories", "Restaurants", "AdditionalData"] //"NearbyHotels","Restaurants","City", const params = new URLSearchParams({ hotelId, language, @@ -182,8 +187,10 @@ export const getHotelData = cache( hotelData.data.attributes.merchantInformationData.alternatePaymentOptions = [] } - if (hotelData.data.attributes.gallery) { - const smallerImages = hotelData.data.attributes.gallery.smallerImages + + const gallery = hotelData.included.additionalData?.gallery + if (gallery) { + const smallerImages = gallery.smallerImages const hotelGalleryImages = hotelData.data.attributes.hotelType === HotelTypeEnum.Signature ? smallerImages.slice(0, 10) @@ -604,7 +611,7 @@ export const hotelQueryRouter = router({ const bedTypes = availableRoomsInCategory .map((availRoom) => { - const matchingRoom = hotelData?.included?.rooms + const matchingRoom = hotelData?.included.rooms ?.find((room) => room.roomTypes .map((roomType) => roomType.code) @@ -1253,4 +1260,82 @@ export const hotelQueryRouter = router({ return validatedMeetingRooms.data.data }), + additionalData: safeProtectedServiceProcedure + .input(getAdditionalDataInputSchema) + .query(async function ({ ctx, input }) { + const { hotelId, language } = input + + const params: Record = { + hotelId, + language, + } + const metricsData = { ...params, hotelId: input.hotelId } + additionalDataCounter.add(1, metricsData) + console.info( + "api.hotels.additionalData start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Hotel.Hotels.additionalData(input.hotelId), + { + cache: undefined, + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + next: { + revalidate: env.CACHE_TIME_HOTELS, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + additionalDataFailCounter.add(1, { + ...metricsData, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.additionalData error", + JSON.stringify({ + query: { params }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const validatedAdditionalData = additionalDataSchema.safeParse(apiJson) + + if (!validatedAdditionalData.success) { + console.error( + "api.hotels.additionalData validation error", + JSON.stringify({ + query: { params }, + error: validatedAdditionalData.error, + }) + ) + throw badRequestError() + } + additionalDataSuccessCounter.add(1, { + hotelId, + }) + console.info( + "api.hotels.additionalData success", + JSON.stringify({ query: { params } }) + ) + + return validatedAdditionalData.data + }), }) diff --git a/server/routers/hotels/schemas/additionalData.ts b/server/routers/hotels/schemas/additionalData.ts new file mode 100644 index 000000000..a47482d8a --- /dev/null +++ b/server/routers/hotels/schemas/additionalData.ts @@ -0,0 +1,67 @@ +import { z } from "zod" + +import { imageSchema } from "./image" + +const specialNeedSchema = z.object({ + name: z.string(), + details: z.string(), +}) + +const specialNeedGroupSchema = z.object({ + name: z.string(), + specialNeeds: z.array(specialNeedSchema), +}) + +export const gallerySchema = z.object({ + heroImages: z.array(imageSchema), + smallerImages: z.array(imageSchema), +}) + +export const facilitySchema = z.object({ + headingText: z.string().default(""), + heroImages: z.array(imageSchema), +}) + +export const restaurantsOverviewPageSchema = z.object({ + restaurantsOverviewPageLinkText: z.string().optional(), + restaurantsOverviewPageLink: z.string().optional(), + restaurantsContentDescriptionShort: z.string().optional(), + restaurantsContentDescriptionMedium: z.string().optional(), +}) + +const extraPageSchema = z.object({ + elevatorPitch: z.string().optional(), + mainBody: z.string().optional(), +}) + +export const additionalDataSchema = z + .object({ + attributes: z.object({ + name: z.string(), + id: z.string(), + displayWebPage: z.object({ + healthGym: z.boolean(), + meetingRoom: z.boolean(), + parking: z.boolean(), + specialNeeds: z.boolean(), + }), + specialNeedGroups: z.array(specialNeedGroupSchema), + gallery: gallerySchema.optional(), + conferencesAndMeetings: facilitySchema.optional(), + healthAndWellness: facilitySchema.optional(), + restaurantImages: facilitySchema.optional(), + restaurantsOverviewPage: restaurantsOverviewPageSchema, + meetingRooms: extraPageSchema, + healthAndFitness: extraPageSchema, + hotelParking: extraPageSchema, + hotelSpecialNeeds: extraPageSchema, + accessibilityElevatorPitchText: z.string().optional(), + hotelRoomElevatorPitchText: z.string().optional(), + }), + type: z.literal("additionalData"), + }) + .transform(({ attributes, type }) => ({ + ...attributes, + type, + id: attributes.id, + })) diff --git a/server/routers/hotels/schemas/restaurants.ts b/server/routers/hotels/schemas/restaurants.ts index 221f0eac0..707de8f23 100644 --- a/server/routers/hotels/schemas/restaurants.ts +++ b/server/routers/hotels/schemas/restaurants.ts @@ -36,6 +36,7 @@ export const restaurantSchema = z attributes: z.object({ name: z.string(), isPublished: z.boolean().default(false), + restaurantPage: z.boolean(), email: z.string().optional(), phoneNumber: z.string().optional(), externalBreakfast: z diff --git a/server/routers/hotels/telemetry.ts b/server/routers/hotels/telemetry.ts index dc5804792..bf5705ce5 100644 --- a/server/routers/hotels/telemetry.ts +++ b/server/routers/hotels/telemetry.ts @@ -82,3 +82,13 @@ export const meetingRoomsSuccessCounter = meter.createCounter( export const meetingRoomsFailCounter = meter.createCounter( "trpc.hotels.meetingRooms-fail" ) + +export const additionalDataCounter = meter.createCounter( + "trpc.hotels.additionalData" +) +export const additionalDataSuccessCounter = meter.createCounter( + "trpc.hotels.additionalData-success" +) +export const additionalDataFailCounter = meter.createCounter( + "trpc.hotels.additionalData-fail" +) diff --git a/types/components/hotelReservation/hotelSidePeek.ts b/types/components/hotelReservation/hotelSidePeek.ts index d188215b5..a1c91e68b 100644 --- a/types/components/hotelReservation/hotelSidePeek.ts +++ b/types/components/hotelReservation/hotelSidePeek.ts @@ -1,8 +1,9 @@ -import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" -import { Hotel } from "@/types/hotel" +import type { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" +import type { AdditionalData, Hotel } from "@/types/hotel" export type HotelSidePeekProps = { hotel: Hotel + additionalHotelData: AdditionalData | undefined activeSidePeek: SidePeekEnum close: () => void showCTA: boolean diff --git a/types/hotel.ts b/types/hotel.ts index d22d04390..8ff28faf6 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -2,11 +2,14 @@ import type { z } from "zod" import type { checkinSchema, - facilitySchema, getHotelDataSchema, parkingSchema, pointOfInterestSchema, } from "@/server/routers/hotels/output" +import type { + additionalDataSchema, + facilitySchema, +} from "@/server/routers/hotels/schemas/additionalData" import type { imageSchema } from "@/server/routers/hotels/schemas/image" import type { restaurantOpeningHoursSchema, @@ -36,6 +39,8 @@ export type RestaurantOpeningHours = z.output< export type GalleryImage = z.infer export type CheckInData = z.infer +export type AdditionalData = z.infer + export type PointOfInterest = z.output export enum PointOfInterestGroupEnum { diff --git a/types/trpc/routers/booking/confirmation.ts b/types/trpc/routers/booking/confirmation.ts index 9df1b3821..d5440f0c7 100644 --- a/types/trpc/routers/booking/confirmation.ts +++ b/types/trpc/routers/booking/confirmation.ts @@ -9,7 +9,7 @@ export interface BookingConfirmation { booking: BookingConfirmationSchema hotel: Hotel & { included?: { - rooms: RoomData[] + rooms: RoomData[] | undefined } } room: