feat: SW-2028 Fixed review comments
This commit is contained in:
@@ -5,13 +5,13 @@ import { useFormContext } from "react-hook-form"
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { REDEMPTION } from "@/constants/booking"
|
import { REDEMPTION } from "@/constants/booking"
|
||||||
|
|
||||||
import Modal from "@/components/Modal"
|
import Modal from "@/components/Modal"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
import { RemoveExtraRooms } from "../BookingCode"
|
import { RemoveExtraRooms } from "../BookingCode"
|
||||||
@@ -97,19 +97,19 @@ export default function RewardNight() {
|
|||||||
<MaterialIcon
|
<MaterialIcon
|
||||||
icon="info"
|
icon="info"
|
||||||
size={20}
|
size={20}
|
||||||
color="Icon/Feedback/Information"
|
color="Icon/Interactive/Placeholder"
|
||||||
className={styles.errorIcon}
|
className={styles.errorIcon}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
title={reward}
|
title={reward}
|
||||||
>
|
>
|
||||||
<Body
|
<Typography
|
||||||
color="uiTextHighContrast"
|
variant="Body/Paragraph/mdRegular"
|
||||||
className={styles.rewardNightTooltip}
|
className={styles.rewardNightTooltip}
|
||||||
>
|
>
|
||||||
{rewardNightTooltip}
|
<span>{rewardNightTooltip}</span>
|
||||||
</Body>
|
</Typography>
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -138,7 +138,8 @@ export default function PriceDetailsTable({
|
|||||||
"corporateCheque" in room.roomRate
|
"corporateCheque" in room.roomRate
|
||||||
? room.roomRate.corporateCheque
|
? room.roomRate.corporateCheque
|
||||||
: undefined
|
: undefined
|
||||||
const redemptionPrice = "redemption" in room.roomRate ? room.roomRate.redemption : undefined
|
const redemptionPrice =
|
||||||
|
"redemption" in room.roomRate ? room.roomRate.redemption : undefined
|
||||||
if (!price && !voucherPrice && !chequePrice && !redemptionPrice) {
|
if (!price && !voucherPrice && !chequePrice && !redemptionPrice) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -283,25 +284,18 @@ export default function PriceDetailsTable({
|
|||||||
})}
|
})}
|
||||||
<TableSection>
|
<TableSection>
|
||||||
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
<TableSectionHeader title={intl.formatMessage({ id: "Total" })} />
|
||||||
{
|
{!noVatCurrencies.includes(totalPrice.local.currency) ? (
|
||||||
// @ts-expect-error Currency type is string instead of CurrencyEnum. Change also impacts packages
|
<>
|
||||||
!noVatCurrencies.includes(totalPrice.local.currency) ? (
|
<Row
|
||||||
<>
|
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
||||||
<Row
|
value={formatPrice(intl, priceExclVat, totalPrice.local.currency)}
|
||||||
label={intl.formatMessage({ id: "Price excluding VAT" })}
|
/>
|
||||||
value={formatPrice(
|
<Row
|
||||||
intl,
|
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
||||||
priceExclVat,
|
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
||||||
totalPrice.local.currency
|
/>
|
||||||
)}
|
</>
|
||||||
/>
|
) : null}
|
||||||
<Row
|
|
||||||
label={intl.formatMessage({ id: "VAT {vat}%" }, { vat })}
|
|
||||||
value={formatPrice(intl, vatAmount, totalPrice.local.currency)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
<tr className={styles.row}>
|
<tr className={styles.row}>
|
||||||
<td>
|
<td>
|
||||||
<Body textTransform="bold">
|
<Body textTransform="bold">
|
||||||
|
|||||||
@@ -262,8 +262,7 @@ export default function SummaryUI({
|
|||||||
{formatPrice(
|
{formatPrice(
|
||||||
intl,
|
intl,
|
||||||
0,
|
0,
|
||||||
room.roomPrice.perStay.local.additionalPriceCurrency ??
|
room.roomPrice.perStay.local.currency
|
||||||
room.roomPrice.perStay.local.currency
|
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ function HotelCard({
|
|||||||
availability.productType?.member?.rateType === RateTypeEnum.Regular
|
availability.productType?.member?.rateType === RateTypeEnum.Regular
|
||||||
const price = availability.productType
|
const price = availability.productType
|
||||||
|
|
||||||
const userHasEnoughPoints = price?.redemptions?.some((r) => r.hasEnoughPoints)
|
const hasInsufficientPoints = !price?.redemptions?.some(
|
||||||
|
(r) => r.hasEnoughPoints
|
||||||
|
)
|
||||||
const notEnoughPointsLabel = intl.formatMessage({ id: "Not enough points" })
|
const notEnoughPointsLabel = intl.formatMessage({ id: "Not enough points" })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -211,7 +213,7 @@ function HotelCard({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{price?.redemptions?.length && !userHasEnoughPoints ? (
|
{price?.redemptions?.length && hasInsufficientPoints ? (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
arrow="left"
|
arrow="left"
|
||||||
position="bottom"
|
position="bottom"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import styles from "./specification.module.css"
|
|||||||
|
|
||||||
import type { SpecificationProps } from "@/types/components/hotelReservation/myStay/receipt"
|
import type { SpecificationProps } from "@/types/components/hotelReservation/myStay/receipt"
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
export default async function Specification({
|
export default async function Specification({
|
||||||
ancillaryPackages,
|
ancillaryPackages,
|
||||||
@@ -19,10 +20,10 @@ export default async function Specification({
|
|||||||
(p) => p.type === "Breakfast"
|
(p) => p.type === "Breakfast"
|
||||||
)
|
)
|
||||||
const breakfastTotalPriceInMoney = breakfastPackages
|
const breakfastTotalPriceInMoney = breakfastPackages
|
||||||
.filter((p) => p.currency !== "Points")
|
.filter((p) => p.currency !== CurrencyEnum.POINTS)
|
||||||
.reduce((acc, curr) => acc + curr.totalPrice, 0)
|
.reduce((acc, curr) => acc + curr.totalPrice, 0)
|
||||||
const breakfastTotalPriceInPoints = breakfastPackages
|
const breakfastTotalPriceInPoints = breakfastPackages
|
||||||
.filter((p) => p.currency === "Points")
|
.filter((p) => p.currency === CurrencyEnum.POINTS)
|
||||||
.reduce((acc, curr) => acc + curr.totalPrice, 0)
|
.reduce((acc, curr) => acc + curr.totalPrice, 0)
|
||||||
|
|
||||||
const breakfastCount = breakfastPackages.reduce(
|
const breakfastCount = breakfastPackages.reduce(
|
||||||
@@ -138,7 +139,7 @@ export default async function Specification({
|
|||||||
<dl className={styles.dl}>
|
<dl className={styles.dl}>
|
||||||
<Typography variant="Body/Supporting text (caption)/smRegular">
|
<Typography variant="Body/Supporting text (caption)/smRegular">
|
||||||
<dt>
|
<dt>
|
||||||
{ancillary.currency !== "Points"
|
{ancillary.currency !== CurrencyEnum.POINTS
|
||||||
? intl.formatMessage({ id: "Price including VAT" })
|
? intl.formatMessage({ id: "Price including VAT" })
|
||||||
: intl.formatMessage({ id: "Price" })}
|
: intl.formatMessage({ id: "Price" })}
|
||||||
</dt>
|
</dt>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getIntl } from "@/i18n"
|
|||||||
import styles from "./total.module.css"
|
import styles from "./total.module.css"
|
||||||
|
|
||||||
import type { TotalProps } from "@/types/components/hotelReservation/myStay/receipt"
|
import type { TotalProps } from "@/types/components/hotelReservation/myStay/receipt"
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
export default async function Total({ booking, currency }: TotalProps) {
|
export default async function Total({ booking, currency }: TotalProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
@@ -14,7 +15,7 @@ export default async function Total({ booking, currency }: TotalProps) {
|
|||||||
const totalPriceInMoneyExclVat = booking.totalPriceExVat
|
const totalPriceInMoneyExclVat = booking.totalPriceExVat
|
||||||
const totalVat = booking.vatAmount
|
const totalVat = booking.vatAmount
|
||||||
const totalPriceInPoints = booking.ancillaries
|
const totalPriceInPoints = booking.ancillaries
|
||||||
.filter((a) => a.currency === "Points")
|
.filter((a) => a.currency === CurrencyEnum.POINTS)
|
||||||
.reduce((acc, curr) => acc + curr.totalPrice, 0)
|
.reduce((acc, curr) => acc + curr.totalPrice, 0)
|
||||||
|
|
||||||
const moneyString =
|
const moneyString =
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import Total from "./Total"
|
|||||||
|
|
||||||
import styles from "./receipt.module.css"
|
import styles from "./receipt.module.css"
|
||||||
|
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
export async function Receipt({ refId }: { refId: string }) {
|
export async function Receipt({ refId }: { refId: string }) {
|
||||||
const value = decrypt(refId)
|
const value = decrypt(refId)
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -52,10 +54,12 @@ export async function Receipt({ refId }: { refId: string }) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const currency =
|
const currency =
|
||||||
booking.currencyCode !== "Points"
|
booking.currencyCode !== CurrencyEnum.POINTS
|
||||||
? booking.currencyCode
|
? booking.currencyCode
|
||||||
: (booking.ancillaries.find((a) => a.currency !== "Points")?.currency ??
|
: (booking.ancillaries.find((a) => a.currency !== CurrencyEnum.POINTS)
|
||||||
booking.packages.find((p) => p.currency !== "Points")?.currency)
|
?.currency ??
|
||||||
|
booking.packages.find((p) => p.currency !== CurrencyEnum.POINTS)
|
||||||
|
?.currency)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ export function calculateTotalPrice(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
local: {
|
local: {
|
||||||
currency: "",
|
currency: CurrencyEnum.Unknown,
|
||||||
price: 0,
|
price: 0,
|
||||||
regularPrice: undefined,
|
regularPrice: undefined,
|
||||||
},
|
},
|
||||||
@@ -103,7 +103,7 @@ export function calculateRedemptionTotalPrice(
|
|||||||
additionalPriceCurrency: redemption.localPrice.currency
|
additionalPriceCurrency: redemption.localPrice.currency
|
||||||
? redemption.localPrice.currency
|
? redemption.localPrice.currency
|
||||||
: undefined,
|
: undefined,
|
||||||
currency: "PTS",
|
currency: CurrencyEnum.POINTS,
|
||||||
price: redemption.localPrice.pointsPerStay,
|
price: redemption.localPrice.pointsPerStay,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
nullableStringValidator,
|
nullableStringValidator,
|
||||||
} from "@/utils/zod/stringValidator"
|
} from "@/utils/zod/stringValidator"
|
||||||
|
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
const guestSchema = z.object({
|
const guestSchema = z.object({
|
||||||
email: nullableStringEmailValidator,
|
email: nullableStringEmailValidator,
|
||||||
firstName: nullableStringValidator,
|
firstName: nullableStringValidator,
|
||||||
@@ -97,7 +99,7 @@ export const packageSchema = z
|
|||||||
unitPrice: z.number(),
|
unitPrice: z.number(),
|
||||||
totalPrice: z.number().nullish(),
|
totalPrice: z.number().nullish(),
|
||||||
totalUnit: z.number().int().nullish(),
|
totalUnit: z.number().int().nullish(),
|
||||||
currency: z.string().default(""),
|
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown),
|
||||||
points: nullableIntValidator,
|
points: nullableIntValidator,
|
||||||
}),
|
}),
|
||||||
comment: z.string().nullish(),
|
comment: z.string().nullish(),
|
||||||
@@ -218,7 +220,7 @@ export const bookingConfirmationSchema = z
|
|||||||
computedReservationStatus: z.string().nullable().default(""),
|
computedReservationStatus: z.string().nullable().default(""),
|
||||||
confirmationNumber: nullableStringValidator,
|
confirmationNumber: nullableStringValidator,
|
||||||
createDateTime: z.date({ coerce: true }),
|
createDateTime: z.date({ coerce: true }),
|
||||||
currencyCode: z.string(),
|
currencyCode: z.nativeEnum(CurrencyEnum),
|
||||||
guest: guestSchema,
|
guest: guestSchema,
|
||||||
linkedReservations: nullableArrayObjectValidator(
|
linkedReservations: nullableArrayObjectValidator(
|
||||||
linkedReservationSchema
|
linkedReservationSchema
|
||||||
|
|||||||
@@ -208,12 +208,12 @@ export const getHotel = cache(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export const getHotelsAvailabilityByCity = async (
|
async function getHotelsAvailabilityByCity(
|
||||||
input: HotelsAvailabilityInputSchema,
|
input: HotelsAvailabilityInputSchema,
|
||||||
apiLang: string,
|
apiLang: string,
|
||||||
token: string, // Either service token or user access token in case of redemption search
|
token: string, // Either service token or user access token in case of redemption search
|
||||||
session?: Session
|
session?: Session
|
||||||
) => {
|
) {
|
||||||
const {
|
const {
|
||||||
cityId,
|
cityId,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
@@ -223,131 +223,124 @@ export const getHotelsAvailabilityByCity = async (
|
|||||||
bookingCode,
|
bookingCode,
|
||||||
redemption,
|
redemption,
|
||||||
} = input
|
} = input
|
||||||
const cacheClient = await getCacheClient()
|
|
||||||
return await cacheClient.cacheOrGet(
|
|
||||||
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}:${redemption ? "isRedemption" : ""}`,
|
|
||||||
async () => {
|
|
||||||
const params: Record<string, string | number> = {
|
|
||||||
roomStayStartDate,
|
|
||||||
roomStayEndDate,
|
|
||||||
adults,
|
|
||||||
...(children && { children }),
|
|
||||||
...(bookingCode && { bookingCode }),
|
|
||||||
...(redemption ? { isRedemption: "true" } : {}),
|
|
||||||
language: apiLang,
|
|
||||||
}
|
|
||||||
metrics.hotelsAvailability.counter.add(1, {
|
|
||||||
cityId,
|
|
||||||
roomStayStartDate,
|
|
||||||
roomStayEndDate,
|
|
||||||
adults,
|
|
||||||
children,
|
|
||||||
bookingCode,
|
|
||||||
redemption,
|
|
||||||
})
|
|
||||||
console.info(
|
|
||||||
"api.hotels.hotelsAvailability start",
|
|
||||||
JSON.stringify({ query: { cityId, params } })
|
|
||||||
)
|
|
||||||
const apiResponse = await api.get(
|
|
||||||
api.endpoints.v1.Availability.city(cityId),
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
params
|
|
||||||
)
|
|
||||||
if (!apiResponse.ok) {
|
|
||||||
const text = await apiResponse.text()
|
|
||||||
metrics.hotelsAvailability.fail.add(1, {
|
|
||||||
cityId,
|
|
||||||
roomStayStartDate,
|
|
||||||
roomStayEndDate,
|
|
||||||
adults,
|
|
||||||
children,
|
|
||||||
bookingCode,
|
|
||||||
error_type: "http_error",
|
|
||||||
error: JSON.stringify({
|
|
||||||
status: apiResponse.status,
|
|
||||||
statusText: apiResponse.statusText,
|
|
||||||
text,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
console.error(
|
|
||||||
"api.hotels.hotelsAvailability error",
|
|
||||||
JSON.stringify({
|
|
||||||
query: { cityId, params },
|
|
||||||
error: {
|
|
||||||
status: apiResponse.status,
|
|
||||||
statusText: apiResponse.statusText,
|
|
||||||
text,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
throw new Error("Failed to fetch hotels availability by city")
|
const params: Record<string, string | number> = {
|
||||||
}
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
const apiJson = await apiResponse.json()
|
adults,
|
||||||
const validateAvailabilityData =
|
...(children && { children }),
|
||||||
hotelsAvailabilitySchema.safeParse(apiJson)
|
...(bookingCode && { bookingCode }),
|
||||||
if (!validateAvailabilityData.success) {
|
...(redemption ? { isRedemption: "true" } : {}),
|
||||||
metrics.hotelsAvailability.fail.add(1, {
|
language: apiLang,
|
||||||
cityId,
|
}
|
||||||
roomStayStartDate,
|
metrics.hotelsAvailability.counter.add(1, {
|
||||||
roomStayEndDate,
|
cityId,
|
||||||
adults,
|
roomStayStartDate,
|
||||||
children,
|
roomStayEndDate,
|
||||||
bookingCode,
|
adults,
|
||||||
redemption,
|
children,
|
||||||
error_type: "validation_error",
|
bookingCode,
|
||||||
error: JSON.stringify(validateAvailabilityData.error),
|
redemption,
|
||||||
})
|
})
|
||||||
console.error(
|
console.info(
|
||||||
"api.hotels.hotelsAvailability validation error",
|
"api.hotels.hotelsAvailability start",
|
||||||
JSON.stringify({
|
JSON.stringify({ query: { cityId, params } })
|
||||||
query: { cityId, params },
|
|
||||||
error: validateAvailabilityData.error,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
throw badRequestError()
|
|
||||||
}
|
|
||||||
metrics.hotelsAvailability.success.add(1, {
|
|
||||||
cityId,
|
|
||||||
roomStayStartDate,
|
|
||||||
roomStayEndDate,
|
|
||||||
adults,
|
|
||||||
children,
|
|
||||||
bookingCode,
|
|
||||||
redemption,
|
|
||||||
})
|
|
||||||
console.info(
|
|
||||||
"api.hotels.hotelsAvailability success",
|
|
||||||
JSON.stringify({
|
|
||||||
query: { cityId, params: params },
|
|
||||||
})
|
|
||||||
)
|
|
||||||
if (redemption && session) {
|
|
||||||
const verifiedUser = await getVerifiedUser({ session })
|
|
||||||
if (!verifiedUser?.error) {
|
|
||||||
const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0
|
|
||||||
validateAvailabilityData.data.data.forEach((data) => {
|
|
||||||
data.attributes.productType?.redemptions?.forEach((r) => {
|
|
||||||
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
availability: validateAvailabilityData.data.data.flatMap(
|
|
||||||
(hotels) => hotels.attributes
|
|
||||||
),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
env.CACHE_TIME_CITY_SEARCH
|
|
||||||
)
|
)
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Availability.city(cityId),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const text = await apiResponse.text()
|
||||||
|
metrics.hotelsAvailability.fail.add(1, {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.hotelsAvailability error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { cityId, params },
|
||||||
|
error: {
|
||||||
|
status: apiResponse.status,
|
||||||
|
statusText: apiResponse.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
throw new Error("Failed to fetch hotels availability by city")
|
||||||
|
}
|
||||||
|
|
||||||
|
const apiJson = await apiResponse.json()
|
||||||
|
const validateAvailabilityData = hotelsAvailabilitySchema.safeParse(apiJson)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
metrics.hotelsAvailability.fail.add(1, {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validateAvailabilityData.error),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.hotelsAvailability validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { cityId, params },
|
||||||
|
error: validateAvailabilityData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
metrics.hotelsAvailability.success.add(1, {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
redemption,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.hotelsAvailability success",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { cityId, params: params },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (redemption && session) {
|
||||||
|
const verifiedUser = await getVerifiedUser({ session })
|
||||||
|
if (!verifiedUser?.error) {
|
||||||
|
const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0
|
||||||
|
validateAvailabilityData.data.data.forEach((data) => {
|
||||||
|
data.attributes.productType?.redemptions?.forEach((r) => {
|
||||||
|
r.hasEnoughPoints = userPoints >= r.localPrice.pointsPerStay
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
availability: validateAvailabilityData.data.data.flatMap(
|
||||||
|
(hotels) => hotels.attributes
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHotelsAvailabilityByHotelIds = async (
|
export const getHotelsAvailabilityByHotelIds = async (
|
||||||
@@ -492,7 +485,23 @@ export const hotelQueryRouter = router({
|
|||||||
.query(async ({ input, ctx }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
const { lang } = ctx
|
const { lang } = ctx
|
||||||
const apiLang = toApiLang(lang)
|
const apiLang = toApiLang(lang)
|
||||||
return getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
const {
|
||||||
|
cityId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
bookingCode,
|
||||||
|
} = input
|
||||||
|
const cacheClient = await getCacheClient()
|
||||||
|
|
||||||
|
return await cacheClient.cacheOrGet(
|
||||||
|
`${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}`,
|
||||||
|
async () => {
|
||||||
|
return getHotelsAvailabilityByCity(input, apiLang, ctx.serviceToken)
|
||||||
|
},
|
||||||
|
env.CACHE_TIME_CITY_SEARCH
|
||||||
|
)
|
||||||
}),
|
}),
|
||||||
hotelsByCityWithRedemption: protectedProcedure
|
hotelsByCityWithRedemption: protectedProcedure
|
||||||
.input(hotelsAvailabilityInputSchema)
|
.input(hotelsAvailabilityInputSchema)
|
||||||
@@ -503,7 +512,7 @@ export const hotelQueryRouter = router({
|
|||||||
input,
|
input,
|
||||||
apiLang,
|
apiLang,
|
||||||
ctx.session.token.access_token,
|
ctx.session.token.access_token,
|
||||||
ctx.session,
|
ctx.session
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
hotelsByHotelIds: serviceProcedure
|
hotelsByHotelIds: serviceProcedure
|
||||||
@@ -519,12 +528,16 @@ export const hotelQueryRouter = router({
|
|||||||
.use(async ({ ctx, input, next }) => {
|
.use(async ({ ctx, input, next }) => {
|
||||||
if (input.redemption) {
|
if (input.redemption) {
|
||||||
if (ctx.session?.token.access_token) {
|
if (ctx.session?.token.access_token) {
|
||||||
return next({
|
const verifiedUser = await getVerifiedUser({ session: ctx.session })
|
||||||
ctx: {
|
if (!verifiedUser?.error) {
|
||||||
token: ctx.session.token.access_token,
|
return next({
|
||||||
},
|
ctx: {
|
||||||
input,
|
token: ctx.session.token.access_token,
|
||||||
})
|
userPoints: verifiedUser?.data.membership?.currentPoints ?? 0,
|
||||||
|
},
|
||||||
|
input,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
throw unauthorizedError()
|
throw unauthorizedError()
|
||||||
}
|
}
|
||||||
@@ -622,16 +635,15 @@ export const hotelQueryRouter = router({
|
|||||||
)?.mustBeGuaranteed
|
)?.mustBeGuaranteed
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redemption && ctx.session) {
|
if (redemption) {
|
||||||
const verifiedUser = await getVerifiedUser({ session: ctx.session })
|
validateAvailabilityData.data.roomConfigurations.forEach(
|
||||||
if (!verifiedUser?.error) {
|
(data) => {
|
||||||
const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0
|
data.redemptions?.forEach((r) => {
|
||||||
validateAvailabilityData.data.roomConfigurations.forEach((data) => {
|
r.redemption.hasEnoughPoints =
|
||||||
data.redemptions?.forEach(r => {
|
ctx.userPoints >= r.redemption.localPrice.pointsPerStay
|
||||||
r.redemption.hasEnoughPoints = userPoints >= r.redemption.localPrice.pointsPerStay
|
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return validateAvailabilityData.data
|
return validateAvailabilityData.data
|
||||||
|
|||||||
@@ -4,18 +4,19 @@ import { imageSizesSchema } from "./image"
|
|||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import { PackageTypeEnum } from "@/types/enums/packages"
|
import { PackageTypeEnum } from "@/types/enums/packages"
|
||||||
|
|
||||||
// TODO: Remove optional and default when the API change has been deployed
|
// TODO: Remove optional and default when the API change has been deployed
|
||||||
export const packagePriceSchema = z
|
export const packagePriceSchema = z
|
||||||
.object({
|
.object({
|
||||||
currency: z.string().default("N/A"),
|
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown),
|
||||||
price: z.number(),
|
price: z.number(),
|
||||||
totalPrice: z.number(),
|
totalPrice: z.number(),
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.default({
|
.default({
|
||||||
currency: "N/A",
|
currency: CurrencyEnum.Unknown,
|
||||||
price: 0,
|
price: 0,
|
||||||
totalPrice: 0,
|
totalPrice: 0,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDet
|
|||||||
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
|
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay"
|
import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay"
|
||||||
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
import type { Packages } from "@/types/requests/packages"
|
import type { Packages } from "@/types/requests/packages"
|
||||||
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
|
||||||
|
|
||||||
@@ -73,7 +74,7 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
|
|||||||
bookingCode: null,
|
bookingCode: null,
|
||||||
cheques: 0,
|
cheques: 0,
|
||||||
vouchers: 0,
|
vouchers: 0,
|
||||||
currencyCode: "",
|
currencyCode: CurrencyEnum.Unknown,
|
||||||
guest: {
|
guest: {
|
||||||
email: "",
|
email: "",
|
||||||
firstName: "",
|
firstName: "",
|
||||||
@@ -97,21 +98,21 @@ export const useMyStayRoomDetailsStore = create<MyStayRoomDetailsState>(
|
|||||||
perNight: {
|
perNight: {
|
||||||
requested: {
|
requested: {
|
||||||
price: 0,
|
price: 0,
|
||||||
currency: "",
|
currency: CurrencyEnum.Unknown,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
price: 0,
|
price: 0,
|
||||||
currency: "",
|
currency: CurrencyEnum.Unknown,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
perStay: {
|
perStay: {
|
||||||
requested: {
|
requested: {
|
||||||
price: 0,
|
price: 0,
|
||||||
currency: "",
|
currency: CurrencyEnum.Unknown,
|
||||||
},
|
},
|
||||||
local: {
|
local: {
|
||||||
price: 0,
|
price: 0,
|
||||||
currency: "",
|
currency: CurrencyEnum.Unknown,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { create } from "zustand"
|
import { create } from "zustand"
|
||||||
|
|
||||||
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
interface RoomPrice {
|
interface RoomPrice {
|
||||||
id: string
|
id: string
|
||||||
totalPrice: number
|
totalPrice: number
|
||||||
currencyCode: string
|
currencyCode: CurrencyEnum
|
||||||
isMainBooking?: boolean
|
isMainBooking?: boolean
|
||||||
roomPoints: number
|
roomPoints: number
|
||||||
}
|
}
|
||||||
@@ -11,7 +13,7 @@ interface RoomPrice {
|
|||||||
interface MyStayTotalPriceState {
|
interface MyStayTotalPriceState {
|
||||||
rooms: RoomPrice[]
|
rooms: RoomPrice[]
|
||||||
totalPrice: number | null
|
totalPrice: number | null
|
||||||
currencyCode: string
|
currencyCode: CurrencyEnum
|
||||||
totalPoints: number
|
totalPoints: number
|
||||||
actions: {
|
actions: {
|
||||||
// Add a single room price
|
// Add a single room price
|
||||||
@@ -26,7 +28,7 @@ export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
|
|||||||
totalPoints: 0,
|
totalPoints: 0,
|
||||||
totalCheques: 0,
|
totalCheques: 0,
|
||||||
totalVouchers: 0,
|
totalVouchers: 0,
|
||||||
currencyCode: "",
|
currencyCode: CurrencyEnum.Unknown,
|
||||||
actions: {
|
actions: {
|
||||||
addRoomPrice: (room) => {
|
addRoomPrice: (room) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -44,7 +46,7 @@ export const useMyStayTotalPriceStore = create<MyStayTotalPriceState>(
|
|||||||
|
|
||||||
// Get currency from main booking or first room
|
// Get currency from main booking or first room
|
||||||
const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0]
|
const mainRoom = newRooms.find((r) => r.isMainBooking) || newRooms[0]
|
||||||
const currencyCode = mainRoom?.currencyCode || ""
|
const currencyCode = mainRoom?.currencyCode ?? CurrencyEnum.Unknown
|
||||||
|
|
||||||
// Calculate total (only same currency for now)
|
// Calculate total (only same currency for now)
|
||||||
const total = newRooms.reduce((sum, r) => {
|
const total = newRooms.reduce((sum, r) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { z } from "zod"
|
|||||||
import { CurrencyEnum } from "@/types/enums/currency"
|
import { CurrencyEnum } from "@/types/enums/currency"
|
||||||
|
|
||||||
interface TPrice {
|
interface TPrice {
|
||||||
currency: string
|
currency: CurrencyEnum
|
||||||
price: number
|
price: number
|
||||||
regularPrice?: number
|
regularPrice?: number
|
||||||
additionalPrice?: number
|
additionalPrice?: number
|
||||||
|
|||||||
Reference in New Issue
Block a user