Merged in fix/SW-2642-select-hotel-corporate-ch (pull request #2003)

fix: SW-2642 Fixed corporate chq and voucher rates city search

* fix: SW-2642 Fixed corporate chq and voucher rates city search

* fix: SW-2642 Fixed no availability alert for all hotels

* fix: SW-2642 Combined flags to suitable variable

* fix: SW-2642 Fixed map view to show prices


Approved-by: Arvid Norlin
This commit is contained in:
Hrishikesh Vaipurkar
2025-05-08 10:46:05 +00:00
parent a99e434d84
commit 74a5b5748a
18 changed files with 232 additions and 80 deletions

View File

@@ -3,7 +3,7 @@
gap: var(--Space-x05);
}
.bookingCodeChip .unavailable {
.unavailable {
text-decoration: line-through;
}

View File

@@ -65,24 +65,16 @@ export default function BookingCodeChip({
icon={<DiscountIcon color="Icon/Feedback/Information" />}
className={alignCenter ? styles.center : undefined}
>
<p className={styles.bookingCodeChip}>
<p
className={`${styles.bookingCodeChip} ${isUnavailable ? styles.unavailable : ""}`}
>
<Typography variant="Body/Supporting text (caption)/smBold">
<strong>
{intl.formatMessage({ defaultMessage: "Booking code" })}
</strong>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<span className={`${isUnavailable ? styles.unavailable : ""}`}>
{isUnavailable
? intl.formatMessage(
{ defaultMessage: "{code} unavailable" },
{ code: bookingCode }
)
: intl.formatMessage(
{ defaultMessage: "{code} applied" },
{ code: bookingCode }
)}
</span>
<span>{bookingCode}</span>
</Typography>
</p>
</IconChip>

View File

@@ -27,7 +27,7 @@ export default function HotelChequeCard({
{productTypeCheque.localPrice.numberOfCheques}
</Subtitle>
<Caption color="uiTextHighContrast">{CurrencyEnum.CC}</Caption>
{productTypeCheque.localPrice.additionalPricePerStay && (
{productTypeCheque.localPrice.additionalPricePerStay > 0 ? (
<>
{"+"}
<Subtitle type="two" color="uiTextHighContrast">
@@ -37,10 +37,11 @@ export default function HotelChequeCard({
{productTypeCheque.localPrice.currency}
</Caption>
</>
)}
) : null}
</div>
</div>
{productTypeCheque.requestedPrice ? (
{productTypeCheque.requestedPrice &&
productTypeCheque.requestedPrice.additionalPricePerStay > 0 ? (
<div className={styles.chequeRow}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({

View File

@@ -69,9 +69,7 @@ function HotelCard({
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
const fullPrice =
!availability.productType?.public?.bookingCode &&
!availability.productType?.member?.bookingCode
const fullPrice = !availability.bookingCode
const price = availability.productType
const hasInsufficientPoints = !price?.redemptions?.some(

View File

@@ -2,6 +2,8 @@
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { selectRate } from "@/constants/routes/hotelReservation"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
@@ -47,6 +49,8 @@ export default function ListingHotelCardDialog({
ratings,
operaId,
redemptionPrice,
chequePrice,
voucherPrice,
} = data
const firstImage = images[0]?.imageSizes?.small
@@ -81,7 +85,11 @@ export default function ListingHotelCardDialog({
</div>
</div>
{publicPrice || memberPrice || redemptionPrice ? (
{publicPrice ||
memberPrice ||
redemptionPrice ||
voucherPrice ||
chequePrice ? (
<div className={styles.bottomContainer}>
<div className={styles.pricesContainer}>
{redemptionPrice ? (
@@ -130,6 +138,63 @@ export default function ListingHotelCardDialog({
{redemptionPrice && (
<HotelPointsRow pointsPerStay={redemptionPrice} />
)}
{chequePrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.numberOfCheques,
currency: "CC",
}
)}
{chequePrice.additionalPricePerStay > 0
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
" + " +
intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.additionalPricePerStay,
currency: chequePrice.currency,
}
)
: null}
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Typography>
</Subtitle>
)}
{voucherPrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: voucherPrice,
currency,
}
)}
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Typography>
</Subtitle>
)}
</div>
</div>
<Button asChild theme="base" size="small" className={styles.button}>

View File

@@ -40,9 +40,11 @@ export default function StandaloneHotelCardDialog({
const isUserLoggedIn = isValidClientSession(session)
const {
name,
chequePrice,
publicPrice,
memberPrice,
redemptionPrice,
voucherPrice,
currency,
amenities,
images,
@@ -85,7 +87,11 @@ export default function StandaloneHotelCardDialog({
})}
</div>
<div className={styles.pricesContainer}>
{publicPrice || memberPrice || redemptionPrice ? (
{publicPrice ||
memberPrice ||
redemptionPrice ||
voucherPrice ||
chequePrice ? (
<>
<div className={styles.priceCard}>
{redemptionPrice ? (
@@ -101,6 +107,63 @@ export default function StandaloneHotelCardDialog({
})}
</Caption>
)}
{chequePrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.numberOfCheques,
currency: "CC",
}
)}
{chequePrice.additionalPricePerStay > 0
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
" + " +
intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: chequePrice.additionalPricePerStay,
currency: chequePrice.currency,
}
)
: null}
<Body asChild>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Body>
</Subtitle>
)}
{voucherPrice && (
<Subtitle type="two">
{intl.formatMessage(
{
defaultMessage: "{price} {currency}",
},
{
price: voucherPrice,
currency,
}
)}
<Body asChild>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<span>
/
{intl.formatMessage({
defaultMessage: "night",
})}
</span>
</Body>
</Subtitle>
)}
{publicPrice && !isUserLoggedIn && (
<Subtitle type="two">
{intl.formatMessage(

View File

@@ -14,17 +14,23 @@ export function getHotelPins(
const redemptionRate = productType?.redemptions?.find(
(r) => r?.localPrice.pointsPerStay
)
const chequePrice = productType?.bonusCheque?.localPrice
const voucherPrice = productType?.voucher?.numberOfVouchers
if (chequePrice || voucherPrice) {
currencyValue = chequePrice ? "CC" : "Voucher"
}
return {
bookingCode:
productType?.public?.bookingCode ?? productType?.member?.bookingCode,
bookingCode: availability.bookingCode,
coordinates: {
lat: hotel.location.latitude,
lng: hotel.location.longitude,
},
name: hotel.name,
chequePrice: chequePrice ?? null,
publicPrice: productType?.public?.localPrice.pricePerNight ?? null,
memberPrice: productType?.member?.localPrice.pricePerNight ?? null,
redemptionPrice: redemptionRate?.localPrice.pointsPerStay ?? null,
voucherPrice: voucherPrice ?? null,
rateType:
productType?.public?.rateType ?? productType?.member?.rateType ?? null,
currency:

View File

@@ -56,11 +56,7 @@ export default function HotelCardListing({
)
const isBookingCodeRateAvailable =
bookingCode && !isSpecialRate
? hotelData.some(
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
? hotelData.some((hotel) => hotel.availability.bookingCode)
: false
const showOnlyBookingCodeRates =
isBookingCodeRateAvailable &&
@@ -74,11 +70,7 @@ export default function HotelCardListing({
})
const updatedHotelsList = showOnlyBookingCodeRates
? sortedHotels.filter(
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
? sortedHotels.filter((hotel) => hotel.availability.bookingCode)
: sortedHotels
if (!activeFilters.length) {

View File

@@ -48,14 +48,10 @@ export function getSortedHotels({
if (bookingCode) {
const bookingCodeRateHotels = availableHotels.filter(
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
(hotel) => hotel.availability.bookingCode
)
const regularRateHotels = availableHotels.filter(
(hotel) =>
!hotel.availability.productType?.public?.bookingCode &&
!hotel?.availability.productType?.member?.bookingCode
(hotel) => !hotel.availability.bookingCode
)
return bookingCodeRateHotels

View File

@@ -79,11 +79,7 @@ export async function SelectHotelMapContainer({
: false
const isBookingCodeRateAvailable = bookingCode
? hotels?.some(
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
? hotels?.some((hotel) => hotel.availability.bookingCode)
: false
const { hotelsTrackingData, pageTrackingData } = getTracking(

View File

@@ -140,15 +140,23 @@ export default function SelectHotelContent({
)
const isRegularRateAvailable = bookingCode
? hotels.some((hotel) => !hotel.availability.bookingCode)
: false
const isSpecialRate = bookingCode
? hotels.some(
(hotel) =>
!(
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
)
hotel.availability.productType?.bonusCheque ||
hotel.availability.productType?.voucher
)
: false
const showBookingCodeFilter =
bookingCode &&
isBookingCodeRateAvailable &&
isRegularRateAvailable &&
!isSpecialRate
return (
<div className={styles.container}>
<div className={styles.listingContainer} ref={listingContainerRef}>
@@ -170,9 +178,7 @@ export default function SelectHotelContent({
filters={filterList}
setShowSkeleton={setShowSkeleton}
/>
{bookingCode &&
isBookingCodeRateAvailable &&
isRegularRateAvailable ? (
{showBookingCodeFilter ? (
<div className={styles.bookingCodeFilter}>
<BookingCodeFilter />
</div>

View File

@@ -101,16 +101,16 @@ export default async function SelectHotel({
const isFullPriceHotelAvailable = bookingCode
? hotels?.some(
(hotel) =>
!hotel.availability.productType?.public?.bookingCode &&
!hotel.availability.productType?.member?.bookingCode
!hotel.availability.bookingCode &&
hotel.availability.status === "Available"
)
: false
const isBookingCodeRateAvailable = bookingCode
? hotels?.some(
(hotel) =>
hotel.availability.productType?.public?.bookingCode ||
hotel.availability.productType?.member?.bookingCode
hotel.availability.bookingCode &&
hotel.availability.status === "Available"
)
: false

View File

@@ -76,7 +76,7 @@ export default function Code({
const pkgsSumRequested = sumPackagesRequestedPrice(selectedPackages)
if ("corporateCheque" in product) {
const { localPrice, rateCode } = product.corporateCheque
const { localPrice, rateCode, requestedPrice } = product.corporateCheque
let price = `${localPrice.numberOfCheques} CC`
if (localPrice.additionalPricePerStay) {
price = `${price} + ${localPrice.additionalPricePerStay + pkgsSum.price}`
@@ -90,11 +90,28 @@ export default function Code({
roomTypeCode
)
const currency = localPrice.currency ?? pkgsSum.currency?.toString() ?? ""
const currency =
localPrice.additionalPricePerStay > 0 || pkgsSum.price > 0
? (localPrice.currency ?? pkgsSum.currency ?? "")
: ""
const approximateRate =
requestedPrice?.additionalPricePerStay && requestedPrice?.currency
? {
label: intl.formatMessage({
defaultMessage: "Approx.",
}),
price:
`${requestedPrice.numberOfCheques} CC + ` +
requestedPrice.additionalPricePerStay,
unit: requestedPrice.currency,
}
: undefined
return (
<CodeRateCard
key={product.rate}
approximateRate={approximateRate}
bannerText={bannerText}
handleChange={() => handleSelectRate(product)}
isSelected={isSelected}

View File

@@ -12,12 +12,16 @@ interface HotelPinProps {
isActive: boolean
hotelPrice: number | null
currency: string
hotelAdditionalPrice?: number
hotelAdditionalCurrency?: string
}
export default function HotelPin({
isActive,
hotelPrice,
currency,
hotelAdditionalPrice,
hotelAdditionalCurrency,
}: HotelPinProps) {
const intl = useIntl()
const isNotAvailable = !hotelPrice
@@ -39,8 +43,18 @@ export default function HotelPin({
)}
</span>
<Typography variant="Body/Paragraph/mdRegular">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<p>{isNotAvailable ? "—" : formatPrice(intl, hotelPrice, currency)}</p>
<p>
{isNotAvailable
? // eslint-disable-next-line formatjs/no-literal-string-in-jsx
"—"
: formatPrice(
intl,
hotelPrice,
currency,
hotelAdditionalPrice,
hotelAdditionalCurrency
)}
</p>
</Typography>
</div>
)

View File

@@ -36,7 +36,19 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
const isActiveOrHovered =
activeHotel === pin.name || hoveredHotel === pin.name
const hotelPrice =
pin.memberPrice ?? pin.publicPrice ?? pin.redemptionPrice
pin.memberPrice ??
pin.publicPrice ??
pin.redemptionPrice ??
pin.voucherPrice ??
pin.chequePrice?.numberOfCheques ??
null
const hotelAdditionalPrice = pin.chequePrice
? pin.chequePrice.additionalPricePerStay
: undefined
const hotelAdditionalCurrency = pin.chequePrice
? pin.chequePrice.currency?.toString()
: undefined
return (
<AdvancedMarker
key={pin.name}
@@ -63,6 +75,8 @@ function HotelListingMapContent({ hotelPins }: HotelListingMapContentProps) {
isActive={isActiveOrHovered}
hotelPrice={hotelPrice}
currency={pin.currency}
hotelAdditionalPrice={hotelAdditionalPrice}
hotelAdditionalCurrency={hotelAdditionalCurrency}
/>
</AdvancedMarker>
)

View File

@@ -95,25 +95,15 @@ export const hotelSchema = z
export const hotelsAvailabilitySchema = z.object({
data: z.array(
z.object({
attributes: z
.object({
bookingCode: z.string().nullish(),
checkInDate: z.string(),
checkOutDate: z.string(),
hotelId: z.number(),
occupancy: occupancySchema,
productType: productTypeSchema,
status: z.string(),
})
.transform((data) => {
if (data.bookingCode && data.productType?.public) {
data.productType.public.bookingCode = data.bookingCode
}
if (data.bookingCode && data.productType?.member) {
data.productType.member.bookingCode = data.bookingCode
}
return data
}),
attributes: z.object({
bookingCode: z.string().nullish(),
checkInDate: z.string(),
checkOutDate: z.string(),
hotelId: z.number(),
occupancy: occupancySchema,
productType: productTypeSchema,
status: z.string(),
}),
relationships: relationshipsSchema.optional(),
type: z.string().optional(),
})

View File

@@ -46,7 +46,6 @@ const partialPriceSchema = z.object({
}
return RateTypeEnum.Regular
}),
bookingCode: z.string().nullish(),
})
export const productTypeCorporateChequeSchema = z

View File

@@ -2,6 +2,7 @@ import type { z } from "zod"
import type { Coordinates } from "@/types/components/maps/coordinates"
import type { Amenities } from "@/types/hotel"
import type { ProductTypeCheque } from "@/types/trpc/routers/hotel/availability"
import type { HotelResponse } from "@/components/HotelReservation/SelectHotel/helpers"
import type { imageSchema } from "@/server/routers/hotels/schemas/image"
import type { CategorizedFilters } from "./hotelFilters"
@@ -32,9 +33,11 @@ export type HotelPin = {
bookingCode?: string | null
name: string
coordinates: Coordinates
chequePrice: ProductTypeCheque["localPrice"] | null
publicPrice: number | null
memberPrice: number | null
redemptionPrice: number | null
voucherPrice: number | null
rateType: string | null
currency: string
images: {