diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index ba645e64c..c1293c833 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -12,6 +12,8 @@ export default async function BookingConfirmationPage({ setLang(params.lang) void getBookingConfirmation(searchParams.confirmationNumber) return ( - + ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx index fee05b878..933780e59 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx @@ -1,4 +1,4 @@ -import { getHotelData } from "@/lib/trpc/memoizedRequests" +import { getHotel } from "@/lib/trpc/memoizedRequests" import SidePeek from "@/components/HotelReservation/SidePeek" @@ -12,9 +12,10 @@ export default async function HotelSidePeek({ return } - const hotel = await getHotelData({ + const hotel = await getHotel({ hotelId: searchParams.hotel, language: params.lang, + isCardOnlyPayment: false, }) return diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index 92d36884c..04e14c36a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -1,4 +1,4 @@ -import { getHotelData } from "@/lib/trpc/memoizedRequests" +import { getHotel } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" @@ -73,15 +73,16 @@ async function enhanceHotels(hotels: { const language = getLang() const hotelFetchers = hotels.availability.map(async (hotel) => { - const hotelData = await getHotelData({ + const hotelData = await getHotel({ hotelId: hotel.hotelId.toString(), + isCardOnlyPayment: false, language, }) if (!hotelData) return { hotelData: null, price: hotel.productType } return { - hotelData: hotelData.data.attributes, + hotelData: hotelData.hotel, price: hotel.productType, } }) diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index d77143664..22d7cd65d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -2,7 +2,7 @@ import { differenceInCalendarDays, format, isWeekend } from "date-fns" import { notFound } from "next/navigation" import { Suspense } from "react" -import { getHotelData } from "@/lib/trpc/memoizedRequests" +import { getHotel } from "@/lib/trpc/memoizedRequests" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import { RoomsContainer } from "@/components/HotelReservation/SelectRate/Rooms/RoomsContainer" @@ -28,13 +28,16 @@ export default async function SelectRatePage({ }: PageArgs) { setLang(params.lang) const searchDetails = await getHotelSearchDetails({ searchParams }) - if (!searchDetails) return notFound() - const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = - searchDetails + if (!searchDetails) { + return notFound() + } + const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = searchDetails - if (!hotel) return notFound() + if (!hotel) { + return notFound() + } - const hotelData = await getHotelData({ + const hotelData = await getHotel({ hotelId: hotel.id, language: params.lang, }) @@ -72,9 +75,9 @@ export default async function SelectRatePage({ leadTime: differenceInCalendarDays(arrivalDate, new Date()), searchType: "hotel", bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: hotelData?.data?.attributes.address.country, + country: hotelData?.hotel.address.country, hotelID: hotel?.id, - region: hotelData?.data?.attributes.address.city, + region: hotelData?.hotel.address.city, } const hotelId = +hotel.id diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx index 5f158c37a..6659c0df2 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsTracking.tsx @@ -16,8 +16,8 @@ import { type TrackingSDKUserData, } from "@/types/components/tracking" import type { Packages } from "@/types/requests/packages" +import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" import type { Lang } from "@/constants/languages" -import type { RoomConfiguration } from "@/server/routers/hotels/output" type Props = { initialHotelsTrackingData: TrackingSDKHotelInfo @@ -135,7 +135,7 @@ export default function EnterDetailsTracking(props: Props) { roomPrice: roomPrice.perStay.local.price, discount: roomRate.memberRate ? roomRate.publicRate.localPrice.pricePerStay - - roomRate.memberRate.localPrice.pricePerStay + roomRate.memberRate.localPrice.pricePerStay : 0, analyticsrateCode: getAnalyticsRateCode(roomRate.publicRate.rateCode), ancillaries: breakfastAncillary ? [breakfastAncillary] : [], diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index afcec344d..f1fb59405 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -4,7 +4,7 @@ import { Suspense } from "react" import { getBreakfastPackages, - getHotelData, + getHotel, getPackages, getProfileSafely, getSelectedRoomAvailability, @@ -106,19 +106,19 @@ export default async function StepPage({ const packages = packageCodes ? await getPackages({ - adults, - children: childrenInRoom?.length, - endDate: toDate, - hotelId, - packageCodes, - startDate: fromDate, - }) + adults, + children: childrenInRoom?.length, + endDate: toDate, + hotelId, + packageCodes, + startDate: fromDate, + }) : null const roomAvailability = await getSelectedRoomAvailability( selectedRoomAvailabilityInput ) - const hotelData = await getHotelData({ + const hotelData = await getHotel({ hotelId, isCardOnlyPayment: roomAvailability?.mustBeGuaranteed, language: lang, @@ -153,14 +153,14 @@ export default async function StepPage({ const memberPrice = roomAvailability.memberRate ? { - price: roomAvailability.memberRate.localPrice.pricePerStay, - currency: roomAvailability.memberRate.localPrice.currency, - } + price: roomAvailability.memberRate.localPrice.pricePerStay, + currency: roomAvailability.memberRate.localPrice.currency, + } : undefined - const arrivalDate = new Date(fromDate) - const departureDate = new Date(toDate) - const hotelAttributes = hotelData?.data.attributes + const arrivalDate = new Date(searchParams.fromDate) + const departureDate = new Date(searchParams.toDate) + const hotelAttributes = hotelData?.hotel const initialHotelsTrackingData: TrackingSDKHotelInfo = { searchTerm: searchParams.city, @@ -207,7 +207,7 @@ export default async function StepPage({ searchParamsStr={selectRoomParams.toString()} step={searchParams.step} user={user} - vat={hotelData.data.attributes.vat} + vat={hotelAttributes.vat} >
@@ -262,11 +262,11 @@ export default async function StepPage({ user={user} roomPrice={roomPrice} otherPaymentOptions={ - hotelData.data.attributes.merchantInformationData + hotelData.hotel.merchantInformationData .alternatePaymentOptions } supportedCards={ - hotelData.data.attributes.merchantInformationData.cards + hotelData.hotel.merchantInformationData.cards } mustBeGuaranteed={mustBeGuaranteed} /> diff --git a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx index e840242a4..c8500a2bc 100644 --- a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx @@ -1,5 +1,5 @@ import { env } from "@/env/server" -import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests" +import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests" import BookingWidget, { preload } from "@/components/BookingWidget" import { getLang } from "@/i18n/serverContext" @@ -21,7 +21,7 @@ export default async function BookingWidgetPage({ if (params.contentType === PageContentTypeEnum.hotelPage) { const hotelPageData = await getHotelPage() - const hotelData = await getHotelData({ + const hotelData = await getHotel({ hotelId: hotelPageData?.hotel_page_id || "", language: getLang(), }) diff --git a/components/Blocks/HotelListing/index.tsx b/components/Blocks/HotelListing/index.tsx index 06e1d9087..b005aefb0 100644 --- a/components/Blocks/HotelListing/index.tsx +++ b/components/Blocks/HotelListing/index.tsx @@ -29,8 +29,8 @@ export default async function HotelListing({ {hotels.map(({ data, url }) => ( diff --git a/components/ContentType/HotelPage/IntroSection/types.ts b/components/ContentType/HotelPage/IntroSection/types.ts index bdb15d8b9..d1c43d1db 100644 --- a/components/ContentType/HotelPage/IntroSection/types.ts +++ b/components/ContentType/HotelPage/IntroSection/types.ts @@ -1,14 +1,15 @@ import type { + Hotel, HotelAddress, - HotelData, + HotelContent, HotelLocation, HotelTripAdvisor, } from "@/types/hotel" export type IntroSectionProps = { - hotelName: HotelData["data"]["attributes"]["name"] - hotelDescription: HotelData["data"]["attributes"]["hotelContent"]["texts"]["descriptions"]["short"] - location: HotelLocation address: HotelAddress + hotelDescription: HotelContent["texts"]["descriptions"]["short"] + hotelName: Hotel["name"] + location: HotelLocation tripAdvisor: HotelTripAdvisor } diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx index 6229b578d..27089a617 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx @@ -10,9 +10,9 @@ import { } from "@/types/components/hotelPage/sidepeek/parking" export default async function ParkingPrices({ - pricing, currency, freeParking, + pricing, }: ParkingPricesProps) { const intl = await getIntl() const day = intl.formatMessage({ id: "Price per day" }) diff --git a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx index 297df54a5..eb013c89a 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/index.tsx @@ -53,8 +53,8 @@ export default async function ParkingAmenity({ @@ -64,8 +64,8 @@ export default async function ParkingAmenity({ diff --git a/components/ContentType/HotelPage/SidePeeks/RestaurantBar/RestaurantBarItem/index.tsx b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/RestaurantBarItem/index.tsx index 8abe5a5b7..422980322 100644 --- a/components/ContentType/HotelPage/SidePeeks/RestaurantBar/RestaurantBarItem/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/RestaurantBarItem/index.tsx @@ -16,8 +16,8 @@ export default async function RestaurantBarItem({ restaurant, }: RestaurantBarItemProps) { const intl = await getIntl() - const { name, openingDetails, menus, content } = restaurant - const { bookTableUrl, images } = restaurant.content + const { bookTableUrl, name, openingDetails, content, menus } = restaurant + const { images } = restaurant.content const visibleImages = restaurant.content.images.slice(0, 2) const imageWidth = images.length === 2 ? 240 : 496 diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index ee788dcc1..4049a2903 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -2,7 +2,7 @@ import { notFound } from "next/navigation" import { Suspense } from "react" import { env } from "@/env/server" -import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests" +import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests" import AccordionSection from "@/components/Blocks/Accordion" import Breadcrumbs from "@/components/Breadcrumbs" @@ -41,23 +41,27 @@ 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 { AdditionalData, Facility } from "@/types/hotel" +import type { Facility } from "@/types/hotel" import { PageContentTypeEnum } from "@/types/requests/contentType" export default async function HotelPage({ hotelId }: HotelPageProps) { const lang = getLang() const [hotelPageData, hotelData] = await Promise.all([ getHotelPage(), - getHotelData({ hotelId, language: lang }), + getHotel({ + hotelId, + isCardOnlyPayment: false, + language: lang, + }), ]) const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID - if (!hotelData?.data || !hotelPageData) { + if (!hotelData?.hotel || !hotelPageData) { return notFound() } - const jsonSchema = generateHotelSchema(hotelData.data.attributes) + const jsonSchema = generateHotelSchema(hotelData.hotel) const { faq, content, tabValues } = hotelPageData const { name, @@ -73,12 +77,9 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { location, ratings, parking, - } = hotelData.data.attributes - const roomCategories = hotelData.included.rooms || [] - const restaurants = hotelData.included.restaurants || [] - const additionalData = - hotelData.included.additionalData || ({} as AdditionalData) - + } = hotelData.hotel + const restaurants = hotelData.restaurants + const roomCategories = hotelData.roomCategories const { healthAndWellness, restaurantImages, @@ -87,7 +88,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { gallery, hotelParking, displayWebPage, - } = additionalData + } = hotelData.additionalData const images = gallery?.smallerImages const description = hotelContent.texts.descriptions.medium diff --git a/components/HotelReservation/BookingConfirmation/index.tsx b/components/HotelReservation/BookingConfirmation/index.tsx index de0c1d1fe..c3573dc33 100644 --- a/components/HotelReservation/BookingConfirmation/index.tsx +++ b/components/HotelReservation/BookingConfirmation/index.tsx @@ -22,8 +22,9 @@ export default async function BookingConfirmation({ confirmationNumber, }: BookingConfirmationProps) { const lang = getLang() - const { booking, hotel, room } = - await getBookingConfirmation(confirmationNumber) + const { booking, hotel, room } = await getBookingConfirmation( + confirmationNumber + ) const arrivalDate = new Date(booking.checkInDate) const departureDate = new Date(booking.checkOutDate) diff --git a/components/HotelReservation/EnterDetails/Breakfast/schema.ts b/components/HotelReservation/EnterDetails/Breakfast/schema.ts index 4766980cb..43bc8bc8f 100644 --- a/components/HotelReservation/EnterDetails/Breakfast/schema.ts +++ b/components/HotelReservation/EnterDetails/Breakfast/schema.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { breakfastPackageSchema } from "@/server/routers/hotels/output" +import { breakfastPackageSchema } from "@/server/routers/hotels/schemas/packages" export const breakfastStoreSchema = z.object({ breakfast: breakfastPackageSchema.or(z.literal(false)), diff --git a/components/HotelReservation/EnterDetails/Header/index.tsx b/components/HotelReservation/EnterDetails/Header/index.tsx index 684b82e1b..d4fd6c58e 100644 --- a/components/HotelReservation/EnterDetails/Header/index.tsx +++ b/components/HotelReservation/EnterDetails/Header/index.tsx @@ -10,10 +10,10 @@ import styles from "./header.module.css" import type { HotelHeaderProps } from "@/types/components/hotelReservation/enterDetails/hotelHeader" -export default async function HotelHeader({ hotelData }: HotelHeaderProps) { +export default async function HotelHeader({ + hotelData: { hotel }, +}: HotelHeaderProps) { const intl = await getIntl() - const hotel = hotelData.data.attributes - const image = hotel.hotelContent?.images const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}` diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx index 5acdb7bff..d1aed3f1d 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx @@ -32,7 +32,7 @@ import { } from "@/types/components/tracking" function isValidHotelData(hotel: NullableHotelData): hotel is HotelData { - return hotel !== null && hotel !== undefined + return hotel != null } export async function SelectHotelMapContainer({ diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 539da5a29..9655e1c12 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -1,6 +1,6 @@ import { Suspense } from "react" -import { getHotelData } from "@/lib/trpc/memoizedRequests" +import { getHotel } from "@/lib/trpc/memoizedRequests" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import ImageGallery from "@/components/ImageGallery" @@ -18,55 +18,44 @@ import { NoRoomsAlert } from "./NoRoomsAlert" import styles from "./hotelInfoCard.module.css" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { Lang } from "@/constants/languages" - -type Props = { - hotelId: number - lang: Lang - fromDate: Date - toDate: Date - adultCount: number - childArray?: Child[] -} +import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCard" export default async function HotelInfoCard({ hotelId, lang, ...props -}: Props) { - const hotelData = await getHotelData({ +}: HotelInfoCardProps) { + const hotelData = await getHotel({ hotelId: hotelId.toString(), + isCardOnlyPayment: false, language: lang, }) - const hotelAttributes = hotelData?.data.attributes + const hotel = hotelData?.hotel const intl = await getIntl() - const sortedFacilities = hotelAttributes?.detailedFacilities + const sortedFacilities = hotel?.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) return (
- {hotelAttributes && ( + {hotel && (
- {hotelAttributes.ratings?.tripAdvisor && ( - + {hotel.ratings?.tripAdvisor && ( + )}
- {hotelAttributes.name} + {hotel.name}
@@ -75,16 +64,16 @@ export default async function HotelInfoCard({ id: "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center", }, { - address: hotelAttributes.address.streetAddress, - city: hotelAttributes.address.city, - distanceToCityCenterInKm: getSingleDecimal( - hotelAttributes.location.distanceToCentre / 1000 + address: hotel.address.streetAddress, + city: hotel.address.city, + distanceToCityCentreInKm: getSingleDecimal( + hotel.location.distanceToCentre / 1000 ), } )} - {hotelAttributes.hotelContent.texts.descriptions.medium} + {hotel.hotelContent.texts.descriptions.medium}
@@ -111,15 +100,15 @@ export default async function HotelInfoCard({
)} - {hotelAttributes?.specialAlerts.map((alert) => { + {hotel?.specialAlerts.map((alert) => { return (
) } diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 387541b98..aa3d46b06 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -25,14 +25,14 @@ import { } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { RoomConfiguration } from "@/server/routers/hotels/output" +import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" export default function Rooms({ - roomsAvailability, - roomCategories = [], availablePackages, hotelType, isUserLoggedIn, + roomsAvailability, + roomCategories = [], }: SelectRateProps) { const router = useRouter() const pathname = usePathname() diff --git a/components/HotelReservation/SelectRate/Rooms/utils.ts b/components/HotelReservation/SelectRate/Rooms/utils.ts index d84aae3a8..4ff593f1b 100644 --- a/components/HotelReservation/SelectRate/Rooms/utils.ts +++ b/components/HotelReservation/SelectRate/Rooms/utils.ts @@ -1,5 +1,4 @@ -import type { RoomParam } from "@/types/components/hotelReservation/selectRate/section" -import type { RoomConfiguration } from "@/server/routers/hotels/output" +import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" /** * Get the lowest priced room for each room type that appears more than once. @@ -64,16 +63,16 @@ export function filterDuplicateRoomTypesByLowestPrice( if ( !previousLowest || currentRequestedPrice < - Math.min( - Number( - previousLowest.products[0].productType.public.requestedPrice - ?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].productType.member?.requestedPrice - ?.pricePerNight - ) ?? Infinity - ) || + Math.min( + Number( + previousLowest.products[0].productType.public.requestedPrice + ?.pricePerNight + ) ?? Infinity, + Number( + previousLowest.products[0].productType.member?.requestedPrice + ?.pricePerNight + ) ?? Infinity + ) || (currentRequestedPrice === Math.min( Number( @@ -86,16 +85,16 @@ export function filterDuplicateRoomTypesByLowestPrice( ) ?? Infinity ) && currentLocalPrice < - Math.min( - Number( - previousLowest.products[0].productType.public.localPrice - ?.pricePerNight - ) ?? Infinity, - Number( - previousLowest.products[0].productType.member?.localPrice - ?.pricePerNight - ) ?? Infinity - )) + Math.min( + Number( + previousLowest.products[0].productType.public.localPrice + ?.pricePerNight + ) ?? Infinity, + Number( + previousLowest.products[0].productType.member?.localPrice + ?.pricePerNight + ) ?? Infinity + )) ) { roomMap.set(roomType, room) } diff --git a/components/HotelReservation/SidePeek/index.tsx b/components/HotelReservation/SidePeek/index.tsx index bf75ed04b..36781e954 100644 --- a/components/HotelReservation/SidePeek/index.tsx +++ b/components/HotelReservation/SidePeek/index.tsx @@ -7,13 +7,11 @@ import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek" import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek" import useLang from "@/hooks/useLang" -import type { HotelData } from "@/types/hotel" +import type { HotelReservationSidePeekProps } from "@/types/components/hotelReservation/sidePeek" export default function HotelReservationSidePeek({ hotel, -}: { - hotel: HotelData | null -}) { +}: HotelReservationSidePeekProps) { const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek) const hotelId = useSidePeekStore((state) => state.hotelId) const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode) @@ -21,7 +19,7 @@ export default function HotelReservationSidePeek({ const close = useSidePeekStore((state) => state.closeSidePeek) const lang = useLang() - const { data: hotelData } = trpc.hotel.hotelData.get.useQuery( + const { data: hotelData } = trpc.hotel.get.useQuery( { hotelId: hotelId ?? "", language: lang, @@ -32,7 +30,7 @@ export default function HotelReservationSidePeek({ } ) - const selectedRoom = hotelData?.included.rooms?.find((room) => + const selectedRoom = hotelData?.roomCategories.find((room) => room.roomTypes.some((type) => type.code === roomTypeCode) ) @@ -41,8 +39,8 @@ export default function HotelReservationSidePeek({ <> {hotelData && ( (GetHotelPageMetadata, variables) const hotelPageData = hotelPageResponse.hotel_page const hotelData = hotelPageData.hotel_page_id - ? await getHotelData( - { hotelId: hotelPageData.hotel_page_id, language: ctx.lang }, + ? await getHotel( + { + hotelId: hotelPageData.hotel_page_id, + isCardOnlyPayment: false, + language: ctx.lang, + }, ctx.serviceToken ) : null return getTransformedMetadata({ ...hotelPageData, - hotelData: hotelData?.data.attributes, + hotelData: hotelData?.hotel, }) default: return null diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 5b49eb355..22252dc80 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -1,9 +1,11 @@ import { z } from "zod" +import { Lang } from "@/constants/languages" + import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { Country } from "@/types/enums/country" -export const getHotelsAvailabilityInputSchema = z.object({ +export const hotelsAvailabilityInputSchema = z.object({ cityId: z.string(), roomStayStartDate: z.string(), roomStayEndDate: z.string(), @@ -21,7 +23,7 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({ bookingCode: z.string().optional().default(""), }) -export const getRoomsAvailabilityInputSchema = z.object({ +export const roomsAvailabilityInputSchema = z.object({ hotelId: z.number(), roomStayStartDate: z.string(), roomStayEndDate: z.string(), @@ -31,7 +33,7 @@ export const getRoomsAvailabilityInputSchema = z.object({ rateCode: z.string().optional(), }) -export const getSelectedRoomAvailabilityInputSchema = z.object({ +export const selectedRoomAvailabilityInputSchema = z.object({ hotelId: z.string(), roomStayStartDate: z.string(), roomStayEndDate: z.string(), @@ -44,25 +46,23 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({ }) export type GetSelectedRoomAvailabilityInput = z.input< - typeof getSelectedRoomAvailabilityInputSchema + typeof selectedRoomAvailabilityInputSchema > export type GetRoomsAvailabilityInput = z.input< - typeof getRoomsAvailabilityInputSchema + typeof roomsAvailabilityInputSchema > -export const getRatesInputSchema = z.object({ +export const ratesInputSchema = z.object({ hotelId: z.string(), }) -export const getHotelDataInputSchema = z.object({ +export const hotelInputSchema = z.object({ hotelId: z.string(), - language: z.string(), - isCardOnlyPayment: z.boolean().optional(), + isCardOnlyPayment: z.boolean().default(false), + language: z.nativeEnum(Lang), }) -export type HotelDataInput = z.input - export const getHotelsInput = z.object({ locationFilter: z .object({ @@ -73,13 +73,13 @@ export const getHotelsInput = z.object({ .nullable(), hotelsToInclude: z.array(z.string()), }) -export interface GetHotelsInput extends z.infer {} +export interface GetHotelsInput extends z.infer { } export const nearbyHotelIdsInput = z.object({ hotelId: z.string(), }) -export const getBreakfastPackageInputSchema = z.object({ +export const breakfastPackageInputSchema = z.object({ adults: z.number().min(1, { message: "at least one adult is required" }), fromDate: z .string() @@ -92,7 +92,7 @@ export const getBreakfastPackageInputSchema = z.object({ .pipe(z.coerce.date()), }) -export const getRoomPackagesInputSchema = z.object({ +export const roomPackagesInputSchema = z.object({ hotelId: z.string(), startDate: z.string(), endDate: z.string(), @@ -100,7 +100,7 @@ export const getRoomPackagesInputSchema = z.object({ children: z.number().optional().default(0), packageCodes: z.array(z.string()).optional().default([]), }) -export const getCityCoordinatesInputSchema = z.object({ +export const cityCoordinatesInputSchema = z.object({ city: z.string(), hotel: z.object({ address: z.string().optional(), diff --git a/server/routers/hotels/metrics.ts b/server/routers/hotels/metrics.ts new file mode 100644 index 000000000..85c15a06d --- /dev/null +++ b/server/routers/hotels/metrics.ts @@ -0,0 +1,65 @@ +import { metrics as opentelemetryMetrics } from "@opentelemetry/api" + +const meter = opentelemetryMetrics.getMeter("trpc.hotels") +export const metrics = { + additionalData: { + counter: meter.createCounter("trpc.hotels.additionalData"), + fail: meter.createCounter("trpc.hotels.additionalData-fail"), + success: meter.createCounter("trpc.hotels.additionalData-success"), + }, + breakfastPackage: { + counter: meter.createCounter("trpc.package.breakfast"), + fail: meter.createCounter("trpc.package.breakfast-fail"), + success: meter.createCounter("trpc.package.breakfast-success"), + }, + hotel: { + counter: meter.createCounter("trpc.hotel.get"), + fail: meter.createCounter("trpc.hotel.get-fail"), + success: meter.createCounter("trpc.hotel.get-success"), + }, + hotels: { + counter: meter.createCounter("trpc.hotel.hotels.get"), + fail: meter.createCounter("trpc.hotel.hotels.get-fail"), + success: meter.createCounter("trpc.hotel.hotels.get-success"), + }, + hotelIds: { + counter: meter.createCounter("trpc.hotel.hotel-ids.get"), + fail: meter.createCounter("trpc.hotel.hotel-ids.get-fail"), + success: meter.createCounter("trpc.hotel.hotel-ids.get-success"), + }, + hotelsAvailability: { + counter: meter.createCounter("trpc.hotel.availability.hotels"), + fail: meter.createCounter("trpc.hotel.availability.hotels-fail"), + success: meter.createCounter("trpc.hotel.availability.hotels-success"), + }, + hotelsByHotelIdAvailability: { + counter: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id"), + fail: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-fail"), + success: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-success"), + }, + meetingRooms: { + counter: meter.createCounter("trpc.hotels.meetingRooms"), + fail: meter.createCounter("trpc.hotels.meetingRooms-fail"), + success: meter.createCounter("trpc.hotels.meetingRooms-success"), + }, + nearbyHotelIds: { + counter: meter.createCounter("trpc.hotel.nearby-hotel-ids.get"), + fail: meter.createCounter("trpc.hotel.nearby-hotel-ids.get-fail"), + success: meter.createCounter("trpc.hotel.nearby-hotel-ids.get-success"), + }, + packages: { + counter: meter.createCounter("trpc.hotel.packages.get"), + fail: meter.createCounter("trpc.hotel.packages.get-fail"), + success: meter.createCounter("trpc.hotel.packages.get-success"), + }, + roomAvailability: { + counter: meter.createCounter("trpc.hotel.availability.rooms"), + fail: meter.createCounter("trpc.hotel.availability.rooms-fail"), + success: meter.createCounter("trpc.hotel.availability.rooms-success"), + }, + selectedRoomAvailability: { + counter: meter.createCounter("trpc.hotel.availability.room"), + fail: meter.createCounter("trpc.hotel.availability.room-fail"), + success: meter.createCounter("trpc.hotel.availability.room-success"), + }, +} diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 40c12ea20..a25b7fb12 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -1,668 +1,120 @@ 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" -import { specialAlertsSchema } from "./schemas/specialAlerts" -import { getPoiGroupByCategoryName } from "./utils" +import { occupancySchema } from "./schemas/availability/occupancy" +import { productTypeSchema } from "./schemas/availability/productType" +import { citySchema } from "./schemas/city" +import { + attributesSchema, + includesSchema, + relationshipsSchema as hotelRelationshipsSchema, +} from "./schemas/hotel" +import { locationCitySchema } from "./schemas/location/city" +import { locationHotelSchema } from "./schemas/location/hotel" +import { breakfastPackageSchema, packageSchema } from "./schemas/packages" +import { rateSchema } from "./schemas/rate" +import { relationshipsSchema } from "./schemas/relationships" +import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration" +import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition" -import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" -import { FacilityEnum } from "@/types/enums/facilities" -import { PackageTypeEnum } from "@/types/enums/packages" -import type { AdditionalData, RestaurantData, RoomData } from "@/types/hotel" - -const ratingsSchema = z - .object({ - tripAdvisor: z.object({ - numberOfReviews: z.number(), - rating: z.number(), - ratingImageUrl: z.string(), - webUrl: z.string(), - awards: z.array( - z.object({ - displayName: z.string(), - images: z.object({ - small: z.string(), - medium: z.string(), - large: z.string(), - }), - }) - ), - reviews: z - .object({ - widgetHtmlTagId: z.string(), - widgetScriptEmbedUrlIframe: z.string(), - widgetScriptEmbedUrlJavaScript: z.string(), - }) - .optional(), - }), - }) - .optional() - -const addressSchema = z.object({ - streetAddress: z.string(), - city: z.string(), - zipCode: z.string(), - country: z.string(), -}) - -const contactInformationSchema = z.object({ - phoneNumber: z.string(), - faxNumber: z.string().optional(), - email: z.string(), - websiteUrl: z.string(), -}) - -export const checkinSchema = z.object({ - checkInTime: z.string(), - checkOutTime: z.string(), - onlineCheckOutAvailableFrom: z.string().nullable().optional(), - onlineCheckout: z.boolean(), -}) - -const ecoLabelsSchema = z.object({ - euEcoLabel: z.boolean(), - greenGlobeLabel: z.boolean(), - nordicEcoLabel: z.boolean(), - svanenEcoLabelCertificateNumber: z.string().optional(), -}) - -const interiorSchema = z.object({ - numberOfBeds: z.number(), - numberOfCribs: z.number(), - numberOfFloors: z.number(), - numberOfRooms: z.object({ - connected: z.number(), - forAllergics: z.number().optional(), - forDisabled: z.number(), - nonSmoking: z.number(), - pet: z.number(), - withExtraBeds: z.number(), - total: z.number(), - }), -}) - -const receptionHoursSchema = z.object({ - alwaysOpen: z.boolean(), - isClosed: z.boolean(), - openingTime: z.string().optional(), - closingTime: z.string().optional(), -}) - -const locationSchema = z.object({ - distanceToCentre: z.number(), - latitude: z.number(), - longitude: z.number(), -}) - -const hotelContentSchema = z.object({ - images: imageSchema, - texts: z.object({ - facilityInformation: z.string().optional(), - surroundingInformation: z.string(), - descriptions: z.object({ - short: z.string(), - medium: z.string(), - }), - meetingDescription: z - .object({ - short: z.string().optional(), - medium: z.string().optional(), - }) - .optional(), - }), - restaurantsOverviewPage: z.object({ - restaurantsOverviewPageLinkText: z.string().optional(), - restaurantsOverviewPageLink: z.string().optional(), - restaurantsContentDescriptionShort: z.string().optional(), - restaurantsContentDescriptionMedium: z.string().optional(), - }), // TODO remove, use new /additionalData endpoint -}) - -const detailedFacilitySchema = z.object({ - id: z.number(), - name: z.string(), - public: z.boolean(), - sortOrder: z.number(), - filter: z.string().optional(), - icon: z.string().optional(), -}) - -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(), - content: z.object({ - images: z.array(imageSchema), - texts: z.object({ - facilityInformation: z.string().optional(), - surroundingInformation: z.string().optional(), - descriptions: z.object({ - short: z.string(), - medium: z.string(), - }), - }), - }), - openingDetails: z.object({ - useManualOpeningHours: z.boolean(), - manualOpeningHours: z.string().optional(), - openingHours: z.object({ - ordinary: z.object({ - alwaysOpen: z.boolean(), - isClosed: z.boolean(), - openingTime: z.string().optional(), - closingTime: z.string().optional(), - sortOrder: z.number().optional(), - }), - weekends: z.object({ - alwaysOpen: z.boolean(), - isClosed: z.boolean(), - openingTime: z.string().optional(), - closingTime: z.string().optional(), - sortOrder: z.number().optional(), - }), - }), - }), - details: z.array( - z.object({ - name: z.string(), - type: z.string(), - value: z.string().optional(), - }) - ), -}) - -const rewardNightSchema = z.object({ - points: z.number(), - campaign: z.object({ - start: z.string(), - end: z.string(), - points: z.number(), - }), -}) - -export const pointOfInterestSchema = z - .object({ - name: z.string().optional(), - distance: z.number().optional(), - category: z.object({ - name: z.string().optional(), - group: z.string().optional(), - }), - location: locationSchema.optional(), - isHighlighted: z.boolean().optional(), - }) - .transform((poi) => ({ - name: poi.name, - distance: poi.distance, - categoryName: poi.category.name, - group: getPoiGroupByCategoryName(poi.category.name), - coordinates: { - lat: poi.location?.latitude ?? 0, - lng: poi.location?.longitude ?? 0, - }, - })) - -const parkingPricingSchema = z.object({ - freeParking: z.boolean(), - paymentType: z.string().optional(), - localCurrency: z.object({ - currency: z.string().default("N/A"), - range: z.object({ - min: z.number().optional(), - max: z.number().optional(), - }), - ordinary: z - .array( - z.object({ - period: z.string().optional(), - amount: z.number().optional(), - startTime: z.string().optional(), - endTime: z.string().optional(), - }) - ) - .optional(), - weekend: z - .array( - z.object({ - period: z.string().optional(), - amount: z.number().optional(), - startTime: z.string().optional(), - endTime: z.string().optional(), - }) - ) - .optional(), - }), - requestedCurrency: z - .object({ - currency: z.string().default("N/A"), - range: z - .object({ - min: z.number().optional(), - max: z.number().optional(), - }) - .optional(), - ordinary: z - .array( - z.object({ - period: z.string().optional(), - amount: z.number().optional(), - startTime: z.string().optional(), - endTime: z.string().optional(), - }) - ) - .optional(), - weekend: z - .array( - z.object({ - period: z.string().optional(), - amount: z.number().optional(), - startTime: z.string().optional(), - endTime: z.string().optional(), - }) - ) - .optional(), - }) - .optional(), -}) - -export const parkingSchema = z.object({ - type: z.string().optional(), - name: z.string().optional(), - address: z.string().optional(), - numberOfParkingSpots: z.number().optional(), - numberOfChargingSpaces: z.number().optional(), - distanceToHotel: z.number().optional(), - canMakeReservation: z.boolean(), - externalParkingUrl: z.string().optional(), - pricing: parkingPricingSchema, -}) - -const socialMediaSchema = z.object({ - instagram: z.string().optional(), - facebook: z.string().optional(), -}) - -const relationshipsSchema = z.object({ - restaurants: z.object({ - links: z.object({ - related: z.string(), - }), - }), - nearbyHotels: z.object({ - links: z.object({ - related: z.string(), - }), - }), - roomCategories: z.object({ - links: z.object({ - related: z.string(), - }), - }), - meetingRooms: z.object({ - links: z.object({ - related: z.string(), - }), - }), -}) - -const merchantInformationSchema = z.object({ - webMerchantId: z.string(), - cards: z.record(z.string(), z.boolean()).transform((val) => { - return Object.entries(val) - .filter(([_, enabled]) => enabled) - .map(([key]) => key) - .filter((key): key is PaymentMethodEnum => !!key) - }), - alternatePaymentOptions: z - .record(z.string(), z.boolean()) - .transform((val) => { - return Object.entries(val) - .filter(([_, enabled]) => enabled) - .map(([key]) => key) - .filter((key): key is PaymentMethodEnum => !!key) - }), -}) - -const hotelFacilityDetailSchema = z - .object({ - description: z.string(), - heading: z.string(), - }) - .optional() - -/** Possibly more values */ -const hotelFacilityDetailsSchema = z.object({ - breakfast: hotelFacilityDetailSchema, - checkout: hotelFacilityDetailSchema, - gym: hotelFacilityDetailSchema, - internet: hotelFacilityDetailSchema, - laundry: hotelFacilityDetailSchema, - luggage: hotelFacilityDetailSchema, - shop: hotelFacilityDetailSchema, - telephone: hotelFacilityDetailSchema, -}) - -const hotelInformationSchema = z - .object({ - description: z.string(), - heading: z.string(), - link: z.string().optional(), - }) - .optional() - -const hotelInformationsSchema = z.object({ - accessibility: hotelInformationSchema, - safety: hotelInformationSchema, - sustainability: hotelInformationSchema, -}) - -const hotelFactsSchema = z.object({ - checkin: checkinSchema, - ecoLabels: ecoLabelsSchema, - hotelFacilityDetail: hotelFacilityDetailsSchema.default({}), - hotelInformation: hotelInformationsSchema.default({}), - interior: interiorSchema, - receptionHours: receptionHoursSchema, - yearBuilt: z.string(), -}) - -type DetailedFacility = { id: FacilityEnum } & z.infer< - typeof detailedFacilitySchema -> -export const hotelAttributesSchema = z.object({ - address: addressSchema, - cityId: z.string(), - cityName: z.string(), - conferencesAndMeetings: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint - contactInformation: contactInformationSchema, - detailedFacilities: z.array(detailedFacilitySchema).transform( - (facilities) => - facilities - // Filter away facilities with ID:s that we don't recognize - .filter( - (f) => f.id !== undefined && f.id !== null && f.id in FacilityEnum - ) - .sort((a, b) => b.sortOrder - a.sortOrder) as DetailedFacility[] - ), - gallery: gallerySchema.optional(), // TODO remove, use new /additionalData endpoint - galleryImages: z.array(imageSchema).optional(), - healthFacilities: z.array(healthFacilitySchema), - hotelContent: hotelContentSchema, - hotelFacts: hotelFactsSchema, - hotelType: z.string().optional(), - isActive: z.boolean(), - isPublished: z.boolean(), - keywords: z.array(z.string()), - location: locationSchema, - merchantInformationData: merchantInformationSchema, - name: z.string(), - operaId: z.string(), - parking: z.array(parkingSchema), - pointsOfInterest: z - .array(pointOfInterestSchema) - .transform((pois) => - pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0)) - ), - ratings: ratingsSchema, - rewardNight: rewardNightSchema, - restaurantImages: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint - socialMedia: socialMediaSchema, - specialAlerts: specialAlertsSchema, - vat: z.number(), -}) - -const includedSchema = z - .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, - } - }) +import type { AdditionalData, City, NearbyHotel, Restaurant, Room } from "@/types/hotel" // NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html -export const getHotelDataSchema = z.object({ - data: z.object({ - id: z.string(), - type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels". - language: z.string().transform((val) => { - const lang = toLang(val) - if (!lang) { - throw new Error("Invalid language") - } - return lang +export const hotelSchema = z + .object({ + data: z.object({ + attributes: attributesSchema, + id: z.string(), + language: z.string().transform((val) => { + const lang = toLang(val) + if (!lang) { + throw new Error("Invalid language") + } + return lang + }), + relationships: hotelRelationshipsSchema, + type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels". }), - attributes: hotelAttributesSchema, - relationships: relationshipsSchema, - }), - // NOTE: We can pass an "include" param to the hotel API to retrieve - // additional data for an individual hotel. - included: includedSchema.optional().transform((incl) => { + // NOTE: We can pass an "include" param to the hotel API to retrieve + // additional data for an individual hotel. + included: includesSchema, + }) + .transform(({ data: { attributes, ...data }, included }) => { + const additionalData = included.find((inc): inc is AdditionalData => inc!.type === "additionalData") + const cities = included.filter((inc): inc is City => inc!.type === "cities") + const nearbyHotels = included.filter( + (inc): inc is NearbyHotel => inc!.type === "hotels" + ) + const restaurants = included.filter( + (inc): inc is Restaurant => inc!.type === "restaurants" + ) + const roomCategories = included.filter( + (inc): inc is Room => inc!.type === "roomcategories" + ) return { - restaurants: incl?.restaurants, - rooms: incl?.rooms, - additionalData: incl?.additionalData[0], - } - }), -}) - -export const childrenSchema = z.object({ - age: z.number(), - bedType: z.nativeEnum(ChildBedTypeEnum), -}) - -const occupancySchema = z.object({ - adults: z.number(), - children: z.array(childrenSchema).default([]), -}) - -const linksSchema = z.object({ - links: z.array( - z.object({ - url: z.string().url(), - type: z.string(), - }) - ), -}) - -export const priceSchema = z.object({ - pricePerNight: z.coerce.number(), - pricePerStay: z.coerce.number(), - currency: z.string().default("N/A"), -}) - -export const productTypePriceSchema = z.object({ - rateCode: z.string(), - rateType: z.string().optional(), - localPrice: priceSchema, - requestedPrice: priceSchema.optional(), -}) - -const productSchema = z.object({ - productType: z.object({ - public: productTypePriceSchema.default({ - rateCode: "", - rateType: "", - localPrice: { - currency: "N/A", - pricePerNight: 0, - pricePerStay: 0, + additionalData, + cities, + hotel: { + ...data, + ...attributes, }, - requestedPrice: undefined, - }), - member: productTypePriceSchema.optional(), - }), -}) + nearbyHotels, + restaurants, + roomCategories, + } + }) -const hotelsAvailabilitySchema = z.object({ +export const hotelsAvailabilitySchema = z.object({ data: z.array( z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), - occupancy: occupancySchema, - status: z.string(), hotelId: z.number(), - productType: z - .object({ - public: productTypePriceSchema.optional(), - member: productTypePriceSchema.optional(), - }) - .optional(), + occupancy: occupancySchema, + productType: productTypeSchema, + status: z.string(), }), - relationships: linksSchema.optional(), + relationships: relationshipsSchema.optional(), type: z.string().optional(), }) ), }) -export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema -export type HotelsAvailability = z.infer -export type ProductType = - HotelsAvailability["data"][number]["attributes"]["productType"] -export type ProductTypePrices = z.infer -export type HotelsAvailabilityItem = - HotelsAvailability["data"][number]["attributes"] - -const roomConfigurationSchema = z.object({ - status: z.string(), - roomTypeCode: z.string(), - roomType: z.string(), - roomsLeft: z.number(), - features: z - .array( - z.object({ - inventory: z.number(), - code: z.enum([ - RoomPackageCodeEnum.PET_ROOM, - RoomPackageCodeEnum.ALLERGY_ROOM, - RoomPackageCodeEnum.ACCESSIBILITY_ROOM, - ]), - }) - ) - .default([]), - products: z.array(productSchema).default([]), -}) - -const rateDefinitionSchema = z.object({ - title: z.string(), - breakfastIncluded: z.boolean(), - rateType: z.string().optional(), - rateCode: z.string(), - generalTerms: z.array(z.string()), - cancellationRule: z.string(), - cancellationText: z.string(), - mustBeGuaranteed: z.boolean(), -}) - -const roomsAvailabilitySchema = z +export const roomsAvailabilitySchema = z .object({ data: z.object({ attributes: z.object({ checkInDate: z.string(), checkOutDate: z.string(), - occupancy: occupancySchema.optional(), hotelId: z.number(), - bookingCode: z.string().optional(), - roomConfigurations: z.array(roomConfigurationSchema), - rateDefinitions: z.array(rateDefinitionSchema), mustBeGuaranteed: z.boolean().optional(), + occupancy: occupancySchema.optional(), + rateDefinitions: z.array(rateDefinitionSchema), + roomConfigurations: z.array(roomConfigurationSchema), }), - relationships: linksSchema.optional(), + relationships: relationshipsSchema.optional(), type: z.string().optional(), }), }) .transform((o) => o.data.attributes) -export const getRoomsAvailabilitySchema = roomsAvailabilitySchema -export type RoomsAvailability = z.infer -export type RoomConfiguration = z.infer -export type Product = z.infer -export type RateDefinition = z.infer +export const ratesSchema = z.array(rateSchema) -const flexibilityPrice = z.object({ - standard: z.number(), - member: z.number(), -}) - -const rate = z.object({ - id: z.number(), - name: z.string(), - description: z.string(), - size: z.string(), - imageSrc: z.string(), - breakfastIncluded: z.boolean(), - prices: z.object({ - currency: z.string(), - nonRefundable: flexibilityPrice, - freeRebooking: flexibilityPrice, - freeCancellation: flexibilityPrice, - }), -}) - -export const getRatesSchema = z.array(rate) -export type Rate = z.infer - -const hotelFilter = z.object({ - roomFacilities: z.array(z.string()), - hotelFacilities: z.array(z.string()), - hotelSurroundings: z.array(z.string()), -}) - -export const getFiltersSchema = hotelFilter -export type HotelFilter = z.infer - -export const apiCitiesByCountrySchema = z.object({ +export const citiesByCountrySchema = z.object({ data: z.array( - z - .object({ - attributes: z.object({ - cityIdentifier: z.string().optional(), - name: z.string(), - keywords: z.array(z.string()).optional(), - timeZoneId: z.string().optional(), - ianaTimeZoneId: z.string().optional(), - isPublished: z.boolean().optional().default(false), - }), - id: z.string(), - type: z.literal("cities"), - }) - .transform((data) => { - return { - ...data.attributes, - id: data.id, - type: data.type, - } - }) + citySchema.transform((data) => { + return { + ...data.attributes, + id: data.id, + type: data.type, + } + }) ), }) -export interface CitiesByCountry - extends z.output {} -export type CitiesGroupedByCountry = Record - -export const apiCountriesSchema = z.object({ +export const countriesSchema = z.object({ data: z .array( z.object({ @@ -689,35 +141,9 @@ export const apiCountriesSchema = z.object({ }), }) -export interface Countries extends z.output {} - -export const apiLocationCitySchema = z.object({ - attributes: z.object({ - cityIdentifier: z.string().optional(), - keyWords: z.array(z.string()).optional(), - name: z.string().optional().default(""), - }), - country: z.string().optional().default(""), - id: z.string().optional().default(""), - type: z.literal("cities"), -}) - -export const apiCitySchema = z +export const citiesSchema = z .object({ - data: z.array( - z.object({ - attributes: z.object({ - cityIdentifier: z.string().optional(), - name: z.string(), - keywords: z.array(z.string()), - timeZoneId: z.string().optional(), - ianaTimeZoneId: z.string().optional(), - isPublished: z.boolean().optional().default(false), - }), - id: z.string().optional(), - type: z.literal("cities"), - }) - ), + data: z.array(citySchema), }) .transform(({ data }) => { if (data.length) { @@ -731,46 +157,11 @@ export const apiCitySchema = z return null }) -export const apiLocationHotelSchema = z.object({ - attributes: z.object({ - distanceToCentre: z.number().optional(), - images: z - .object({ - large: z.string().optional(), - medium: z.string().optional(), - small: z.string().optional(), - tiny: z.string().optional(), - }) - .optional(), - keyWords: z.array(z.string()).optional(), - name: z.string().optional().default(""), - operaId: z.string().optional(), - }), - id: z.string().optional().default(""), - relationships: z - .object({ - city: z - .object({ - links: z - .object({ - related: z.string().optional(), - }) - .optional(), - }) - .optional(), - }) - .optional(), - type: z.literal("hotels"), -}) - -export const apiLocationsSchema = z.object({ +export const locationsSchema = z.object({ data: z .array( z - .discriminatedUnion("type", [ - apiLocationCitySchema, - apiLocationHotelSchema, - ]) + .discriminatedUnion("type", [locationCitySchema, locationHotelSchema]) .transform((location) => { if (location.type === "cities") { return { @@ -813,20 +204,6 @@ export const apiLocationsSchema = z.object({ ), }) -export const packagePriceSchema = z.object({ - currency: z.string().default("N/A"), - price: z.string(), - totalPrice: z.string(), -}) - -export const breakfastPackageSchema = z.object({ - code: z.string(), - description: z.string(), - localPrice: packagePriceSchema, - requestedPrice: packagePriceSchema, - packageType: z.literal(PackageTypeEnum.BreakfastAdult), -}) - export const breakfastPackagesSchema = z .object({ data: z.object({ @@ -838,38 +215,23 @@ export const breakfastPackagesSchema = z }), }) .transform(({ data }) => - data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm)) + data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm)) ) -export const packagesSchema = z.object({ - code: z.nativeEnum(RoomPackageCodeEnum), - itemCode: z.string().optional(), - description: z.string(), - localPrice: packagePriceSchema, - requestedPrice: packagePriceSchema, - inventories: z.array( - z.object({ - date: z.string(), - total: z.number(), - available: z.number(), - }) - ), -}) - -export const getRoomPackagesSchema = z +export const packagesSchema = z .object({ data: z .object({ attributes: z.object({ hotelId: z.number(), - packages: z.array(packagesSchema).optional().default([]), + packages: z.array(packageSchema).default([]), }), relationships: z .object({ links: z.array( z.object({ - url: z.string(), type: z.string(), + url: z.string(), }) ), }) @@ -878,7 +240,7 @@ export const getRoomPackagesSchema = z }) .optional(), }) - .transform((data) => data.data?.attributes?.packages ?? []) + .transform(({ data }) => data?.attributes.packages) export const getHotelIdsByCityIdSchema = z .object({ @@ -900,50 +262,4 @@ export const getNearbyHotelIdsSchema = z }) ), }) - .transform((data) => data.data.map((hotel) => hotel.id)) - -export const getMeetingRoomsSchema = z.object({ - data: z.array( - z.object({ - attributes: z.object({ - name: z.string(), - email: z.string().optional(), - phoneNumber: z.string(), - size: z.number(), - doorWidth: z.number(), - doorHeight: z.number(), - length: z.number(), - width: z.number(), - height: z.number(), - floorNumber: z.number(), - content: z.object({ - images: z.array(imageSchema), - texts: z.object({ - facilityInformation: z.string().optional(), - surroundingInformation: z.string().optional(), - descriptions: z.object({ - short: z.string().optional(), - medium: z.string().optional(), - }), - meetingDescription: z - .object({ - short: z.string().optional(), - medium: z.string().optional(), - }) - .optional(), - }), - }), - seatings: z.array( - z.object({ - type: z.string(), - capacity: z.number(), - }) - ), - lighting: z.string(), - sortOrder: z.number().optional(), - }), - id: z.string(), - type: z.string(), - }) - ), -}) + .transform((data) => data.data.map((hotel) => hotel.id)) \ No newline at end of file diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 9a4a87aa7..52fe76898 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,5 +1,4 @@ import { ApiLang } from "@/constants/languages" -import { env } from "@/env/server" import * as api from "@/lib/api" import { dt } from "@/lib/dt" import { badRequestError } from "@/server/errors/trpc" @@ -17,67 +16,30 @@ import { cache } from "@/utils/cache" import { getHotelPageUrl } from "../contentstack/hotelPage/utils" import { getVerifiedUser, parsedUser } from "../user/query" import { + breakfastPackageInputSchema, + cityCoordinatesInputSchema, getAdditionalDataInputSchema, - getBreakfastPackageInputSchema, - getCityCoordinatesInputSchema, - getHotelDataInputSchema, - getHotelsAvailabilityInputSchema, getHotelsByHotelIdsAvailabilityInputSchema, getHotelsInput, getMeetingRoomsInputSchema, - getRatesInputSchema, - getRoomPackagesInputSchema, - getRoomsAvailabilityInputSchema, - getSelectedRoomAvailabilityInputSchema, - type HotelDataInput, + hotelInputSchema, + hotelsAvailabilityInputSchema, nearbyHotelIdsInput, + ratesInputSchema, + roomPackagesInputSchema, + roomsAvailabilityInputSchema, + selectedRoomAvailabilityInputSchema, } from "./input" +import { metrics } from "./metrics" import { breakfastPackagesSchema, - getHotelDataSchema, - getHotelsAvailabilitySchema, - getMeetingRoomsSchema, getNearbyHotelIdsSchema, - getRatesSchema, - getRoomPackagesSchema, - getRoomsAvailabilitySchema, + hotelsAvailabilitySchema, + hotelSchema, + packagesSchema, + ratesSchema, + roomsAvailabilitySchema, } from "./output" -import { additionalDataSchema } from "./schemas/additionalData" -import { - additionalDataCounter, - additionalDataFailCounter, - additionalDataSuccessCounter, - breakfastPackagesCounter, - breakfastPackagesFailCounter, - breakfastPackagesSuccessCounter, - getHotelCounter, - getHotelFailCounter, - getHotelsCounter, - getHotelsFailCounter, - getHotelsSuccessCounter, - getHotelSuccessCounter, - getPackagesCounter, - getPackagesFailCounter, - getPackagesSuccessCounter, - hotelsAvailabilityCounter, - hotelsAvailabilityFailCounter, - hotelsAvailabilitySuccessCounter, - hotelsByHotelIdAvailabilityCounter, - hotelsByHotelIdAvailabilityFailCounter, - hotelsByHotelIdAvailabilitySuccessCounter, - meetingRoomsCounter, - meetingRoomsFailCounter, - meetingRoomsSuccessCounter, - nearbyHotelIdsCounter, - nearbyHotelIdsFailCounter, - nearbyHotelIdsSuccessCounter, - roomsAvailabilityCounter, - roomsAvailabilityFailCounter, - roomsAvailabilitySuccessCounter, - selectedRoomAvailabilityCounter, - selectedRoomAvailabilityFailCounter, - selectedRoomAvailabilitySuccessCounter, -} from "./telemetry" import tempRatesData from "./tempRatesData.json" import { getCitiesByCountry, @@ -92,23 +54,33 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { HotelTypeEnum } from "@/types/enums/hotelType" import type { RequestOptionsWithOutBody } from "@/types/fetch" -import type { Hotel } from "@/types/hotel" +import type { HotelInput } from "@/types/trpc/routers/hotel/hotel" +import type { HotelData } from "@/types/hotel" import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage" -import type { CityLocation } from "@/types/trpc/routers/hotel/locations" +import { CityLocation } from "@/types/trpc/routers/hotel/locations" +import { meetingRoomsSchema } from "./schemas/meetingRoom" +import { env } from "@/env/server" +import { additionalDataSchema } from "./schemas/additionalData" -export const getHotelData = cache( - async (input: HotelDataInput, serviceToken: string) => { - const { hotelId, language, isCardOnlyPayment } = input - - const includes = ["RoomCategories", "Restaurants", "AdditionalData"] //"NearbyHotels","Restaurants","City", - const params = new URLSearchParams({ - hotelId, - language, - }) - - includes.forEach((include) => params.append("include", include)) - - getHotelCounter.add(1, { +export const getHotel = cache( + async (input: HotelInput, serviceToken: string) => { + const { hotelId, isCardOnlyPayment, language } = input + /** + * 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([ + ["hotelId", hotelId], + ["include", "AdditionalData"], + ["include", "City"], + ["include", "NearbyHotels"], + ["include", "Restaurants"], + ["include", "RoomCategories"], + ["language", toApiLang(language)], + ]) + metrics.hotel.counter.add(1, { hotelId, language, }) @@ -128,6 +100,7 @@ export const getHotelData = cache( cache: undefined, next: { revalidate: env.CACHE_TIME_HOTELDATA, + tags: [`${language}:hotel:${hotelId}`], }, }, params @@ -135,7 +108,8 @@ export const getHotelData = cache( if (!apiResponse.ok) { const text = await apiResponse.text() - getHotelFailCounter.add(1, { + console.log({ text }) + metrics.hotel.fail.add(1, { hotelId, language, error_type: "http_error", @@ -160,10 +134,10 @@ export const getHotelData = cache( } const apiJson = await apiResponse.json() - const validateHotelData = getHotelDataSchema.safeParse(apiJson) + const validateHotelData = hotelSchema.safeParse(apiJson) if (!validateHotelData.success) { - getHotelFailCounter.add(1, { + metrics.hotel.fail.add(1, { hotelId, language, error_type: "validation_error", @@ -180,7 +154,7 @@ export const getHotelData = cache( throw badRequestError() } - getHotelSuccessCounter.add(1, { + metrics.hotel.success.add(1, { hotelId, language, }) @@ -193,18 +167,17 @@ export const getHotelData = cache( const hotelData = validateHotelData.data if (isCardOnlyPayment) { - hotelData.data.attributes.merchantInformationData.alternatePaymentOptions = - [] + hotelData.hotel.merchantInformationData.alternatePaymentOptions = [] } - const gallery = hotelData.included.additionalData?.gallery + const gallery = hotelData.additionalData?.gallery if (gallery) { const smallerImages = gallery.smallerImages const hotelGalleryImages = - hotelData.data.attributes.hotelType === HotelTypeEnum.Signature + hotelData.hotel.hotelType === HotelTypeEnum.Signature ? smallerImages.slice(0, 10) : smallerImages.slice(0, 6) - hotelData.data.attributes.galleryImages = hotelGalleryImages + hotelData.hotel.galleryImages = hotelGalleryImages } return hotelData @@ -214,7 +187,7 @@ export const getHotelData = cache( export const hotelQueryRouter = router({ availability: router({ hotelsByCity: serviceProcedure - .input(getHotelsAvailabilityInputSchema) + .input(hotelsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { lang } = ctx const apiLang = toApiLang(lang) @@ -235,7 +208,7 @@ export const hotelQueryRouter = router({ ...(bookingCode && { bookingCode }), language: apiLang, } - hotelsAvailabilityCounter.add(1, { + metrics.hotelsAvailability.counter.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -258,7 +231,7 @@ export const hotelQueryRouter = router({ ) if (!apiResponse.ok) { const text = await apiResponse.text() - hotelsAvailabilityFailCounter.add(1, { + metrics.hotelsAvailability.fail.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -287,9 +260,9 @@ export const hotelQueryRouter = router({ } const apiJson = await apiResponse.json() const validateAvailabilityData = - getHotelsAvailabilitySchema.safeParse(apiJson) + hotelsAvailabilitySchema.safeParse(apiJson) if (!validateAvailabilityData.success) { - hotelsAvailabilityFailCounter.add(1, { + metrics.hotelsAvailability.fail.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -308,7 +281,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - hotelsAvailabilitySuccessCounter.add(1, { + metrics.hotelsAvailability.success.add(1, { cityId, roomStayStartDate, roomStayEndDate, @@ -351,7 +324,7 @@ export const hotelQueryRouter = router({ ...(bookingCode && { bookingCode }), language: apiLang, } - hotelsByHotelIdAvailabilityCounter.add(1, { + metrics.hotelsByHotelIdAvailability.counter.add(1, { hotelIds, roomStayStartDate, roomStayEndDate, @@ -374,7 +347,7 @@ export const hotelQueryRouter = router({ ) if (!apiResponse.ok) { const text = await apiResponse.text() - hotelsByHotelIdAvailabilityFailCounter.add(1, { + metrics.hotelsByHotelIdAvailability.fail.add(1, { hotelIds, roomStayStartDate, roomStayEndDate, @@ -403,9 +376,9 @@ export const hotelQueryRouter = router({ } const apiJson = await apiResponse.json() const validateAvailabilityData = - getHotelsAvailabilitySchema.safeParse(apiJson) + hotelsAvailabilitySchema.safeParse(apiJson) if (!validateAvailabilityData.success) { - hotelsByHotelIdAvailabilityFailCounter.add(1, { + metrics.hotelsByHotelIdAvailability.fail.add(1, { hotelIds, roomStayStartDate, roomStayEndDate, @@ -424,7 +397,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - hotelsByHotelIdAvailabilitySuccessCounter.add(1, { + metrics.hotelsByHotelIdAvailability.success.add(1, { hotelIds, roomStayStartDate, roomStayEndDate, @@ -445,7 +418,7 @@ export const hotelQueryRouter = router({ } }), rooms: serviceProcedure - .input(getRoomsAvailabilityInputSchema) + .input(roomsAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { lang } = ctx const apiLang = toApiLang(lang) @@ -468,7 +441,7 @@ export const hotelQueryRouter = router({ language: apiLang, } - roomsAvailabilityCounter.add(1, { + metrics.roomAvailability.counter.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -492,7 +465,7 @@ export const hotelQueryRouter = router({ if (!apiResponse.ok) { const text = await apiResponse.text() - roomsAvailabilityFailCounter.add(1, { + metrics.roomAvailability.fail.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -522,9 +495,9 @@ export const hotelQueryRouter = router({ const apiJson = await apiResponse.json() const validateAvailabilityData = - getRoomsAvailabilitySchema.safeParse(apiJson) + roomsAvailabilitySchema.safeParse(apiJson) if (!validateAvailabilityData.success) { - roomsAvailabilityFailCounter.add(1, { + metrics.roomAvailability.fail.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -543,7 +516,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - roomsAvailabilitySuccessCounter.add(1, { + metrics.roomAvailability.success.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -568,7 +541,7 @@ export const hotelQueryRouter = router({ return validateAvailabilityData.data }), room: serviceProcedure - .input(getSelectedRoomAvailabilityInputSchema) + .input(selectedRoomAvailabilityInputSchema) .query(async ({ input, ctx }) => { const { hotelId, @@ -591,7 +564,7 @@ export const hotelQueryRouter = router({ language: toApiLang(ctx.lang), } - selectedRoomAvailabilityCounter.add(1, { + metrics.selectedRoomAvailability.counter.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -615,7 +588,7 @@ export const hotelQueryRouter = router({ if (!apiResponseAvailability.ok) { const text = await apiResponseAvailability.text() - selectedRoomAvailabilityFailCounter.add(1, { + metrics.selectedRoomAvailability.fail.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -644,9 +617,9 @@ export const hotelQueryRouter = router({ } const apiJsonAvailability = await apiResponseAvailability.json() const validateAvailabilityData = - getRoomsAvailabilitySchema.safeParse(apiJsonAvailability) + roomsAvailabilitySchema.safeParse(apiJsonAvailability) if (!validateAvailabilityData.success) { - selectedRoomAvailabilityFailCounter.add(1, { + metrics.selectedRoomAvailability.fail.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -666,10 +639,11 @@ export const hotelQueryRouter = router({ throw badRequestError() } - const hotelData = await getHotelData( + const hotelData = await getHotel( { hotelId, - language: toApiLang(ctx.lang), + isCardOnlyPayment: false, + language: ctx.lang, }, ctx.serviceToken ) @@ -696,7 +670,7 @@ export const hotelQueryRouter = router({ (room) => room.roomType === selectedRoom?.roomType ) if (!selectedRoom) { - selectedRoomAvailabilityFailCounter.add(1, { + metrics.selectedRoomAvailability.fail.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -717,7 +691,7 @@ export const hotelQueryRouter = router({ ) if (!rateTypes) { - selectedRoomAvailabilityFailCounter.add(1, { + metrics.selectedRoomAvailability.fail.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -739,7 +713,7 @@ export const hotelQueryRouter = router({ const bedTypes = availableRoomsInCategory .map((availRoom) => { - const matchingRoom = hotelData?.included.rooms + const matchingRoom = hotelData?.roomCategories ?.find((room) => room.roomTypes .map((roomType) => roomType.code) @@ -757,16 +731,16 @@ export const hotelQueryRouter = router({ type: matchingRoom.mainBed.type, extraBed: matchingRoom.fixedExtraBed ? { - type: matchingRoom.fixedExtraBed.type, - description: matchingRoom.fixedExtraBed.description, - } + type: matchingRoom.fixedExtraBed.type, + description: matchingRoom.fixedExtraBed.description, + } : undefined, } } }) .filter((bed): bed is BedTypeSelection => Boolean(bed)) - selectedRoomAvailabilitySuccessCounter.add(1, { + metrics.selectedRoomAvailability.success.add(1, { hotelId, roomStayStartDate, roomStayEndDate, @@ -795,7 +769,7 @@ export const hotelQueryRouter = router({ }), rates: router({ get: publicProcedure - .input(getRatesInputSchema) + .input(ratesInputSchema) .query(async ({ input, ctx }) => { // TODO: Do a real API call when the endpoint is ready // const { hotelId } = input @@ -806,7 +780,7 @@ export const hotelQueryRouter = router({ // params.set("language", apiLang) console.info("api.hotels.rates start", JSON.stringify({})) - const validatedHotelData = getRatesSchema.safeParse(tempRatesData) + const validatedHotelData = ratesSchema.safeParse(tempRatesData) if (!tempRatesData) { console.error( @@ -829,13 +803,11 @@ export const hotelQueryRouter = router({ return validatedHotelData.data }), }), - hotelData: router({ - get: serviceProcedure - .input(getHotelDataInputSchema) - .query(async ({ ctx, input }) => { - return getHotelData(input, ctx.serviceToken) - }), - }), + get: serviceProcedure + .input(hotelInputSchema) + .query(async ({ ctx, input }) => { + return getHotel(input, ctx.serviceToken) + }), hotels: router({ get: contentStackBaseWithServiceProcedure .input(getHotelsInput) @@ -858,7 +830,7 @@ export const hotelQueryRouter = router({ let hotelsToFetch: string[] = [] - getHotelsCounter.add(1, { + metrics.hotels.counter.add(1, { input: JSON.stringify(input), language, }) @@ -889,11 +861,11 @@ export const hotelQueryRouter = router({ } const cityId = locations - .filter((loc): loc is CityLocation => loc.type === "cities") + .filter((loc): loc is CityLocation => "type" in loc && loc.type === "cities") .find((loc) => loc.cityIdentifier === locationFilter.city)?.id if (!cityId) { - getHotelsFailCounter.add(1, { + metrics.hotels.fail.add(1, { input: JSON.stringify(input), language, error_type: "not_found", @@ -921,7 +893,7 @@ export const hotelQueryRouter = router({ ) if (!hotelIds?.length) { - getHotelsFailCounter.add(1, { + metrics.hotels.fail.add(1, { cityId, language, error_type: "not_found", @@ -956,7 +928,7 @@ export const hotelQueryRouter = router({ ) if (!hotelIds?.length) { - getHotelsFailCounter.add(1, { + metrics.hotels.fail.add(1, { country: locationFilter.country, language, error_type: "not_found", @@ -981,7 +953,7 @@ export const hotelQueryRouter = router({ } if (!hotelsToFetch.length) { - getHotelsFailCounter.add(1, { + metrics.hotels.fail.add(1, { input: JSON.stringify(input), language, error_type: "not_found", @@ -1001,18 +973,18 @@ export const hotelQueryRouter = router({ const hotels = await Promise.all( hotelsToFetch.map(async (hotelId) => { const [hotelData, url] = await Promise.all([ - getHotelData({ hotelId, language }, ctx.serviceToken), + getHotel({ hotelId, language }, ctx.serviceToken), getHotelPageUrl(language, hotelId), ]) return { - data: hotelData?.data.attributes, + data: hotelData, url, } }) ) - getHotelsSuccessCounter.add(1, { + metrics.hotels.success.add(1, { input: JSON.stringify(input), language, }) @@ -1028,7 +1000,7 @@ export const hotelQueryRouter = router({ ) return hotels.filter( - (hotel): hotel is { data: Hotel; url: HotelPageUrl } => !!hotel.data + (hotel): hotel is { data: HotelData; url: HotelPageUrl } => !!hotel.data ) }), }), @@ -1042,7 +1014,7 @@ export const hotelQueryRouter = router({ const params: Record = { language: apiLang, } - nearbyHotelIdsCounter.add(1, { + metrics.nearbyHotelIds.counter.add(1, { hotelId, }) console.info( @@ -1060,7 +1032,7 @@ export const hotelQueryRouter = router({ ) if (!apiResponse.ok) { const text = await apiResponse.text() - nearbyHotelIdsFailCounter.add(1, { + metrics.nearbyHotelIds.fail.add(1, { hotelId, error_type: "http_error", error: JSON.stringify({ @@ -1085,7 +1057,7 @@ export const hotelQueryRouter = router({ const apiJson = await apiResponse.json() const validateHotelData = getNearbyHotelIdsSchema.safeParse(apiJson) if (!validateHotelData.success) { - nearbyHotelIdsFailCounter.add(1, { + metrics.nearbyHotelIds.fail.add(1, { hotelId, error_type: "validation_error", error: JSON.stringify(validateHotelData.error), @@ -1099,7 +1071,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - nearbyHotelIdsSuccessCounter.add(1, { + metrics.nearbyHotelIds.success.add(1, { hotelId, }) console.info( @@ -1156,204 +1128,9 @@ export const hotelQueryRouter = router({ return locations }), }), - packages: router({ - get: serviceProcedure - .input(getRoomPackagesInputSchema) - .query(async ({ input, ctx }) => { - const { hotelId, startDate, endDate, adults, children, packageCodes } = - input - - const { lang } = ctx - - const apiLang = toApiLang(lang) - - const searchParams = new URLSearchParams({ - startDate, - endDate, - adults: adults.toString(), - children: children.toString(), - language: apiLang, - }) - - packageCodes.forEach((code) => { - searchParams.append("packageCodes", code) - }) - - const params = searchParams.toString() - - getPackagesCounter.add(1, { - hotelId, - }) - console.info( - "api.hotels.packages start", - JSON.stringify({ query: { hotelId, params } }) - ) - - const apiResponse = await api.get( - api.endpoints.v1.Package.Packages.hotel(hotelId), - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - }, - searchParams - ) - - if (!apiResponse.ok) { - getPackagesFailCounter.add(1, { - hotelId, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - }), - }) - console.error( - "api.hotels.packages error", - JSON.stringify({ query: { hotelId, params } }) - ) - } - - const apiJson = await apiResponse.json() - const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson) - if (!validatedPackagesData.success) { - getHotelFailCounter.add(1, { - hotelId, - error_type: "validation_error", - error: JSON.stringify(validatedPackagesData.error), - }) - - console.error( - "api.hotels.packages validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validatedPackagesData.error, - }) - ) - throw badRequestError() - } - - getPackagesSuccessCounter.add(1, { - hotelId, - }) - console.info( - "api.hotels.packages success", - JSON.stringify({ query: { hotelId, params: params } }) - ) - - return validatedPackagesData.data - }), - breakfast: safeProtectedServiceProcedure - .input(getBreakfastPackageInputSchema) - .query(async function ({ ctx, input }) { - const { lang } = ctx - - const apiLang = toApiLang(lang) - const params = { - Adults: input.adults, - EndDate: dt(input.toDate).format("YYYY-MM-DD"), - StartDate: dt(input.fromDate).format("YYYY-MM-DD"), - language: apiLang, - } - - const metricsData = { ...params, hotelId: input.hotelId } - breakfastPackagesCounter.add(1, metricsData) - console.info( - "api.package.breakfast start", - JSON.stringify({ query: metricsData }) - ) - - const apiResponse = await api.get( - api.endpoints.v1.Package.Breakfast.hotel(input.hotelId), - { - cache: undefined, - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - next: { - revalidate: 60, - }, - }, - params - ) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - breakfastPackagesFailCounter.add(1, { - ...metricsData, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), - }) - console.error( - "api.hotels.hotelsAvailability error", - JSON.stringify({ - query: metricsData, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }, - }) - ) - return null - } - - const apiJson = await apiResponse.json() - const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson) - if (!breakfastPackages.success) { - hotelsAvailabilityFailCounter.add(1, { - ...metricsData, - error_type: "validation_error", - error: JSON.stringify(breakfastPackages.error), - }) - console.error( - "api.package.breakfast validation error", - JSON.stringify({ - query: metricsData, - error: breakfastPackages.error, - }) - ) - return null - } - - breakfastPackagesSuccessCounter.add(1, metricsData) - console.info( - "api.package.breakfast success", - JSON.stringify({ - query: metricsData, - }) - ) - - if (ctx.session?.token) { - const apiUser = await getVerifiedUser({ session: ctx.session }) - if (apiUser && !("error" in apiUser)) { - const user = parsedUser(apiUser.data, false) - if ( - user.membership && - ["L6", "L7"].includes(user.membership.membershipLevel) - ) { - const freeBreakfastPackage = breakfastPackages.data.find( - (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST - ) - if (freeBreakfastPackage?.localPrice) { - return [freeBreakfastPackage] - } - } - } - } - - return breakfastPackages.data.filter( - (pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST - ) - }), - }), map: router({ city: serviceProcedure - .input(getCityCoordinatesInputSchema) + .input(cityCoordinatesInputSchema) .query(async function ({ input }) { const apiKey = process.env.GOOGLE_STATIC_MAP_KEY const { city, hotel } = input @@ -1399,7 +1176,7 @@ export const hotelQueryRouter = router({ language, } const metricsData = { ...params, hotelId: input.hotelId } - meetingRoomsCounter.add(1, metricsData) + metrics.meetingRooms.counter.add(1, metricsData) console.info( "api.hotels.meetingRooms start", JSON.stringify({ query: { hotelId, params } }) @@ -1421,7 +1198,7 @@ export const hotelQueryRouter = router({ if (!apiResponse.ok) { const text = await apiResponse.text() - meetingRoomsFailCounter.add(1, { + metrics.meetingRooms.fail.add(1, { ...metricsData, error_type: "http_error", error: JSON.stringify({ @@ -1445,7 +1222,7 @@ export const hotelQueryRouter = router({ } const apiJson = await apiResponse.json() - const validatedMeetingRooms = getMeetingRoomsSchema.safeParse(apiJson) + const validatedMeetingRooms = meetingRoomsSchema.safeParse(apiJson) if (!validatedMeetingRooms.success) { console.error( @@ -1457,7 +1234,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - meetingRoomsSuccessCounter.add(1, { + metrics.meetingRooms.success.add(1, { hotelId, }) console.info( @@ -1477,7 +1254,7 @@ export const hotelQueryRouter = router({ language, } const metricsData = { ...params, hotelId: input.hotelId } - additionalDataCounter.add(1, metricsData) + metrics.additionalData.counter.add(1, metricsData) console.info( "api.hotels.additionalData start", JSON.stringify({ query: { hotelId, params } }) @@ -1499,7 +1276,7 @@ export const hotelQueryRouter = router({ if (!apiResponse.ok) { const text = await apiResponse.text() - additionalDataFailCounter.add(1, { + metrics.additionalData.fail.add(1, { ...metricsData, error_type: "http_error", error: JSON.stringify({ @@ -1535,7 +1312,7 @@ export const hotelQueryRouter = router({ ) throw badRequestError() } - additionalDataSuccessCounter.add(1, { + metrics.additionalData.success.add(1, { hotelId, }) console.info( @@ -1545,4 +1322,199 @@ export const hotelQueryRouter = router({ return validatedAdditionalData.data }), + packages: router({ + get: serviceProcedure + .input(roomPackagesInputSchema) + .query(async ({ input, ctx }) => { + const { hotelId, startDate, endDate, adults, children, packageCodes } = + input + + const { lang } = ctx + + const apiLang = toApiLang(lang) + + const searchParams = new URLSearchParams({ + startDate, + endDate, + adults: adults.toString(), + children: children.toString(), + language: apiLang, + }) + + packageCodes.forEach((code) => { + searchParams.append("packageCodes", code) + }) + + const params = searchParams.toString() + + metrics.packages.counter.add(1, { + hotelId, + }) + console.info( + "api.hotels.packages start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Package.Packages.hotel(hotelId), + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + searchParams + ) + + if (!apiResponse.ok) { + metrics.packages.fail.add(1, { + hotelId, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + }), + }) + console.error( + "api.hotels.packages error", + JSON.stringify({ query: { hotelId, params } }) + ) + } + + const apiJson = await apiResponse.json() + const validatedPackagesData = packagesSchema.safeParse(apiJson) + if (!validatedPackagesData.success) { + metrics.packages.fail.add(1, { + hotelId, + error_type: "validation_error", + error: JSON.stringify(validatedPackagesData.error), + }) + + console.error( + "api.hotels.packages validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validatedPackagesData.error, + }) + ) + throw badRequestError() + } + + metrics.packages.success.add(1, { + hotelId, + }) + console.info( + "api.hotels.packages success", + JSON.stringify({ query: { hotelId, params: params } }) + ) + + return validatedPackagesData.data + }), + breakfast: safeProtectedServiceProcedure + .input(breakfastPackageInputSchema) + .query(async function ({ ctx, input }) { + const { lang } = ctx + + const apiLang = toApiLang(lang) + const params = { + Adults: input.adults, + EndDate: dt(input.toDate).format("YYYY-MM-DD"), + StartDate: dt(input.fromDate).format("YYYY-MM-DD"), + language: apiLang, + } + + const metricsData = { ...params, hotelId: input.hotelId } + metrics.breakfastPackage.counter.add(1, metricsData) + console.info( + "api.package.breakfast start", + JSON.stringify({ query: metricsData }) + ) + + const apiResponse = await api.get( + api.endpoints.v1.Package.Breakfast.hotel(input.hotelId), + { + cache: undefined, + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + next: { + revalidate: 60, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + metrics.breakfastPackage.fail.add(1, { + ...metricsData, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.hotelsAvailability error", + JSON.stringify({ + query: metricsData, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const breakfastPackages = breakfastPackagesSchema.safeParse(apiJson) + if (!breakfastPackages.success) { + metrics.breakfastPackage.fail.add(1, { + ...metricsData, + error_type: "validation_error", + error: JSON.stringify(breakfastPackages.error), + }) + console.error( + "api.package.breakfast validation error", + JSON.stringify({ + query: metricsData, + error: breakfastPackages.error, + }) + ) + return null + } + + metrics.breakfastPackage.success.add(1, metricsData) + console.info( + "api.package.breakfast success", + JSON.stringify({ + query: metricsData, + }) + ) + + if (ctx.session?.token) { + const apiUser = await getVerifiedUser({ session: ctx.session }) + if (apiUser && !("error" in apiUser)) { + const user = parsedUser(apiUser.data, false) + if ( + user.membership && + ["L6", "L7"].includes(user.membership.membershipLevel) + ) { + const freeBreakfastPackage = breakfastPackages.data.find( + (pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ) + if (freeBreakfastPackage?.localPrice) { + return [freeBreakfastPackage] + } + } + } + } + + return breakfastPackages.data.filter( + (pkg) => pkg.code !== BreakfastPackageEnum.FREE_MEMBER_BREAKFAST + ) + }), + }), }) diff --git a/server/routers/hotels/schemas/additionalData.ts b/server/routers/hotels/schemas/additionalData.ts index ab2f26020..86c0d14eb 100644 --- a/server/routers/hotels/schemas/additionalData.ts +++ b/server/routers/hotels/schemas/additionalData.ts @@ -60,8 +60,13 @@ export const additionalDataSchema = z }), type: z.literal("additionalData"), }) - .transform(({ attributes, type }) => ({ - ...attributes, - type, - id: attributes.id, - })) + +export function transformAdditionalData( + data: z.output +) { + return { + ...data.attributes, + id: data.attributes.id, + type: data.type + } +} \ No newline at end of file diff --git a/server/routers/hotels/schemas/availability/occupancy.ts b/server/routers/hotels/schemas/availability/occupancy.ts new file mode 100644 index 000000000..c066008c3 --- /dev/null +++ b/server/routers/hotels/schemas/availability/occupancy.ts @@ -0,0 +1,13 @@ +import { z } from "zod" + +import { ChildBedTypeEnum } from "@/constants/booking" + +export const childrenSchema = z.object({ + age: z.number(), + bedType: z.nativeEnum(ChildBedTypeEnum), +}) + +export const occupancySchema = z.object({ + adults: z.number(), + children: z.array(childrenSchema), +}) diff --git a/server/routers/hotels/schemas/availability/productType.ts b/server/routers/hotels/schemas/availability/productType.ts new file mode 100644 index 000000000..aa5d6a1e5 --- /dev/null +++ b/server/routers/hotels/schemas/availability/productType.ts @@ -0,0 +1,10 @@ +import { z } from "zod" + +import { productTypePriceSchema } from "../productTypePrice" + +export const productTypeSchema = z + .object({ + public: productTypePriceSchema.optional(), + member: productTypePriceSchema.optional(), + }) + .optional() diff --git a/server/routers/hotels/schemas/city.ts b/server/routers/hotels/schemas/city.ts new file mode 100644 index 000000000..aa31775b6 --- /dev/null +++ b/server/routers/hotels/schemas/city.ts @@ -0,0 +1,14 @@ +import { z } from "zod" + +export const citySchema = z.object({ + attributes: z.object({ + cityIdentifier: z.string().default(""), + ianaTimeZoneId: z.string().default(""), + isPublished: z.boolean().default(false), + keywords: z.array(z.string()).default([]), + name: z.string(), + timeZoneId: z.string().default(""), + }), + id: z.string(), + type: z.literal("cities"), +}) diff --git a/server/routers/hotels/schemas/hotel.ts b/server/routers/hotels/schemas/hotel.ts new file mode 100644 index 000000000..6e5257097 --- /dev/null +++ b/server/routers/hotels/schemas/hotel.ts @@ -0,0 +1,73 @@ +import { z } from "zod" + +import { addressSchema } from "./hotel/address" +import { contactInformationSchema } from "./hotel/contactInformation" +import { hotelContentSchema } from "./hotel/content" +import { detailedFacilitiesSchema } from "./hotel/detailedFacility" +import { hotelFactsSchema } from "./hotel/facts" +import { gallerySchema } from "./hotel/gallery" +import { healthFacilitySchema } from "./hotel/healthFacilities" +import { includeSchema } from "./hotel/include/include" +import { locationSchema } from "./hotel/location" +import { merchantInformationSchema } from "./hotel/merchantInformation" +import { parkingSchema } from "./hotel/parking" +import { pointOfInterestsSchema } from "./hotel/poi" +import { ratingsSchema } from "./hotel/rating" +import { rewardNightSchema } from "./hotel/rewardNight" +import { socialMediaSchema } from "./hotel/socialMedia" +import { specialAlertsSchema } from "./hotel/specialAlerts" +import { specialNeedGroupSchema } from "./hotel/specialNeedGroups" +import { imageSchema } from "./image" +import { facilitySchema } from "./additionalData" + +export const attributesSchema = z.object({ + accessibilityElevatorPitchText: z.string().optional(), + address: addressSchema, + cityId: z.string(), + cityName: z.string(), + conferencesAndMeetings: facilitySchema.optional(), + contactInformation: contactInformationSchema, + detailedFacilities: detailedFacilitiesSchema, + gallery: gallerySchema.optional(), + 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(), + keywords: z.array(z.string()), + location: locationSchema, + merchantInformationData: merchantInformationSchema, + name: z.string(), + operaId: z.string(), + parking: z.array(parkingSchema), + pointsOfInterest: pointOfInterestsSchema, + ratings: ratingsSchema, + rewardNight: rewardNightSchema, + restaurantImages: facilitySchema.optional(), + socialMedia: socialMediaSchema, + specialAlerts: specialAlertsSchema, + specialNeedGroups: z.array(specialNeedGroupSchema), + vat: z.number(), +}) + +export const includesSchema = z + .array(includeSchema) + .default([]) + .transform((data) => data.filter((item) => !!item)) + +const relationshipSchema = z.object({ + links: z.object({ + related: z.string(), + }), +}) + +export const relationshipsSchema = z.object({ + meetingRooms: relationshipSchema, + nearbyHotels: relationshipSchema, + restaurants: relationshipSchema, + roomCategories: relationshipSchema, +}) diff --git a/server/routers/hotels/schemas/hotel/address.ts b/server/routers/hotels/schemas/hotel/address.ts new file mode 100644 index 000000000..401734a53 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/address.ts @@ -0,0 +1,8 @@ +import { z } from "zod" + +export const addressSchema = z.object({ + city: z.string(), + country: z.string(), + streetAddress: z.string(), + zipCode: z.string(), +}) diff --git a/server/routers/hotels/schemas/hotel/contactInformation.ts b/server/routers/hotels/schemas/hotel/contactInformation.ts new file mode 100644 index 000000000..c3bff3cc6 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/contactInformation.ts @@ -0,0 +1,8 @@ +import { z } from "zod" + +export const contactInformationSchema = z.object({ + email: z.string(), + faxNumber: z.string().optional(), + phoneNumber: z.string(), + websiteUrl: z.string(), +}) diff --git a/server/routers/hotels/schemas/hotel/content.ts b/server/routers/hotels/schemas/hotel/content.ts new file mode 100644 index 000000000..e6d1757e1 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/content.ts @@ -0,0 +1,40 @@ +import { z } from "zod" + +import { imageSchema } from "../image" + +export const hotelContentSchema = z.object({ + images: imageSchema.default({ + metaData: { + altText: "default image", + altText_En: "default image", + copyRight: "default image", + title: "default image", + }, + imageSizes: { + large: "https://placehold.co/1280x720", + medium: "https://placehold.co/1280x720", + small: "https://placehold.co/1280x720", + tiny: "https://placehold.co/1280x720", + }, + }), + restaurantsOverviewPage: z.object({ + restaurantsContentDescriptionMedium: z.string().optional(), + restaurantsContentDescriptionShort: z.string().optional(), + restaurantsOverviewPageLink: z.string().optional(), + restaurantsOverviewPageLinkText: z.string().optional(), + }), + texts: z.object({ + descriptions: z.object({ + medium: z.string(), + short: z.string(), + }), + facilityInformation: z.string().optional(), + meetingDescription: z + .object({ + medium: z.string().optional(), + short: z.string().optional(), + }) + .optional(), + surroundingInformation: z.string(), + }), +}) diff --git a/server/routers/hotels/schemas/hotel/detailedFacility.ts b/server/routers/hotels/schemas/hotel/detailedFacility.ts new file mode 100644 index 000000000..e30353df5 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/detailedFacility.ts @@ -0,0 +1,18 @@ +import { z } from "zod" + +import { FacilityEnum } from "@/types/enums/facilities" + +const detailedFacilitySchema = z.object({ + filter: z.string().optional(), + icon: z.string().optional(), + id: z.nativeEnum(FacilityEnum), + name: z.string(), + public: z.boolean(), + sortOrder: z.number(), +}) + +export const detailedFacilitiesSchema = z + .array(detailedFacilitySchema) + .transform((facilities) => + facilities.sort((a, b) => b.sortOrder - a.sortOrder) + ) diff --git a/server/routers/hotels/schemas/hotel/facts.ts b/server/routers/hotels/schemas/hotel/facts.ts new file mode 100644 index 000000000..f538b26f7 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/facts.ts @@ -0,0 +1,80 @@ +import { z } from "zod" + +const ecoLabelsSchema = z.object({ + euEcoLabel: z.boolean(), + greenGlobeLabel: z.boolean(), + nordicEcoLabel: z.boolean(), + svanenEcoLabelCertificateNumber: z.string().optional(), +}) + +export const checkinSchema = z.object({ + checkInTime: z.string(), + checkOutTime: z.string(), + onlineCheckout: z.boolean(), + onlineCheckOutAvailableFrom: z.string().nullable().optional(), +}) + +const hotelFacilityDetailSchema = z + .object({ + description: z.string(), + heading: z.string(), + }) + .optional() + +/** Possibly more values */ +const hotelFacilityDetailsSchema = z.object({ + breakfast: hotelFacilityDetailSchema, + checkout: hotelFacilityDetailSchema, + gym: hotelFacilityDetailSchema, + internet: hotelFacilityDetailSchema, + laundry: hotelFacilityDetailSchema, + luggage: hotelFacilityDetailSchema, + shop: hotelFacilityDetailSchema, + telephone: hotelFacilityDetailSchema, +}) + +const hotelInformationSchema = z + .object({ + description: z.string(), + heading: z.string(), + link: z.string().optional(), + }) + .optional() + +const hotelInformationsSchema = z.object({ + accessibility: hotelInformationSchema, + safety: hotelInformationSchema, + sustainability: hotelInformationSchema, +}) + +const interiorSchema = z.object({ + numberOfBeds: z.number(), + numberOfCribs: z.number(), + numberOfFloors: z.number(), + numberOfRooms: z.object({ + connected: z.number(), + forAllergics: z.number().optional(), + forDisabled: z.number(), + nonSmoking: z.number(), + pet: z.number(), + withExtraBeds: z.number(), + total: z.number(), + }), +}) + +const receptionHoursSchema = z.object({ + alwaysOpen: z.boolean(), + isClosed: z.boolean(), + openingTime: z.string().optional(), + closingTime: z.string().optional(), +}) + +export const hotelFactsSchema = z.object({ + checkin: checkinSchema, + ecoLabels: ecoLabelsSchema, + hotelFacilityDetail: hotelFacilityDetailsSchema.default({}), + hotelInformation: hotelInformationsSchema.default({}), + interior: interiorSchema, + receptionHours: receptionHoursSchema, + yearBuilt: z.string(), +}) diff --git a/server/routers/hotels/schemas/hotel/gallery.ts b/server/routers/hotels/schemas/hotel/gallery.ts new file mode 100644 index 000000000..82c01c729 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/gallery.ts @@ -0,0 +1,8 @@ +import { z } from "zod" + +import { imageSchema } from "../image" + +export const gallerySchema = z.object({ + heroImages: z.array(imageSchema), + smallerImages: z.array(imageSchema), +}) diff --git a/server/routers/hotels/schemas/hotel/healthFacilities.ts b/server/routers/hotels/schemas/hotel/healthFacilities.ts new file mode 100644 index 000000000..37f767ab9 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/healthFacilities.ts @@ -0,0 +1,41 @@ +import { z } from "zod" + +import { imageSchema } from "../image" + +const healthFacilitiesOpenHoursSchema = z.object({ + alwaysOpen: z.boolean(), + closingTime: z.string().optional(), + isClosed: z.boolean(), + openingTime: z.string().optional(), + sortOrder: z.number().optional(), +}) + +export const healthFacilitySchema = z.object({ + content: z.object({ + images: z.array(imageSchema), + texts: z.object({ + descriptions: z.object({ + short: z.string(), + medium: z.string(), + }), + facilityInformation: z.string().optional(), + surroundingInformation: z.string().optional(), + }), + }), + details: z.array( + z.object({ + name: z.string(), + type: z.string(), + value: z.string().optional(), + }) + ), + openingDetails: z.object({ + manualOpeningHours: z.string().optional(), + openingHours: z.object({ + ordinary: healthFacilitiesOpenHoursSchema, + weekends: healthFacilitiesOpenHoursSchema, + }), + useManualOpeningHours: z.boolean(), + }), + type: z.string(), +}) diff --git a/server/routers/hotels/schemas/hotel/include/include.ts b/server/routers/hotels/schemas/hotel/include/include.ts new file mode 100644 index 000000000..6c5ba45e9 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/include/include.ts @@ -0,0 +1,37 @@ +import { z } from "zod" + +import { citySchema } from "@/server/routers/hotels/schemas/city" +import { nearbyHotelsSchema } from "@/server/routers/hotels/schemas/hotel/include/nearbyHotels" +import { restaurantsSchema } from "@/server/routers/hotels/schemas/hotel/include/restaurants" +import { + roomCategoriesSchema, + transformRoomCategories, +} from "@/server/routers/hotels/schemas/hotel/include/roomCategories" +import { additionalDataSchema, transformAdditionalData } from "../../additionalData" + +export const includeSchema = z + .discriminatedUnion("type", [ + additionalDataSchema, + citySchema, + nearbyHotelsSchema, + restaurantsSchema, + roomCategoriesSchema, + ]) + .transform((data) => { + switch (data.type) { + case "additionalData": + return transformAdditionalData(data) + case "cities": + case "hotels": + case "restaurants": + return { + ...data.attributes, + id: data.id, + type: data.type, + } + case "roomcategories": + return transformRoomCategories(data) + default: + return null + } + }) diff --git a/server/routers/hotels/schemas/hotel/include/nearbyHotels.ts b/server/routers/hotels/schemas/hotel/include/nearbyHotels.ts new file mode 100644 index 000000000..175cd806a --- /dev/null +++ b/server/routers/hotels/schemas/hotel/include/nearbyHotels.ts @@ -0,0 +1,41 @@ +import { z } from "zod" + +import { attributesSchema } from "@/server/routers/hotels/schemas/hotel" + +export const nearbyHotelsSchema = z.object({ + attributes: z.lazy(() => + z + .object({ + displayWebPage: z + .object({ + healthGym: z.boolean().default(false), + meetingRoom: z.boolean().default(false), + parking: z.boolean().default(false), + specialNeeds: z.boolean().default(false), + }) + .default({ + healthGym: false, + meetingRoom: false, + parking: false, + specialNeeds: false, + }), + }) + .merge( + attributesSchema.pick({ + address: true, + cityId: true, + cityName: true, + detailedFacilities: true, + hotelContent: true, + isActive: true, + isPublished: true, + location: true, + name: true, + operaId: true, + ratings: true, + }) + ) + ), + id: z.string(), + type: z.literal("hotels"), +}) diff --git a/server/routers/hotels/schemas/hotel/include/restaurants.ts b/server/routers/hotels/schemas/hotel/include/restaurants.ts new file mode 100644 index 000000000..849e9fd4c --- /dev/null +++ b/server/routers/hotels/schemas/hotel/include/restaurants.ts @@ -0,0 +1,72 @@ +import { z } from "zod" + +import { imageSchema } from "@/server/routers/hotels/schemas/image" + +import { CurrencyEnum } from "@/types/enums/currency" + +const contentSchema = z.object({ + images: z.array(imageSchema).default([]), + texts: z.object({ + descriptions: z.object({ + medium: z.string().default(""), + short: z.string().default(""), + }), + }), +}) + +const externalBreakfastSchema = z.object({ + isAvailable: z.boolean().default(false), + localPriceForExternalGuests: z.object({ + amount: z.number().default(0), + currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.SEK), + }), +}) + +const menuItemSchema = z.object({ + name: z.string(), + url: z.string().url(), +}) + +const daySchema = z.object({ + alwaysOpen: z.boolean().default(false), + closingTime: z.string().default(""), + isClosed: z.boolean().default(false), + openingTime: z.string().default(""), + sortOrder: z.number().int().default(0), +}) + +const openingDetailsSchema = z.object({ + alternateOpeningHours: z.object({ + isActive: z.boolean().default(false), + }), + openingHours: z.object({ + friday: daySchema, + isActive: z.boolean().default(false), + monday: daySchema, + name: z.string().default(""), + saturday: daySchema, + sunday: daySchema, + thursday: daySchema, + tuesday: daySchema, + wednesday: daySchema, + }), +}) + +export const restaurantsSchema = z.object({ + attributes: z.object({ + bookTableUrl: z.string().default(""), + content: contentSchema, + // When using .email().default("") is not sufficent + // so .optional also needs to be chained + email: z.string().email().optional(), + externalBreakfast: externalBreakfastSchema, + isPublished: z.boolean().default(false), + menus: z.array(menuItemSchema).default([]), + name: z.string().default(""), + openingDetails: z.array(openingDetailsSchema).default([]), + restaurantPage: z.boolean().default(false), + specialAlerts: z.array(z.object({})).default([]), + }), + id: z.string(), + type: z.literal("restaurants"), +}) diff --git a/server/routers/hotels/schemas/hotel/include/roomCategories.ts b/server/routers/hotels/schemas/hotel/include/roomCategories.ts new file mode 100644 index 000000000..42574835a --- /dev/null +++ b/server/routers/hotels/schemas/hotel/include/roomCategories.ts @@ -0,0 +1,97 @@ +import { z } from "zod" + +import { imageMetaDataSchema, imageSizesSchema } from "../../image" + +const minMaxSchema = z.object({ + max: z.number(), + min: z.number(), +}) + +const bedTypeSchema = z.object({ + description: z.string().default(""), + type: z.string(), + widthRange: minMaxSchema, +}) + +const occupancySchema = z.object({ + adults: z.number(), + children: z.number(), + total: z.number(), +}) + +const roomContentSchema = z.object({ + images: z.array( + z.object({ + imageSizes: imageSizesSchema, + metaData: imageMetaDataSchema, + }) + ), + texts: z.object({ + descriptions: z.object({ + medium: z.string().optional(), + short: z.string().optional(), + }), + }), +}) + +const roomTypesSchema = z.object({ + code: z.string(), + description: z.string(), + fixedExtraBed: bedTypeSchema, + isLackingCribs: z.boolean(), + isLackingExtraBeds: z.boolean(), + mainBed: bedTypeSchema, + name: z.string(), + occupancy: occupancySchema, + roomCount: z.number(), + roomSize: minMaxSchema, +}) + +const roomFacilitiesSchema = z.object({ + availableInAllRooms: z.boolean(), + icon: z.string().optional(), + isUniqueSellingPoint: z.boolean(), + name: z.string(), + sortOrder: z.number(), +}) + +export const roomCategoriesSchema = z.object({ + attributes: z.object({ + content: roomContentSchema, + name: z.string(), + occupancy: minMaxSchema, + roomFacilities: z.array(roomFacilitiesSchema), + roomSize: minMaxSchema, + roomTypes: z.array(roomTypesSchema), + sortOrder: z.number(), + }), + id: z.string(), + type: z.literal("roomcategories"), +}) + +export function transformRoomCategories( + data: z.output +) { + return { + descriptions: data.attributes.content.texts.descriptions, + id: data.id, + images: data.attributes.content.images, + name: data.attributes.name, + occupancy: data.attributes.occupancy, + roomFacilities: data.attributes.roomFacilities, + roomSize: data.attributes.roomSize, + roomTypes: data.attributes.roomTypes, + sortOrder: data.attributes.sortOrder, + type: data.type, + totalOccupancy: + data.attributes.occupancy.min === data.attributes.occupancy.max + ? { + max: data.attributes.occupancy.max, + range: `${data.attributes.occupancy.max}`, + } + : { + max: data.attributes.occupancy.max, + range: `${data.attributes.occupancy.min}-${data.attributes.occupancy.max}`, + }, + } +} diff --git a/server/routers/hotels/schemas/hotel/location.ts b/server/routers/hotels/schemas/hotel/location.ts new file mode 100644 index 000000000..d18f6d942 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/location.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +export const locationSchema = z.object({ + distanceToCentre: z.number(), + latitude: z.number(), + longitude: z.number(), +}) diff --git a/server/routers/hotels/schemas/hotel/merchantInformation.ts b/server/routers/hotels/schemas/hotel/merchantInformation.ts new file mode 100644 index 000000000..cf0daeb01 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/merchantInformation.ts @@ -0,0 +1,21 @@ +import { z } from "zod" + +import type { PaymentMethodEnum } from "@/constants/booking" + +export const merchantInformationSchema = z.object({ + alternatePaymentOptions: z + .record(z.string(), z.boolean()) + .transform((val) => { + return Object.entries(val) + .filter(([_, enabled]) => enabled) + .map(([key]) => key) + .filter((key): key is PaymentMethodEnum => !!key) + }), + cards: z.record(z.string(), z.boolean()).transform((val) => { + return Object.entries(val) + .filter(([_, enabled]) => enabled) + .map(([key]) => key) + .filter((key): key is PaymentMethodEnum => !!key) + }), + webMerchantId: z.string(), +}) diff --git a/server/routers/hotels/schemas/hotel/parking.ts b/server/routers/hotels/schemas/hotel/parking.ts new file mode 100644 index 000000000..4e067bcc1 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/parking.ts @@ -0,0 +1,41 @@ +import { z } from "zod" + +const periodSchema = z.object({ + amount: z.number().optional(), + endTime: z.string().optional(), + period: z.string().optional(), + startTime: z.string().optional(), +}) + +const currencySchema = z + .object({ + currency: z.string().optional(), + ordinary: z.array(periodSchema).optional(), + range: z + .object({ + min: z.number().optional(), + max: z.number().optional(), + }) + .optional(), + weekend: z.array(periodSchema).optional(), + }) + .optional() + +const pricingSchema = z.object({ + freeParking: z.boolean(), + localCurrency: currencySchema, + paymentType: z.string().optional(), + requestedCurrency: currencySchema, +}) + +export const parkingSchema = z.object({ + address: z.string().optional(), + canMakeReservation: z.boolean(), + distanceToHotel: z.number().optional(), + externalParkingUrl: z.string().optional(), + name: z.string().optional(), + numberOfChargingSpaces: z.number().optional(), + numberOfParkingSpots: z.number().optional(), + pricing: pricingSchema, + type: z.string().optional(), +}) diff --git a/server/routers/hotels/schemas/hotel/poi.ts b/server/routers/hotels/schemas/hotel/poi.ts new file mode 100644 index 000000000..8ca47ad4f --- /dev/null +++ b/server/routers/hotels/schemas/hotel/poi.ts @@ -0,0 +1,32 @@ +import { z } from "zod" + +import { getPoiGroupByCategoryName } from "../../utils" +import { locationSchema } from "./location" + +export const pointOfInterestSchema = z + .object({ + category: z.object({ + name: z.string().optional(), + group: z.string().optional(), + }), + distance: z.number().optional(), + isHighlighted: z.boolean().optional(), + location: locationSchema.optional(), + name: z.string().optional(), + }) + .transform((poi) => ({ + categoryName: poi.category.name, + coordinates: { + lat: poi.location?.latitude ?? 0, + lng: poi.location?.longitude ?? 0, + }, + distance: poi.distance, + group: getPoiGroupByCategoryName(poi.category.name), + name: poi.name, + })) + +export const pointOfInterestsSchema = z + .array(pointOfInterestSchema) + .transform((pois) => + pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0)) + ) diff --git a/server/routers/hotels/schemas/hotel/rating.ts b/server/routers/hotels/schemas/hotel/rating.ts new file mode 100644 index 000000000..2f33fb850 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/rating.ts @@ -0,0 +1,31 @@ +import { z } from "zod" + +const awardSchema = z.object({ + displayName: z.string(), + images: z.object({ + large: z.string(), + medium: z.string(), + small: z.string(), + }), +}) + +const reviewsSchema = z + .object({ + widgetHtmlTagId: z.string(), + widgetScriptEmbedUrlIframe: z.string(), + widgetScriptEmbedUrlJavaScript: z.string(), + }) + .optional() + +export const ratingsSchema = z + .object({ + tripAdvisor: z.object({ + awards: z.array(awardSchema), + numberOfReviews: z.number(), + rating: z.number(), + ratingImageUrl: z.string(), + reviews: reviewsSchema, + webUrl: z.string(), + }), + }) + .optional() diff --git a/server/routers/hotels/schemas/hotel/rewardNight.ts b/server/routers/hotels/schemas/hotel/rewardNight.ts new file mode 100644 index 000000000..bd562062b --- /dev/null +++ b/server/routers/hotels/schemas/hotel/rewardNight.ts @@ -0,0 +1,10 @@ +import { z } from "zod" + +export const rewardNightSchema = z.object({ + campaign: z.object({ + end: z.string(), + points: z.number(), + start: z.string(), + }), + points: z.number(), +}) diff --git a/server/routers/hotels/schemas/hotel/socialMedia.ts b/server/routers/hotels/schemas/hotel/socialMedia.ts new file mode 100644 index 000000000..66bd1e06e --- /dev/null +++ b/server/routers/hotels/schemas/hotel/socialMedia.ts @@ -0,0 +1,6 @@ +import { z } from "zod" + +export const socialMediaSchema = z.object({ + facebook: z.string().optional(), + instagram: z.string().optional(), +}) diff --git a/server/routers/hotels/schemas/hotel/specialAlerts.ts b/server/routers/hotels/schemas/hotel/specialAlerts.ts new file mode 100644 index 000000000..8687a81a6 --- /dev/null +++ b/server/routers/hotels/schemas/hotel/specialAlerts.ts @@ -0,0 +1,35 @@ +import { z } from "zod" + +import { dt } from "@/lib/dt" + +import { AlertTypeEnum } from "@/types/enums/alert" + +const specialAlertSchema = z.object({ + description: z.string().optional(), + displayInBookingFlow: z.boolean(), + endDate: z.string().optional(), + startDate: z.string().optional(), + title: z.string().optional(), + type: z.string(), +}) + +export const specialAlertsSchema = z + .array(specialAlertSchema) + .transform((data) => { + const now = dt().utc().format("YYYY-MM-DD") + const filteredAlerts = data.filter((alert) => { + const shouldShowNow = + alert.startDate && alert.endDate + ? alert.startDate <= now && alert.endDate >= now + : true + const hasText = alert.description || alert.title + return shouldShowNow && hasText + }) + return filteredAlerts.map((alert, idx) => ({ + heading: alert.title || null, + id: `alert-${alert.type}-${idx}`, + text: alert.description || null, + type: AlertTypeEnum.Info, + })) + }) + .default([]) diff --git a/server/routers/hotels/schemas/hotel/specialNeedGroups.ts b/server/routers/hotels/schemas/hotel/specialNeedGroups.ts new file mode 100644 index 000000000..7f81aaaad --- /dev/null +++ b/server/routers/hotels/schemas/hotel/specialNeedGroups.ts @@ -0,0 +1,11 @@ +import { z } from "zod" + +const specialNeedSchema = z.object({ + details: z.string(), + name: z.string(), +}) + +export const specialNeedGroupSchema = z.object({ + name: z.string(), + specialNeeds: z.array(specialNeedSchema), +}) diff --git a/server/routers/hotels/schemas/hotelFilter.ts b/server/routers/hotels/schemas/hotelFilter.ts new file mode 100644 index 000000000..5abd664df --- /dev/null +++ b/server/routers/hotels/schemas/hotelFilter.ts @@ -0,0 +1,7 @@ +import { z } from "zod" + +export const hotelFilterSchema = z.object({ + hotelFacilities: z.array(z.string()), + hotelSurroundings: z.array(z.string()), + roomFacilities: z.array(z.string()), +}) diff --git a/server/routers/hotels/schemas/image.ts b/server/routers/hotels/schemas/image.ts index 2674c1d89..de7cc4ded 100644 --- a/server/routers/hotels/schemas/image.ts +++ b/server/routers/hotels/schemas/image.ts @@ -1,17 +1,17 @@ import { z } from "zod" -const imageSizesSchema = z.object({ - tiny: z.string(), - small: z.string(), - medium: z.string(), +export const imageSizesSchema = z.object({ large: z.string(), + medium: z.string(), + small: z.string(), + tiny: z.string(), }) -const imageMetaDataSchema = z.object({ - title: z.string(), +export const imageMetaDataSchema = z.object({ altText: z.string(), altText_En: z.string(), copyRight: z.string(), + title: z.string(), }) const DEFAULT_IMAGE_OBJ = { diff --git a/server/routers/hotels/schemas/location/city.ts b/server/routers/hotels/schemas/location/city.ts new file mode 100644 index 000000000..dc3404f31 --- /dev/null +++ b/server/routers/hotels/schemas/location/city.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const locationCitySchema = z.object({ + attributes: z.object({ + cityIdentifier: z.string().optional(), + keyWords: z.array(z.string()).optional(), + name: z.string().optional().default(""), + }), + country: z.string().optional().default(""), + id: z.string().optional().default(""), + type: z.literal("cities"), +}) diff --git a/server/routers/hotels/schemas/location/hotel.ts b/server/routers/hotels/schemas/location/hotel.ts new file mode 100644 index 000000000..01a407ab3 --- /dev/null +++ b/server/routers/hotels/schemas/location/hotel.ts @@ -0,0 +1,33 @@ +import { z } from "zod" + +export const locationHotelSchema = z.object({ + attributes: z.object({ + distanceToCentre: z.number().optional(), + images: z + .object({ + large: z.string().optional(), + medium: z.string().optional(), + small: z.string().optional(), + tiny: z.string().optional(), + }) + .optional(), + keyWords: z.array(z.string()).optional(), + name: z.string().optional().default(""), + operaId: z.string().optional(), + }), + id: z.string().optional().default(""), + relationships: z + .object({ + city: z + .object({ + links: z + .object({ + related: z.string().optional(), + }) + .optional(), + }) + .optional(), + }) + .optional(), + type: z.literal("hotels"), +}) diff --git a/server/routers/hotels/schemas/meetingRoom.ts b/server/routers/hotels/schemas/meetingRoom.ts new file mode 100644 index 000000000..01b30d316 --- /dev/null +++ b/server/routers/hotels/schemas/meetingRoom.ts @@ -0,0 +1,50 @@ +import { z } from "zod" + +import { imageSchema } from "./image" + +export const meetingRoomsSchema = z + .object({ + data: z.array( + z.object({ + attributes: z.object({ + name: z.string(), + email: z.string().optional(), + phoneNumber: z.string(), + size: z.number(), + doorWidth: z.number(), + doorHeight: z.number(), + length: z.number(), + width: z.number(), + height: z.number(), + floorNumber: z.number(), + content: z.object({ + images: z.array(imageSchema), + texts: z.object({ + facilityInformation: z.string().optional(), + surroundingInformation: z.string().optional(), + descriptions: z.object({ + short: z.string().optional(), + medium: z.string().optional(), + }), + meetingDescription: z + .object({ + short: z.string().optional(), + medium: z.string().optional(), + }) + .optional(), + }), + }), + seatings: z.array( + z.object({ + type: z.string(), + capacity: z.number(), + }) + ), + lighting: z.string(), + sortOrder: z.number().optional(), + }), + id: z.string(), + type: z.string(), + }) + ), + }) \ No newline at end of file diff --git a/server/routers/hotels/schemas/packages.ts b/server/routers/hotels/schemas/packages.ts index 035882fc4..0f32f11a5 100644 --- a/server/routers/hotels/schemas/packages.ts +++ b/server/routers/hotels/schemas/packages.ts @@ -1,16 +1,9 @@ import { z } from "zod" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { PackageTypeEnum } from "@/types/enums/packages" -export const getRoomPackagesInputSchema = z.object({ - hotelId: z.string(), - startDate: z.string(), - endDate: z.string(), - adults: z.number(), - children: z.number().optional().default(0), - packageCodes: z.array(z.string()).optional().default([]), -}) - +// TODO: Remove optional and default when the API change has been deployed export const packagePriceSchema = z .object({ currency: z.string().default("N/A"), @@ -22,40 +15,27 @@ export const packagePriceSchema = z currency: "N/A", price: "0", totalPrice: "0", - }) // TODO: Remove optional and default when the API change has been deployed + }) -export const packagesSchema = z.object({ +const inventorySchema = z.object({ + date: z.string(), + total: z.number(), + available: z.number(), +}) + +export const packageSchema = z.object({ code: z.nativeEnum(RoomPackageCodeEnum), description: z.string(), + inventories: z.array(inventorySchema), + itemCode: z.string().default(""), + localPrice: packagePriceSchema, + requestedPrice: packagePriceSchema, +}) + +export const breakfastPackageSchema = z.object({ + code: z.string(), + description: z.string(), localPrice: packagePriceSchema, requestedPrice: packagePriceSchema, - inventories: z.array( - z.object({ - date: z.string(), - total: z.number(), - available: z.number(), - }) - ), + packageType: z.literal(PackageTypeEnum.BreakfastAdult), }) - -export const getRoomPackagesSchema = z - .object({ - data: z.object({ - attributes: z.object({ - hotelId: z.number(), - packages: z.array(packagesSchema).default([]), - }), - relationships: z - .object({ - links: z.array( - z.object({ - url: z.string(), - type: z.string(), - }) - ), - }) - .optional(), - type: z.string(), - }), - }) - .transform((data) => data.data.attributes.packages) diff --git a/server/routers/hotels/schemas/productTypePrice.ts b/server/routers/hotels/schemas/productTypePrice.ts new file mode 100644 index 000000000..2d8e206db --- /dev/null +++ b/server/routers/hotels/schemas/productTypePrice.ts @@ -0,0 +1,16 @@ +import { z } from "zod" + +import { CurrencyEnum } from "@/types/enums/currency" + +export const priceSchema = z.object({ + currency: z.nativeEnum(CurrencyEnum), + pricePerNight: z.coerce.number(), + pricePerStay: z.coerce.number(), +}) + +export const productTypePriceSchema = z.object({ + localPrice: priceSchema, + rateCode: z.string(), + rateType: z.string().optional(), + requestedPrice: priceSchema.optional(), +}) diff --git a/server/routers/hotels/schemas/rate.ts b/server/routers/hotels/schemas/rate.ts new file mode 100644 index 000000000..f82345261 --- /dev/null +++ b/server/routers/hotels/schemas/rate.ts @@ -0,0 +1,21 @@ +import { z } from "zod" + +const flexibilityPrice = z.object({ + member: z.number(), + standard: z.number(), +}) + +export const rateSchema = z.object({ + breakfastIncluded: z.boolean(), + description: z.string(), + id: z.number(), + imageSrc: z.string(), + name: z.string(), + prices: z.object({ + currency: z.string(), + freeCancellation: flexibilityPrice, + freeRebooking: flexibilityPrice, + nonRefundable: flexibilityPrice, + }), + size: z.string(), +}) diff --git a/server/routers/hotels/schemas/relationships.ts b/server/routers/hotels/schemas/relationships.ts new file mode 100644 index 000000000..9c2426c2c --- /dev/null +++ b/server/routers/hotels/schemas/relationships.ts @@ -0,0 +1,10 @@ +import { z } from "zod" + +export const relationshipsSchema = z.object({ + links: z.array( + z.object({ + type: z.string(), + url: z.string().url(), + }) + ), +}) diff --git a/server/routers/hotels/schemas/roomAvailability/configuration.ts b/server/routers/hotels/schemas/roomAvailability/configuration.ts new file mode 100644 index 000000000..4d433917b --- /dev/null +++ b/server/routers/hotels/schemas/roomAvailability/configuration.ts @@ -0,0 +1,23 @@ +import { z } from "zod" + +import { productSchema } from "./product" + +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" + +export const roomConfigurationSchema = z.object({ + features: z.array( + z.object({ + inventory: z.number(), + code: z.enum([ + RoomPackageCodeEnum.PET_ROOM, + RoomPackageCodeEnum.ALLERGY_ROOM, + RoomPackageCodeEnum.ACCESSIBILITY_ROOM, + ]), + }) + ), + products: z.array(productSchema), + roomsLeft: z.number(), + roomType: z.string(), + roomTypeCode: z.string(), + status: z.string(), +}) diff --git a/server/routers/hotels/schemas/roomAvailability/product.ts b/server/routers/hotels/schemas/roomAvailability/product.ts new file mode 100644 index 000000000..94ff02ab9 --- /dev/null +++ b/server/routers/hotels/schemas/roomAvailability/product.ts @@ -0,0 +1,21 @@ +import { z } from "zod" + +import { productTypePriceSchema } from "../productTypePrice" + +import { CurrencyEnum } from "@/types/enums/currency" + +export const productSchema = z.object({ + productType: z.object({ + member: productTypePriceSchema.optional(), + public: productTypePriceSchema.default({ + localPrice: { + currency: CurrencyEnum.SEK, + pricePerNight: 0, + pricePerStay: 0, + }, + rateCode: "", + rateType: "", + requestedPrice: undefined, + }), + }), +}) diff --git a/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts b/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts new file mode 100644 index 000000000..815c78447 --- /dev/null +++ b/server/routers/hotels/schemas/roomAvailability/rateDefinition.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const rateDefinitionSchema = z.object({ + breakfastIncluded: z.boolean(), + cancellationRule: z.string(), + cancellationText: z.string(), + generalTerms: z.array(z.string()), + mustBeGuaranteed: z.boolean(), + rateCode: z.string(), + rateType: z.string().optional(), + title: z.string(), +}) diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts index abfb53bf5..eae6946f6 100644 --- a/server/routers/hotels/utils.ts +++ b/server/routers/hotels/utils.ts @@ -4,25 +4,24 @@ import { unstable_cache } from "next/cache" import * as api from "@/lib/api" import { - apiCitiesByCountrySchema, - apiCitySchema, - apiCountriesSchema, - apiLocationsSchema, - type CitiesGroupedByCountry, + citiesByCountrySchema, + citiesSchema, + countriesSchema, getHotelIdsByCityIdSchema, + locationsSchema, } from "./output" -import { - getHotelIdsCounter, - getHotelIdsFailCounter, - getHotelIdsSuccessCounter, -} from "./telemetry" -import type { Country } from "@/types/enums/country" +import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest" import type { RequestOptionsWithOutBody } from "@/types/fetch" -import { PointOfInterestGroupEnum } from "@/types/hotel" -import type { HotelLocation } from "@/types/trpc/routers/hotel/locations" +import type { + CitiesGroupedByCountry, + Countries, + HotelLocation, +} from "@/types/trpc/routers/hotel/locations" import type { Lang } from "@/constants/languages" import type { Endpoint } from "@/lib/api/endpoints" +import { Country } from "@/types/enums/country" +import { metrics } from "./metrics" export function getPoiGroupByCategoryName(category: string | undefined) { if (!category) return PointOfInterestGroupEnum.LOCATION @@ -75,7 +74,7 @@ export async function getCity( } const cityJson = await cityResponse.json() - const city = apiCitySchema.safeParse(cityJson) + const city = citiesSchema.safeParse(cityJson) if (!city.success) { console.info(`Validation of city failed`) console.info(`cityUrl: ${locationCityUrl}`) @@ -108,7 +107,7 @@ export async function getCountries( } const countriesJson = await countryResponse.json() - const countries = apiCountriesSchema.safeParse(countriesJson) + const countries = countriesSchema.safeParse(countriesJson) if (!countries.success) { console.info(`Validation for countries failed`) console.error(countries.error) @@ -149,8 +148,7 @@ export async function getCitiesByCountry( } const countryJson = await countryResponse.json() - const citiesByCountry = - apiCitiesByCountrySchema.safeParse(countryJson) + const citiesByCountry = citiesByCountrySchema.safeParse(countryJson) if (!citiesByCountry.success) { console.info(`Failed to validate Cities by Country payload`) console.error(citiesByCountry.error) @@ -200,7 +198,7 @@ export async function getLocations( } const apiJson = await apiResponse.json() - const verifiedLocations = apiLocationsSchema.safeParse(apiJson) + const verifiedLocations = locationsSchema.safeParse(apiJson) if (!verifiedLocations.success) { console.info(`Locations Verification Failed`) console.error(verifiedLocations.error) @@ -273,7 +271,7 @@ export async function getHotelIdsByCityId( ) { return unstable_cache( async function (params: URLSearchParams) { - getHotelIdsCounter.add(1, { params: params.toString() }) + metrics.hotelIds.counter.add(1, { params: params.toString() }) console.info( "api.hotel.hotel-ids start", JSON.stringify({ params: params.toString() }) @@ -286,7 +284,7 @@ export async function getHotelIdsByCityId( if (!apiResponse.ok) { const responseMessage = await apiResponse.text() - getHotelIdsFailCounter.add(1, { + metrics.hotelIds.fail.add(1, { params: params.toString(), error_type: "http_error", error: responseMessage, @@ -309,7 +307,7 @@ export async function getHotelIdsByCityId( const apiJson = await apiResponse.json() const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson) if (!validatedHotelIds.success) { - getHotelIdsFailCounter.add(1, { + metrics.hotelIds.fail.add(1, { params: params.toString(), error_type: "validation_error", error: JSON.stringify(validatedHotelIds.error), @@ -324,7 +322,7 @@ export async function getHotelIdsByCityId( return null } - getHotelIdsSuccessCounter.add(1, { cityId }) + metrics.hotelIds.success.add(1, { cityId }) console.info( "api.hotel.hotel-ids success", JSON.stringify({ params: params.toString() }) @@ -344,7 +342,7 @@ export async function getHotelIdsByCountry( ) { return unstable_cache( async function (params: URLSearchParams) { - getHotelIdsCounter.add(1, { country }) + metrics.hotelIds.counter.add(1, { country }) console.info( "api.hotel.hotel-ids start", JSON.stringify({ query: { country } }) @@ -357,7 +355,7 @@ export async function getHotelIdsByCountry( if (!apiResponse.ok) { const responseMessage = await apiResponse.text() - getHotelIdsFailCounter.add(1, { + metrics.hotelIds.fail.add(1, { country, error_type: "http_error", error: responseMessage, @@ -380,7 +378,7 @@ export async function getHotelIdsByCountry( const apiJson = await apiResponse.json() const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson) if (!validatedHotelIds.success) { - getHotelIdsFailCounter.add(1, { + metrics.hotelIds.fail.add(1, { country, error_type: "validation_error", error: JSON.stringify(validatedHotelIds.error), @@ -395,7 +393,7 @@ export async function getHotelIdsByCountry( return null } - getHotelIdsSuccessCounter.add(1, { country }) + metrics.hotelIds.success.add(1, { country }) console.info( "api.hotel.hotel-ids success", JSON.stringify({ query: { country } }) diff --git a/types/components/hotelPage/meetingRooms.ts b/types/components/hotelPage/meetingRooms.ts index c49c3b536..c73b68a79 100644 --- a/types/components/hotelPage/meetingRooms.ts +++ b/types/components/hotelPage/meetingRooms.ts @@ -1,6 +1,6 @@ +import { meetingRoomsSchema } from "@/server/routers/hotels/schemas/meetingRoom" + import type { z } from "zod" -import type { getMeetingRoomsSchema } from "@/server/routers/hotels/output" - -export type MeetingRoomData = z.infer +export type MeetingRoomData = z.output export type MeetingRooms = MeetingRoomData["data"] diff --git a/types/components/hotelPage/room.ts b/types/components/hotelPage/room.ts index e66ff1b56..048958f43 100644 --- a/types/components/hotelPage/room.ts +++ b/types/components/hotelPage/room.ts @@ -1,10 +1,10 @@ -import type { RoomData } from "@/types/hotel" +import type { Room } from "@/types/hotel" export interface RoomCardProps { - room: RoomData + room: Room } export type RoomsProps = { - rooms: RoomData[] preamble?: string + rooms: Room[] } diff --git a/types/components/hotelPage/sidepeek/amenities.ts b/types/components/hotelPage/sidepeek/amenities.ts index 0ae272bef..e85b9ad19 100644 --- a/types/components/hotelPage/sidepeek/amenities.ts +++ b/types/components/hotelPage/sidepeek/amenities.ts @@ -1,6 +1,6 @@ import type { Hotel, - RestaurantData, + Restaurant, RestaurantOpeningHours, } from "@/types/hotel" import type { ParkingAmenityProps } from "./parking" @@ -10,7 +10,7 @@ export type AmenitiesSidePeekProps = { parking: ParkingAmenityProps checkInInformation: Hotel["hotelFacts"]["checkin"] accessibility: Hotel["hotelFacts"]["hotelInformation"]["accessibility"] - restaurants: RestaurantData[] + restaurants: Restaurant[] } export type FilteredAmenitiesProps = { diff --git a/types/components/hotelPage/sidepeek/parking.ts b/types/components/hotelPage/sidepeek/parking.ts index 08981c286..216a48463 100644 --- a/types/components/hotelPage/sidepeek/parking.ts +++ b/types/components/hotelPage/sidepeek/parking.ts @@ -1,27 +1,29 @@ -import type { Hotel } from "@/types/hotel" +import type { Hotel, Parking } from "@/types/hotel" export enum Periods { + allDay = "AllDay", day = "Day", night = "Night", - allDay = "AllDay", } export type ParkingAmenityProps = { + hasExtraParkingPage: boolean parking: Hotel["parking"] parkingElevatorPitch: string - hasExtraParkingPage: boolean } -export type ParkingListProps = { - numberOfChargingSpaces: Hotel["parking"][number]["numberOfChargingSpaces"] - canMakeReservation: Hotel["parking"][number]["canMakeReservation"] - numberOfParkingSpots: Hotel["parking"][number]["numberOfParkingSpots"] - distanceToHotel: Hotel["parking"][number]["distanceToHotel"] - address: Hotel["parking"][number]["address"] -} +export interface ParkingListProps + extends Pick< + Parking, + | "address" + | "canMakeReservation" + | "distanceToHotel" + | "numberOfChargingSpaces" + | "numberOfParkingSpots" + > { } -export type ParkingPricesProps = { - pricing: Hotel["parking"][number]["pricing"]["localCurrency"]["ordinary"] - currency: Hotel["parking"][number]["pricing"]["localCurrency"]["currency"] - freeParking: Hotel["parking"][number]["pricing"]["freeParking"] +export interface ParkingPricesProps + extends Pick, + Pick, "currency"> { + pricing: NonNullable["ordinary"] } diff --git a/types/components/hotelPage/sidepeek/restaurantBar.ts b/types/components/hotelPage/sidepeek/restaurantBar.ts index bbd0ba91a..31ab6b4b7 100644 --- a/types/components/hotelPage/sidepeek/restaurantBar.ts +++ b/types/components/hotelPage/sidepeek/restaurantBar.ts @@ -1,9 +1,9 @@ -import type { RestaurantData } from "@/types/hotel" +import type { Restaurant } from "@/types/hotel" export interface RestaurantBarSidePeekProps { - restaurants: RestaurantData[] + restaurants: Restaurant[] } export interface RestaurantBarItemProps { - restaurant: RestaurantData + restaurant: Restaurant } diff --git a/types/components/hotelPage/sidepeek/room.ts b/types/components/hotelPage/sidepeek/room.ts index 7ff699273..dcc442b08 100644 --- a/types/components/hotelPage/sidepeek/room.ts +++ b/types/components/hotelPage/sidepeek/room.ts @@ -1,5 +1,5 @@ -import type { RoomData } from "@/types/hotel" +import type { Room } from "@/types/hotel" export interface RoomSidePeekProps { - room: RoomData + room: Room } diff --git a/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts b/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts index 73007a82d..88fb770f3 100644 --- a/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts +++ b/types/components/hotelReservation/bookingConfirmation/bookingConfirmation.ts @@ -4,4 +4,4 @@ export interface BookingConfirmationProps { confirmationNumber: string } -export interface ConfirmationProps extends BookingConfirmation {} +export interface ConfirmationProps extends BookingConfirmation { } diff --git a/types/components/hotelReservation/enterDetails/breakfast.ts b/types/components/hotelReservation/enterDetails/breakfast.ts index acb936abd..e6e2a4e0f 100644 --- a/types/components/hotelReservation/enterDetails/breakfast.ts +++ b/types/components/hotelReservation/enterDetails/breakfast.ts @@ -1,19 +1,17 @@ -import { type z } from "zod" +import type { z } from "zod" import type { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" -import type { - breakfastPackageSchema, - breakfastPackagesSchema, -} from "@/server/routers/hotels/output" +import type { breakfastPackagesSchema } from "@/server/routers/hotels/output" +import type { breakfastPackageSchema } from "@/server/routers/hotels/schemas/packages" export interface BreakfastFormSchema - extends z.output {} + extends z.output { } export interface BreakfastPackages - extends z.output {} + extends z.output { } export interface BreakfastPackage - extends z.output {} + extends z.output { } export interface BreakfastProps { packages: BreakfastPackages diff --git a/types/components/hotelReservation/enterDetails/hotelHeader.ts b/types/components/hotelReservation/enterDetails/hotelHeader.ts index afb69676d..f8a15e87c 100644 --- a/types/components/hotelReservation/enterDetails/hotelHeader.ts +++ b/types/components/hotelReservation/enterDetails/hotelHeader.ts @@ -1,7 +1,5 @@ -import type { RouterOutput } from "@/lib/trpc/client" - -type HotelDataGet = RouterOutput["hotel"]["hotelData"]["get"] +import type { HotelData } from "@/types/hotel" export interface HotelHeaderProps { - hotelData: NonNullable + hotelData: HotelData } diff --git a/types/components/hotelReservation/enterDetails/room.ts b/types/components/hotelReservation/enterDetails/room.ts index 60e5e2646..f96146620 100644 --- a/types/components/hotelReservation/enterDetails/room.ts +++ b/types/components/hotelReservation/enterDetails/room.ts @@ -1,7 +1,7 @@ -import { RoomConfiguration } from "@/server/routers/hotels/output" +import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" export interface SelectedRoomProps { hotelId: string - room: RoomConfiguration rateDescription: string + room: RoomConfiguration } diff --git a/types/components/hotelReservation/enterDetails/summary.ts b/types/components/hotelReservation/enterDetails/summary.ts index 54d896d92..62372f226 100644 --- a/types/components/hotelReservation/enterDetails/summary.ts +++ b/types/components/hotelReservation/enterDetails/summary.ts @@ -1,5 +1,5 @@ import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability" +import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability" export interface ClientSummaryProps extends Pick< diff --git a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts index 4144abf45..769edf97f 100644 --- a/types/components/hotelReservation/selectHotel/hotePriceListProps.ts +++ b/types/components/hotelReservation/selectHotel/hotePriceListProps.ts @@ -1,6 +1,6 @@ -import type { ProductType } from "@/server/routers/hotels/output" +import type { ProductType } from "@/types/trpc/routers/hotel/availability" export type HotelPriceListProps = { - price: ProductType hotelId: string + price: ProductType } diff --git a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts index d8404024b..24336f390 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -1,21 +1,21 @@ import type { Hotel } from "@/types/hotel" -import type { ProductType } from "@/server/routers/hotels/output" +import type { ProductType } from "@/types/trpc/routers/hotel/availability" export enum HotelCardListingTypeEnum { MapListing = "mapListing", PageListing = "pageListing", } -export type HotelCardListingProps = { - hotelData: HotelData[] - type?: HotelCardListingTypeEnum -} - export type HotelData = { hotelData: Hotel price: ProductType } +export type HotelCardListingProps = { + hotelData: HotelData[] + type?: HotelCardListingTypeEnum +} + export interface NullableHotelData extends Omit { hotelData: HotelData["hotelData"] | null } diff --git a/types/components/hotelReservation/selectHotel/priceCardProps.ts b/types/components/hotelReservation/selectHotel/priceCardProps.ts index a56a67d0f..d82b5d31f 100644 --- a/types/components/hotelReservation/selectHotel/priceCardProps.ts +++ b/types/components/hotelReservation/selectHotel/priceCardProps.ts @@ -1,4 +1,4 @@ -import { ProductTypePrices } from "@/server/routers/hotels/output" +import type { ProductTypePrices } from "@/types/trpc/routers/hotel/availability" export type PriceCardProps = { productTypePrices: ProductTypePrices diff --git a/types/components/hotelReservation/selectHotel/selectHotel.ts b/types/components/hotelReservation/selectHotel/selectHotel.ts index 8beba9035..253f64408 100644 --- a/types/components/hotelReservation/selectHotel/selectHotel.ts +++ b/types/components/hotelReservation/selectHotel/selectHotel.ts @@ -1,10 +1,9 @@ -import type { CheckInData, Hotel, ParkingData } from "@/types/hotel" -import type { HotelLocation } from "@/types/trpc/routers/hotel/locations" -import type { Lang } from "@/constants/languages" import type { AlternativeHotelsSearchParams, SelectHotelSearchParams, } from "./selectHotelSearchParams" +import type { CheckInData, Hotel, Parking } from "@/types/hotel" +import type { Lang } from "@/constants/languages" export enum AvailabilityEnum { Available = "Available", @@ -23,7 +22,7 @@ export interface ContactProps { } export interface ParkingProps { - parking: ParkingData[] + parking: Parking[] } export interface AccessibilityProps { diff --git a/types/components/hotelReservation/selectRate/flexibilityOption.ts b/types/components/hotelReservation/selectRate/flexibilityOption.ts index fdeef83f1..e5e06de47 100644 --- a/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -1,11 +1,13 @@ import type { z } from "zod" import type { - priceSchema, Product, - productTypePriceSchema, RoomConfiguration, -} from "@/server/routers/hotels/output" +} from "@/types/trpc/routers/hotel/roomAvailability" +import type { + priceSchema, + productTypePriceSchema, +} from "@/server/routers/hotels/schemas/productTypePrice" import type { RoomPackage } from "./roomFilter" type ProductPrice = z.output diff --git a/types/components/hotelReservation/selectRate/hotelInfoCard.ts b/types/components/hotelReservation/selectRate/hotelInfoCard.ts new file mode 100644 index 000000000..8fc9a8c2d --- /dev/null +++ b/types/components/hotelReservation/selectRate/hotelInfoCard.ts @@ -0,0 +1,11 @@ +import type { Lang } from "@/constants/languages" +import type { Child } from "./selectRate" + +export interface HotelInfoCardProps { + adultCount: number + childArray?: Child[] + fromDate: Date + hotelId: number + lang: Lang + toDate: Date +} diff --git a/types/components/hotelReservation/selectRate/rateSummary.ts b/types/components/hotelReservation/selectRate/rateSummary.ts index 28fed15d7..470f2bf3d 100644 --- a/types/components/hotelReservation/selectRate/rateSummary.ts +++ b/types/components/hotelReservation/selectRate/rateSummary.ts @@ -1,9 +1,9 @@ -import type { RoomsAvailability } from "@/server/routers/hotels/output" -import type { RoomPackageData } from "./roomFilter" +import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" +import type { RoomPackages } from "./roomFilter" import type { Rate } from "./selectRate" export interface RateSummaryProps { isUserLoggedIn: boolean - packages: RoomPackageData | undefined + packages: RoomPackages | undefined roomsAvailability: RoomsAvailability } diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 5e5d58eae..1b3366d52 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -1,23 +1,25 @@ import type { z } from "zod" -import type { RoomData } from "@/types/hotel" +import type { Room } from "@/types/hotel" import type { - packagePriceSchema, RateDefinition, RoomConfiguration, -} from "@/server/routers/hotels/output" +} from "@/types/trpc/routers/hotel/roomAvailability" +import type { packagePriceSchema } from "@/server/routers/hotels/schemas/packages" import type { RoomPriceSchema } from "./flexibilityOption" -import type { RoomPackageCodes, RoomPackageData } from "./roomFilter" +import type { RoomPackageCodes, RoomPackages } from "./roomFilter" +import type { RateCode } from "./selectRate" export type RoomCardProps = { hotelId: string hotelType: string | undefined roomConfiguration: RoomConfiguration rateDefinitions: RateDefinition[] - roomCategories: RoomData[] + roomCategories: Room[] selectedPackages: RoomPackageCodes[] - packages: RoomPackageData | undefined roomListIndex: number + packages: RoomPackages | undefined + handleSelectRate: React.Dispatch> } type RoomPackagePriceSchema = z.output diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index c6698df9b..2ac6d743d 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -1,6 +1,6 @@ import type { z } from "zod" -import type { packagesSchema } from "@/server/routers/hotels/output" +import type { packageSchema } from "@/server/routers/hotels/schemas/packages" export enum RoomPackageCodeEnum { PET_ROOM = "PETR", @@ -24,7 +24,6 @@ export interface RoomFilterProps { roomListIndex: number } -export type RoomPackage = z.output -export interface RoomPackageData extends Array {} - +export type RoomPackage = z.output export type RoomPackageCodes = RoomPackage["code"] +export type RoomPackages = RoomPackage[] diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index 014f3a731..cdae49d50 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,31 +1,32 @@ -import type { RoomData } from "@/types/hotel" -import type { RoomsAvailability } from "@/server/routers/hotels/output" import type { DefaultFilterOptions, RoomPackage, RoomPackageCodes, - RoomPackageData, + RoomPackages, } from "./roomFilter" +import type { Room } from "@/types/hotel" +import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" + export interface RoomTypeListProps { roomsAvailability: RoomsAvailability - roomCategories: RoomData[] - availablePackages: RoomPackageData | undefined + roomCategories: Room[] + availablePackages: RoomPackage | undefined selectedPackages: RoomPackageCodes[] hotelType: string | undefined roomListIndex: number } export interface SelectRateProps { - roomsAvailability: RoomsAvailability - roomCategories: RoomData[] - availablePackages: RoomPackageData + availablePackages: RoomPackages hotelType: string | undefined isUserLoggedIn: boolean + roomsAvailability: RoomsAvailability + roomCategories: Room[] } export interface RoomSelectionPanelProps { - roomCategories: RoomData[] + roomCategories: Room[] availablePackages: RoomPackage[] selectedPackages: RoomPackageCodes[] hotelType: string | undefined diff --git a/types/components/hotelReservation/selectRate/roomsContainer.ts b/types/components/hotelReservation/selectRate/roomsContainer.ts new file mode 100644 index 000000000..74f52aa2d --- /dev/null +++ b/types/components/hotelReservation/selectRate/roomsContainer.ts @@ -0,0 +1,11 @@ +import type { Lang } from "@/constants/languages" +import type { Child } from "./selectRate" + +export interface RoomsContainerProps { + adultCount: number + childArray?: Child[] + fromDate: Date + hotelId: number + lang: Lang + toDate: Date +} diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index a993bed94..8ca10f6b1 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -1,4 +1,7 @@ -import type { Product, RoomConfiguration } from "@/server/routers/hotels/output" +import type { + Product, + RoomConfiguration, +} from "@/types/trpc/routers/hotel/roomAvailability" import type { ChildBedMapEnum } from "../../bookingWidget/enums" import type { RoomPackageCodeEnum } from "./roomFilter" diff --git a/types/components/hotelReservation/sidePeek.ts b/types/components/hotelReservation/sidePeek.ts index adeb99ec5..8502d6b23 100644 --- a/types/components/hotelReservation/sidePeek.ts +++ b/types/components/hotelReservation/sidePeek.ts @@ -1,10 +1,10 @@ -import { Hotel } from "@/types/hotel" +import { HotelData } from "@/types/hotel" export enum SidePeekEnum { hotelDetails = "hotel-detail-side-peek", roomDetails = "room-detail-side-peek", } -export type SidePeekProps = { - hotel: Hotel +export type HotelReservationSidePeekProps = { + hotel: HotelData | null } diff --git a/types/components/hotelReservation/summary.ts b/types/components/hotelReservation/summary.ts index c60194d71..0edd00d72 100644 --- a/types/components/hotelReservation/summary.ts +++ b/types/components/hotelReservation/summary.ts @@ -5,11 +5,11 @@ import type { Price, RoomPrice, } from "@/types/stores/enter-details" -import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability" import type { BedTypeSchema } from "./enterDetails/bedType" import type { BreakfastPackage } from "./enterDetails/breakfast" import type { DetailsSchema } from "./enterDetails/details" import type { Child, SelectRateSearchParams } from "./selectRate/selectRate" +import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability" export type RoomsData = Pick & Pick & @@ -21,7 +21,7 @@ export type RoomsData = Pick & export interface SummaryProps extends Pick, - Pick { + Pick { isMember: boolean breakfastIncluded: boolean } diff --git a/types/components/maps/poiMarker.ts b/types/components/maps/poiMarker.ts index abab6fc9d..3d33caeb4 100644 --- a/types/components/maps/poiMarker.ts +++ b/types/components/maps/poiMarker.ts @@ -1,8 +1,7 @@ -import { poiVariants } from "@/components/Maps/Markers/Poi/variants" - import type { VariantProps } from "class-variance-authority" -import type { PointOfInterestGroupEnum } from "@/types/hotel" +import type { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest" +import type { poiVariants } from "@/components/Maps/Markers/Poi/variants" export interface PoiMarkerProps extends VariantProps { group: PointOfInterestGroupEnum diff --git a/types/components/sidePeeks/roomSidePeek.ts b/types/components/sidePeeks/roomSidePeek.ts index 0298cb83c..cd978578f 100644 --- a/types/components/sidePeeks/roomSidePeek.ts +++ b/types/components/sidePeeks/roomSidePeek.ts @@ -1,9 +1,8 @@ -import { SidePeekEnum } from "../hotelReservation/sidePeek" - -import type { RoomData } from "@/types/hotel" +import type { Room } from "@/types/hotel" +import type { SidePeekEnum } from "../hotelReservation/sidePeek" export type RoomSidePeekProps = { - room: RoomData + room: Room activeSidePeek: SidePeekEnum | null close: () => void } diff --git a/types/enums/pointOfInterest.ts b/types/enums/pointOfInterest.ts new file mode 100644 index 000000000..8fc582389 --- /dev/null +++ b/types/enums/pointOfInterest.ts @@ -0,0 +1,8 @@ +export enum PointOfInterestGroupEnum { + PUBLIC_TRANSPORT = "Public transport", + ATTRACTIONS = "Attractions", + BUSINESS = "Business", + LOCATION = "Location", + PARKING = "Parking", + SHOPPING_DINING = "Shopping & Dining", +} diff --git a/types/hotel.ts b/types/hotel.ts index 54448dd66..3e46ec5b4 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -1,66 +1,66 @@ import type { z } from "zod" -import type { - checkinSchema, - getHotelDataSchema, - parkingSchema, - pointOfInterestSchema, -} from "@/server/routers/hotels/output" +import type { hotelSchema } from "@/server/routers/hotels/output" +import type { citySchema } from "@/server/routers/hotels/schemas/city" +import type { attributesSchema } from "@/server/routers/hotels/schemas/hotel" +import type { addressSchema } from "@/server/routers/hotels/schemas/hotel/address" +import type { hotelContentSchema } from "@/server/routers/hotels/schemas/hotel/content" +import type { detailedFacilitiesSchema } from "@/server/routers/hotels/schemas/hotel/detailedFacility" +import type { checkinSchema } from "@/server/routers/hotels/schemas/hotel/facts" +import type { nearbyHotelsSchema } from "@/server/routers/hotels/schemas/hotel/include/nearbyHotels" +import type { restaurantsSchema } from "@/server/routers/hotels/schemas/hotel/include/restaurants" +import type { transformRoomCategories } from "@/server/routers/hotels/schemas/hotel/include/roomCategories" +import type { locationSchema } from "@/server/routers/hotels/schemas/hotel/location" +import type { parkingSchema } from "@/server/routers/hotels/schemas/hotel/parking" +import type { pointOfInterestSchema } from "@/server/routers/hotels/schemas/hotel/poi" +import type { ratingsSchema } from "@/server/routers/hotels/schemas/hotel/rating" +import type { imageSchema } from "@/server/routers/hotels/schemas/image" +import { restaurantOpeningHoursSchema } from "@/server/routers/hotels/schemas/restaurants" +import { healthFacilitySchema } from "@/server/routers/hotels/schemas/hotel/healthFacilities" import type { additionalDataSchema, extraPageSchema, facilitySchema, + transformAdditionalData, } from "@/server/routers/hotels/schemas/additionalData" -import type { imageSchema } from "@/server/routers/hotels/schemas/image" -import type { - restaurantOpeningHoursSchema, - restaurantSchema, -} from "@/server/routers/hotels/schemas/restaurants" -import type { roomSchema } from "@/server/routers/hotels/schemas/room" -export type HotelData = z.output +export type HotelData = z.output -export type Hotel = HotelData["data"]["attributes"] -export type HotelAddress = HotelData["data"]["attributes"]["address"] -export type HotelLocation = HotelData["data"]["attributes"]["location"] -export type Amenities = HotelData["data"]["attributes"]["detailedFacilities"] -export type HealthFacilities = - HotelData["data"]["attributes"]["healthFacilities"] +export type Amenities = z.output +export type CheckInData = z.output +type CitySchema = z.output +export type City = Pick & CitySchema["attributes"] +export type Facility = z.output & { id: string } +export type GalleryImage = z.output +export type HealthFacility = z.output +export type HealthFacilities = HealthFacility[] +export type Hotel = z.output +export type HotelAddress = z.output +export type HotelContent = z.output +export type HotelLocation = z.output +export type HotelRatings = z.output +type NearbyHotelsSchema = z.output +export type NearbyHotel = Pick & + NearbyHotelsSchema["attributes"] +export type Parking = z.output +export type PointOfInterest = z.output +type RestaurantSchema = z.output +export type RestaurantOpeningHours = z.output +export type Restaurant = Pick & + RestaurantSchema["attributes"] +export type Room = ReturnType -type HotelRatings = HotelData["data"]["attributes"]["ratings"] +export type HotelMapContentProps = { + activePoi?: string | null + coordinates: { lat: number; lng: number } + onActivePoiChange?: (poiName: string | null) => void + pointsOfInterest: PointOfInterest[] +} export type HotelTripAdvisor = | NonNullable["tripAdvisor"] | undefined -export type RoomData = z.infer -export type RestaurantData = z.output -export type RestaurantOpeningHours = z.output< - typeof restaurantOpeningHoursSchema -> -export type GalleryImage = z.infer -export type CheckInData = z.infer +export type AdditionalData = ReturnType -export type AdditionalData = z.infer +export type ExtraPageSchema = z.output -export type ExtraPageSchema = z.infer - -export type PointOfInterest = z.output - -export enum PointOfInterestGroupEnum { - PUBLIC_TRANSPORT = "Public transport", - ATTRACTIONS = "Attractions", - BUSINESS = "Business", - LOCATION = "Location", - PARKING = "Parking", - SHOPPING_DINING = "Shopping & Dining", -} - -export type ParkingData = z.infer -export type Facility = z.infer & { id: string } - -export type HotelMapContentProps = { - coordinates: { lat: number; lng: number } - pointsOfInterest: PointOfInterest[] - onActivePoiChange?: (poiName: string | null) => void - activePoi?: string | null -} diff --git a/types/providers/enter-details.ts b/types/providers/enter-details.ts index 05db0ee74..12a03251e 100644 --- a/types/providers/enter-details.ts +++ b/types/providers/enter-details.ts @@ -1,6 +1,6 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { StepEnum } from "@/types/enums/step" -import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability" +import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { SafeUser } from "@/types/user" import type { SelectRateSearchParams } from "../components/hotelReservation/selectRate/selectRate" import type { Packages } from "../requests/packages" diff --git a/types/requests/packages.ts b/types/requests/packages.ts index fb242917f..f0d501332 100644 --- a/types/requests/packages.ts +++ b/types/requests/packages.ts @@ -1,15 +1,15 @@ -import { z } from "zod" +import type { z } from "zod" -import { - getBreakfastPackageInputSchema, - getRoomPackagesInputSchema, +import type { + breakfastPackageInputSchema, + roomPackagesInputSchema, } from "@/server/routers/hotels/input" -import { getRoomPackagesSchema } from "@/server/routers/hotels/output" +import type { packagesSchema } from "@/server/routers/hotels/output" export interface BreackfastPackagesInput - extends z.input {} + extends z.input {} export interface PackagesInput - extends z.input {} + extends z.input {} -export interface Packages extends z.output {} +export type Packages = z.output diff --git a/types/trpc/routers/booking/confirmation.ts b/types/trpc/routers/booking/confirmation.ts index d5440f0c7..713842cfe 100644 --- a/types/trpc/routers/booking/confirmation.ts +++ b/types/trpc/routers/booking/confirmation.ts @@ -1,20 +1,17 @@ import type { z } from "zod" -import type { Hotel, RoomData } from "@/types/hotel" +import type { Hotel, Room } from "@/types/hotel" import type { bookingConfirmationSchema } from "@/server/routers/booking/output" export interface BookingConfirmationSchema - extends z.output {} + extends z.output { } + export interface BookingConfirmation { booking: BookingConfirmationSchema - hotel: Hotel & { - included?: { - rooms: RoomData[] | undefined - } - } + hotel: Hotel room: - | (RoomData & { - bedType: RoomData["roomTypes"][number] - }) - | null + | (Room & { + bedType: Room["roomTypes"][number] + }) + | null } diff --git a/types/trpc/routers/hotel/availability.ts b/types/trpc/routers/hotel/availability.ts index 760766837..90f1ccb07 100644 --- a/types/trpc/routers/hotel/availability.ts +++ b/types/trpc/routers/hotel/availability.ts @@ -1,5 +1,9 @@ -import type { RouterOutput } from "@/lib/trpc/client" +import type { z } from "zod" -export type RoomAvailability = NonNullable< - RouterOutput["hotel"]["availability"]["room"] -> +import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output" +import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType" +import type { productTypePriceSchema } from "@/server/routers/hotels/schemas/productTypePrice" + +export type HotelsAvailability = z.output +export type ProductType = z.output +export type ProductTypePrices = z.output diff --git a/types/trpc/routers/hotel/filter.ts b/types/trpc/routers/hotel/filter.ts new file mode 100644 index 000000000..5f4108b68 --- /dev/null +++ b/types/trpc/routers/hotel/filter.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" + +import type { hotelFilterSchema } from "@/server/routers/hotels/schemas/hotelFilter" + +export type HotelFilter = z.output diff --git a/types/trpc/routers/hotel/hotel.ts b/types/trpc/routers/hotel/hotel.ts new file mode 100644 index 000000000..d086488ef --- /dev/null +++ b/types/trpc/routers/hotel/hotel.ts @@ -0,0 +1,12 @@ +import type { z } from "zod" + +import type { Room } from "@/types/hotel" +import type { + cityCoordinatesInputSchema, + hotelInputSchema, +} from "@/server/routers/hotels/input" + +export type CityCoordinatesInput = z.input +export type HotelInput = z.input + +export type RoomType = Pick diff --git a/types/trpc/routers/hotel/locations.ts b/types/trpc/routers/hotel/locations.ts index 40d56dc7f..30e668777 100644 --- a/types/trpc/routers/hotel/locations.ts +++ b/types/trpc/routers/hotel/locations.ts @@ -1,8 +1,12 @@ import type { z } from "zod" -import type { apiLocationsSchema } from "@/server/routers/hotels/output" +import type { + citiesByCountrySchema, + countriesSchema, + locationsSchema, +} from "@/server/routers/hotels/output" -export interface LocationSchema extends z.output {} +export interface LocationSchema extends z.output { } export type Locations = LocationSchema["data"] export type Location = Locations[number] @@ -15,3 +19,8 @@ export function isHotelLocation( ): location is HotelLocation { return location?.type === "hotels" } +export interface CitiesByCountry + extends z.output { } +export type CitiesGroupedByCountry = Record + +export interface Countries extends z.output { } diff --git a/types/trpc/routers/hotel/rate.ts b/types/trpc/routers/hotel/rate.ts new file mode 100644 index 000000000..2ebcb02af --- /dev/null +++ b/types/trpc/routers/hotel/rate.ts @@ -0,0 +1,5 @@ +import type { z } from "zod" + +import type { rateSchema } from "@/server/routers/hotels/schemas/rate" + +export type Rate = z.output diff --git a/types/trpc/routers/hotel/roomAvailability.ts b/types/trpc/routers/hotel/roomAvailability.ts new file mode 100644 index 000000000..5f7afd495 --- /dev/null +++ b/types/trpc/routers/hotel/roomAvailability.ts @@ -0,0 +1,16 @@ +import type { z } from "zod" + +import type { RouterOutput } from "@/lib/trpc/client" +import type { roomsAvailabilitySchema } from "@/server/routers/hotels/output" +import type { roomConfigurationSchema } from "@/server/routers/hotels/schemas/roomAvailability/configuration" +import type { productSchema } from "@/server/routers/hotels/schemas/roomAvailability/product" +import type { rateDefinitionSchema } from "@/server/routers/hotels/schemas/roomAvailability/rateDefinition" + +export type RoomAvailability = NonNullable< + RouterOutput["hotel"]["availability"]["room"] +> + +export type Product = z.output +export type RateDefinition = z.output +export type RoomConfiguration = z.output +export type RoomsAvailability = z.output