diff --git a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts index 2aaffa0f6..cc2af5e1c 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/PriceDetails/mapToPrice.ts @@ -71,6 +71,16 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) { break case PriceTypeEnum.points: { + if ( + room.roomPoints && + room.roomPointType && + room.roomPointType !== "Scandic" + ) { + total.local.currency = + roomPointTypeToCurrencyMap[room.roomPointType] + total.local.price = total.local.price + room.roomPoints + break + } total.local.currency = CurrencyEnum.POINTS total.local.price = total.local.price + room.totalPoints } @@ -135,3 +145,11 @@ export function calculateTotalPrice(rooms: Room[], currency: CurrencyEnum) { } ) } + +const roomPointTypeToCurrencyMap: Record< + NonNullable, + CurrencyEnum +> = { + Scandic: CurrencyEnum.POINTS, + EuroBonus: CurrencyEnum.EUROBONUS, +} diff --git a/apps/scandic-web/stores/my-stay/helpers.test.ts b/apps/scandic-web/stores/my-stay/helpers.test.ts new file mode 100644 index 000000000..dc538c2d4 --- /dev/null +++ b/apps/scandic-web/stores/my-stay/helpers.test.ts @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { beforeAll, describe, expect, it, vi } from "vitest" + +import { calculateTotalPrice } from "./helpers" + +describe("calculateTotalPrice", () => { + const baseRoom: Parameters[0][0] = { + totalPrice: 0, + isCancelled: false, + cheques: 0, + roomPoints: 0, + roomPointType: null, + totalPoints: 0, + vouchers: 0, + } + + const mockIntlSimple = { + formatMessage: vi.fn(({}, values) => { + if (values?.numberOfVouchers === 1) return "Voucher" + return "Vouchers" + }), + formatNumber: vi.fn((num) => String(num)), + } as any + + vi.mock("@scandic-hotels/common/utils/numberFormatting", () => ({ + formatPrice: (_intl: any, price: number, currency: string) => + `${price} ${currency}`, + })) + + beforeAll(() => { + vi.clearAllMocks() + }) + + it("should return correct price for conflicting rooms (cash)", () => { + const rooms = [ + { totalPrice: 1000, isCancelled: false }, + { totalPrice: 500, isCancelled: false }, + ] as any + + const result = calculateTotalPrice( + rooms, + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toBe("1500 SEK") + }) + + it("should ignore cancelled rooms if not all are cancelled", () => { + vi.mock("@scandic-hotels/common/utils/numberFormatting", () => ({ + formatPrice: (_intl: any, price: number, currency: string) => + `${price} ${currency}`, + })) + + const rooms = [ + { totalPrice: 1000, isCancelled: false }, + { totalPrice: 500, isCancelled: true }, + ] as any + + const result = calculateTotalPrice( + rooms, + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toBe("1000 SEK") + }) + + it("should format string for Vouchers", () => { + const result = calculateTotalPrice( + [{ ...baseRoom, vouchers: 2, totalPrice: -1, isCancelled: false }], + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toContain("2 Vouchers") + }) + + it("should handle mixed Cash and Points", () => { + const result = calculateTotalPrice( + [ + { + ...baseRoom, + totalPrice: 100, + isCancelled: false, + totalPoints: 0, + roomPoints: 0, + }, + { + ...baseRoom, + totalPrice: 0, + totalPoints: 20000, + roomPoints: 20000, + roomPointType: "Scandic", + isCancelled: false, + }, + ], + "SEK" as any, + mockIntlSimple, + false + ) + + expect(result).toMatch(/20000 Points \+ 100 SEK/) + }) + + it("should sum up Cheques correctly", () => { + const rooms = [{ cheques: 2, isCancelled: false }] as any + // CurrencyEnum.CC is usually imported, assuming it resolves or we check specific output + const result = calculateTotalPrice( + rooms, + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toContain("2 CC") + }) + + it("should combine Vouchers and Cash", () => { + const result = calculateTotalPrice( + [ + { + ...baseRoom, + vouchers: 1, + totalPrice: -1, + isCancelled: false, + }, + { + ...baseRoom, + totalPrice: 500, + isCancelled: false, + }, + ], + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toMatch(/1 Voucher \+ 500 SEK/) + }) + + it("should combine Eurobonus points and Cash", () => { + const result = calculateTotalPrice( + [ + { + ...baseRoom, + roomPoints: 1, + roomPointType: "EuroBonus", + }, + { + ...baseRoom, + totalPrice: 500, + }, + ], + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toMatch(/1 EuroBonus \+ 500 SEK/) + }) + + it("should combine Eurobonus points, Scandic Friends points and Cash", () => { + const result = calculateTotalPrice( + [ + { + ...baseRoom, + roomPoints: 1, + roomPointType: "EuroBonus", + totalPoints: 500, + }, + { + ...baseRoom, + totalPrice: 500, + }, + ], + "SEK" as any, + mockIntlSimple, + false + ) + expect(result).toMatch(/500 Points \+ 1 EuroBonus \+ 500 SEK/) + }) +}) diff --git a/apps/scandic-web/stores/my-stay/helpers.ts b/apps/scandic-web/stores/my-stay/helpers.ts index e306b33a3..1f52aa81c 100644 --- a/apps/scandic-web/stores/my-stay/helpers.ts +++ b/apps/scandic-web/stores/my-stay/helpers.ts @@ -6,7 +6,16 @@ import type { IntlShape } from "react-intl" import type { Room } from "@/types/stores/my-stay" export function calculateTotalPrice( - rooms: Room[], + rooms: Pick< + Room, + | "cheques" + | "vouchers" + | "roomPoints" + | "roomPointType" + | "totalPoints" + | "totalPrice" + | "isCancelled" + >[], currency: CurrencyEnum, intl: IntlShape, allRoomsAreCancelled: boolean @@ -20,55 +29,77 @@ export function calculateTotalPrice( if (room.cheques) { total.cheques = total.cheques + room.cheques } + if (room.vouchers) { total.vouchers = total.vouchers + room.vouchers } - if (room.totalPoints) { - total.points = total.points + room.totalPoints + + if ( + room.roomPoints && + room.roomPointType && + room.roomPointType !== "Scandic" + ) { + total.partnerPoints = total.partnerPoints + room.roomPoints + total.partnerPointsCurrency = room.roomPointType ?? null } + + if (room.totalPoints) { + total.scandicFriendsPoints = + total.scandicFriendsPoints + room.totalPoints + } + // room.totalPrice is a negative value when // its a vouchers booking (╯°□°)╯︵ ┻━┻ if (room.totalPrice > 0) { total.cash = total.cash + room.totalPrice } + return total }, { cash: 0, cheques: 0, - points: 0, + scandicFriendsPoints: 0, + partnerPoints: 0, + partnerPointsCurrency: null as Room["roomPointType"], vouchers: 0, } ) - let totalPrice = "" + const priceParts: string[] = [] if (totals.vouchers) { - const appendTotalPrice = totalPrice ? `${totalPrice} + ` : "" - totalPrice = `${appendTotalPrice}${totals.vouchers} ${intl.formatMessage( - { - id: "price.numberOfVouchers", - defaultMessage: - "{numberOfVouchers, plural, one {Voucher} other {Vouchers}}", - }, - { - numberOfVouchers: totals.vouchers, - } - )}` - } - if (totals.cheques) { - totalPrice = `${totals.cheques} ${CurrencyEnum.CC}` - } - if (totals.points) { - const appendTotalPrice = totalPrice ? `${totalPrice} + ` : "" - totalPrice = `${appendTotalPrice}${totals.points} ${CurrencyEnum.POINTS}` - } - if (totals.cash) { - const appendTotalPrice = totalPrice ? `${totalPrice} + ` : "" - const cashPrice = formatPrice(intl, totals.cash, currency) - totalPrice = `${appendTotalPrice}${cashPrice}` + priceParts.push( + `${totals.vouchers} ${intl.formatMessage( + { + id: "price.numberOfVouchers", + defaultMessage: + "{numberOfVouchers, plural, one {Voucher} other {Vouchers}}", + }, + { + numberOfVouchers: totals.vouchers, + } + )}` + ) } - return totalPrice + if (totals.cheques) { + priceParts.push(`${totals.cheques} ${CurrencyEnum.CC}`) + } + + if (totals.scandicFriendsPoints) { + priceParts.push(`${totals.scandicFriendsPoints} ${CurrencyEnum.POINTS}`) + } + + if (totals.partnerPoints) { + priceParts.push(`${totals.partnerPoints} ${totals.partnerPointsCurrency}`) + } + + if (totals.cash) { + const cashPrice = formatPrice(intl, totals.cash, currency) + priceParts.push(cashPrice) + } + + return priceParts.join(" + ") } export function calculateTotalPoints( diff --git a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx index 8dc92dcb8..d38f41987 100644 --- a/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx +++ b/packages/booking-flow/lib/components/PriceDetailsModal/PriceDetailsTable/Row/Vat.tsx @@ -17,6 +17,7 @@ interface VatProps { const noVatCurrencies = [ CurrencyEnum.CC, CurrencyEnum.POINTS, + CurrencyEnum.EUROBONUS, CurrencyEnum.Voucher, CurrencyEnum.Unknown, ] diff --git a/packages/trpc/lib/routers/booking/output.ts b/packages/trpc/lib/routers/booking/output.ts index bb4597bc2..ec3ff9ff8 100644 --- a/packages/trpc/lib/routers/booking/output.ts +++ b/packages/trpc/lib/routers/booking/output.ts @@ -176,6 +176,7 @@ export const bookingConfirmationSchema = z rateDefinition: rateDefinitionSchema, reservationStatus: z.string().nullable().default(""), roomPoints: z.number(), + roomPointType: z.nullable(z.enum(["Scandic", "EuroBonus"])).catch(null), roomPrice: z.number(), roomTypeCode: z.string().default(""), totalPoints: z.number(),