Files
web/packages/trpc/lib/routers/booking/output.ts
Anton Gunnarsson 16fbdb7ae0 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
2026-01-15 09:32:17 +00:00

243 lines
7.5 KiB
TypeScript

import { z } from "zod"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { dt } from "@scandic-hotels/common/dt"
import { nullableArrayObjectValidator } from "@scandic-hotels/common/utils/zod/arrayValidator"
import {
nullableStringEmailValidator,
nullableStringValidator,
} from "@scandic-hotels/common/utils/zod/stringValidator"
import { BookingStatusEnum } from "../../enums/bookingStatus"
import { BreakfastPackageEnum } from "../../enums/breakfast"
import { ChildBedTypeEnum } from "../../enums/childBedTypeEnum"
import { calculateRefId } from "../../utils/refId"
export const guestSchema = z.object({
email: nullableStringEmailValidator,
firstName: nullableStringValidator,
lastName: nullableStringValidator,
membershipNumber: nullableStringValidator,
phoneNumber: nullableStringValidator,
countryCode: nullableStringValidator,
})
export type Guest = z.output<typeof guestSchema>
// QUERY
const childBedPreferencesSchema = z.object({
bedType: z.nativeEnum(ChildBedTypeEnum),
quantity: z.number().int(),
code: z.string().nullable().default(""),
})
const priceSchema = z.object({
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.Unknown),
totalPrice: z.number().nullish(),
totalUnit: z.number().int().nullish(),
unit: z.number().int().nullish(),
unitPrice: z.number(),
})
export const packageSchema = z
.object({
code: nullableStringValidator,
comment: z.string().nullish(),
description: nullableStringValidator,
price: priceSchema,
type: z.string().nullish(),
})
.transform((packageData) => ({
code: packageData.code,
comment: packageData.comment,
currency: packageData.price.currency,
description: packageData.description,
totalPrice: packageData.price.totalPrice ?? 0,
totalUnit: packageData.price.totalUnit ?? 0,
type: packageData.type,
unit: packageData.price.unit ?? 0,
unitPrice: packageData.price.unitPrice,
}))
const ancillarySchema = z
.object({
comment: z.string().default(""),
deliveryTime: z.string().default(""),
})
.nullable()
.default({
comment: "",
deliveryTime: "",
})
const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean().default(false),
cancellationRule: z.string().nullable().default(""),
cancellationText: z.string().nullable().default(""),
generalTerms: z.array(z.string()).default([]),
isMemberRate: z.boolean().default(false),
mustBeGuaranteed: z.boolean().default(false),
rateCode: z.string().default(""),
title: z.string().nullable().default(""),
isCampaignRate: z.boolean().default(false),
})
export const linkedReservationSchema = z.object({
confirmationNumber: z.string().default(""),
hotelId: z.string().default(""),
checkinDate: z.string(),
checkoutDate: z.string(),
cancellationNumber: nullableStringValidator,
roomTypeCode: z.string().default(""),
adults: z.number().int(),
children: z.number().int(),
profileId: z.string().default(""),
})
const linksSchema = z.object({
addAncillary: z
.object({
href: z.string(),
meta: z.object({
method: z.string(),
}),
})
.nullable(),
cancel: z
.object({
href: z.string(),
meta: z.object({
method: z.string(),
}),
})
.nullable(),
guarantee: z
.object({
href: z.string(),
meta: z.object({
method: z.string(),
}),
})
.nullable(),
modify: z
.object({
href: z.string(),
meta: z.object({
method: z.string(),
}),
})
.nullable(),
self: z
.object({
href: z.string(),
meta: z.object({
method: z.string(),
}),
})
.nullable(),
})
export const bookingConfirmationSchema = z
.object({
data: z.object({
attributes: z.object({
adults: z.number().int(),
ancillary: ancillarySchema,
bookingType: z.string().optional(),
cancelationNumber: z.string().nullable().default(""),
checkInDate: z.string().refine((val) => dt(val).isValid()),
checkOutDate: z.string().refine((val) => dt(val).isValid()),
childBedPreferences: z.array(childBedPreferencesSchema).default([]),
childrenAges: z.array(z.number().int()).default([]),
canChangeDate: z.boolean(),
bookingCode: z.string().nullable(),
cheques: z.number(),
vouchers: z.number(),
guaranteeInfo: z
.object({
maskedCard: z.string(),
cardType: z.string(),
paymentMethod: z.string(),
paymentMethodDescription: z.string(),
})
.nullish(),
computedReservationStatus: z.string().nullable().default(""),
confirmationNumber: nullableStringValidator,
createDateTime: z.string().refine((val) => dt(val).isValid()),
currencyCode: z.nativeEnum(CurrencyEnum),
guest: guestSchema,
linkedReservations: nullableArrayObjectValidator(
linkedReservationSchema
),
hotelId: z.string(),
mainRoom: z.boolean(),
multiRoom: z.boolean(),
packages: z.array(packageSchema).default([]),
rateDefinition: rateDefinitionSchema,
reservationStatus: z.string().nullable().default(""),
roomPoints: z.number(),
roomPointType: z
.enum(["Scandic", "EuroBonus"])
.nullable()
.default(null)
.catch(null),
roomPrice: z.number(),
roomTypeCode: z.string().default(""),
totalPoints: z.number(),
totalPrice: z.number(),
totalPriceExVat: z.number(),
vatAmount: z.number(),
vatPercentage: z.number(),
}),
id: z.string(),
type: z.literal("booking"),
links: linksSchema,
}),
})
.transform(({ data }) => ({
...data.attributes,
refId: calculateRefId(
data.attributes.confirmationNumber,
data.attributes.guest.lastName
),
linkedReservations: data.attributes.linkedReservations.map(
(linkedReservation) => {
/**
* We lazy load linked reservations in the client.
* The problem is that we need to load the reservation in order to
* calculate the refId for the reservation as the refId uses the guest's
* lastname in it. Ideally we should pass a promise to the React
* component that uses `use()` to resolve it. But right now we use tRPC
* in the client. That tRPC endpoint only uses the confirmationNumber
* from the refId. So that means we can pass whatever as the lastname
* here, because it is actually never read. We should change this ASAP.
*/
return {
...linkedReservation,
refId: calculateRefId(
linkedReservation.confirmationNumber,
"" // TODO: Empty lastname here, see comment above
),
}
}
),
packages: data.attributes.packages.filter((p) => p.type !== "Ancillary"),
ancillaries: data.attributes.packages.filter((p) => p.type === "Ancillary"),
extraBedTypes: data.attributes.childBedPreferences,
showAncillaries:
!!(
data.links.addAncillary ||
data.attributes.packages.some(
(p) =>
p.type === "Ancillary" ||
p.code === BreakfastPackageEnum.ANCILLARY_REGULAR_BREAKFAST
)
) && data.attributes.reservationStatus !== BookingStatusEnum.Cancelled,
isCancelable: !!data.links.cancel,
isModifiable: !!data.links.modify,
isGuaranteeable: !!data.links.guarantee,
canModifyAncillaries: !!data.links.addAncillary,
// Typo from API
cancellationNumber: data.attributes.cancelationNumber,
}))