diff --git a/apps/scandic-web/components/Forms/BookingWidget/FormContent/RewardNight/index.tsx b/apps/scandic-web/components/Forms/BookingWidget/FormContent/RewardNight/index.tsx index a85d2213b..ec28ddcf7 100644 --- a/apps/scandic-web/components/Forms/BookingWidget/FormContent/RewardNight/index.tsx +++ b/apps/scandic-web/components/Forms/BookingWidget/FormContent/RewardNight/index.tsx @@ -5,13 +5,13 @@ import { useFormContext } from "react-hook-form" import { useIntl } from "react-intl" import { MaterialIcon } from "@scandic-hotels/design-system/Icons" +import { Typography } from "@scandic-hotels/design-system/Typography" import { REDEMPTION } from "@/constants/booking" import Modal from "@/components/Modal" import Button from "@/components/TempDesignSystem/Button" import Checkbox from "@/components/TempDesignSystem/Form/Checkbox" -import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import { RemoveExtraRooms } from "../BookingCode" @@ -97,19 +97,19 @@ export default function RewardNight() { } title={reward} > - - {rewardNightTooltip} - + {rewardNightTooltip} + diff --git a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx index 379f7af50..ce6fd5fcc 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/PriceDetailsTable/index.tsx @@ -138,7 +138,8 @@ export default function PriceDetailsTable({ "corporateCheque" in room.roomRate ? room.roomRate.corporateCheque : 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) { return null } @@ -283,25 +284,18 @@ export default function PriceDetailsTable({ })} - { - // @ts-expect-error Currency type is string instead of CurrencyEnum. Change also impacts packages - !noVatCurrencies.includes(totalPrice.local.currency) ? ( - <> - - - - ) : null - } + {!noVatCurrencies.includes(totalPrice.local.currency) ? ( + <> + + + + ) : null} 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 06bf48ce6..83828369f 100644 --- a/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx +++ b/apps/scandic-web/components/HotelReservation/EnterDetails/Summary/UI/index.tsx @@ -262,8 +262,7 @@ export default function SummaryUI({ {formatPrice( intl, 0, - room.roomPrice.perStay.local.additionalPriceCurrency ?? - room.roomPrice.perStay.local.currency + room.roomPrice.perStay.local.currency )} diff --git a/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx b/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx index d730dd7e5..ddf9cd558 100644 --- a/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx +++ b/apps/scandic-web/components/HotelReservation/HotelCard/index.tsx @@ -73,7 +73,9 @@ function HotelCard({ availability.productType?.member?.rateType === RateTypeEnum.Regular 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" }) return ( @@ -211,7 +213,7 @@ function HotelCard({ ))} ) : null} - {price?.redemptions?.length && !userHasEnoughPoints ? ( + {price?.redemptions?.length && hasInsufficientPoints ? ( p.type === "Breakfast" ) const breakfastTotalPriceInMoney = breakfastPackages - .filter((p) => p.currency !== "Points") + .filter((p) => p.currency !== CurrencyEnum.POINTS) .reduce((acc, curr) => acc + curr.totalPrice, 0) const breakfastTotalPriceInPoints = breakfastPackages - .filter((p) => p.currency === "Points") + .filter((p) => p.currency === CurrencyEnum.POINTS) .reduce((acc, curr) => acc + curr.totalPrice, 0) const breakfastCount = breakfastPackages.reduce( @@ -138,7 +139,7 @@ export default async function Specification({
- {ancillary.currency !== "Points" + {ancillary.currency !== CurrencyEnum.POINTS ? intl.formatMessage({ id: "Price including VAT" }) : intl.formatMessage({ id: "Price" })}
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Receipt/Total/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Receipt/Total/index.tsx index 78109caf5..3060d3021 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Receipt/Total/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Receipt/Total/index.tsx @@ -6,6 +6,7 @@ import { getIntl } from "@/i18n" import styles from "./total.module.css" import type { TotalProps } from "@/types/components/hotelReservation/myStay/receipt" +import { CurrencyEnum } from "@/types/enums/currency" export default async function Total({ booking, currency }: TotalProps) { const intl = await getIntl() @@ -14,7 +15,7 @@ export default async function Total({ booking, currency }: TotalProps) { const totalPriceInMoneyExclVat = booking.totalPriceExVat const totalVat = booking.vatAmount const totalPriceInPoints = booking.ancillaries - .filter((a) => a.currency === "Points") + .filter((a) => a.currency === CurrencyEnum.POINTS) .reduce((acc, curr) => acc + curr.totalPrice, 0) const moneyString = diff --git a/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx index 2f3798db2..47f28cd6c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx +++ b/apps/scandic-web/components/HotelReservation/MyStay/Receipt/index.tsx @@ -26,6 +26,8 @@ import Total from "./Total" import styles from "./receipt.module.css" +import { CurrencyEnum } from "@/types/enums/currency" + export async function Receipt({ refId }: { refId: string }) { const value = decrypt(refId) if (!value) { @@ -52,10 +54,12 @@ export async function Receipt({ refId }: { refId: string }) { }) const currency = - booking.currencyCode !== "Points" + booking.currencyCode !== CurrencyEnum.POINTS ? booking.currencyCode - : (booking.ancillaries.find((a) => a.currency !== "Points")?.currency ?? - booking.packages.find((p) => p.currency !== "Points")?.currency) + : (booking.ancillaries.find((a) => a.currency !== CurrencyEnum.POINTS) + ?.currency ?? + booking.packages.find((p) => p.currency !== CurrencyEnum.POINTS) + ?.currency) return (
diff --git a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts index 4849477b9..190ae82d5 100644 --- a/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts +++ b/apps/scandic-web/components/HotelReservation/SelectRate/RoomsContainer/RateSummary/utils.ts @@ -83,7 +83,7 @@ export function calculateTotalPrice( }, { local: { - currency: "", + currency: CurrencyEnum.Unknown, price: 0, regularPrice: undefined, }, @@ -103,7 +103,7 @@ export function calculateRedemptionTotalPrice( additionalPriceCurrency: redemption.localPrice.currency ? redemption.localPrice.currency : undefined, - currency: "PTS", + currency: CurrencyEnum.POINTS, price: redemption.localPrice.pointsPerStay, }, } diff --git a/apps/scandic-web/server/routers/booking/output.ts b/apps/scandic-web/server/routers/booking/output.ts index b9f827ce5..d490e5c5f 100644 --- a/apps/scandic-web/server/routers/booking/output.ts +++ b/apps/scandic-web/server/routers/booking/output.ts @@ -9,6 +9,8 @@ import { nullableStringValidator, } from "@/utils/zod/stringValidator" +import { CurrencyEnum } from "@/types/enums/currency" + const guestSchema = z.object({ email: nullableStringEmailValidator, firstName: nullableStringValidator, @@ -97,7 +99,7 @@ export const packageSchema = z unitPrice: z.number(), totalPrice: z.number().nullish(), totalUnit: z.number().int().nullish(), - currency: z.string().default(""), + currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown), points: nullableIntValidator, }), comment: z.string().nullish(), @@ -218,7 +220,7 @@ export const bookingConfirmationSchema = z computedReservationStatus: z.string().nullable().default(""), confirmationNumber: nullableStringValidator, createDateTime: z.date({ coerce: true }), - currencyCode: z.string(), + currencyCode: z.nativeEnum(CurrencyEnum), guest: guestSchema, linkedReservations: nullableArrayObjectValidator( linkedReservationSchema diff --git a/apps/scandic-web/server/routers/hotels/query.ts b/apps/scandic-web/server/routers/hotels/query.ts index 5ba49a66b..5a59c1bb1 100644 --- a/apps/scandic-web/server/routers/hotels/query.ts +++ b/apps/scandic-web/server/routers/hotels/query.ts @@ -208,12 +208,12 @@ export const getHotel = cache( } ) -export const getHotelsAvailabilityByCity = async ( +async function getHotelsAvailabilityByCity( input: HotelsAvailabilityInputSchema, apiLang: string, token: string, // Either service token or user access token in case of redemption search session?: Session -) => { +) { const { cityId, roomStayStartDate, @@ -223,131 +223,124 @@ export const getHotelsAvailabilityByCity = async ( bookingCode, redemption, } = input - const cacheClient = await getCacheClient() - return await cacheClient.cacheOrGet( - `${cityId}:${roomStayStartDate}:${roomStayEndDate}:${adults}:${children}:${bookingCode}:${redemption ? "isRedemption" : ""}`, - async () => { - const params: Record = { - 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 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 - ), - } - }, - env.CACHE_TIME_CITY_SEARCH + const params: Record = { + 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 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 ( @@ -492,7 +485,23 @@ export const hotelQueryRouter = router({ .query(async ({ input, ctx }) => { const { lang } = ctx 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 .input(hotelsAvailabilityInputSchema) @@ -503,7 +512,7 @@ export const hotelQueryRouter = router({ input, apiLang, ctx.session.token.access_token, - ctx.session, + ctx.session ) }), hotelsByHotelIds: serviceProcedure @@ -519,12 +528,16 @@ export const hotelQueryRouter = router({ .use(async ({ ctx, input, next }) => { if (input.redemption) { if (ctx.session?.token.access_token) { - return next({ - ctx: { - token: ctx.session.token.access_token, - }, - input, - }) + const verifiedUser = await getVerifiedUser({ session: ctx.session }) + if (!verifiedUser?.error) { + return next({ + ctx: { + token: ctx.session.token.access_token, + userPoints: verifiedUser?.data.membership?.currentPoints ?? 0, + }, + input, + }) + } } throw unauthorizedError() } @@ -622,16 +635,15 @@ export const hotelQueryRouter = router({ )?.mustBeGuaranteed } - if (redemption && ctx.session) { - const verifiedUser = await getVerifiedUser({ session: ctx.session }) - if (!verifiedUser?.error) { - const userPoints = verifiedUser?.data.membership?.currentPoints ?? 0 - validateAvailabilityData.data.roomConfigurations.forEach((data) => { - data.redemptions?.forEach(r => { - r.redemption.hasEnoughPoints = userPoints >= r.redemption.localPrice.pointsPerStay + if (redemption) { + validateAvailabilityData.data.roomConfigurations.forEach( + (data) => { + data.redemptions?.forEach((r) => { + r.redemption.hasEnoughPoints = + ctx.userPoints >= r.redemption.localPrice.pointsPerStay }) - }) - } + } + ) } return validateAvailabilityData.data diff --git a/apps/scandic-web/server/routers/hotels/schemas/packages.ts b/apps/scandic-web/server/routers/hotels/schemas/packages.ts index fc2bd1860..3e1820d52 100644 --- a/apps/scandic-web/server/routers/hotels/schemas/packages.ts +++ b/apps/scandic-web/server/routers/hotels/schemas/packages.ts @@ -4,18 +4,19 @@ import { imageSizesSchema } from "./image" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { BreakfastPackageEnum } from "@/types/enums/breakfast" +import { CurrencyEnum } from "@/types/enums/currency" import { PackageTypeEnum } from "@/types/enums/packages" // TODO: Remove optional and default when the API change has been deployed export const packagePriceSchema = z .object({ - currency: z.string().default("N/A"), + currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown), price: z.number(), totalPrice: z.number(), }) .optional() .default({ - currency: "N/A", + currency: CurrencyEnum.Unknown, price: 0, totalPrice: 0, }) diff --git a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts b/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts index f81eba050..caf709091 100644 --- a/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts +++ b/apps/scandic-web/stores/my-stay/myStayRoomDetailsStore.ts @@ -5,6 +5,7 @@ import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDet import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details" import type { PriceType } from "@/types/components/hotelReservation/myStay/myStay" import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import { CurrencyEnum } from "@/types/enums/currency" import type { Packages } from "@/types/requests/packages" import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation" @@ -73,7 +74,7 @@ export const useMyStayRoomDetailsStore = create( bookingCode: null, cheques: 0, vouchers: 0, - currencyCode: "", + currencyCode: CurrencyEnum.Unknown, guest: { email: "", firstName: "", @@ -97,21 +98,21 @@ export const useMyStayRoomDetailsStore = create( perNight: { requested: { price: 0, - currency: "", + currency: CurrencyEnum.Unknown, }, local: { price: 0, - currency: "", + currency: CurrencyEnum.Unknown, }, }, perStay: { requested: { price: 0, - currency: "", + currency: CurrencyEnum.Unknown, }, local: { price: 0, - currency: "", + currency: CurrencyEnum.Unknown, }, }, }, diff --git a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts index aeb824e34..1f6ca5594 100644 --- a/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts +++ b/apps/scandic-web/stores/my-stay/myStayTotalPrice.ts @@ -1,9 +1,11 @@ import { create } from "zustand" +import { CurrencyEnum } from "@/types/enums/currency" + interface RoomPrice { id: string totalPrice: number - currencyCode: string + currencyCode: CurrencyEnum isMainBooking?: boolean roomPoints: number } @@ -11,7 +13,7 @@ interface RoomPrice { interface MyStayTotalPriceState { rooms: RoomPrice[] totalPrice: number | null - currencyCode: string + currencyCode: CurrencyEnum totalPoints: number actions: { // Add a single room price @@ -26,7 +28,7 @@ export const useMyStayTotalPriceStore = create( totalPoints: 0, totalCheques: 0, totalVouchers: 0, - currencyCode: "", + currencyCode: CurrencyEnum.Unknown, actions: { addRoomPrice: (room) => { set((state) => { @@ -44,7 +46,7 @@ export const useMyStayTotalPriceStore = create( // Get currency from main booking or first room 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) const total = newRooms.reduce((sum, r) => { diff --git a/apps/scandic-web/types/components/hotelReservation/price.ts b/apps/scandic-web/types/components/hotelReservation/price.ts index d03abe2f4..bcdc91d15 100644 --- a/apps/scandic-web/types/components/hotelReservation/price.ts +++ b/apps/scandic-web/types/components/hotelReservation/price.ts @@ -3,7 +3,7 @@ import { z } from "zod" import { CurrencyEnum } from "@/types/enums/currency" interface TPrice { - currency: string + currency: CurrencyEnum price: number regularPrice?: number additionalPrice?: number