feat: add multiroom tracking to booking flow

This commit is contained in:
Simon Emanuelsson
2025-03-05 11:53:05 +01:00
parent 540402b969
commit 1812591903
72 changed files with 2277 additions and 1308 deletions

View File

@@ -3,52 +3,67 @@ import { z } from "zod"
import { ChildBedTypeEnum } from "@/constants/booking"
import { Lang, langToApiLang } from "@/constants/languages"
const signupSchema = z.discriminatedUnion("becomeMember", [
z.object({
dateOfBirth: z.string(),
postalCode: z.string(),
becomeMember: z.literal<boolean>(true),
}),
z.object({ becomeMember: z.literal<boolean>(false) }),
])
const roomsSchema = z.array(
z.object({
adults: z.number().int().nonnegative(),
childrenAges: z
.array(
z.object({
age: z.number().int().nonnegative(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
)
.default([]),
rateCode: z.string(),
roomTypeCode: z.coerce.string(),
guest: z.intersection(
z.object({
const roomsSchema = z
.array(
z.object({
adults: z.number().int().nonnegative(),
childrenAges: z
.array(
z.object({
age: z.number().int().nonnegative(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
)
.default([]),
rateCode: z.string(),
roomTypeCode: z.coerce.string(),
guest: z.object({
becomeMember: z.boolean(),
countryCode: z.string(),
dateOfBirth: z.string().nullish(),
email: z.string().email(),
firstName: z.string(),
lastName: z.string(),
email: z.string().email(),
membershipNumber: z.string().nullish(),
postalCode: z.string().nullish(),
phoneNumber: z.string(),
countryCode: z.string(),
membershipNumber: z.string().optional(),
}),
signupSchema
),
smsConfirmationRequested: z.boolean(),
packages: z.object({
breakfast: z.boolean(),
allergyFriendly: z.boolean(),
petFriendly: z.boolean(),
accessibility: z.boolean(),
}),
roomPrice: z.object({
memberPrice: z.number().nullish(),
publicPrice: z.number().nullish(),
}),
smsConfirmationRequested: z.boolean(),
packages: z.object({
breakfast: z.boolean(),
allergyFriendly: z.boolean(),
petFriendly: z.boolean(),
accessibility: z.boolean(),
}),
roomPrice: z.object({
memberPrice: z.number().nullish(),
publicPrice: z.number().nullish(),
}),
})
)
.superRefine((data, ctx) => {
data.forEach((room, idx) => {
if (idx === 0 && room.guest.becomeMember) {
if (!room.guest.dateOfBirth) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: "string",
received: typeof room.guest.dateOfBirth,
path: ["guest", "dateOfBirth"],
})
}
if (!room.guest.postalCode) {
ctx.addIssue({
code: z.ZodIssueCode.invalid_type,
expected: "string",
received: typeof room.guest.postalCode,
path: ["guest", "postalCode"],
})
}
}
})
})
)
const paymentSchema = z.object({
paymentMethod: z.string(),

View File

@@ -26,21 +26,25 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
})
export const roomsCombinedAvailabilityInputSchema = z.object({
hotelId: z.number(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
uniqueAdultsCount: z.array(z.number()),
adultsCount: z.array(z.number()),
bookingCode: z.string().optional(),
childArray: z
.array(
z.object({
bed: z.nativeEnum(ChildBedMapEnum),
age: z.number(),
})
z
.array(
z.object({
age: z.number(),
bed: z.nativeEnum(ChildBedMapEnum),
})
)
.nullable()
)
.optional(),
bookingCode: z.string().optional(),
rateCode: z.string().optional(),
.nullish(),
hotelId: z.number(),
lang: z.nativeEnum(Lang),
rateCode: z.string().optional(),
roomStayEndDate: z.string(),
roomStayStartDate: z.string(),
})
export const selectedRoomAvailabilityInputSchema = z.object({

View File

@@ -22,6 +22,7 @@ import { relationshipsSchema } from "./schemas/relationships"
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type {
AdditionalData,
City,
@@ -137,6 +138,13 @@ const cancellationRules = {
NotCancellable: 0,
} as const
// Used to ensure `Available` rooms
// are shown before all `NotAvailable`
const statusLookup = {
[AvailabilityEnum.Available]: 1,
[AvailabilityEnum.NotAvailable]: 2,
}
export const roomsAvailabilitySchema = z
.object({
data: z.object({
@@ -161,8 +169,8 @@ export const roomsAvailabilitySchema = z
return acc
}, {})
attributes.roomConfigurations = attributes.roomConfigurations.map(
(room) => {
const roomConfigurations = attributes.roomConfigurations
.map((room) => {
if (room.products.length) {
room.breakfastIncludedInAllRatesMember = room.products.every(
(product) =>
@@ -222,10 +230,16 @@ export const roomsAvailabilitySchema = z
}
return room
}
)
})
.sort(
// @ts-expect-error - array indexing
(a, b) => statusLookup[a.status] - statusLookup[b.status]
)
return attributes
return {
...attributes,
roomConfigurations,
}
})
export const ratesSchema = z.array(rateSchema)

View File

@@ -499,20 +499,20 @@ export const hotelQueryRouter = router({
const { lang } = input
const apiLang = toApiLang(lang)
const {
hotelId,
roomStayStartDate,
roomStayEndDate,
uniqueAdultsCount,
childArray,
adultsCount,
bookingCode,
childArray,
hotelId,
rateCode,
roomStayEndDate,
roomStayStartDate,
} = input
const metricsData = {
hotelId,
roomStayStartDate,
roomStayEndDate,
uniqueAdultsCount,
adultsCount,
childArray: childArray ? JSON.stringify(childArray) : undefined,
bookingCode,
}
@@ -525,15 +525,15 @@ export const hotelQueryRouter = router({
)
const availabilityResponses = await Promise.allSettled(
uniqueAdultsCount.map(async (adultCount: number) => {
adultsCount.map(async (adultCount: number, idx: number) => {
const kids = childArray?.[idx]
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults: adultCount,
...(childArray &&
childArray.length > 0 && {
children: generateChildrenString(childArray),
}),
...(kids?.length && {
children: generateChildrenString(kids),
}),
...(bookingCode && { bookingCode }),
language: apiLang,
}
@@ -769,9 +769,9 @@ export const hotelQueryRouter = router({
type: matchingRoom.mainBed.type,
extraBed: matchingRoom.fixedExtraBed
? {
type: matchingRoom.fixedExtraBed.type,
description: matchingRoom.fixedExtraBed.description,
}
type: matchingRoom.fixedExtraBed.type,
description: matchingRoom.fixedExtraBed.description,
}
: undefined,
}
}
@@ -794,23 +794,27 @@ export const hotelQueryRouter = router({
)
return {
selectedRoom,
rateDetails: rateDefinition?.generalTerms,
bedTypes,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
cancellationRule: rateDefinition?.cancellationRule,
cancellationText: rateDefinition?.cancellationText ?? "",
isFlexRate:
rateDefinition?.cancellationRule ===
CancellationRuleEnum.CancellableBefore6PM,
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
memberMustBeGuaranteed: !!memberRateDefinition?.mustBeGuaranteed,
breakfastIncluded: !!rateDefinition?.breakfastIncluded,
memberRate: rates?.member,
mustBeGuaranteed: !!rateDefinition?.mustBeGuaranteed,
publicRate: rates?.public,
rate: selectedRoom.products[0].rate,
rateDefinitionTitle: rateDefinition?.title ?? "",
rateDetails: rateDefinition?.generalTerms,
// Send rate Title when it is a booking code rate
rateTitle:
rateDefinition?.rateType !== RateTypeEnum.Regular
? rateDefinition?.title
: undefined,
memberRate: rates?.member,
publicRate: rates?.public,
bedTypes,
rateType: rateDefinition?.rateType ?? "",
selectedRoom,
}
}),
hotelsByCityWithBookingCode: serviceProcedure
@@ -1096,9 +1100,9 @@ export const hotelQueryRouter = router({
return hotelData
? {
...hotelData,
url: hotelPage?.url ?? null,
}
...hotelData,
url: hotelPage?.url ?? null,
}
: null
})
)

View File

@@ -38,9 +38,15 @@ export const roomConfigurationSchema = z
(product) => !product.public?.rateCode && !product.member?.rateCode
)
if (allProductsMissBothRateCodes) {
data.status = AvailabilityEnum.NotAvailable
return {
...data,
status: AvailabilityEnum.NotAvailable,
}
}
}
return data
// Creating a new objekt since data is frozen (readony)
// and can cause errors to be thrown if trying to manipulate
// object elsewhere
return { ...data }
})