Merged in feature/SW-3616-partner-points-my-stay (pull request #3407)

Feature/SW-3616 partner points my stay

* feat(SW-3616): display partner points in my stays

* null check roomPointType

* Lowercase POINTS in my stay

* include other than Scandic points when displaying price details modal


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2026-01-12 09:24:04 +00:00
parent d371d45fd2
commit 488a396cfa
5 changed files with 260 additions and 29 deletions

View File

@@ -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<Room["roomPointType"]>,
CurrencyEnum
> = {
Scandic: CurrencyEnum.POINTS,
EuroBonus: CurrencyEnum.EUROBONUS,
}

View File

@@ -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<typeof calculateTotalPrice>[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/)
})
})

View File

@@ -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(

View File

@@ -17,6 +17,7 @@ interface VatProps {
const noVatCurrencies = [
CurrencyEnum.CC,
CurrencyEnum.POINTS,
CurrencyEnum.EUROBONUS,
CurrencyEnum.Voucher,
CurrencyEnum.Unknown,
]

View File

@@ -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(),