diff --git a/__mocks__/hotelReservation/index.ts b/__mocks__/hotelReservation/index.ts index 74a78f7be..7d820b749 100644 --- a/__mocks__/hotelReservation/index.ts +++ b/__mocks__/hotelReservation/index.ts @@ -9,6 +9,7 @@ import type { } from "@/types/components/hotelReservation/enterDetails/details" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { CurrencyEnum } from "@/types/enums/currency" import { PackageTypeEnum } from "@/types/enums/packages" import type { RoomPrice, RoomRate } from "@/types/stores/enter-details" @@ -47,12 +48,12 @@ export const roomRate: RoomRate = { localPrice: { pricePerNight: 1508, pricePerStay: 1508, - currency: "SEK", + currency: CurrencyEnum.SEK, }, requestedPrice: { pricePerNight: 132, pricePerStay: 132, - currency: "EUR", + currency: CurrencyEnum.EUR, }, }, publicRate: { @@ -60,12 +61,12 @@ export const roomRate: RoomRate = { localPrice: { pricePerNight: 1525, pricePerStay: 1525, - currency: "SEK", + currency: CurrencyEnum.SEK, }, requestedPrice: { pricePerNight: 133, pricePerStay: 133, - currency: "EUR", + currency: CurrencyEnum.EUR, }, }, } diff --git a/actions/editProfile.ts b/actions/editProfile.ts index acf5d1b12..e72f8c336 100644 --- a/actions/editProfile.ts +++ b/actions/editProfile.ts @@ -10,8 +10,8 @@ import { protectedServerActionProcedure } from "@/server/trpc" import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries" import { getIntl } from "@/i18n" -import { phoneValidator } from "@/utils/phoneValidator" import { getMembership } from "@/utils/user" +import { phoneValidator } from "@/utils/zod/phoneValidator" import { Status } from "@/types/components/myPages/myProfile/edit" 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 c1293c833..ba645e64c 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -12,8 +12,6 @@ export default async function BookingConfirmationPage({ setLang(params.lang) void getBookingConfirmation(searchParams.confirmationNumber) 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 04e14c36a..3edbe088f 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -15,7 +15,7 @@ import type { CategorizedFilters, Filter, } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -import type { HotelsAvailabilityItem } from "@/server/routers/hotels/output" +import type { HotelsAvailabilityItem } from "@/types/trpc/routers/hotel/availability" const hotelSurroundingsFilterNames = [ "Hotel surroundings", 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 22d7cd65d..e9f4ce451 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -28,14 +28,11 @@ 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 (!hotel) { + if (!searchDetails?.hotel) { return notFound() } + const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = + searchDetails const hotelData = await getHotel({ hotelId: hotel.id, diff --git a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx index c8500a2bc..5e397579d 100644 --- a/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx +++ b/app/[lang]/(live)/@bookingwidget/[contentType]/[uid]/page.tsx @@ -27,8 +27,8 @@ export default async function BookingWidgetPage({ }) const hotelPageParams = { - hotel: hotelData?.data?.id || "", - city: hotelData?.data?.attributes?.cityName || "", + hotel: hotelData?.hotel?.id || "", + city: hotelData?.hotel?.cityName || "", } return 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 27089a617..d01e6f5b1 100644 --- a/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx +++ b/components/ContentType/HotelPage/SidePeeks/Amenities/AccordionAmenities/Parking/ParkingPrices/index.tsx @@ -10,7 +10,7 @@ import { } from "@/types/components/hotelPage/sidepeek/parking" export default async function ParkingPrices({ - currency, + currency = "", freeParking, pricing, }: ParkingPricesProps) { diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index 4049a2903..52685e9dd 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -125,10 +125,10 @@ export default async function HotelPage({ hotelId }: HotelPageProps) { const trackingPageData = getTrackingPageData( hotelPageData.system, - hotelData.data.attributes, + hotelData.hotel, lang ) - const trackingHotelData = getTrackingHotelData(hotelData.data) + const trackingHotelData = getTrackingHotelData(hotelData.hotel) return (
diff --git a/components/ContentType/HotelPage/utils.ts b/components/ContentType/HotelPage/utils.ts index 4e9bb06f5..a725548f1 100644 --- a/components/ContentType/HotelPage/utils.ts +++ b/components/ContentType/HotelPage/utils.ts @@ -35,7 +35,7 @@ export function getTrackingPageData( return tracking } -export function getTrackingHotelData(hotelData: HotelData["data"]) { +export function getTrackingHotelData(hotelData: HotelData["hotel"]) { const tracking: TrackingSDKHotelInfo = { hotelID: hotelData.id, } diff --git a/components/Forms/Edit/Profile/schema.ts b/components/Forms/Edit/Profile/schema.ts index e785b4716..8aad6c1fd 100644 --- a/components/Forms/Edit/Profile/schema.ts +++ b/components/Forms/Edit/Profile/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { passwordValidator } from "@/utils/passwordValidator" -import { phoneValidator } from "@/utils/phoneValidator" +import { passwordValidator } from "@/utils/zod/passwordValidator" +import { phoneValidator } from "@/utils/zod/phoneValidator" const countryRequiredMsg = "Country is required" export const editProfileSchema = z diff --git a/components/Forms/Signup/schema.ts b/components/Forms/Signup/schema.ts index 2962d9b90..37969893e 100644 --- a/components/Forms/Signup/schema.ts +++ b/components/Forms/Signup/schema.ts @@ -1,7 +1,7 @@ import { z } from "zod" -import { passwordValidator } from "@/utils/passwordValidator" -import { phoneValidator } from "@/utils/phoneValidator" +import { passwordValidator } from "@/utils/zod/passwordValidator" +import { phoneValidator } from "@/utils/zod/phoneValidator" const countryRequiredMsg = "Country is required" export const signUpSchema = z.object({ diff --git a/components/HotelReservation/BookingConfirmation/index.tsx b/components/HotelReservation/BookingConfirmation/index.tsx index c3573dc33..de0c1d1fe 100644 --- a/components/HotelReservation/BookingConfirmation/index.tsx +++ b/components/HotelReservation/BookingConfirmation/index.tsx @@ -22,9 +22,8 @@ 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/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index 8371fe31f..8c3309044 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { dt } from "@/lib/dt" -import { phoneValidator } from "@/utils/phoneValidator" +import { phoneValidator } from "@/utils/zod/phoneValidator" // stringMatcher regex is copied from current web as specified by requirements. const stringMatcher = diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx index d1aed3f1d..d4379e22b 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx @@ -31,10 +31,6 @@ import { type TrackingSDKPageData, } from "@/types/components/tracking" -function isValidHotelData(hotel: NullableHotelData): hotel is HotelData { - return hotel != null -} - export async function SelectHotelMapContainer({ searchParams, isAlternativeHotels, @@ -89,7 +85,7 @@ export async function SelectHotelMapContainer({ const [hotels] = await fetchAvailableHotelsPromise - const validHotels = hotels?.filter(isValidHotelData) || [] + const validHotels = (hotels?.filter(Boolean) as HotelData[]) || [] const hotelPins = getHotelPins(validHotels) const filterList = getFiltersFromHotels(validHotels) diff --git a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx index 9655e1c12..7af28f4dc 100644 --- a/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx +++ b/components/HotelReservation/SelectRate/HotelInfoCard/index.tsx @@ -66,7 +66,7 @@ export default async function HotelInfoCard({ { address: hotel.address.streetAddress, city: hotel.address.city, - distanceToCityCentreInKm: getSingleDecimal( + distanceToCityCenterInKm: getSingleDecimal( hotel.location.distanceToCentre / 1000 ), } diff --git a/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx b/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx index fac022eb9..e083c14a4 100644 --- a/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx +++ b/components/HotelReservation/SelectRate/SelectedRoomPanel/index.tsx @@ -12,13 +12,13 @@ import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import styles from "./selectedRoomPanel.module.css" -import type { Room } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { RoomData } from "@/types/hotel" +import type { Room as SelectedRateRoom } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Room } from "@/types/hotel" interface SelectedRoomPanelProps { roomIndex: number - room: Room - roomCategories: RoomData[] + room: SelectedRateRoom + roomCategories: Room[] } export default function SelectedRoomPanel({ diff --git a/components/TempDesignSystem/Form/NewPassword/index.tsx b/components/TempDesignSystem/Form/NewPassword/index.tsx index 8affe22e6..69cfaecc2 100644 --- a/components/TempDesignSystem/Form/NewPassword/index.tsx +++ b/components/TempDesignSystem/Form/NewPassword/index.tsx @@ -14,7 +14,7 @@ import { } from "@/components/Icons" import AriaInputWithLabel from "@/components/TempDesignSystem/Form/Input/AriaInputWithLabel" import Caption from "@/components/TempDesignSystem/Text/Caption" -import { passwordValidators } from "@/utils/passwordValidator" +import { passwordValidators } from "@/utils/zod/passwordValidator" import Button from "../../Button" import { type IconProps, type NewPasswordProps } from "./newPassword" diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 442fb5810..07dfee4e1 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -2,7 +2,7 @@ import { z } from "zod" import { ChildBedTypeEnum } from "@/constants/booking" -import { phoneValidator } from "@/utils/phoneValidator" +import { phoneValidator } from "@/utils/zod/phoneValidator" // MUTATION export const createBookingSchema = z diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index a25b7fb12..ee7130c1f 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -7,7 +7,7 @@ import { productTypeSchema } from "./schemas/availability/productType" import { citySchema } from "./schemas/city" import { attributesSchema, - includesSchema, + includedSchema, relationshipsSchema as hotelRelationshipsSchema, } from "./schemas/hotel" import { locationCitySchema } from "./schemas/location/city" @@ -18,7 +18,13 @@ import { relationshipsSchema } from "./schemas/relationships" import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration" import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition" -import type { AdditionalData, City, NearbyHotel, Restaurant, Room } from "@/types/hotel" +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 hotelSchema = z @@ -38,10 +44,13 @@ export const hotelSchema = z }), // NOTE: We can pass an "include" param to the hotel API to retrieve // additional data for an individual hotel. - included: includesSchema, + included: includedSchema, }) .transform(({ data: { attributes, ...data }, included }) => { - const additionalData = included.find((inc): inc is AdditionalData => inc!.type === "additionalData") + const additionalData = + included.find( + (inc): inc is AdditionalData => inc!.type === "additionalData" + ) ?? ({} as AdditionalData) const cities = included.filter((inc): inc is City => inc!.type === "cities") const nearbyHotels = included.filter( (inc): inc is NearbyHotel => inc!.type === "hotels" @@ -262,4 +271,4 @@ export const getNearbyHotelIdsSchema = z }) ), }) - .transform((data) => data.data.map((hotel) => hotel.id)) \ No newline at end of file + .transform((data) => data.data.map((hotel) => hotel.id)) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 52fe76898..38531c501 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -1,4 +1,7 @@ +import { unstable_cache } from "next/cache" + 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" @@ -15,6 +18,8 @@ import { cache } from "@/utils/cache" import { getHotelPageUrl } from "../contentstack/hotelPage/utils" import { getVerifiedUser, parsedUser } from "../user/query" +import { additionalDataSchema } from "./schemas/additionalData" +import { meetingRoomsSchema } from "./schemas/meetingRoom" import { breakfastPackageInputSchema, cityCoordinatesInputSchema, @@ -54,133 +59,144 @@ 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 { HotelInput } from "@/types/trpc/routers/hotel/hotel" import type { HotelData } from "@/types/hotel" import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage" -import { CityLocation } from "@/types/trpc/routers/hotel/locations" -import { meetingRoomsSchema } from "./schemas/meetingRoom" -import { env } from "@/env/server" -import { additionalDataSchema } from "./schemas/additionalData" +import type { HotelInput } from "@/types/trpc/routers/hotel/hotel" +import type { CityLocation } from "@/types/trpc/routers/hotel/locations" 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, - }) - console.info( - "api.hotels.hotelData start", - JSON.stringify({ query: { hotelId, params } }) - ) + const callable = unstable_cache( + async function ( + hotelId: HotelInput["hotelId"], + language: HotelInput["language"], + isCardOnlyPayment?: HotelInput["isCardOnlyPayment"] + ) { + /** + * Since API expects the params appended and not just + * a comma separated string we need to initialize the + * SearchParams with a sequence of pairs + * (include=City&include=NearbyHotels&include=Restaurants etc.) + **/ + const params = new URLSearchParams([ + ["include", "AdditionalData"], + ["include", "City"], + ["include", "NearbyHotels"], + ["include", "Restaurants"], + ["include", "RoomCategories"], + ["language", toApiLang(language)], + ]) + metrics.hotel.counter.add(1, { + hotelId, + language, + }) + console.info( + "api.hotels.hotelData start", + JSON.stringify({ query: { hotelId, params } }) + ) - const apiResponse = await api.get( - api.endpoints.v1.Hotel.Hotels.hotel(hotelId), - { - headers: { - Authorization: `Bearer ${serviceToken}`, - }, - // needs to clear default option as only - // cache or next.revalidate is permitted - cache: undefined, - next: { - revalidate: env.CACHE_TIME_HOTELDATA, - tags: [`${language}:hotel:${hotelId}`], - }, - }, - params - ) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - console.log({ text }) - metrics.hotel.fail.add(1, { - hotelId, - language, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), - }) - console.error( - "api.hotels.hotelData error", - JSON.stringify({ - query: { hotelId, params }, - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, + const apiResponse = await api.get( + api.endpoints.v1.Hotel.Hotels.hotel(hotelId), + { + headers: { + Authorization: `Bearer ${serviceToken}`, + }, + // needs to clear default option as only + // cache or next.revalidate is permitted + cache: undefined, + next: { + revalidate: env.CACHE_TIME_HOTELS, + tags: [`${language}:hotel:${hotelId}`], + }, }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + metrics.hotel.fail.add(1, { + hotelId, + language, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) + console.error( + "api.hotels.hotelData error", + JSON.stringify({ + query: { hotelId, params }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const validateHotelData = hotelSchema.safeParse(apiJson) + + if (!validateHotelData.success) { + metrics.hotel.fail.add(1, { + hotelId, + language, + error_type: "validation_error", + error: JSON.stringify(validateHotelData.error), + }) + + console.error( + "api.hotels.hotelData validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validateHotelData.error, + }) + ) + throw badRequestError() + } + + metrics.hotel.success.add(1, { + hotelId, + language, }) - ) - return null - } + console.info( + "api.hotels.hotelData success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + const hotelData = validateHotelData.data - const apiJson = await apiResponse.json() - const validateHotelData = hotelSchema.safeParse(apiJson) + if (isCardOnlyPayment) { + hotelData.hotel.merchantInformationData.alternatePaymentOptions = [] + } - if (!validateHotelData.success) { - metrics.hotel.fail.add(1, { - hotelId, - language, - error_type: "validation_error", - error: JSON.stringify(validateHotelData.error), - }) + const gallery = hotelData.additionalData?.gallery + if (gallery) { + const smallerImages = gallery.smallerImages + const hotelGalleryImages = + hotelData.hotel.hotelType === HotelTypeEnum.Signature + ? smallerImages.slice(0, 10) + : smallerImages.slice(0, 6) + hotelData.hotel.galleryImages = hotelGalleryImages + } - console.error( - "api.hotels.hotelData validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validateHotelData.error, - }) - ) - throw badRequestError() - } - - metrics.hotel.success.add(1, { - hotelId, - language, - }) - console.info( - "api.hotels.hotelData success", - JSON.stringify({ - query: { hotelId, params: params }, - }) + return hotelData + }, + [`${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`], + { + revalidate: env.CACHE_TIME_HOTELS, + tags: [ + `${input.language}:hotel:${input.hotelId}:${!!input.isCardOnlyPayment}`, + ], + } ) - const hotelData = validateHotelData.data - if (isCardOnlyPayment) { - hotelData.hotel.merchantInformationData.alternatePaymentOptions = [] - } - - const gallery = hotelData.additionalData?.gallery - if (gallery) { - const smallerImages = gallery.smallerImages - const hotelGalleryImages = - hotelData.hotel.hotelType === HotelTypeEnum.Signature - ? smallerImages.slice(0, 10) - : smallerImages.slice(0, 6) - hotelData.hotel.galleryImages = hotelGalleryImages - } - - return hotelData + return callable(input.hotelId, input.language, input.isCardOnlyPayment) } ) @@ -731,9 +747,9 @@ 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, } } @@ -861,7 +877,10 @@ export const hotelQueryRouter = router({ } const cityId = locations - .filter((loc): loc is CityLocation => "type" in loc && loc.type === "cities") + .filter( + (loc): loc is CityLocation => + "type" in loc && loc.type === "cities" + ) .find((loc) => loc.cityIdentifier === locationFilter.city)?.id if (!cityId) { @@ -973,7 +992,10 @@ export const hotelQueryRouter = router({ const hotels = await Promise.all( hotelsToFetch.map(async (hotelId) => { const [hotelData, url] = await Promise.all([ - getHotel({ hotelId, language }, ctx.serviceToken), + getHotel( + { hotelId, isCardOnlyPayment: false, language }, + ctx.serviceToken + ), getHotelPageUrl(language, hotelId), ]) @@ -1000,7 +1022,8 @@ export const hotelQueryRouter = router({ ) return hotels.filter( - (hotel): hotel is { data: HotelData; url: HotelPageUrl } => !!hotel.data + (hotel): hotel is { data: HotelData; url: HotelPageUrl } => + !!hotel.data ) }), }), @@ -1096,7 +1119,7 @@ export const hotelQueryRouter = router({ Authorization: `Bearer ${ctx.serviceToken}`, }, next: { - revalidate: TWENTYFOUR_HOURS, + revalidate: env.CACHE_TIME_HOTELS, }, } diff --git a/server/routers/hotels/schemas/availability/occupancy.ts b/server/routers/hotels/schemas/availability/occupancy.ts index c066008c3..c9b26924f 100644 --- a/server/routers/hotels/schemas/availability/occupancy.ts +++ b/server/routers/hotels/schemas/availability/occupancy.ts @@ -9,5 +9,5 @@ export const childrenSchema = z.object({ export const occupancySchema = z.object({ adults: z.number(), - children: z.array(childrenSchema), + children: z.array(childrenSchema).default([]), }) diff --git a/server/routers/hotels/schemas/hotel.ts b/server/routers/hotels/schemas/hotel.ts index 6e5257097..103579e5e 100644 --- a/server/routers/hotels/schemas/hotel.ts +++ b/server/routers/hotels/schemas/hotel.ts @@ -1,5 +1,7 @@ import { z } from "zod" +import { nullableNumberValidator } from "@/utils/zod/numberValidator" + import { addressSchema } from "./hotel/address" import { contactInformationSchema } from "./hotel/contactInformation" import { hotelContentSchema } from "./hotel/content" @@ -17,8 +19,8 @@ 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" +import { imageSchema } from "./image" export const attributesSchema = z.object({ accessibilityElevatorPitchText: z.string().optional(), @@ -51,13 +53,23 @@ export const attributesSchema = z.object({ socialMedia: socialMediaSchema, specialAlerts: specialAlertsSchema, specialNeedGroups: z.array(specialNeedGroupSchema), - vat: z.number(), + vat: nullableNumberValidator, }) -export const includesSchema = z +export const includedSchema = z .array(includeSchema) .default([]) - .transform((data) => data.filter((item) => !!item)) + .transform((data) => + data.filter((item) => { + if (item) { + if ("isPublished" in item && item.isPublished === false) { + return false + } + return true + } + return false + }) + ) const relationshipSchema = z.object({ links: z.object({ diff --git a/server/routers/hotels/schemas/hotel/include/restaurants.ts b/server/routers/hotels/schemas/hotel/include/restaurants.ts index 849e9fd4c..b55365bd1 100644 --- a/server/routers/hotels/schemas/hotel/include/restaurants.ts +++ b/server/routers/hotels/schemas/hotel/include/restaurants.ts @@ -2,70 +2,92 @@ import { z } from "zod" import { imageSchema } from "@/server/routers/hotels/schemas/image" +import { + nullableIntValidator, + nullableNumberValidator, +} from "@/utils/zod/numberValidator" +import { + nullableStringUrlValidator, + nullableStringValidator, +} from "@/utils/zod/stringValidator" + +import { specialAlertsSchema } from "../specialAlerts" + import { CurrencyEnum } from "@/types/enums/currency" +const descriptionSchema = z.object({ + medium: nullableStringValidator, + short: nullableStringValidator, +}) + +const textSchema = z.object({ + descriptions: descriptionSchema, + facilityInformation: nullableStringValidator, + meetingDescription: descriptionSchema.optional(), + surroundingInformation: nullableStringValidator, +}) + const contentSchema = z.object({ images: z.array(imageSchema).default([]), - texts: z.object({ - descriptions: z.object({ - medium: z.string().default(""), - short: z.string().default(""), - }), - }), + texts: textSchema, +}) + +const restaurantPriceSchema = z.object({ + amount: nullableNumberValidator, + currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.SEK), }) const externalBreakfastSchema = z.object({ isAvailable: z.boolean().default(false), - localPriceForExternalGuests: z.object({ - amount: z.number().default(0), - currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.SEK), - }), + localPriceForExternalGuests: restaurantPriceSchema.optional(), + requestedPriceForExternalGuests: restaurantPriceSchema.optional(), }) const menuItemSchema = z.object({ - name: z.string(), - url: z.string().url(), + name: nullableStringValidator, + url: nullableStringUrlValidator, }) -const daySchema = z.object({ +export const openingHoursDetailsSchema = z.object({ alwaysOpen: z.boolean().default(false), - closingTime: z.string().default(""), + closingTime: nullableStringValidator, isClosed: z.boolean().default(false), - openingTime: z.string().default(""), - sortOrder: z.number().int().default(0), + openingTime: nullableStringValidator, + sortOrder: nullableIntValidator, +}) + +export const openingHoursSchema = z.object({ + friday: openingHoursDetailsSchema.optional(), + isActive: z.boolean().default(false), + monday: openingHoursDetailsSchema.optional(), + name: nullableStringValidator, + saturday: openingHoursDetailsSchema.optional(), + sunday: openingHoursDetailsSchema.optional(), + thursday: openingHoursDetailsSchema.optional(), + tuesday: openingHoursDetailsSchema.optional(), + wednesday: openingHoursDetailsSchema.optional(), }) 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, - }), + alternateOpeningHours: openingHoursSchema.optional(), + openingHours: openingHoursSchema, + ordinary: openingHoursSchema.optional(), + weekends: openingHoursSchema.optional(), }) export const restaurantsSchema = z.object({ attributes: z.object({ - bookTableUrl: z.string().default(""), + bookTableUrl: nullableStringValidator, 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([]), + phoneNumber: z.string().optional(), restaurantPage: z.boolean().default(false), - specialAlerts: z.array(z.object({})).default([]), + specialAlerts: specialAlertsSchema, }), id: z.string(), type: z.literal("restaurants"), diff --git a/server/routers/hotels/schemas/hotel/specialAlerts.ts b/server/routers/hotels/schemas/hotel/specialAlerts.ts index 8687a81a6..5d89fe936 100644 --- a/server/routers/hotels/schemas/hotel/specialAlerts.ts +++ b/server/routers/hotels/schemas/hotel/specialAlerts.ts @@ -2,15 +2,17 @@ import { z } from "zod" import { dt } from "@/lib/dt" +import { nullableStringValidator } from "@/utils/zod/stringValidator" + 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(), + description: nullableStringValidator, + displayInBookingFlow: z.boolean().default(false), + endDate: nullableStringValidator, + startDate: nullableStringValidator, + title: nullableStringValidator, + type: nullableStringValidator, }) export const specialAlertsSchema = z diff --git a/server/routers/hotels/schemas/roomAvailability/configuration.ts b/server/routers/hotels/schemas/roomAvailability/configuration.ts index 4d433917b..128512d7f 100644 --- a/server/routers/hotels/schemas/roomAvailability/configuration.ts +++ b/server/routers/hotels/schemas/roomAvailability/configuration.ts @@ -5,17 +5,19 @@ 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), + 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([]), roomsLeft: z.number(), roomType: z.string(), roomTypeCode: z.string(), diff --git a/stores/select-rate/rate-selection.ts b/stores/select-rate/rate-selection.ts index cd156a392..55b2bbcfd 100644 --- a/stores/select-rate/rate-selection.ts +++ b/stores/select-rate/rate-selection.ts @@ -4,17 +4,17 @@ import { calculateRoomSummary } from "./helper" import type { RoomPackageCodeEnum, - RoomPackageData, + RoomPackages, } from "@/types/components/hotelReservation/selectRate/roomFilter" import type { Rate, RateCode, } from "@/types/components/hotelReservation/selectRate/selectRate" -import type { RoomConfiguration } from "@/server/routers/hotels/output" +import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability" export interface RateSummaryParams { getFilteredRooms: (roomIndex: number) => RoomConfiguration[] - availablePackages: RoomPackageData + availablePackages: RoomPackages roomCategories: Array<{ name: string; roomTypes: Array<{ code: string }> }> selectedPackagesByRoom: Record } diff --git a/stores/select-rate/room-filtering.ts b/stores/select-rate/room-filtering.ts index 9454ce692..6d6ef3556 100644 --- a/stores/select-rate/room-filtering.ts +++ b/stores/select-rate/room-filtering.ts @@ -10,7 +10,7 @@ import type { import type { RoomConfiguration, RoomsAvailability, -} from "@/server/routers/hotels/output" +} from "@/types/trpc/routers/hotel/roomAvailability" interface RoomFilteringState { selectedPackagesByRoom: Record diff --git a/types/components/form/newPassword.ts b/types/components/form/newPassword.ts index cd6aa0c50..a3cbe91da 100644 --- a/types/components/form/newPassword.ts +++ b/types/components/form/newPassword.ts @@ -1,3 +1,3 @@ -import { passwordValidators } from "@/utils/passwordValidator" +import type { passwordValidators } from "@/utils/zod/passwordValidator" export type PasswordValidatorKey = keyof typeof passwordValidators diff --git a/types/components/hotelPage/sidepeek/amenities.ts b/types/components/hotelPage/sidepeek/amenities.ts index e85b9ad19..1f591741e 100644 --- a/types/components/hotelPage/sidepeek/amenities.ts +++ b/types/components/hotelPage/sidepeek/amenities.ts @@ -1,9 +1,5 @@ -import type { - Hotel, - Restaurant, - RestaurantOpeningHours, -} from "@/types/hotel" import type { ParkingAmenityProps } from "./parking" +import type { Hotel, Restaurant, RestaurantOpeningHours } from "@/types/hotel" export type AmenitiesSidePeekProps = { amenitiesList: Hotel["detailedFacilities"] diff --git a/types/components/hotelReservation/selectRate/roomCard.ts b/types/components/hotelReservation/selectRate/roomCard.ts index 1b3366d52..c38c0b62c 100644 --- a/types/components/hotelReservation/selectRate/roomCard.ts +++ b/types/components/hotelReservation/selectRate/roomCard.ts @@ -8,7 +8,6 @@ import type { import type { packagePriceSchema } from "@/server/routers/hotels/schemas/packages" import type { RoomPriceSchema } from "./flexibilityOption" import type { RoomPackageCodes, RoomPackages } from "./roomFilter" -import type { RateCode } from "./selectRate" export type RoomCardProps = { hotelId: string @@ -19,7 +18,6 @@ export type RoomCardProps = { selectedPackages: RoomPackageCodes[] roomListIndex: number packages: RoomPackages | undefined - handleSelectRate: React.Dispatch> } type RoomPackagePriceSchema = z.output diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index cdae49d50..3f3512ea7 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -1,17 +1,15 @@ +import type { Room } from "@/types/hotel" +import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" import type { DefaultFilterOptions, - RoomPackage, RoomPackageCodes, RoomPackages, } from "./roomFilter" -import type { Room } from "@/types/hotel" -import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability" - export interface RoomTypeListProps { roomsAvailability: RoomsAvailability roomCategories: Room[] - availablePackages: RoomPackage | undefined + availablePackages: RoomPackages | undefined selectedPackages: RoomPackageCodes[] hotelType: string | undefined roomListIndex: number @@ -27,7 +25,7 @@ export interface SelectRateProps { export interface RoomSelectionPanelProps { roomCategories: Room[] - availablePackages: RoomPackage[] + availablePackages: RoomPackages selectedPackages: RoomPackageCodes[] hotelType: string | undefined defaultPackages: DefaultFilterOptions[] diff --git a/types/enums/currency.ts b/types/enums/currency.ts new file mode 100644 index 000000000..580041361 --- /dev/null +++ b/types/enums/currency.ts @@ -0,0 +1,8 @@ +export enum CurrencyEnum { + DKK = "DKK", + EUR = "EUR", + NOK = "NOK", + PLN = "PLN", + SEK = "SEK", + Unknown = "Unknown", +} diff --git a/types/hotel.ts b/types/hotel.ts index 3e46ec5b4..6a755e99c 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -7,18 +7,20 @@ import type { addressSchema } from "@/server/routers/hotels/schemas/hotel/addres 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 { healthFacilitySchema } from "@/server/routers/hotels/schemas/hotel/healthFacilities" import type { nearbyHotelsSchema } from "@/server/routers/hotels/schemas/hotel/include/nearbyHotels" -import type { restaurantsSchema } from "@/server/routers/hotels/schemas/hotel/include/restaurants" +import type { + openingHoursDetailsSchema, + openingHoursSchema, + 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, @@ -45,9 +47,12 @@ export type NearbyHotel = Pick & 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 RestaurantOpeningHours = z.output +export type RestaurantOpeningHoursDay = z.output< + typeof openingHoursDetailsSchema +> export type Room = ReturnType export type HotelMapContentProps = { diff --git a/types/trpc/routers/hotel/availability.ts b/types/trpc/routers/hotel/availability.ts index 90f1ccb07..2aaba9363 100644 --- a/types/trpc/routers/hotel/availability.ts +++ b/types/trpc/routers/hotel/availability.ts @@ -7,3 +7,6 @@ import type { productTypePriceSchema } from "@/server/routers/hotels/schemas/pro export type HotelsAvailability = z.output export type ProductType = z.output export type ProductTypePrices = z.output + +export type HotelsAvailabilityItem = + HotelsAvailability["data"][number]["attributes"] diff --git a/utils/zod/numberValidator.ts b/utils/zod/numberValidator.ts new file mode 100644 index 000000000..7b6927bc9 --- /dev/null +++ b/utils/zod/numberValidator.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const nullableNumberValidator = z + .number() + .nullish() + .transform((num) => (typeof num === "number" ? num : 0)) + +export const nullableIntValidator = z + .number() + .int() + .nullish() + .transform((num) => (typeof num === "number" ? num : 0)) diff --git a/utils/passwordValidator.ts b/utils/zod/passwordValidator.ts similarity index 100% rename from utils/passwordValidator.ts rename to utils/zod/passwordValidator.ts diff --git a/utils/phoneValidator.ts b/utils/zod/phoneValidator.ts similarity index 100% rename from utils/phoneValidator.ts rename to utils/zod/phoneValidator.ts diff --git a/utils/zod/stringValidator.ts b/utils/zod/stringValidator.ts new file mode 100644 index 000000000..f7adfca42 --- /dev/null +++ b/utils/zod/stringValidator.ts @@ -0,0 +1,12 @@ +import { z } from "zod" + +export const nullableStringValidator = z + .string() + .nullish() + .transform((str) => (str ? str : "")) + +export const nullableStringUrlValidator = z + .string() + .url() + .nullish() + .transform((str) => (str ? str : ""))