Merged in fix/refactor-currency-display (pull request #3434)

fix(SW-3616): Handle EuroBonus point type everywhere

* Add tests to formatPrice

* formatPrice

* More work replacing config with api points type

* More work replacing config with api points type

* More fixing with currency

* maybe actually fixed it

* Fix MyStay

* Clean up

* Fix comments

* Merge branch 'master' into fix/refactor-currency-display

* Fix calculateTotalPrice for EB points + SF points + cash


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2026-01-15 09:32:17 +00:00
parent c61ddaf94d
commit 16fbdb7ae0
59 changed files with 729 additions and 282 deletions

View File

@@ -1,27 +1,25 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createIntl, createIntlCache } from "react-intl"
import { beforeAll, describe, expect, it, vi } from "vitest"
import { calculateTotalPrice } from "./helpers"
const cache = createIntlCache()
const createTestIntl = (locale: string = "en-US") =>
createIntl({ locale, messages: {}, onError: () => {} }, cache)
describe("calculateTotalPrice", () => {
const baseRoom: Parameters<typeof calculateTotalPrice>[0][0] = {
totalPrice: 0,
isCancelled: false,
cheques: 0,
roomPoints: 0,
roomPointType: null,
totalPoints: 0,
roomPoints: 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}`,
@@ -40,7 +38,7 @@ describe("calculateTotalPrice", () => {
const result = calculateTotalPrice(
rooms,
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toBe("1500 SEK")
@@ -60,7 +58,7 @@ describe("calculateTotalPrice", () => {
const result = calculateTotalPrice(
rooms,
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toBe("1000 SEK")
@@ -70,7 +68,7 @@ describe("calculateTotalPrice", () => {
const result = calculateTotalPrice(
[{ ...baseRoom, vouchers: 2, totalPrice: -1, isCancelled: false }],
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toContain("2 Vouchers")
@@ -84,19 +82,17 @@ describe("calculateTotalPrice", () => {
totalPrice: 100,
isCancelled: false,
totalPoints: 0,
roomPoints: 0,
},
{
...baseRoom,
totalPrice: 0,
totalPoints: 20000,
roomPoints: 20000,
roomPointType: "Scandic",
isCancelled: false,
},
],
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
@@ -109,7 +105,7 @@ describe("calculateTotalPrice", () => {
const result = calculateTotalPrice(
rooms,
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toContain("2 CC")
@@ -131,7 +127,7 @@ describe("calculateTotalPrice", () => {
},
],
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toMatch(/1 Voucher \+ 500 SEK/)
@@ -151,10 +147,10 @@ describe("calculateTotalPrice", () => {
},
],
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toMatch(/1 EuroBonus \+ 500 SEK/)
expect(result).toMatch(/1 EB Point \+ 500 SEK/)
})
it("should combine Eurobonus points, Scandic Friends points and Cash", () => {
@@ -172,9 +168,9 @@ describe("calculateTotalPrice", () => {
},
],
"SEK" as any,
mockIntlSimple,
createTestIntl(),
false
)
expect(result).toMatch(/500 Points \+ 1 EuroBonus \+ 500 SEK/)
expect(result).toMatch(/1 EB Point \+ 500 Points \+ 500 SEK/)
})
})

View File

@@ -1,4 +1,5 @@
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { PointType } from "@scandic-hotels/common/constants/pointType"
import { formatPrice } from "@scandic-hotels/common/utils/numberFormatting"
import type { IntlShape } from "react-intl"
@@ -10,8 +11,8 @@ export function calculateTotalPrice(
Room,
| "cheques"
| "vouchers"
| "roomPoints"
| "roomPointType"
| "roomPoints"
| "totalPoints"
| "totalPrice"
| "isCancelled"
@@ -34,18 +35,19 @@ export function calculateTotalPrice(
total.vouchers = total.vouchers + room.vouchers
}
// If roomPointType isn't Scandic, these should be counted as partnerPoints.
// If they're not Scandic points, we can ignore them on roomPoints as they
// are included in totalPoints, which in turn never contain partner points.
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
total.scandicPoints = total.scandicPoints + room.totalPoints
}
// room.totalPrice is a negative value when
@@ -59,9 +61,8 @@ export function calculateTotalPrice(
{
cash: 0,
cheques: 0,
scandicFriendsPoints: 0,
scandicPoints: 0,
partnerPoints: 0,
partnerPointsCurrency: null as Room["roomPointType"],
vouchers: 0,
}
)
@@ -86,12 +87,24 @@ export function calculateTotalPrice(
priceParts.push(`${totals.cheques} ${CurrencyEnum.CC}`)
}
if (totals.scandicFriendsPoints) {
priceParts.push(`${totals.scandicFriendsPoints} ${CurrencyEnum.POINTS}`)
if (totals.partnerPoints) {
// We can assume that all rooms has the same point type
const roomPointType = rooms[0]?.roomPointType || PointType.SCANDIC
const currencyText = getPointsCurrencyText(
totals.partnerPoints,
roomPointType,
intl
)
priceParts.push(`${totals.partnerPoints} ${currencyText}`)
}
if (totals.partnerPoints) {
priceParts.push(`${totals.partnerPoints} ${totals.partnerPointsCurrency}`)
if (totals.scandicPoints) {
const currencyText = getPointsCurrencyText(
totals.scandicPoints,
PointType.SCANDIC,
intl
)
priceParts.push(`${totals.scandicPoints} ${currencyText}`)
}
if (totals.cash) {
@@ -102,18 +115,43 @@ export function calculateTotalPrice(
return priceParts.join(" + ")
}
export function calculateTotalPoints(
rooms: Room[],
allRoomsAreCancelled: boolean
) {
return rooms.reduce((total, room) => {
if (!allRoomsAreCancelled && room.isCancelled) {
return total
}
return total + room.totalPoints
}, 0)
}
export function isAllRoomsCancelled(rooms: Room[]) {
return !rooms.some((room) => room.isCancelled === false)
}
function getPointsCurrencyText(
points: number,
pointsType: PointType,
intl: IntlShape
) {
switch (pointsType) {
case PointType.SCANDIC: {
return intl.formatMessage(
{
id: "price.numberOfScandicPoints",
defaultMessage:
"{numberOfScandicPoints, plural, one {Point} other {Points}}",
},
{
numberOfScandicPoints: points,
}
)
}
case PointType.EUROBONUS: {
return intl.formatMessage(
{
id: "price.numberOfEuroBonusPoints",
defaultMessage:
"{numberOfEuroBonusPoints, plural, one {EB Point} other {EB Points}}",
},
{
numberOfEuroBonusPoints: points,
}
)
}
default: {
const _exhaustiveCheck: never = pointsType
return "Points"
}
}
}

View File

@@ -8,11 +8,7 @@ import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails"
import { MyStayContext } from "@/contexts/MyStay"
import {
calculateTotalPoints,
calculateTotalPrice,
isAllRoomsCancelled,
} from "./helpers"
import { calculateTotalPrice, isAllRoomsCancelled } from "./helpers"
import type { InitialState, MyStayState } from "@/types/stores/my-stay"
@@ -56,8 +52,6 @@ export function createMyStayStore({
const allRoomsAreCancelled = isAllRoomsCancelled(mappedRooms)
const totalPoints = calculateTotalPoints(mappedRooms, allRoomsAreCancelled)
const totalPrice = calculateTotalPrice(
mappedRooms,
bookedRoom.currencyCode,
@@ -80,7 +74,6 @@ export function createMyStayStore({
refId,
rooms: mappedRooms,
savedCreditCards,
totalPoints,
totalPrice,
isPastBooking,