Merged in feature/SW-3516-pass-eurobonus-number-on-booking (pull request #2902)

* feat(SW-3516): Include partnerLoyaltyNumber on bookings

- Added user context to BookingFlowProviders for user state management.
- Updated booking input and output schemas to accommodate new user data.
- Refactored booking mutation logic to include user-related information.
- Improved type definitions for better TypeScript support across booking components.


Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-10-08 10:48:42 +00:00
parent b685c928e4
commit 17df3ee71a
18 changed files with 510 additions and 370 deletions

View File

@@ -1,23 +1,35 @@
"use client"
import { createContext, useContext } from "react"
import { createContext } from "react"
type BaseUser = {
firstName: string | null
lastName: string | null
email: string
}
export type BookingFlowUser =
| (BaseUser & {
type: "partner-sas"
partnerLoyaltyNumber: `EB${string}`
})
| (BaseUser & {
type: "scandic"
/**
* This will always be null for Scandic Friends members
*/
partnerLoyaltyNumber: null
membershipNumber: string
})
export type BookingFlowContextData = {
isLoggedIn: boolean
user:
| { state: "loading"; data: undefined }
| { state: "error"; data: undefined }
| { state: "loaded"; data: BookingFlowUser | undefined }
}
export const BookingFlowContext = createContext<
BookingFlowContextData | undefined
>(undefined)
export const useBookingFlowContext = (): BookingFlowContextData => {
const context = useContext(BookingFlowContext)
if (!context) {
throw new Error(
"useBookingFlowContext must be used within a BookingFlowContextProvider. Did you forget to use the provider in the consuming app?"
)
}
return context
}

View File

@@ -38,6 +38,7 @@ import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { env } from "../../../../env/client"
import { useAvailablePaymentOptions } from "../../../hooks/useAvailablePaymentOptions"
import { useBookingFlowContext } from "../../../hooks/useBookingFlowContext"
import { useHandleBookingStatus } from "../../../hooks/useHandleBookingStatus"
import { useIsLoggedIn } from "../../../hooks/useIsLoggedIn"
import useLang from "../../../hooks/useLang"
@@ -59,6 +60,7 @@ import TermsAndConditions from "./TermsAndConditions"
import styles from "./payment.module.css"
import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { PriceChangeData } from "../PriceChangeData"
@@ -83,6 +85,7 @@ export default function PaymentClient({
const searchParams = useSearchParams()
const isUserLoggedIn = useIsLoggedIn()
const { getTopOffset } = useStickyPosition({})
const { user } = useBookingFlowContext()
const [showBookingAlert, setShowBookingAlert] = useState(false)
@@ -397,39 +400,33 @@ export default function PaymentClient({
}
writePaymentInfoToSessionStorage(paymentMethodType, !!savedCreditCard)
const payload = {
const payload: CreateBookingInput = {
checkInDate: fromDate,
checkOutDate: toDate,
hotelId,
language: lang,
payment,
rooms: rooms.map(({ room }, idx) => {
const isMainRoom = idx === 0
let rateCode = ""
if (isMainRoom && isUserLoggedIn) {
rateCode = booking.rooms[idx].rateCode
} else if (
(room.guest.join || room.guest.membershipNo) &&
booking.rooms[idx].counterRateCode
) {
rateCode = booking.rooms[idx].counterRateCode
} else {
rateCode = booking.rooms[idx].rateCode
}
rooms: rooms.map(
({ room }, idx): CreateBookingInput["rooms"][number] => {
const isMainRoom = idx === 0
let rateCode = ""
if (isMainRoom && isUserLoggedIn) {
rateCode = booking.rooms[idx].rateCode
} else if (
(room.guest.join || room.guest.membershipNo) &&
booking.rooms[idx].counterRateCode
) {
rateCode = booking.rooms[idx].counterRateCode
} else {
rateCode = booking.rooms[idx].rateCode
}
const phoneNumber = formatPhoneNumber(
room.guest.phoneNumber,
room.guest.phoneNumberCC
)
const phoneNumber = formatPhoneNumber(
room.guest.phoneNumber,
room.guest.phoneNumberCC
)
return {
adults: room.adults,
bookingCode: room.roomRate.bookingCode,
childrenAges: room.childrenInRoom?.map((child) => ({
age: child.age,
bedType: bedTypeMap[parseInt(child.bed.toString())],
})),
guest: {
const guest: CreateBookingInput["rooms"][number]["guest"] = {
becomeMember: room.guest.join,
countryCode: room.guest.countryCode,
email: room.guest.email,
@@ -437,19 +434,24 @@ export default function PaymentClient({
lastName: room.guest.lastName,
membershipNumber: room.guest.membershipNo,
phoneNumber,
// Only allowed for room one
...(idx === 0 && {
dateOfBirth:
"dateOfBirth" in room.guest && room.guest.dateOfBirth
? room.guest.dateOfBirth
: undefined,
postalCode:
"zipCode" in room.guest && room.guest.zipCode
? room.guest.zipCode
: undefined,
}),
},
packages: {
partnerLoyaltyNumber: null,
}
if (isMainRoom) {
// Only valid for main room
guest.partnerLoyaltyNumber =
user?.data?.partnerLoyaltyNumber || null
guest.dateOfBirth =
"dateOfBirth" in room.guest && room.guest.dateOfBirth
? room.guest.dateOfBirth
: undefined
guest.postalCode =
"zipCode" in room.guest && room.guest.zipCode
? room.guest.zipCode
: undefined
}
const packages: CreateBookingInput["rooms"][number]["packages"] = {
accessibility:
room.roomFeatures?.some(
(feature) =>
@@ -464,47 +466,59 @@ export default function PaymentClient({
room.roomFeatures?.some(
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
) ?? false,
},
rateCode,
roomPrice: {
memberPrice:
"member" in room.roomRate
? room.roomRate.member?.localPrice.pricePerStay
}
return {
adults: room.adults,
bookingCode: room.roomRate.bookingCode,
childrenAges: room.childrenInRoom?.map((child) => ({
age: child.age,
bedType: bedTypeMap[parseInt(child.bed.toString())],
})),
guest,
packages,
rateCode,
roomPrice: {
memberPrice:
"member" in room.roomRate
? room.roomRate.member?.localPrice.pricePerStay
: undefined,
publicPrice:
"public" in room.roomRate
? room.roomRate.public?.localPrice.pricePerStay
: undefined,
},
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
smsConfirmationRequested: data.smsConfirmation,
specialRequest: {
comment: room.specialRequest.comment
? room.specialRequest.comment
: undefined,
publicPrice:
"public" in room.roomRate
? room.roomRate.public?.localPrice.pricePerStay
: undefined,
},
roomTypeCode: room.bedType!.roomTypeCode, // A selection has been made in order to get to this step.
smsConfirmationRequested: data.smsConfirmation,
specialRequest: {
comment: room.specialRequest.comment
? room.specialRequest.comment
: undefined,
},
},
}
}
}),
),
}
initiateBooking.mutate(payload)
},
[
setIsSubmitting,
preSubmitCallbacks,
rooms,
getPaymentMethod,
savedCreditCards,
lang,
initiateBooking,
hotelId,
bookingMustBeGuaranteed,
hasOnlyFlexRates,
fromDate,
toDate,
rooms,
booking.rooms,
getPaymentMethod,
hasOnlyFlexRates,
bookingMustBeGuaranteed,
preSubmitCallbacks,
isUserLoggedIn,
hotelId,
initiateBooking,
getTopOffset,
setIsSubmitting,
isUserLoggedIn,
booking.rooms,
user?.data?.partnerLoyaltyNumber,
]
)

View File

@@ -0,0 +1,18 @@
import { useContext } from "react"
import {
BookingFlowContext,
type BookingFlowContextData,
} from "../bookingFlowContext"
export const useBookingFlowContext = (): BookingFlowContextData => {
const context = useContext(BookingFlowContext)
if (!context) {
throw new Error(
"useBookingFlowContext must be used within a BookingFlowContextProvider. Did you forget to use the provider in the consuming app?"
)
}
return context
}

View File

@@ -1,4 +1,4 @@
import { useBookingFlowContext } from "../bookingFlowContext"
import { useBookingFlowContext } from "./useBookingFlowContext"
export function useIsLoggedIn() {
const data = useBookingFlowContext()