diff --git a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx index 63107fc79..2dd221915 100644 --- a/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx +++ b/apps/scandic-web/app/[lang]/(live)/(public)/hotelreservation/(standard)/details/page.tsx @@ -1,6 +1,7 @@ import { notFound, redirect } from "next/navigation" import { Suspense } from "react" +import { REDEMPTION } from "@/constants/booking" import { selectRate } from "@/constants/routes/hotelReservation" import { getBreakfastPackages, @@ -83,6 +84,7 @@ export default async function DetailsPage({ roomStayEndDate: booking.toDate, roomStayStartDate: booking.fromDate, roomTypeCode: room.roomTypeCode, + redemption: booking.searchType === REDEMPTION, }) if (!roomAvailability) { @@ -107,6 +109,7 @@ export default async function DetailsPage({ roomRate: { memberRate: roomAvailability?.memberRate, publicRate: roomAvailability.publicRate, + redemptionRate: roomAvailability.redemptionRate, }, isAvailable: roomAvailability.selectedRoom.status === AvailabilityEnum.Available, diff --git a/apps/scandic-web/components/Forms/BookingWidget/FormContent/Voucher/index.tsx b/apps/scandic-web/components/Forms/BookingWidget/FormContent/Voucher/index.tsx index cd90bccfc..4d92f30ba 100644 --- a/apps/scandic-web/components/Forms/BookingWidget/FormContent/Voucher/index.tsx +++ b/apps/scandic-web/components/Forms/BookingWidget/FormContent/Voucher/index.tsx @@ -30,9 +30,7 @@ export default function Voucher() { return (
-
- -
+
{env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? ( <> @@ -81,7 +79,6 @@ export function VoucherSkeleton() { const intl = useIntl() const vouchers = intl.formatMessage({ id: "Code / Voucher" }) - const bonus = intl.formatMessage({ id: "Use Bonus Cheque" }) const reward = intl.formatMessage({ id: "Book Reward Night" }) const form = useForm() @@ -89,7 +86,7 @@ export function VoucherSkeleton() { return (
-
+
- {env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE ? null : ( -
- - - {bonus} - - -
- )}
- - - {reward} - - + + + {reward} +
diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx index 57c4ab566..5f976012c 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/Mobile/BottomSheet/index.tsx @@ -9,7 +9,7 @@ import { formId } from "@/components/HotelReservation/EnterDetails/Payment/Payme import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { formatPrice } from "@/utils/numberFormatting" +import { formatPriceWithAdditionalPrice } from "@/utils/numberFormatting" import styles from "./bottomSheet.module.css" @@ -57,10 +57,12 @@ export default function SummaryBottomSheet({ children }: PropsWithChildren) { > {intl.formatMessage({ id: "Total price" })} - {formatPrice( + {formatPriceWithAdditionalPrice( intl, totalPrice.local.price, - totalPrice.local.currency + totalPrice.local.currency, + totalPrice.local.additionalPrice, + totalPrice.local.additionalPriceCurrency )} diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx index 23da5c80a..dc78bc4e8 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -20,7 +20,10 @@ import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import useLang from "@/hooks/useLang" -import { formatPrice } from "@/utils/numberFormatting" +import { + formatPrice, + formatPriceWithAdditionalPrice, +} from "@/utils/numberFormatting" import PriceDetailsTable from "./PriceDetailsTable" @@ -170,10 +173,12 @@ export default function SummaryUI({ memberPrice.amount, memberPrice.currency ) - : formatPrice( + : formatPriceWithAdditionalPrice( intl, room.roomPrice.perStay.local.price, - room.roomPrice.perStay.local.currency + room.roomPrice.perStay.local.currency, + room.roomPrice.perStay.local.additionalPrice, + room.roomPrice.perStay.local.additionalPriceCurrency )}
@@ -383,10 +388,12 @@ export default function SummaryUI({
- {formatPrice( + {formatPriceWithAdditionalPrice( intl, totalPrice.local.price, - totalPrice.local.currency + totalPrice.local.currency, + totalPrice.local.additionalPrice, + totalPrice.local.additionalPriceCurrency )} {totalPrice.local.regularPrice ? ( diff --git a/apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsCard/hotelPointsCard.module.css b/apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsRow/hotelPointsRow.module.css similarity index 100% rename from apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsCard/hotelPointsCard.module.css rename to apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsRow/hotelPointsRow.module.css diff --git a/apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsCard/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsRow/index.tsx similarity index 56% rename from apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsCard/index.tsx rename to apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsRow/index.tsx index b07ec2402..47cc0d524 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCard/HotelPointsRow/index.tsx @@ -3,17 +3,16 @@ import { useIntl } from "react-intl" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import styles from "./hotelPointsCard.module.css" +import styles from "./hotelPointsRow.module.css" -import type { PointsCardProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps" +import type { PointsRowProps } from "@/types/components/hotelReservation/selectHotel/priceCardProps" -export default function HotelPointsCard({ - productTypePoints, - redemptionPrice, -}: PointsCardProps) { +export default function HotelPointsRow({ + pointsPerStay, + additionalPricePerStay, + additionalPriceCurrency, +}: PointsRowProps) { const intl = useIntl() - const pointsPerStay = - productTypePoints?.localPrice.pointsPerStay ?? redemptionPrice return (
@@ -23,14 +22,14 @@ export default function HotelPointsCard({ {intl.formatMessage({ id: "Points" })} - {productTypePoints?.localPrice.pricePerStay ? ( + {additionalPricePerStay ? ( <> + - {productTypePoints.localPrice.pricePerStay} + {additionalPricePerStay} - {productTypePoints.localPrice.currency} + {additionalPriceCurrency} ) : null} diff --git a/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx index d8486a428..3c9271992 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx @@ -22,7 +22,7 @@ import { getSingleDecimal } from "@/utils/numberFormatting" import ReadMore from "../ReadMore" import TripAdvisorChip from "../TripAdvisorChip" -import HotelPointsCard from "./HotelPointsCard" +import HotelPointsRow from "./HotelPointsRow" import HotelPriceCard from "./HotelPriceCard" import NoPriceAvailableCard from "./NoPriceAvailableCard" import { hotelCardVariants } from "./variants" @@ -154,9 +154,7 @@ function HotelCard({ ) : ( <> {bookingCode && ( - + {bookingCode} @@ -173,21 +171,23 @@ function HotelCard({ isMemberPrice /> )} - {price?.redemption && ( + {!!price?.redemptions?.length && (
{intl.formatMessage({ id: "Available rates" })} - {/* Display rate with full points option */} - - {/* Display rate with partial points option A */} - {price.redemptionA && ( - - )} - {/* Display rate with partial points option B */} - {price.redemptionB && ( - - )} + {price.redemptions.map((redemption) => ( + + ))}
)}
diff --git a/apps/scandic-web/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx index dc828732b..93f14cda5 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx @@ -13,7 +13,7 @@ import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import { isValidClientSession } from "@/utils/clientSession" -import HotelPointsCard from "../../HotelCard/HotelPointsCard" +import HotelPointsRow from "../../HotelCard/HotelPointsRow" import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard" import HotelCardDialogImage from "../HotelCardDialogImage" @@ -132,7 +132,7 @@ export default function StandaloneHotelCardDialog({ )} {redemptionPrice && ( - + )}
} - title={rateTitle ? rateTitle : title} - subtitle={rateTitle ? `${title} (${paymentTerm})` : paymentTerm} + title={rateName ? rateName : title} + subtitle={rateName ? `${title} (${paymentTerm})` : paymentTerm} >
{priceInformation?.map((info) => ( @@ -150,7 +150,7 @@ export default function FlexibilityOption({ memberPrice={product.member} petRoomPackage={petRoomPackage} publicPrice={product.public} - rateTitle={rateTitle} + rateName={rateName} />
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/PointsList/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/PointsList/index.tsx new file mode 100644 index 000000000..00b487028 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/PointsList/index.tsx @@ -0,0 +1,53 @@ +import { useIntl } from "react-intl" + +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import { formatPrice } from "@/utils/numberFormatting" + +import styles from "./pointsList.module.css" + +import type { ProductTypePoints } from "@/types/trpc/routers/hotel/availability" +import type { Product } from "@/types/trpc/routers/hotel/roomAvailability" + +export default function PointsList({ + product, + handleSelect, + redemptions, +}: { + product: Product + handleSelect: (product: Product, selectedRateCode: string) => void + redemptions: ProductTypePoints[] +}) { + const intl = useIntl() + + return ( +
+ {redemptions.map((redemption) => ( + + ))} +
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/PointsList/pointsList.module.css b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/PointsList/pointsList.module.css new file mode 100644 index 000000000..2e69c55f7 --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/PointsList/pointsList.module.css @@ -0,0 +1,10 @@ +.pointsList { + margin: 0; +} + +.pointsRow { + display: flex; + align-items: baseline; + justify-content: flex-start; + gap: var(--Spacing-x-half); +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/index.tsx new file mode 100644 index 000000000..96bb5500b --- /dev/null +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/FlexibilityOptionPoints/index.tsx @@ -0,0 +1,125 @@ +"use client" +import { useIntl } from "react-intl" + +import { CheckIcon, InfoCircleIcon } from "@/components/Icons" +import Modal from "@/components/Modal" +import Button from "@/components/TempDesignSystem/Button" +import Label from "@/components/TempDesignSystem/Form/Label" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import { useRoomContext } from "@/contexts/SelectRate/Room" + +import PointsList from "./PointsList" + +import styles from "../FlexibilityOption/flexibilityOption.module.css" + +import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" +import type { Product } from "@/types/trpc/routers/hotel/roomAvailability" + +export default function FlexibilityOptionPoints({ + features, + paymentTerm, + priceInformation, + product, + roomType, + roomTypeCode, + title, + // Reward night rate tile obtianed from the ratedefinition + rateName, +}: FlexibilityOptionProps) { + const intl = useIntl() + const rewardNightTitle = + rateName ?? intl.formatMessage({ id: "Reward night" }) + const { + actions: { selectRateRedemption }, + } = useRoomContext() + + if (!product?.redemptions?.length) { + return ( +
+
+ +
+ {title} + ({paymentTerm}) +
+
+ +
+ ) + } + + function handleSelect(product: Product, selectedRateCode?: string) { + selectRateRedemption( + { + features, + product, + roomType, + roomTypeCode, + }, + selectedRateCode + ) + } + + return ( +
+
+ + {rewardNightTitle} + {" ∙ "} + {intl.formatMessage({ id: "Breakfast included" })} + +
+
+
+ + + + } + title={rewardNightTitle} + subtitle={`${title} ${paymentTerm}`} + > + {priceInformation?.length ? ( +
+ {priceInformation.map((info) => ( + + + {info} + + ))} +
+ ) : null} +
+
+ {title} + ({paymentTerm}) +
+
+ +
+
+ ) +} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx index f4f66bf79..f495927db 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/Rooms/RoomSelectionPanel/RoomCard/index.tsx @@ -4,6 +4,7 @@ import { useSearchParams } from "next/navigation" import { createElement } from "react" import { useIntl } from "react-intl" +import { REDEMPTION } from "@/constants/booking" import { useRatesStore } from "@/stores/select-rate" import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek" @@ -18,6 +19,7 @@ import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { cardVariants } from "./cardVariants" import FlexibilityOption from "./FlexibilityOption" +import FlexibilityOptionPoints from "./FlexibilityOptionPoints" import RoomSize from "./RoomSize" import styles from "./roomCard.module.css" @@ -72,6 +74,7 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { const searchParams = useSearchParams() const bookingCode = searchParams.get("bookingCode") + const isRedemption = searchParams.get("searchtype") === REDEMPTION const { hotelId, hotelType, isUserLoggedIn, petRoomPackage, roomCategories } = useRatesStore((state) => ({ @@ -152,6 +155,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { * Get terms and rate title from the rate definitions when booking code rate * or public promotion is in play. Returns undefined when product is not available * + * In case of redemption it will always return first redemption as terms + * and title are same for all various redemption rates + * * @param product - Either public or member product type * @param rateDefinitions - List of rate definitions * @returns RateDefinition | undefined @@ -160,10 +166,18 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { product: Product, rateDefinitions: RateDefinition[] ) { - return rateDefinitions.find((rateDefinition) => - isUserLoggedIn && product.member && isMainRoom - ? rateDefinition.rateCode === product.member?.rateCode - : rateDefinition.rateCode === product.public?.rateCode + let rateCode = "" + if (isUserLoggedIn && product.member && isMainRoom) { + rateCode = product.member.rateCode + } else if (product.public?.rateCode) { + rateCode = product.public.rateCode + } else if (product.redemptions?.length) { + // In case of redemption there will be same rate terms and title + // irrespective of ratecodes + return rateDefinitions[0] + } + return rateDefinitions.find( + (rateDefinition) => rateDefinition.rateCode === rateCode ) } @@ -263,7 +277,9 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { ) : ( <> - {breakfastMessage} + {isRedemption ? null : ( + {breakfastMessage} + )} {bookingCode ? ( @@ -275,29 +291,30 @@ export default function RoomCard({ roomConfiguration }: RoomCardProps) { const rateTitle = getRateTitle(product.rate) const isAvailable = product.public || - (product.member && isUserLoggedIn && isMainRoom) + (product.member && isUserLoggedIn && isMainRoom) || + product.redemptions?.length const rateDefinition = getRateDefinition( product, roomAvailability.rateDefinitions ) - return ( - + const props = { + features: roomConfiguration.features, + paymentTerm: product.isFlex ? payLater : payNow, + petRoomPackage: petRoomPackageSelected, + priceInformation: rateDefinition?.generalTerms, + product: isAvailable ? product : undefined, + roomType: roomConfiguration.roomType, + roomTypeCode: roomConfiguration.roomTypeCode, + title: rateTitle, + rateName: + isBookingCodeRate || isRedemption + ? rateDefinition?.title + : undefined, + } + return isRedemption ? ( + + ) : ( + ) })} diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx index 42da1c5a6..2ccecc808 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/index.tsx @@ -1,4 +1,5 @@ "use client" +import { REDEMPTION } from "@/constants/booking" import { dt } from "@/lib/dt" import useLang from "@/hooks/useLang" @@ -24,6 +25,9 @@ export function RoomsContainer({ const fromDateString = dt(fromDate).format("YYYY-MM-DD") const toDateString = dt(toDate).format("YYYY-MM-DD") + const redemption = booking.searchType + ? booking.searchType === REDEMPTION + : undefined const { data: roomsAvailability, isPending: isLoadingAvailability } = useRoomsAvailability( @@ -33,7 +37,8 @@ export function RoomsContainer({ toDateString, lang, childArray, - booking.bookingCode + booking.bookingCode, + redemption ) const { data: packages, isPending: isLoadingPackages } = useHotelPackages( diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts index 8d073bed2..8a8672f50 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/utils.ts @@ -11,9 +11,10 @@ export function useRoomsAvailability( toDateString: string, lang: Lang, childArray: ChildrenInRoom, - bookingCode?: string + bookingCode?: string, + redemption?: boolean ) { - return trpc.hotel.availability.roomsCombinedAvailability.useQuery({ + const params = { adultsCount, bookingCode, childArray, @@ -21,7 +22,17 @@ export function useRoomsAvailability( lang, roomStayEndDate: toDateString, roomStayStartDate: fromDateString, - }) + redemption, + } + + const roomsAvailability = redemption + ? trpc.hotel.availability.roomsCombinedAvailabilityWithRedemption.useQuery( + params + ) + : trpc.hotel.availability.roomsCombinedAvailability.useQuery(params) + + + return roomsAvailability } export function useHotelPackages( diff --git a/apps/scandic-web/i18n/dictionaries/da.json b/apps/scandic-web/i18n/dictionaries/da.json index 88e9fc22d..9df9985ec 100644 --- a/apps/scandic-web/i18n/dictionaries/da.json +++ b/apps/scandic-web/i18n/dictionaries/da.json @@ -648,6 +648,7 @@ "Restaurants": "Restauranter", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Gentag den nye adgangskode", + "Reward night": "Bonusnat", "Room": "Værelse", "Room & Terms": "Værelse & Vilkår", "Room amenities": "Værelsesfaciliteter", diff --git a/apps/scandic-web/i18n/dictionaries/de.json b/apps/scandic-web/i18n/dictionaries/de.json index bfcc9db4a..367f73c07 100644 --- a/apps/scandic-web/i18n/dictionaries/de.json +++ b/apps/scandic-web/i18n/dictionaries/de.json @@ -647,6 +647,7 @@ "Restaurants": "Restaurants", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Neues Passwort erneut eingeben", + "Reward night": "Bonusnacht", "Room": "Zimmer", "Room & Terms": "Zimmer & Bedingungen", "Room amenities": "Zimmerausstattung", diff --git a/apps/scandic-web/i18n/dictionaries/en.json b/apps/scandic-web/i18n/dictionaries/en.json index 4120ce09a..7e596f98c 100644 --- a/apps/scandic-web/i18n/dictionaries/en.json +++ b/apps/scandic-web/i18n/dictionaries/en.json @@ -646,6 +646,7 @@ "Restaurants": "Restaurants", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Retype new password", + "Reward night": "Reward night", "Room": "Room", "Room & Terms": "Room & Terms", "Room amenities": "Room amenities", diff --git a/apps/scandic-web/i18n/dictionaries/fi.json b/apps/scandic-web/i18n/dictionaries/fi.json index f78e7db81..3f94e41d7 100644 --- a/apps/scandic-web/i18n/dictionaries/fi.json +++ b/apps/scandic-web/i18n/dictionaries/fi.json @@ -646,6 +646,7 @@ "Restaurants": "Ravintolat", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Kirjoita uusi salasana uudelleen", + "Reward night": "Palkintoyö", "Room": "Huone", "Room & Terms": "Huone & Ehdot", "Room amenities": "Huoneen mukavuudet", diff --git a/apps/scandic-web/i18n/dictionaries/no.json b/apps/scandic-web/i18n/dictionaries/no.json index c7d07eaa7..677cc9eb9 100644 --- a/apps/scandic-web/i18n/dictionaries/no.json +++ b/apps/scandic-web/i18n/dictionaries/no.json @@ -645,6 +645,7 @@ "Restaurants": "Restauranter", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Skriv inn nytt passord på nytt", + "Reward night": "Bonusnatt", "Room": "Rom", "Room & Terms": "Rom & Vilkår", "Room amenities": "Romfasiliteter", diff --git a/apps/scandic-web/i18n/dictionaries/sv.json b/apps/scandic-web/i18n/dictionaries/sv.json index 53269c162..b6a478f8c 100644 --- a/apps/scandic-web/i18n/dictionaries/sv.json +++ b/apps/scandic-web/i18n/dictionaries/sv.json @@ -645,6 +645,7 @@ "Restaurants": "Restauranger", "Restaurants & Bars": "Restaurants & Bars", "Retype new password": "Upprepa nytt lösenord", + "Reward night": "Bonusnatt", "Room": "Rum", "Room & Terms": "Rum & Villkor", "Room amenities": "Bekvämligheter på rummet", diff --git a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts index b56916508..0c6f051c2 100644 --- a/apps/scandic-web/lib/trpc/memoizedRequests/index.ts +++ b/apps/scandic-web/lib/trpc/memoizedRequests/index.ts @@ -88,7 +88,11 @@ export const getSelectedRoomAvailability = cache( function getMemoizedSelectedRoomAvailability( input: GetSelectedRoomAvailabilityInput ) { - return serverClient().hotel.availability.room(input) + if (input.redemption) { + return serverClient().hotel.availability.roomWithRedemption(input) + } else { + return serverClient().hotel.availability.room(input) + } } ) diff --git a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx index 1c00eab66..6aee36644 100644 --- a/apps/scandic-web/providers/SelectRate/RoomProvider.tsx +++ b/apps/scandic-web/providers/SelectRate/RoomProvider.tsx @@ -19,6 +19,9 @@ export default function RoomProvider({ ) const selectFilter = useRatesStore((state) => state.actions.selectFilter(idx)) const selectRate = useRatesStore((state) => state.actions.selectRate(idx)) + const selectRateRedemption = useRatesStore((state) => + state.actions.selectRateRedemption(idx) + ) const roomNr = idx + 1 return ( { - const rateDefinitions = attributes.rateDefinitions - const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => { - // @ts-expect-error - index of cancellationRule TS - acc[val.rateCode] = cancellationRules[val.cancellationRule] - return acc - }, {}) - const roomConfigurations = attributes.roomConfigurations - .map((room) => { - if (room.products.length) { - room.breakfastIncludedInAllRatesMember = room.products.every( - (product) => - everyRateHasBreakfastIncluded(product, rateDefinitions, "member") - ) - room.breakfastIncludedInAllRatesPublic = room.products.every( - (product) => - everyRateHasBreakfastIncluded(product, rateDefinitions, "public") - ) +function transformRoomConfigs({ + data: { attributes }, +}: typeof baseRoomsCombinedAvailabilitySchema._type) { + const rateDefinitions = attributes.rateDefinitions + const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => { + // @ts-expect-error - index of cancellationRule TS + acc[val.rateCode] = cancellationRules[val.cancellationRule] + return acc + }, {}) - room.products = room.products.map((product) => { - const publicRate = product.public - if (publicRate?.rateCode) { - const publicRateDefinition = rateDefinitions.find( - (rateDefinition) => - rateDefinition.rateCode === publicRate.rateCode - ) - if (publicRateDefinition) { - const rate = getRate(publicRateDefinition) - if (rate) { - product.rate = rate - if (rate === "flex") { - product.isFlex = true - } + const roomConfigurations = attributes.roomConfigurations + .map((room) => { + if (room.products.length) { + room.breakfastIncludedInAllRatesMember = room.products.every( + (product) => + everyRateHasBreakfastIncluded(product, rateDefinitions, "member") + ) + room.breakfastIncludedInAllRatesPublic = room.products.every( + (product) => + everyRateHasBreakfastIncluded(product, rateDefinitions, "public") + ) + + room.products = room.products.map((product) => { + const publicRate = product.public + if (publicRate?.rateCode) { + const publicRateDefinition = rateDefinitions.find( + (rateDefinition) => + rateDefinition.rateCode === publicRate.rateCode + ) + if (publicRateDefinition) { + const rate = getRate(publicRateDefinition) + if (rate) { + product.rate = rate + if (rate === "flex") { + product.isFlex = true } } } + } - const memberRate = product.member - if (memberRate?.rateCode) { - const memberRateDefinition = rateDefinitions.find( - (rate) => rate.rateCode === memberRate.rateCode - ) - if (memberRateDefinition) { - const rate = getRate(memberRateDefinition) - if (rate) { - product.rate = rate - if (rate === "flex") { - product.isFlex = true - } + const memberRate = product.member + if (memberRate?.rateCode) { + const memberRateDefinition = rateDefinitions.find( + (rate) => rate.rateCode === memberRate.rateCode + ) + if (memberRateDefinition) { + const rate = getRate(memberRateDefinition) + if (rate) { + product.rate = rate + if (rate === "flex") { + product.isFlex = true } } } + } - return product - }) + return product + }) - // CancellationRule is the same for public and member per product - // Sorting to guarantee order based on rate - room.products = room.products.sort( - (a, b) => - // @ts-expect-error - index - cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] - - // @ts-expect-error - index - cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode] - ) - } + // CancellationRule is the same for public and member per product + // Sorting to guarantee order based on rate + room.products = room.products.sort( + (a, b) => + // @ts-expect-error - index + cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] - + // @ts-expect-error - index + cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode] + ) + } - return room - }) - .sort(sortRoomConfigs) + return room + }) + .sort(sortRoomConfigs) - return { - ...attributes, - roomConfigurations, - } - }) + return { + ...attributes, + roomConfigurations, + } +} export const roomsAvailabilitySchema = z .object({ @@ -298,85 +301,30 @@ export const roomsAvailabilitySchema = z type: z.string().optional(), }), }) - .transform(({ data: { attributes } }) => { - const rateDefinitions = attributes.rateDefinitions - const cancellationRuleLookup = rateDefinitions.reduce((acc, val) => { - // @ts-expect-error - index of cancellationRule TS - acc[val.rateCode] = cancellationRules[val.cancellationRule] - return acc - }, {}) + .transform(transformRoomConfigs) - const roomConfigurations = attributes.roomConfigurations - .map((room) => { - if (room.products.length) { - room.breakfastIncludedInAllRatesMember = room.products.every( - (product) => - everyRateHasBreakfastIncluded(product, rateDefinitions, "member") - ) - room.breakfastIncludedInAllRatesPublic = room.products.every( - (product) => - everyRateHasBreakfastIncluded(product, rateDefinitions, "public") - ) +export const roomsCombinedAvailabilitySchema = + baseRoomsCombinedAvailabilitySchema.transform(transformRoomConfigs) +export const redemptionRoomsCombinedAvailabilitySchema = + baseRoomsCombinedAvailabilitySchema.transform((data) => { + // In Redemption, rates are always Flex terms + data.data.attributes.roomConfigurations = + data.data.attributes.roomConfigurations + .map((room) => { room.products = room.products.map((product) => { - const publicRate = product.public - if (publicRate?.rateCode) { - const publicRateDefinition = rateDefinitions.find( - (rateDefinition) => - rateDefinition.rateCode === publicRate.rateCode - ) - if (publicRateDefinition) { - const rate = getRate(publicRateDefinition) - if (rate) { - product.rate = rate - if (rate === "flex") { - product.isFlex = true - } - } - } - } - - const memberRate = product.member - if (memberRate?.rateCode) { - const memberRateDefinition = rateDefinitions.find( - (rate) => rate.rateCode === memberRate.rateCode - ) - if (memberRateDefinition) { - const rate = getRate(memberRateDefinition) - if (rate) { - product.rate = rate - if (rate === "flex") { - product.isFlex = true - } - } - } - } - + product.rate = "flex" + product.isFlex = true return product }) + return room + }) + .sort( + // @ts-expect-error - array indexing + (a, b) => statusLookup[a.status] - statusLookup[b.status] + ) - // CancellationRule is the same for public and member per product - // Sorting to guarantee order based on rate - room.products = room.products.sort( - (a, b) => - // @ts-expect-error - index - cancellationRuleLookup[a.public?.rateCode || a.member?.rateCode] - - // @ts-expect-error - index - cancellationRuleLookup[b.public?.rateCode || b.member?.rateCode] - ) - } - - return room - }) - .sort( - // @ts-expect-error - array indexing - (a, b) => statusLookup[a.status] - statusLookup[b.status] - ) - - return { - ...attributes, - roomConfigurations, - } + return transformRoomConfigs(data) }) export const ratesSchema = z.array(rateSchema) diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts index b6a724dc1..9f04e0df6 100644 --- a/apps/scandic-web/server/routers/hotels/query.ts +++ b/apps/scandic-web/server/routers/hotels/query.ts @@ -6,6 +6,7 @@ import { badRequestError } from "@/server/errors/trpc" import { contentStackBaseWithServiceProcedure, protectedProcedure, + protectedServcieProcedure, publicProcedure, router, safeProtectedServiceProcedure, @@ -50,6 +51,7 @@ import { hotelSchema, packagesSchema, ratesSchema, + redemptionRoomsCombinedAvailabilitySchema, roomsAvailabilitySchema, roomsCombinedAvailabilitySchema, } from "./output" @@ -72,9 +74,12 @@ import type { HotelDataWithUrl } from "@/types/hotel" import type { HotelsAvailabilityInputSchema, HotelsByHotelIdsAvailabilityInputSchema, + RoomsCombinedAvailabilityInputSchema, + SelectedRoomAvailabilitySchema, } from "@/types/trpc/routers/hotel/availability" import type { HotelInput } from "@/types/trpc/routers/hotel/hotel" import type { CityLocation } from "@/types/trpc/routers/hotel/locations" +import type { Lang } from "@/constants/routes/hotelReservation" export const getHotel = cache( async (input: HotelInput, serviceToken: string) => { @@ -467,6 +472,346 @@ export const getHotelsAvailabilityByHotelIds = async ( ) } +async function getRoomsCombinedAvailability( + input: RoomsCombinedAvailabilityInputSchema, + token: string // Either service token or user access token in case of redemption search +) { + const { lang } = input + const apiLang = toApiLang(lang) + const { + adultsCount, + bookingCode, + childArray, + hotelId, + rateCode, + roomStayEndDate, + roomStayStartDate, + redemption, + } = input + + const metricsData = { + hotelId, + roomStayStartDate, + roomStayEndDate, + adultsCount, + childArray: childArray ? JSON.stringify(childArray) : undefined, + bookingCode, + } + + metrics.roomsCombinedAvailability.counter.add(1, metricsData) + + console.info( + "api.hotels.roomsCombinedAvailability start", + JSON.stringify({ query: { hotelId, params: metricsData } }) + ) + + const availabilityResponses = await Promise.allSettled( + adultsCount.map(async (adultCount: number, idx: number) => { + const kids = childArray?.[idx] + const params: Record = { + roomStayStartDate, + roomStayEndDate, + adults: adultCount, + ...(kids?.length && { + children: generateChildrenString(kids), + }), + ...(bookingCode && { bookingCode }), + ...(redemption && { isRedemption: "true" }), + language: apiLang, + } + + const apiResponse = await api.get( + api.endpoints.v1.Availability.hotel(hotelId.toString()), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + metrics.roomsCombinedAvailability.fail.add(1, metricsData) + console.error("Failed API call", { params, text }) + return { error: "http_error", details: text } + } + + const apiJson = await apiResponse.json() + const validateAvailabilityData = redemption + ? redemptionRoomsCombinedAvailabilitySchema.safeParse(apiJson) + : roomsCombinedAvailabilitySchema.safeParse(apiJson) + + if (!validateAvailabilityData.success) { + console.error("Validation error", { + params, + error: validateAvailabilityData.error, + }) + metrics.roomsCombinedAvailability.fail.add(1, metricsData) + return { + error: "validation_error", + details: validateAvailabilityData.error, + } + } + + if (rateCode) { + validateAvailabilityData.data.mustBeGuaranteed = + validateAvailabilityData.data.rateDefinitions.find( + (rate) => rate.rateCode === rateCode + )?.mustBeGuaranteed + } + + return validateAvailabilityData.data + }) + ) + metrics.roomsCombinedAvailability.success.add(1, metricsData) + return availabilityResponses.map((availability) => { + if (availability.status === "fulfilled") { + return availability.value + } + return { + details: availability.reason, + error: "request_failure", + } + }) +} + +export const getRoomAvailability = async ( + input: SelectedRoomAvailabilitySchema, + lang: Lang, + token: string, // Either service token or user access token in case of redemption search + serviceToken?: string // In Redemption we need serviceToken for hotel api call +) => { + const { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + rateCode, + counterRateCode, + roomTypeCode, + redemption, + } = input + + const params: Record = { + roomStayStartDate, + roomStayEndDate, + adults, + ...(children && { children }), + ...(bookingCode && { bookingCode }), + ...(redemption && { isRedemption: "true" }), + language: toApiLang(lang), + } + + metrics.selectedRoomAvailability.counter.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + }) + console.info( + "api.hotels.selectedRoomAvailability start", + JSON.stringify({ query: { hotelId, params } }) + ) + const apiResponseAvailability = await api.get( + api.endpoints.v1.Availability.hotel(hotelId.toString()), + { + headers: { + Authorization: `Bearer ${token}`, + }, + }, + params + ) + + if (!apiResponseAvailability.ok) { + const text = await apiResponseAvailability.text() + metrics.selectedRoomAvailability.fail.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponseAvailability.status, + statusText: apiResponseAvailability.statusText, + text, + }), + }) + console.error( + "api.hotels.selectedRoomAvailability error", + JSON.stringify({ + query: { hotelId, params }, + error: { + status: apiResponseAvailability.status, + statusText: apiResponseAvailability.statusText, + text, + }, + }) + ) + + throw new Error("Failed to fetch selected room availability") + } + const apiJsonAvailability = await apiResponseAvailability.json() + const validateAvailabilityData = + roomsAvailabilitySchema.safeParse(apiJsonAvailability) + if (!validateAvailabilityData.success) { + metrics.selectedRoomAvailability.fail.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + error_type: "validation_error", + error: JSON.stringify(validateAvailabilityData.error), + }) + console.error( + "api.hotels.selectedRoomAvailability validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validateAvailabilityData.error, + }) + ) + throw badRequestError() + } + + const hotelData = await getHotel( + { + hotelId, + isCardOnlyPayment: false, + language: lang, + }, + serviceToken ?? token + ) + + const rooms = validateAvailabilityData.data.roomConfigurations + const selectedRoom = rooms.find((room) => room.roomTypeCode === roomTypeCode) + + if (!selectedRoom) { + metrics.selectedRoomAvailability.fail.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + roomTypeCode, + error_type: "not_found", + error: `Couldn't find selected room with input: ${roomTypeCode}`, + }) + console.error("No matching room found") + return null + } + + const availableRoomsInCategory = rooms.filter( + (room) => room.roomType === selectedRoom?.roomType + ) + + const rateTypes = selectedRoom.products.find( + (rate) => + rate.public?.rateCode === rateCode || + rate.member?.rateCode === rateCode || + rate.redemptions?.find((r) => r?.rateCode === rateCode) + ) + + if (!rateTypes) { + metrics.selectedRoomAvailability.fail.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + error_type: "not_found", + error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`, + }) + console.error("No matching rate found") + return null + } + const rates = rateTypes + + const rateDefinition = validateAvailabilityData.data.rateDefinitions.find( + (rate) => rate.rateCode === rateCode + ) + const memberRateDefinition = + validateAvailabilityData.data.rateDefinitions.find( + (rate) => rate.rateCode === counterRateCode + ) + + const bedTypes = availableRoomsInCategory + .map((availRoom) => { + const matchingRoom = hotelData?.roomCategories + ?.find((room) => + room.roomTypes + .map((roomType) => roomType.code) + .includes(availRoom.roomTypeCode) + ) + ?.roomTypes.find((roomType) => roomType.code === availRoom.roomTypeCode) + + if (matchingRoom) { + return { + description: matchingRoom.description, + size: matchingRoom.mainBed.widthRange, + value: matchingRoom.code, + type: matchingRoom.mainBed.type, + extraBed: matchingRoom.fixedExtraBed + ? { + type: matchingRoom.fixedExtraBed.type, + description: matchingRoom.fixedExtraBed.description, + } + : undefined, + } + } + }) + .filter((bed): bed is BedTypeSelection => Boolean(bed)) + + metrics.selectedRoomAvailability.success.add(1, { + hotelId, + roomStayStartDate, + roomStayEndDate, + adults, + children, + bookingCode, + }) + console.info( + "api.hotels.selectedRoomAvailability success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + + return { + bedTypes, + breakfastIncluded: !!rateDefinition?.breakfastIncluded, + cancellationRule: rateDefinition?.cancellationRule, + cancellationText: rateDefinition?.cancellationText ?? "", + isFlexRate: + rateDefinition?.cancellationRule === + CancellationRuleEnum.CancellableBefore6PM, + memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed, + memberRate: rates?.member, + mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed, + publicRate: rates?.public, + redemptionRate: rates?.redemptions?.find((r) => r?.rateCode === rateCode), + rate: selectedRoom.products[0].rate, + rateDefinitionTitle: rateDefinition?.title ?? "", + rateDetails: rateDefinition?.generalTerms, + // Send rate Title when it is a booking code rate + rateTitle: + rateDefinition?.rateType !== RateTypeEnum.Regular + ? rateDefinition?.title + : undefined, + rateType: rateDefinition?.rateType ?? "", + selectedRoom, + } +} + export const hotelQueryRouter = router({ availability: router({ hotelsByCity: serviceProcedure @@ -497,344 +842,31 @@ export const hotelQueryRouter = router({ roomsCombinedAvailability: serviceProcedure .input(roomsCombinedAvailabilityInputSchema) - .query( - async ({ - ctx, - input: { - adultsCount, - bookingCode, - childArray, - hotelId, - lang, - rateCode, - roomStayEndDate, - roomStayStartDate, - }, - }) => { - const apiLang = toApiLang(lang) - - const metricsData = { - hotelId, - roomStayStartDate, - roomStayEndDate, - adultsCount, - childArray: childArray ? JSON.stringify(childArray) : undefined, - bookingCode, - } - - metrics.roomsCombinedAvailability.counter.add(1, metricsData) - - console.info( - "api.hotels.roomsCombinedAvailability start", - JSON.stringify({ query: { hotelId, params: metricsData } }) - ) - - const availabilityResponses = await Promise.allSettled( - adultsCount.map(async (adultCount: number, idx: number) => { - const kids = childArray?.[idx] - const params: Record = { - roomStayStartDate, - roomStayEndDate, - adults: adultCount, - ...(kids?.length && { - children: generateChildrenString(kids), - }), - ...(bookingCode && { bookingCode }), - language: apiLang, - } - - const apiResponse = await api.get( - api.endpoints.v1.Availability.hotel(hotelId.toString()), - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - }, - params - ) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - metrics.roomsCombinedAvailability.fail.add(1, metricsData) - console.error("Failed API call", { params, text }) - return { error: "http_error", details: text } - } - - const apiJson = await apiResponse.json() - const validateAvailabilityData = - roomsCombinedAvailabilitySchema.safeParse(apiJson) - - if (!validateAvailabilityData.success) { - console.error("Validation error", { - params, - error: validateAvailabilityData.error, - }) - metrics.roomsCombinedAvailability.fail.add(1, metricsData) - return { - error: "validation_error", - details: validateAvailabilityData.error, - } - } - - if (rateCode) { - validateAvailabilityData.data.mustBeGuaranteed = - validateAvailabilityData.data.rateDefinitions.find( - (rate) => rate.rateCode === rateCode - )?.mustBeGuaranteed - } - - return validateAvailabilityData.data - }) - ) - metrics.roomsCombinedAvailability.success.add(1, metricsData) - - const data = availabilityResponses.map((availability) => { - if (availability.status === "fulfilled") { - return availability.value - } - return { - details: availability.reason, - error: "request_failure", - } - }) - - return data - } - ), + .query(async ({ input, ctx }) => { + return getRoomsCombinedAvailability(input, ctx.serviceToken) + }), + roomsCombinedAvailabilityWithRedemption: protectedProcedure + .input(roomsCombinedAvailabilityInputSchema) + .query(async ({ input, ctx }) => { + return getRoomsCombinedAvailability( + input, + ctx.session.token.access_token + ) + }), room: serviceProcedure .input(selectedRoomAvailabilityInputSchema) .query(async ({ input, ctx }) => { - const { lang } = input - - const { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - rateCode, - counterRateCode, - roomTypeCode, - } = input - - const params: Record = { - roomStayStartDate, - roomStayEndDate, - adults, - ...(children && { children }), - ...(bookingCode && { bookingCode }), - language: lang ?? toApiLang(ctx.lang), - } - - metrics.selectedRoomAvailability.counter.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - }) - console.info( - "api.hotels.selectedRoomAvailability start", - JSON.stringify({ query: { hotelId, params } }) - ) - const apiResponseAvailability = await api.get( - api.endpoints.v1.Availability.hotel(hotelId.toString()), - { - headers: { - Authorization: `Bearer ${ctx.serviceToken}`, - }, - }, - params - ) - - if (!apiResponseAvailability.ok) { - const text = await apiResponseAvailability.text() - metrics.selectedRoomAvailability.fail.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - error_type: "http_error", - error: JSON.stringify({ - status: apiResponseAvailability.status, - statusText: apiResponseAvailability.statusText, - text, - }), - }) - console.error( - "api.hotels.selectedRoomAvailability error", - JSON.stringify({ - query: { hotelId, params }, - error: { - status: apiResponseAvailability.status, - statusText: apiResponseAvailability.statusText, - text, - }, - }) - ) - - throw new Error("Failed to fetch selected room availability") - } - const apiJsonAvailability = await apiResponseAvailability.json() - const validateAvailabilityData = - roomsAvailabilitySchema.safeParse(apiJsonAvailability) - if (!validateAvailabilityData.success) { - metrics.selectedRoomAvailability.fail.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - error_type: "validation_error", - error: JSON.stringify(validateAvailabilityData.error), - }) - console.error( - "api.hotels.selectedRoomAvailability validation error", - JSON.stringify({ - query: { hotelId, params }, - error: validateAvailabilityData.error, - }) - ) - throw badRequestError() - } - - const hotelData = await getHotel( - { - hotelId, - isCardOnlyPayment: false, - language: lang ?? ctx.lang, - }, + return getRoomAvailability(input, ctx.lang, ctx.serviceToken) + }), + roomWithRedemption: protectedServcieProcedure + .input(selectedRoomAvailabilityInputSchema) + .query(async ({ input, ctx }) => { + return getRoomAvailability( + input, + ctx.lang, + ctx.session.token.access_token, ctx.serviceToken ) - - const rooms = validateAvailabilityData.data.roomConfigurations - const selectedRoom = rooms.find( - (room) => room.roomTypeCode === roomTypeCode - ) - - if (!selectedRoom) { - metrics.selectedRoomAvailability.fail.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - roomTypeCode, - error_type: "not_found", - error: `Couldn't find selected room with input: ${roomTypeCode}`, - }) - console.error("No matching room found") - return null - } - - const availableRoomsInCategory = rooms.filter( - (room) => room.roomType === selectedRoom?.roomType - ) - - const rateTypes = selectedRoom.products.find( - (rate) => - rate.public?.rateCode === rateCode || - rate.member?.rateCode === rateCode - ) - - if (!rateTypes) { - metrics.selectedRoomAvailability.fail.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - error_type: "not_found", - error: `Couldn't find rateTypes for selected room: ${JSON.stringify(selectedRoom)}`, - }) - console.error("No matching rate found") - return null - } - const rates = rateTypes - - const rateDefinition = - validateAvailabilityData.data.rateDefinitions.find( - (rate) => rate.rateCode === rateCode - ) - const memberRateDefinition = - validateAvailabilityData.data.rateDefinitions.find( - (rate) => rate.rateCode === counterRateCode - ) - - const bedTypes = availableRoomsInCategory - .map((availRoom) => { - const matchingRoom = hotelData?.roomCategories - ?.find((room) => - room.roomTypes - .map((roomType) => roomType.code) - .includes(availRoom.roomTypeCode) - ) - ?.roomTypes.find( - (roomType) => roomType.code === availRoom.roomTypeCode - ) - - if (matchingRoom) { - return { - description: matchingRoom.description, - size: matchingRoom.mainBed.widthRange, - value: matchingRoom.code, - type: matchingRoom.mainBed.type, - extraBed: matchingRoom.fixedExtraBed - ? { - type: matchingRoom.fixedExtraBed.type, - description: matchingRoom.fixedExtraBed.description, - } - : undefined, - } - } - }) - .filter((bed): bed is BedTypeSelection => Boolean(bed)) - - metrics.selectedRoomAvailability.success.add(1, { - hotelId, - roomStayStartDate, - roomStayEndDate, - adults, - children, - bookingCode, - }) - console.info( - "api.hotels.selectedRoomAvailability success", - JSON.stringify({ - query: { hotelId, params: params }, - }) - ) - - return { - bedTypes, - breakfastIncluded: !!rateDefinition?.breakfastIncluded, - cancellationRule: rateDefinition?.cancellationRule, - cancellationText: rateDefinition?.cancellationText ?? "", - isFlexRate: - rateDefinition?.cancellationRule === - CancellationRuleEnum.CancellableBefore6PM, - memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed, - memberRate: rates?.member, - mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed, - publicRate: rates?.public, - rate: selectedRoom.products[0].rate, - rateDefinitionTitle: rateDefinition?.title ?? "", - rateDetails: rateDefinition?.generalTerms, - // Send rate Title when it is a booking code rate - rateTitle: - rateDefinition?.rateType !== RateTypeEnum.Regular - ? rateDefinition?.title - : undefined, - rateType: rateDefinition?.rateType ?? "", - selectedRoom, - } }), hotelsByCityWithBookingCode: serviceProcedure .input(hotelsAvailabilityInputSchema) diff --git a/apps/scandic-web/server/routers/hotels/schemas/availability/productType.ts b/apps/scandic-web/server/routers/hotels/schemas/availability/productType.ts index 3191c260a..65c7f4a53 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/availability/productType.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/availability/productType.ts @@ -9,8 +9,6 @@ export const productTypeSchema = z .object({ public: productTypePriceSchema.optional(), member: productTypePriceSchema.optional(), - redemption: productTypePointsSchema.optional(), - redemptionA: productTypePointsSchema.optional(), - redemptionB: productTypePointsSchema.optional(), + redemptions: z.array(productTypePointsSchema).optional(), }) .optional() diff --git a/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts b/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts index 828dc17ea..af68e610b 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/productTypePrice.ts @@ -10,22 +10,31 @@ export const priceSchema = z.object({ regularPricePerStay: z.coerce.number().optional(), }) -export const pointsSchema = z.object({ - currency: z.nativeEnum(CurrencyEnum).optional(), - pricePerNight: z.coerce.number().optional(), - pricePerStay: z.coerce.number().optional(), - pointsPerNight: z.number(), - pointsPerStay: z.number(), -}) +export const pointsSchema = z + .object({ + currency: z.nativeEnum(CurrencyEnum).optional(), + pricePerStay: z.coerce.number().optional(), + pointsPerStay: z.coerce.number(), + additionalPricePerStay: z.coerce.number().optional(), + additionalPriceCurrency: z.nativeEnum(CurrencyEnum).optional(), + }) + .transform((data) => ({ + ...data, + additionalPriceCurrency: data.currency, + currency: CurrencyEnum.POINTS, + pricePerStay: data.pointsPerStay, + price: data.pointsPerStay, + additionalPrice: data.additionalPricePerStay, + })) const partialPriceSchema = z.object({ rateCode: z.string(), rateType: z.string().optional(), - requestedPrice: priceSchema.optional(), }) export const productTypePriceSchema = partialPriceSchema.extend({ localPrice: priceSchema, + requestedPrice: priceSchema.optional(), }) export const productTypePointsSchema = partialPriceSchema.extend({ diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts index 25e0723f5..888dd467a 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/configuration.ts @@ -29,21 +29,25 @@ export const roomConfigurationSchema = z }) .transform((data) => { if (data.products.length) { - /** - * Just guaranteeing that if all products all miss - * both public and member rateCode that status is - * set to `NotAvailable` - */ - const allProductsMissBothRateCodes = data.products.every( - (product) => !product.public?.rateCode && !product.member?.rateCode - ) - if (allProductsMissBothRateCodes) { - return { - ...data, - status: AvailabilityEnum.NotAvailable, + if (data.products[0].redemptions) { + // No need of rate check in reward night scenario + return { ...data } + } else { + /** + * Just guaranteeing that if all products all miss + * both public and member rateCode that status is + * set to `NotAvailable` + */ + const allProductsMissBothRateCodes = data.products.every( + (product) => !product.public?.rateCode && !product.member?.rateCode + ) + if (allProductsMissBothRateCodes) { + return { + ...data, + status: AvailabilityEnum.NotAvailable, + } } } } - return data }) diff --git a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts index 5a944710d..02449a601 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/roomAvailability/product.ts @@ -1,6 +1,9 @@ import { z } from "zod" -import { productTypePriceSchema } from "../productTypePrice" +import { + productTypePointsSchema, + productTypePriceSchema, +} from "../productTypePrice" export const productSchema = z .object({ @@ -9,6 +12,7 @@ export const productSchema = z productType: z.object({ member: productTypePriceSchema.optional(), public: productTypePriceSchema.optional(), + redemptions: z.array(productTypePointsSchema).optional(), }), // Used to set the rate that we use to chose titles etc. rate: z.enum(["change", "flex", "save"]).default("save"), diff --git a/apps/scandic-web/server/trpc.ts b/apps/scandic-web/server/trpc.ts index 5ca672b25..027800981 100644 --- a/apps/scandic-web/server/trpc.ts +++ b/apps/scandic-web/server/trpc.ts @@ -206,3 +206,6 @@ export const contentStackBaseWithProtectedProcedure = export const safeProtectedServiceProcedure = safeProtectedProcedure.unstable_concat(serviceProcedure) + +export const protectedServcieProcedure = + protectedProcedure.unstable_concat(serviceProcedure) diff --git a/apps/scandic-web/stores/enter-details/helpers.ts b/apps/scandic-web/stores/enter-details/helpers.ts index b77d9a250..565c13502 100644 --- a/apps/scandic-web/stores/enter-details/helpers.ts +++ b/apps/scandic-web/stores/enter-details/helpers.ts @@ -2,9 +2,10 @@ import isEqual from "fast-deep-equal" import { detailsStorageName } from "." -import type { RoomRate } from "@/types/components/hotelReservation/enterDetails/details" +import { type RoomRate } from "@/types/components/hotelReservation/enterDetails/details" import type { Price } from "@/types/components/hotelReservation/price" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" +import { CurrencyEnum } from "@/types/enums/currency" import { StepEnum } from "@/types/enums/step" import type { DetailsState, @@ -131,18 +132,43 @@ export function getRoomPrice(roomRate: RoomRate, isMember: boolean) { } } + if (roomRate.redemptionRate) { + return { + // ToDo Handle perNight as undefined + perNight: { + requested: undefined, + local: { + currency: + roomRate.redemptionRate.localPrice.currency ?? CurrencyEnum.POINTS, + price: roomRate.redemptionRate.localPrice.pointsPerStay, + additionalPrice: + roomRate.redemptionRate.localPrice.additionalPricePerStay, + additionalPriceCurrency: + roomRate.redemptionRate.localPrice.additionalPriceCurrency, + }, + }, + perStay: { + requested: undefined, + local: { + currency: + roomRate.redemptionRate.localPrice.currency ?? CurrencyEnum.POINTS, + price: roomRate.redemptionRate.localPrice.pointsPerStay, + additionalPrice: + roomRate.redemptionRate.localPrice.additionalPricePerStay, + additionalPriceCurrency: + roomRate.redemptionRate.localPrice.additionalPriceCurrency, + }, + }, + } + } + throw new Error( `Unable to calculate RoomPrice since user is neither a member or memberRate is missing, or publicRate is missing` ) } -type TotalPrice = { - requested: { currency: string; price: number } | undefined - local: { currency: string; price: number; regularPrice?: number } -} - export function getTotalPrice(roomRates: RoomRate[], isMember: boolean) { - return roomRates.reduce( + return roomRates.reduce( (total, roomRate, idx) => { const isFirstRoom = idx === 0 const rate = diff --git a/apps/scandic-web/stores/enter-details/index.ts b/apps/scandic-web/stores/enter-details/index.ts index c164141e7..abc11e77a 100644 --- a/apps/scandic-web/stores/enter-details/index.ts +++ b/apps/scandic-web/stores/enter-details/index.ts @@ -3,6 +3,7 @@ import { produce } from "immer" import { useContext } from "react" import { create, useStore } from "zustand" +import { REDEMPTION } from "@/constants/booking" import { dt } from "@/lib/dt" import { DetailsContext } from "@/contexts/Details" @@ -20,6 +21,9 @@ import { } from "./helpers" import type { BreakfastPackages } from "@/types/components/hotelReservation/breakfast" +import { + PointsPriceSchema, + type Price} from "@/types/components/hotelReservation/price"; import { StepEnum } from "@/types/enums/step" import type { DetailsState, @@ -49,11 +53,20 @@ export function createDetailsStore( breakfastPackages: BreakfastPackages | null ) { const isMember = !!user + const isRedemption = + new URLSearchParams(searchParams).get("searchtype") === REDEMPTION - const initialTotalPrice = getTotalPrice( - initialState.rooms.map((r) => r.roomRate), - isMember - ) + let initialTotalPrice: Price + if (isRedemption && initialState.rooms[0].roomRate.redemptionRate) { + initialTotalPrice = PointsPriceSchema.parse( + initialState.rooms[0].roomRate.redemptionRate + ) + } else { + initialTotalPrice = getTotalPrice( + initialState.rooms.map((r) => r.roomRate), + isMember + ) + } initialState.rooms.forEach((room) => { if (room.roomFeatures) { diff --git a/apps/scandic-web/stores/select-rate/index.ts b/apps/scandic-web/stores/select-rate/index.ts index c950c7c63..a32fa3398 100644 --- a/apps/scandic-web/stores/select-rate/index.ts +++ b/apps/scandic-web/stores/select-rate/index.ts @@ -91,10 +91,15 @@ export function createRatesStore({ rc.products.find( (product) => product.public?.rateCode === room.rateCode || - product.member?.rateCode === room.rateCode + product.member?.rateCode === room.rateCode || + product.redemptions?.find( + (redemption) => redemption?.rateCode === room.rateCode + ) ) ) - + const redemptionProduct = selectedRoom?.products[0].redemptions?.find( + (r) => r?.rateCode === room.rateCode + ) const product = selectedRoom?.products.find( (p) => p.public?.rateCode === room.rateCode || @@ -105,10 +110,19 @@ export function createRatesStore({ features: selectedRoom.features, member: product.member, public: product.public, + redemption: undefined, rate: product.rate, roomType: selectedRoom.roomType, roomTypeCode: selectedRoom.roomTypeCode, } + } else if (selectedRoom && redemptionProduct) { + rateSummary[idx] = { + features: selectedRoom.features, + redemption: redemptionProduct, + rate: selectedRoom?.products[0].rate, + roomType: selectedRoom.roomType, + roomTypeCode: selectedRoom.roomTypeCode, + } } } }) @@ -200,6 +214,7 @@ export function createRatesStore({ package: state.rooms[idx].selectedPackage, rate: selectedRate.product.rate, public: selectedRate.product.public, + redemption: undefined, roomType: selectedRate.roomType, roomTypeCode: selectedRate.roomTypeCode, } @@ -240,6 +255,51 @@ export function createRatesStore({ state.activeRoom = idx + 1 } + state.searchParams = new ReadonlyURLSearchParams(searchParams) + window.history.pushState( + {}, + "", + `${state.pathname}?${searchParams}` + ) + }) + ) + } + }, + selectRateRedemption(idx) { + return function (selectedRate, selectedRateCode?: string) { + return set( + produce((state: RatesState) => { + const redemptionRate = selectedRate.product.redemptions?.find( + (r) => r?.rateCode === selectedRateCode + ) + if (!redemptionRate) { + return + } + + state.rooms[idx].selectedRate = selectedRate + state.rateSummary[idx] = { + features: selectedRate.features, + package: state.rooms[idx].selectedPackage, + rate: selectedRate.product.rate, + roomType: selectedRate.roomType, + roomTypeCode: selectedRate.roomTypeCode, + redemption: redemptionRate, + } + + const searchParams = new URLSearchParams(state.searchParams) + + if (redemptionRate.rateCode) { + searchParams.set( + `room[${idx}].ratecode`, + redemptionRate.rateCode + ) + } + + searchParams.set( + `room[${idx}].roomtype`, + selectedRate.roomTypeCode + ) + state.searchParams = new ReadonlyURLSearchParams(searchParams) window.history.pushState( {}, @@ -291,11 +351,11 @@ export function createRatesStore({ selectedRate: selectedRate && product ? { - features: selectedRate.features, - product, - roomType: selectedRate.roomType, - roomTypeCode: selectedRate.roomTypeCode, - } + features: selectedRate.features, + product, + roomType: selectedRate.roomType, + roomTypeCode: selectedRate.roomTypeCode, + } : null, } }), diff --git a/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts b/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts index 3549298f7..e5d54529f 100644 --- a/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts +++ b/apps/scandic-web/types/components/hotelReservation/enterDetails/details.ts @@ -7,11 +7,13 @@ import type { guestDetailsSchema, signedInDetailsSchema, } from "@/components/HotelReservation/EnterDetails/Details/RoomOne/schema" +import type { productTypePointsSchema } from "@/server/routers/hotels/schemas/productTypePrice" import type { Price } from "../price" export type DetailsSchema = z.output export type MultiroomDetailsSchema = z.output export type SignedInDetailsSchema = z.output +export type ProductTypePointsSchema = z.output export interface RoomPrice { perNight: Price @@ -29,4 +31,5 @@ export type JoinScandicFriendsCardProps = { export type RoomRate = { memberRate?: Product["member"] publicRate?: Product["public"] + redemptionRate?: ProductTypePointsSchema } diff --git a/apps/scandic-web/types/components/hotelReservation/price.ts b/apps/scandic-web/types/components/hotelReservation/price.ts index 7a22eca22..d03abe2f4 100644 --- a/apps/scandic-web/types/components/hotelReservation/price.ts +++ b/apps/scandic-web/types/components/hotelReservation/price.ts @@ -1,10 +1,31 @@ +import { z } from "zod" + +import { CurrencyEnum } from "@/types/enums/currency" + interface TPrice { currency: string price: number regularPrice?: number + additionalPrice?: number + additionalPriceCurrency?: string } export interface Price { - requested: TPrice | undefined + requested?: TPrice local: TPrice } + +export const PointsPriceSchema = z + .object({ + localPrice: z.object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.number(), + additionalPrice: z.number().optional(), + additionalPriceCurrency: z.nativeEnum(CurrencyEnum).optional(), + }), + }) + .transform((data) => ({ + local: { + ...data.localPrice, + }, + })) diff --git a/apps/scandic-web/types/components/hotelReservation/selectHotel/priceCardProps.ts b/apps/scandic-web/types/components/hotelReservation/selectHotel/priceCardProps.ts index c5a8df6ff..c5a0824b0 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectHotel/priceCardProps.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectHotel/priceCardProps.ts @@ -1,14 +1,12 @@ -import type { - ProductTypePoints, - ProductTypePrices, -} from "@/types/trpc/routers/hotel/availability" +import type { ProductTypePrices } from "@/types/trpc/routers/hotel/availability" export type PriceCardProps = { productTypePrices: ProductTypePrices isMemberPrice?: boolean } -export type PointsCardProps = { - productTypePoints?: ProductTypePoints - redemptionPrice?: number +export type PointsRowProps = { + pointsPerStay: number + additionalPricePerStay?: number + additionalPriceCurrency?: string } diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts index 714c5aeef..f39ce8c03 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/flexibilityOption.ts @@ -22,12 +22,12 @@ export type FlexibilityOptionProps = { roomType: RoomConfiguration["roomType"] roomTypeCode: RoomConfiguration["roomTypeCode"] title: string - rateTitle?: string // This is for the rates via booking codes + rateName?: string // Obtained in case of booking code and redemption rates } export interface PriceListProps { publicPrice?: ProductPrice | Record memberPrice?: ProductPrice | Record petRoomPackage?: RoomPackage - rateTitle?: string // This is for the rates via booking codes + rateName?: string // Obtained in case of booking code and redemption rates } diff --git a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts index 2d3b30900..db99748df 100644 --- a/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts +++ b/apps/scandic-web/types/components/hotelReservation/selectRate/selectRate.ts @@ -3,6 +3,7 @@ import type { RoomConfiguration, } from "@/types/trpc/routers/hotel/roomAvailability" import type { ChildBedMapEnum } from "../../bookingWidget/enums" +import type { ProductTypePointsSchema } from "../enterDetails/details" import type { RoomPackageCodeEnum } from "./roomFilter" export interface Child { @@ -43,20 +44,14 @@ export type Rate = { roomTypeCode: RoomConfiguration["roomTypeCode"] } & ( | { - member?: undefined - public?: undefined + member?: NonNullable + public?: NonNullable + redemption?: never } | { member?: never - public: NonNullable - } - | { - member: NonNullable public?: never - } - | { - member: NonNullable - public: NonNullable + redemption: NonNullable } ) diff --git a/apps/scandic-web/types/contexts/select-rate/room.ts b/apps/scandic-web/types/contexts/select-rate/room.ts index 885cf161f..0b0b7b755 100644 --- a/apps/scandic-web/types/contexts/select-rate/room.ts +++ b/apps/scandic-web/types/contexts/select-rate/room.ts @@ -11,6 +11,10 @@ export interface RoomContextValue extends SelectedRoom { modifyRate: () => void selectFilter: (code: RoomPackageCodeEnum | undefined) => void selectRate: (rate: SelectedRate) => void + selectRateRedemption: ( + rate: SelectedRate, + selectedRateCode?: string + ) => void } isActiveRoom: boolean isMainRoom: boolean diff --git a/apps/scandic-web/types/enums/currency.ts b/apps/scandic-web/types/enums/currency.ts index 580041361..6f7bd78e6 100644 --- a/apps/scandic-web/types/enums/currency.ts +++ b/apps/scandic-web/types/enums/currency.ts @@ -4,5 +4,6 @@ export enum CurrencyEnum { NOK = "NOK", PLN = "PLN", SEK = "SEK", + POINTS = "POINTS", Unknown = "Unknown", } diff --git a/apps/scandic-web/types/stores/rates.ts b/apps/scandic-web/types/stores/rates.ts index c9ced819a..a4f374eca 100644 --- a/apps/scandic-web/types/stores/rates.ts +++ b/apps/scandic-web/types/stores/rates.ts @@ -27,6 +27,9 @@ interface Actions { modifyRate: (idx: number) => () => void selectFilter: (idx: number) => (code: RoomPackageCodeEnum | undefined) => void selectRate: (idx: number) => (rate: SelectedRate) => void + selectRateRedemption: ( + idx: number + ) => (rate: SelectedRate, selectedRateCode?: string) => void } export interface SelectedRate { diff --git a/apps/scandic-web/types/trpc/routers/hotel/availability.ts b/apps/scandic-web/types/trpc/routers/hotel/availability.ts index 9f1bb3850..68b95cbb0 100644 --- a/apps/scandic-web/types/trpc/routers/hotel/availability.ts +++ b/apps/scandic-web/types/trpc/routers/hotel/availability.ts @@ -1,9 +1,12 @@ +import { + type getHotelsByHotelIdsAvailabilityInputSchema, + type hotelsAvailabilityInputSchema, + type roomsCombinedAvailabilityInputSchema, + type selectedRoomAvailabilityInputSchema, +} from "@/server/routers/hotels/input" + import type { z } from "zod" -import type { - getHotelsByHotelIdsAvailabilityInputSchema, - hotelsAvailabilityInputSchema, -} from "@/server/routers/hotels/input" import type { hotelsAvailabilitySchema } from "@/server/routers/hotels/output" import type { productTypeSchema } from "@/server/routers/hotels/schemas/availability/productType" import type { @@ -18,6 +21,12 @@ export type HotelsAvailabilityInputSchema = z.output< export type HotelsByHotelIdsAvailabilityInputSchema = z.output< typeof getHotelsByHotelIdsAvailabilityInputSchema > +export type RoomsCombinedAvailabilityInputSchema = z.output< + typeof roomsCombinedAvailabilityInputSchema +> +export type SelectedRoomAvailabilitySchema = z.output< + typeof selectedRoomAvailabilityInputSchema +> export type ProductType = z.output export type ProductTypePrices = z.output export type ProductTypePoints = z.output diff --git a/apps/scandic-web/utils/numberFormatting.ts b/apps/scandic-web/utils/numberFormatting.ts index 213297a27..98e97c972 100644 --- a/apps/scandic-web/utils/numberFormatting.ts +++ b/apps/scandic-web/utils/numberFormatting.ts @@ -22,3 +22,19 @@ export function formatPrice(intl: IntlShape, price: number, currency: string) { }) return `${localizedPrice} ${currency}` } + +// This will handle redemption and bonus cheque (corporate cheque) scneario with partial payments +export function formatPriceWithAdditionalPrice( + intl: IntlShape, + points: number, + pointsCurrency: string, + additionalPrice?: number, + additionalPriceCurrency?: string +) { + const formattedAdditionalPrice = + additionalPrice && additionalPriceCurrency + ? `+ ${formatPrice(intl, additionalPrice, additionalPriceCurrency)}` + : "" + + return `${formatPrice(intl, points, pointsCurrency)} ${formattedAdditionalPrice}` +}