Merged in feat/sw-2862-move-booking-router-to-trpc-package (pull request #2421)

feat(SW-2861): Move booking router to trpc package

* Use direct imports from trpc package

* Add lint-staged config to trpc

* Move lang enum to common

* Restructure trpc package folder structure

* WIP first step

* update internal imports in trpc

* Fix most errors in scandic-web

Just 100 left...

* Move Props type out of trpc

* Fix CategorizedFilters types

* Move more schemas in hotel router

* Fix deps

* fix getNonContentstackUrls

* Fix import error

* Fix entry error handling

* Fix generateMetadata metrics

* Fix alertType enum

* Fix duplicated types

* lint:fix

* Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package

* Fix broken imports

* Move booking router to trpc package

* Move partners router to trpc package

* Move autocomplete router to trpc package

* Move booking router to trpc package

* Merge branch 'master' into feat/sw-2862-move-booking-router-to-trpc-package


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-06-26 13:21:16 +00:00
parent ded9e1278f
commit e572d9e7e9
69 changed files with 179 additions and 178 deletions

View File

@@ -3,9 +3,9 @@
import { z } from "zod"
import * as api from "@scandic-hotels/trpc/api"
import { ApiLang } from "@scandic-hotels/trpc/constants/apiLang"
import { countriesMap } from "@scandic-hotels/trpc/constants/countries"
import { ApiLang } from "@/constants/languages"
import { getProfile } from "@/lib/trpc/memoizedRequests"
import { protectedServerActionProcedure } from "@/server/trpc"

View File

@@ -1,11 +1,12 @@
import { cookies } from "next/headers"
import { notFound, redirect } from "next/navigation"
import { decrypt } from "@scandic-hotels/trpc/utils/encryption"
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests"
import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation"
import { decrypt } from "@/utils/encryption"
import type { LangParams, PageArgs } from "@/types/params"

View File

@@ -2,6 +2,9 @@ import { notFound } from "next/navigation"
import { getServiceToken } from "@scandic-hotels/common/tokenManager"
import { BookingErrorCodeEnum } from "@scandic-hotels/trpc/enums/bookingErrorCode"
import { getBooking } from "@scandic-hotels/trpc/routers/booking/utils"
import { encrypt } from "@scandic-hotels/trpc/utils/encryption"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { PaymentCallbackStatusEnum } from "@/constants/booking"
import {
@@ -9,13 +12,10 @@ import {
details,
} from "@/constants/routes/hotelReservation"
import { serverClient } from "@/lib/trpc/server"
import { getBooking } from "@/server/routers/booking/utils"
import { auth } from "@/auth"
import HandleErrorCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleErrorCallback"
import HandleSuccessCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/HandleSuccessCallback"
import { encrypt } from "@/utils/encryption"
import { isValidSession } from "@/utils/session"
import type { LangParams, PageArgs } from "@/types/params"

View File

@@ -1,5 +1,7 @@
import { type NextRequest, NextResponse } from "next/server"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { DTMC_SUCCESS_BANNER_KEY } from "@/constants/dtmc"
import { linkEmploymentError } from "@/constants/routes/dtmc"
import { overview } from "@/constants/routes/myPages"
@@ -9,7 +11,6 @@ import { getPublicURL } from "@/server/utils"
import { auth } from "@/auth"
import { auth as dtmcAuth } from "@/auth.dtmc"
import { getLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
async function linkEmployeeToUser(employeeId: string) {
try {

View File

@@ -1,6 +1,7 @@
import React from "react"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { dtmcLogin } from "@/constants/routes/dtmc"
import { login } from "@/constants/routes/handleAuth"
@@ -10,7 +11,6 @@ import { auth } from "@/auth"
import ButtonLink from "@/components/ButtonLink"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
import styles from "./callToActions.module.css"

View File

@@ -5,7 +5,9 @@ import { useEffect, useState } from "react"
import { FormProvider, useForm } from "react-hook-form"
import { useIntl } from "react-intl"
import { getDefaultCountryFromLang, langToApiLang } from "@/constants/languages"
import { langToApiLang } from "@scandic-hotels/trpc/constants/apiLang"
import { getDefaultCountryFromLang } from "@/constants/languages"
import { logout } from "@/constants/routes/handleAuth"
import { profile } from "@/constants/routes/myPages"
import { trpc } from "@/lib/trpc/client"

View File

@@ -7,7 +7,7 @@ import { formatPrice } from "@/utils/numberFormatting"
import styles from "./breakfast.module.css"
import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation"
import type { PackageSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
interface BreakfastProps {
breakfast: PackageSchema | false | undefined

View File

@@ -5,12 +5,12 @@ import { useIntl } from "react-intl"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { getBookedHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import { RoomSidePeekContent } from "@/components/SidePeeks/RoomSidePeek/RoomSidePeekContent"
import SidePeekSelfControlled from "@/components/TempDesignSystem/SidePeekSelfControlled"
import { getBookedHotelRoom } from "@/utils/booking"
interface RoomDetailsSidePeekProps {
roomTypeCode: string

View File

@@ -11,8 +11,9 @@ import { useSearchHistory } from "@/hooks/useSearchHistory"
import { clearPaymentInfoSessionStorage } from "../../EnterDetails/Payment/helpers"
import { getTracking } from "./tracking"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@/types/stores/booking-confirmation"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export default function Tracking({
bookingConfirmation,

View File

@@ -13,6 +13,7 @@ import { getSpecialRoomType } from "@/utils/specialRoomType"
import { invertedBedTypeMap } from "../../utils"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { RateDefinition } from "@scandic-hotels/trpc/types/roomAvailability"
import {
@@ -23,7 +24,6 @@ import {
type TrackingSDKPaymentInfo,
} from "@/types/components/tracking"
import type { Room } from "@/types/stores/booking-confirmation"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
function getRate(cancellationRule: RateDefinition["cancellationRule"] | null) {
switch (cancellationRule) {

View File

@@ -1,15 +1,16 @@
import { type IntlShape } from "react-intl"
import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { formatPrice } from "@/utils/numberFormatting"
import type { IntlShape } from "react-intl"
import type { BookingConfirmationRoom } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
import type {
BookingConfirmationSchema,
PackageSchema,
} from "@/types/trpc/routers/booking/confirmation"
} from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { BookingConfirmationRoom } from "@/types/components/hotelReservation/bookingConfirmation/bookingConfirmation"
export function mapRoomState(
booking: BookingConfirmationSchema,

View File

@@ -3,7 +3,9 @@
import { useRouter } from "next/navigation"
import { useEffect } from "react"
import { BookingStatusEnum, MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { MEMBERSHIP_FAILED_ERROR } from "@/constants/booking"
import LoadingSpinner from "@/components/LoadingSpinner"
import { useHandleBookingStatus } from "@/hooks/booking/useHandleBookingStatus"

View File

@@ -14,9 +14,10 @@ import { Button } from "@scandic-hotels/design-system/Button"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { bedTypeMap } from "@scandic-hotels/trpc/constants/bedTypeMap"
import { REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { BookingStatusEnum, PAYMENT_METHOD_TITLES } from "@/constants/booking"
import { PAYMENT_METHOD_TITLES } from "@/constants/booking"
import { bookingConfirmation } from "@/constants/routes/hotelReservation"
import { env } from "@/env/client"
import { trpc } from "@/lib/trpc/client"

View File

@@ -11,8 +11,9 @@ import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
import { trackRemoveAncillary } from "@/utils/tracking/myStay"
import type { PackageSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@/types/stores/my-stay"
import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation"
export default function RemoveButton({
refId,

View File

@@ -16,9 +16,10 @@ import RemoveButton from "./RemoveButton"
import styles from "./addedAncillaries.module.css"
import type { PackageSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { AddedAncillariesProps } from "@/types/components/myPages/myStay/ancillaries"
import type { Room } from "@/types/stores/my-stay"
import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation"
export function AddedAncillaries({
ancillaries,

View File

@@ -24,13 +24,14 @@ import ModifyContact from "../ModifyContact"
import styles from "./guestDetails.module.css"
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
import {
type ModifyContactSchema,
modifyContactSchema,
} from "@/types/components/hotelReservation/myStay/modifyContact"
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import type { SafeUser } from "@/types/user"
import type { Guest } from "@/server/routers/booking/output"
interface GuestDetailsProps {
refId: string

View File

@@ -10,7 +10,7 @@ import Body from "@/components/TempDesignSystem/Text/Body"
import styles from "./modifyContact.module.css"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
interface ModifyContactProps {
guest: BookingConfirmation["booking"]["guest"]

View File

@@ -5,8 +5,9 @@ import Points from "./Points"
import Price from "./Price"
import Vouchers from "./Vouchers"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface PriceTypeProps
extends Pick<

View File

@@ -5,6 +5,7 @@ import { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import { dt } from "@scandic-hotels/common/dt"
import ScandicLogoIcon from "@scandic-hotels/design-system/Icons/ScandicLogoIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { parseRefId } from "@scandic-hotels/trpc/utils/refId"
import {
findBooking,
@@ -16,7 +17,6 @@ import {
import { getIntl } from "@/i18n"
import { isLoggedInUser } from "@/utils/isLoggedInUser"
import * as maskValue from "@/utils/maskValue"
import { parseRefId } from "@/utils/refId"
import AdditionalInfoForm from "../../FindMyBooking/AdditionalInfoForm"
import accessBooking, {
@@ -31,7 +31,7 @@ import Tracking from "./tracking"
import styles from "./receipt.module.css"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export async function Receipt({ refId }: { refId: string }) {
const { confirmationNumber, lastName } = parseRefId(refId)

View File

@@ -8,8 +8,9 @@ import accessBooking, {
ERROR_UNAUTHORIZED,
} from "./accessBooking"
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
import type { SafeUser } from "@/types/user"
import type { Guest } from "@/server/routers/booking/output"
describe("Access booking", () => {
describe("for logged in booking", () => {

View File

@@ -1,5 +1,6 @@
import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
import type { SafeUser } from "@/types/user"
import type { Guest } from "@/server/routers/booking/output"
export {
ACCESS_GRANTED,

View File

@@ -4,6 +4,7 @@ import { notFound } from "next/navigation"
import { dt } from "@scandic-hotels/common/dt"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { parseRefId } from "@scandic-hotels/trpc/utils/refId"
import { env } from "@/env/server"
import {
@@ -35,14 +36,13 @@ import { getIntl } from "@/i18n"
import MyStayProvider from "@/providers/MyStay"
import { isLoggedInUser } from "@/utils/isLoggedInUser"
import * as maskValue from "@/utils/maskValue"
import { parseRefId } from "@/utils/refId"
import { getCurrentWebUrl } from "@/utils/url"
import styles from "./index.module.css"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { SafeUser } from "@/types/user"
export default async function MyStay(props: {

View File

@@ -1,20 +1,21 @@
import { dt } from "@scandic-hotels/common/dt"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { PackageTypeEnum } from "@scandic-hotels/trpc/enums/packages"
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import { BookingStatusEnum, CancellationRuleEnum } from "@/constants/booking"
import { CancellationRuleEnum } from "@/constants/booking"
import { convertToChildType } from "../../utils/convertToChildType"
import { getPriceType } from "../../utils/getPriceType"
import { formatChildBedPreferences } from "../utils"
import type { RateEnum } from "@scandic-hotels/trpc/enums/rate"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { Room as MyStayRoom } from "@/types/stores/my-stay"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
interface MapRoomDetailsParams {
booking: BookingConfirmation["booking"]

View File

@@ -1,13 +1,5 @@
import { AvailabilityEnum } from "@scandic-hotels/trpc/enums/selectHotel"
import { generateChildrenString } from "@scandic-hotels/trpc/routers/hotels/helpers"
import {
type AdditionalData,
type Hotel,
} from "@scandic-hotels/trpc/types/hotel"
import {
type HotelLocation,
type Location,
} from "@scandic-hotels/trpc/types/locations"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
@@ -16,6 +8,11 @@ import { getLang } from "@/i18n/serverContext"
import type { HotelsAvailabilityItem } from "@scandic-hotels/trpc/types/availability"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { AdditionalData, Hotel } from "@scandic-hotels/trpc/types/hotel"
import type {
HotelLocation,
Location,
} from "@scandic-hotels/trpc/types/locations"
import type {
AlternativeHotelsAvailabilityInput,

View File

@@ -1,9 +1,8 @@
import { ChildBedMapEnum } from "@scandic-hotels/trpc/enums/childBedMapEnum"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function convertToChildType(
childrenAges: number[],
childBedPreferences: BookingConfirmation["booking"]["childBedPreferences"]

View File

@@ -1,13 +1,14 @@
import { headers } from "next/headers"
import { redirect } from "next/navigation"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { overview } from "@/constants/routes/myPages"
import { getProfile } from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
export async function ProtectedLayout({ children }: React.PropsWithChildren) {
const intl = await getIntl()

View File

@@ -25,6 +25,7 @@ import RoomDetails from "./RoomDetails"
import styles from "./bookedRoomSidePeek.module.css"
import type { BookingConfirmationSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { Room as HotelRoom } from "@scandic-hotels/trpc/types/hotel"
import type { Packages } from "@scandic-hotels/trpc/types/packages"
@@ -32,7 +33,6 @@ import type { Packages } from "@scandic-hotels/trpc/types/packages"
import type { BreakfastPackage } from "@/types/components/hotelReservation/breakfast"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { BookingConfirmationSchema } from "@/types/trpc/routers/booking/confirmation"
import type { SafeUser } from "@/types/user"
type PartialHotelRoom = Pick<

View File

@@ -17,23 +17,6 @@ import type {
} from "@scandic-hotels/trpc/enums/bedType"
import type { JSX } from "react"
export enum BookingStatusEnum {
BookingCompleted = "BookingCompleted",
Cancelled = "Cancelled",
CheckedOut = "CheckedOut",
ConfirmedInScorpio = "ConfirmedInScorpio",
CreatedInOhip = "CreatedInOhip",
PaymentAuthorized = "PaymentAuthorized",
PaymentCancelled = "PaymentCancelled",
PaymentError = "PaymentError",
PaymentFailed = "PaymentFailed",
PaymentRegistered = "PaymentRegistered",
PaymentSucceeded = "PaymentSucceeded",
PendingAcceptPriceChange = "PendingAcceptPriceChange",
PendingGuarantee = "PendingGuarantee",
PendingPayment = "PendingPayment",
}
export const FamilyAndFriendsCodes = ["D000029555", "D000029271", "D000029195"]
export const bookingSearchTypes = [REDEMPTION] as const

View File

@@ -1,4 +1,5 @@
import { Lang } from "@scandic-hotels/common/constants/language"
import { ApiLang } from "@scandic-hotels/trpc/constants/apiLang"
import type { LowerCaseCountryCode } from "@/types/components/form/phone"
@@ -51,27 +52,6 @@ export const localeToLang: Record<string, Lang> = {
"se-NO": Lang.no,
} as const
export enum ApiLang {
Da = "Da",
De = "De",
En = "En",
Fi = "Fi",
No = "No",
Sv = "Sv",
Unknown = "Unknown",
}
type ApiLangKey = keyof typeof ApiLang
export const langToApiLang: Record<Lang, ApiLangKey> = {
[Lang.da]: ApiLang.Da,
[Lang.de]: ApiLang.De,
[Lang.en]: ApiLang.En,
[Lang.fi]: ApiLang.Fi,
[Lang.no]: ApiLang.No,
[Lang.sv]: ApiLang.Sv,
}
export const languageSelect = [
{ label: "Danish", value: ApiLang.Da },
{ label: "German", value: ApiLang.De },

View File

@@ -2,7 +2,8 @@ import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react"
import { useIntl } from "react-intl"
import { BookingStatusEnum } from "@/constants/booking"
import { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
import { trpc } from "@/lib/trpc/client"
import { toast } from "@/components/TempDesignSystem/Toasts"

View File

@@ -6,7 +6,7 @@ import { trpc } from "@/lib/trpc/client"
import useLang from "@/hooks/useLang"
import type { BookingStatusEnum } from "@/constants/booking"
import type { BookingStatusEnum } from "@scandic-hotels/trpc/enums/bookingStatus"
export function useHandleBookingStatus({
refId,

View File

@@ -2,13 +2,13 @@ import { type NextMiddleware, NextResponse } from "next/server"
import { findLang } from "@scandic-hotels/common/utils/languages"
import { REDEMPTION } from "@scandic-hotels/trpc/constants/booking"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { SEARCHTYPE } from "@/constants/booking"
import { login } from "@/constants/routes/handleAuth"
import { getPublicNextURL } from "@/server/utils"
import { auth } from "@/auth"
import { isValidSession } from "@/utils/session"
import { getDefaultRequestHeaders } from "./utils"

View File

@@ -3,8 +3,6 @@ import { notFound } from "next/navigation"
import { use, useRef } from "react"
import { useIntl } from "react-intl"
import { type RoomCategories } from "@scandic-hotels/trpc/types/hotel"
import { trpc } from "@/lib/trpc/client"
import { createMyStayStore } from "@/stores/my-stay"
@@ -12,14 +10,15 @@ import { MyStaySkeleton } from "@/components/HotelReservation/MyStay/myStaySkele
import { MyStayContext } from "@/contexts/MyStay"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type {
BookingConfirmation,
BookingConfirmationSchema,
} from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { Packages } from "@/types/components/myPages/myStay/ancillaries"
import type { MyStayStore } from "@/types/contexts/my-stay"
import type {
BookingConfirmation,
BookingConfirmationSchema,
} from "@/types/trpc/routers/booking/confirmation"
interface MyStayProviderProps {
bookingConfirmation: BookingConfirmation

View File

@@ -1,11 +1,11 @@
/** Routers */
import { router } from "@scandic-hotels/trpc"
import { autocompleteRouter } from "@scandic-hotels/trpc/routers/autocomplete"
import { bookingRouter } from "@scandic-hotels/trpc/routers/booking"
import { contentstackRouter } from "@scandic-hotels/trpc/routers/contentstack"
import { hotelsRouter } from "@scandic-hotels/trpc/routers/hotels"
import { partnerRouter } from "@scandic-hotels/trpc/routers/partners"
import { bookingRouter } from "./routers/booking"
import { navigationRouter } from "./routers/navigation"
import { userRouter } from "./routers/user"

View File

@@ -1,46 +0,0 @@
import { initTRPC } from "@trpc/server"
import { z } from "zod"
import { parseRefId } from "@/utils/refId"
import type { Meta } from "@scandic-hotels/trpc"
import type { Context } from "@scandic-hotels/trpc/context"
export function createRefIdPlugin() {
const t = initTRPC.context<Context>().meta<Meta>().create()
return {
toConfirmationNumber: t.procedure
.input(
z.object({
refId: z.string(),
})
)
.use(({ input, next }) => {
const { confirmationNumber } = parseRefId(input.refId)
return next({
ctx: {
confirmationNumber,
},
})
}),
toConfirmationNumbers: t.procedure
.input(
z.object({
refIds: z.array(z.string()),
})
)
.use(({ input, next }) => {
const confirmationNumbers = input.refIds.map((refId) => {
const { confirmationNumber } = parseRefId(refId)
return confirmationNumber
})
return next({
ctx: {
confirmationNumbers,
},
})
}),
}
}

View File

@@ -1,9 +0,0 @@
import { mergeRouters } from "@scandic-hotels/trpc"
import { bookingMutationRouter } from "./mutation"
import { bookingQueryRouter } from "./query"
export const bookingRouter = mergeRouters(
bookingMutationRouter,
bookingQueryRouter
)

View File

@@ -1,187 +0,0 @@
import { z } from "zod"
import { Lang } from "@scandic-hotels/common/constants/language"
import { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
import { langToApiLang } from "@/constants/languages"
const roomsSchema = z
.array(
z.object({
adults: z.number().int().nonnegative(),
bookingCode: z.string().nullish(),
childrenAges: z
.array(
z.object({
age: z.number().int().nonnegative(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
)
.default([]),
rateCode: z.string(),
redemptionCode: z.string().optional(),
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(),
membershipNumber: z.string().nullish(),
postalCode: z.string().nullish(),
phoneNumber: z.string(),
}),
smsConfirmationRequested: z.boolean(),
specialRequest: z.object({
comment: z.string().optional(),
}),
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(),
card: z
.object({
alias: z.string(),
expiryDate: z.string(),
cardType: z.string(),
})
.optional(),
cardHolder: z
.object({
email: z.string().email(),
name: z.string(),
phoneCountryCode: z.string(),
phoneSubscriber: z.string(),
})
.optional(),
success: z.string(),
error: z.string(),
cancel: z.string(),
})
// Mutation
export const createBookingInput = z.object({
hotelId: z.string(),
checkInDate: z.string(),
checkOutDate: z.string(),
rooms: roomsSchema,
payment: paymentSchema.optional(),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const addPackageInput = z.object({
ancillaryComment: z.string(),
ancillaryDeliveryTime: z.string().nullish(),
packages: z.array(
z.object({
code: z.string(),
quantity: z.number(),
comment: z.string().optional(),
})
),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const removePackageInput = z.object({
codes: z.array(z.string()),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const cancelBookingsInput = z.object({
language: z.nativeEnum(Lang),
})
export const guaranteeBookingInput = z.object({
card: z
.object({
alias: z.string(),
expiryDate: z.string(),
cardType: z.string(),
})
.optional(),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
success: z.string().nullable(),
error: z.string().nullable(),
cancel: z.string().nullable(),
})
export const createRefIdInput = z.object({
confirmationNumber: z
.string()
.trim()
.regex(/^\s*[0-9]+(-[0-9])?\s*$/)
.min(1),
lastName: z.string().trim().max(250).min(1),
})
export const updateBookingInput = z.object({
checkInDate: z.string().optional(),
checkOutDate: z.string().optional(),
guest: z
.object({
email: z.string().optional(),
phoneNumber: z.string().optional(),
countryCode: z.string().optional(),
})
.optional(),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
// Query
export const getBookingInput = z.object({
lang: z.nativeEnum(Lang).optional(),
})
export const getLinkedReservationsInput = z.object({
lang: z.nativeEnum(Lang).optional(),
})
export const findBookingInput = z.object({
confirmationNumber: z.string(),
firstName: z.string(),
lastName: z.string(),
email: z.string(),
lang: z.nativeEnum(Lang).optional(),
})
export type LinkedReservationsInput = z.input<typeof getLinkedReservationsInput>
export const getBookingStatusInput = z.object({
lang: z.nativeEnum(Lang).optional(),
})

View File

@@ -1,404 +0,0 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { router } from "@scandic-hotels/trpc"
import * as api from "@scandic-hotels/trpc/api"
import { safeProtectedServiceProcedure } from "@scandic-hotels/trpc/procedures"
import { createRefIdPlugin } from "@/server/plugins/refIdToConfirmationNumber"
import { getMembershipNumber } from "@/server/routers/user/utils"
import { encrypt } from "@/utils/encryption"
import { isValidSession } from "@/utils/session"
import {
addPackageInput,
cancelBookingsInput,
createBookingInput,
guaranteeBookingInput,
removePackageInput,
updateBookingInput,
} from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { cancelBooking } from "./utils"
const refIdPlugin = createRefIdPlugin()
export const bookingMutationRouter = router({
create: safeProtectedServiceProcedure
.input(createBookingInput)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { language, ...inputWithoutLang } = input
const { rooms, ...loggableInput } = inputWithoutLang
const createBookingCounter = createCounter("trpc.booking", "create")
const metricsCreateBooking = createBookingCounter.init({
membershipNumber: await getMembershipNumber(ctx.session),
language,
...loggableInput,
rooms: inputWithoutLang.rooms.map(({ guest, ...room }) => {
const { becomeMember, membershipNumber } = guest
return { ...room, guest: { becomeMember, membershipNumber } }
}),
})
metricsCreateBooking.start()
const headers = {
Authorization: `Bearer ${ctx.token}`,
}
const apiResponse = await api.post(
api.endpoints.v1.Booking.bookings,
{
headers,
body: inputWithoutLang,
},
{ language }
)
if (!apiResponse.ok) {
await metricsCreateBooking.httpError(apiResponse)
const apiJson = await apiResponse.json()
if ("errors" in apiJson && apiJson.errors.length) {
const error = apiJson.errors[0]
return { error: true, cause: error.code } as const
}
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsCreateBooking.validationError(verifiedData.error)
return null
}
metricsCreateBooking.success()
const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry
return {
booking: verifiedData.data,
sig: encrypt(expire.toString()),
}
}),
priceChange: safeProtectedServiceProcedure
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx }) {
const { confirmationNumber, token } = ctx
const priceChangeCounter = createCounter("trpc.booking", "price-change")
const metricsPriceChange = priceChangeCounter.init({ confirmationNumber })
metricsPriceChange.start()
const headers = {
Authorization: `Bearer ${token}`,
}
const apiResponse = await api.put(
api.endpoints.v1.Booking.priceChange(confirmationNumber),
{
headers,
}
)
if (!apiResponse.ok) {
await metricsPriceChange.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsPriceChange.validationError(verifiedData.error)
return null
}
metricsPriceChange.success()
return verifiedData.data
}),
cancel: safeProtectedServiceProcedure
.input(cancelBookingsInput)
.concat(refIdPlugin.toConfirmationNumbers)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumbers, token } = ctx
const { language } = input
const responses = await Promise.allSettled(
confirmationNumbers.map((confirmationNumber) =>
cancelBooking(confirmationNumber, language, token)
)
)
const cancelledRoomsSuccessfully: (string | null)[] = []
for (const [idx, response] of responses.entries()) {
if (response.status === "fulfilled") {
if (response.value) {
cancelledRoomsSuccessfully.push(confirmationNumbers[idx])
continue
}
} else {
console.info(
`Cancelling booking failed for confirmationNumber: ${confirmationNumbers[idx]}`
)
console.error(response.reason)
}
cancelledRoomsSuccessfully.push(null)
}
return cancelledRoomsSuccessfully
}),
packages: safeProtectedServiceProcedure
.input(addPackageInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber, token } = ctx
const { language, refId, ...body } = input
const addPackageCounter = createCounter("trpc.booking", "package.add")
const metricsAddPackage = addPackageCounter.init({
confirmationNumber,
language,
})
metricsAddPackage.start()
const headers = {
Authorization: `Bearer ${token}`,
}
const apiResponse = await api.post(
api.endpoints.v1.Booking.packages(confirmationNumber),
{
headers,
body: body,
},
{ language }
)
if (!apiResponse.ok) {
await metricsAddPackage.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsAddPackage.validationError(verifiedData.error)
return null
}
metricsAddPackage.success()
return verifiedData.data
}),
guarantee: safeProtectedServiceProcedure
.input(guaranteeBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber, token } = ctx
const { language, refId, ...body } = input
const guaranteeBookingCounter = createCounter("trpc.booking", "guarantee")
const metricsGuaranteeBooking = guaranteeBookingCounter.init({
confirmationNumber,
language,
})
metricsGuaranteeBooking.start()
const headers = {
Authorization: `Bearer ${token}`,
}
const apiResponse = await api.put(
api.endpoints.v1.Booking.guarantee(confirmationNumber),
{
headers,
body: body,
},
{ language }
)
if (!apiResponse.ok) {
await metricsGuaranteeBooking.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsGuaranteeBooking.validationError(verifiedData.error)
return null
}
metricsGuaranteeBooking.success()
return verifiedData.data
}),
update: safeProtectedServiceProcedure
.input(updateBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber, token } = ctx
const { language, refId, ...body } = input
const updateBookingCounter = createCounter("trpc.booking", "update")
const metricsUpdateBooking = updateBookingCounter.init({
confirmationNumber,
language,
})
metricsUpdateBooking.start()
const apiResponse = await api.put(
api.endpoints.v1.Booking.booking(confirmationNumber),
{
body,
headers: {
Authorization: `Bearer ${token}`,
},
},
{ language }
)
if (!apiResponse.ok) {
await metricsUpdateBooking.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = bookingConfirmationSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsUpdateBooking.validationError(verifiedData.error)
return null
}
metricsUpdateBooking.success()
return verifiedData.data
}),
removePackage: safeProtectedServiceProcedure
.input(removePackageInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, next }) => {
const token = isValidSession(ctx.session)
? ctx.session.token.access_token
: ctx.serviceToken
return next({
ctx: {
token,
},
})
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber, token } = ctx
const { codes, language } = input
const removePackageCounter = createCounter(
"trpc.booking",
"package.remove"
)
const metricsRemovePackage = removePackageCounter.init({
confirmationNumber,
codes,
language,
})
metricsRemovePackage.start()
const headers = {
Authorization: `Bearer ${token}`,
}
const apiResponse = await api.remove(
api.endpoints.v1.Booking.packages(confirmationNumber),
{
headers,
},
[["language", language], ...codes.map((code) => ["codes", code])]
)
if (!apiResponse.ok) {
await metricsRemovePackage.httpError(apiResponse)
return false
}
metricsRemovePackage.success()
return true
}),
})

View File

@@ -1,301 +0,0 @@
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 { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
import { BookingStatusEnum } from "@/constants/booking"
import { calculateRefId } from "@/utils/refId"
const guestSchema = z.object({
email: nullableStringEmailValidator,
firstName: nullableStringValidator,
lastName: nullableStringValidator,
membershipNumber: nullableStringValidator,
phoneNumber: nullableStringValidator,
countryCode: nullableStringValidator,
})
export type Guest = z.output<typeof guestSchema>
// MUTATION
export const createBookingSchema = z
.object({
data: z.object({
attributes: z.object({
reservationStatus: z.string(),
guest: guestSchema.optional(),
paymentUrl: z.string().nullable().optional(),
rooms: z
.array(
z.object({
confirmationNumber: z.string(),
cancellationNumber: z.string().nullable(),
priceChangedMetadata: z
.object({
roomPrice: z.number(),
totalPrice: z.number(),
})
.nullable()
.optional(),
})
)
.default([]),
errors: z
.array(
z.object({
confirmationNumber: z.string().nullable().optional(),
errorCode: z.string(),
description: z.string().nullable().optional(),
meta: z
.record(z.string(), z.union([z.string(), z.number()]))
.nullable()
.optional(),
})
)
.default([]),
}),
type: z.string(),
id: z.string(),
links: z.object({
self: z.object({
href: z.string().url(),
meta: z.object({
method: z.string(),
}),
}),
}),
}),
})
.transform((d) => ({
id: d.data.id,
links: d.data.links,
type: d.data.type,
reservationStatus: d.data.attributes.reservationStatus,
paymentUrl: d.data.attributes.paymentUrl,
rooms: d.data.attributes.rooms.map((room) => {
const lastName = d.data.attributes.guest?.lastName ?? ""
return {
...room,
refId: calculateRefId(room.confirmationNumber, lastName),
}
}),
errors: d.data.attributes.errors,
guest: d.data.attributes.guest,
}))
// 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(""),
})
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,
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(),
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,
canModifyAncillaries: !!data.links.addAncillary,
// Typo from API
cancellationNumber: data.attributes.cancelationNumber,
}))

View File

@@ -1,272 +0,0 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { router } from "@scandic-hotels/trpc"
import * as api from "@scandic-hotels/trpc/api"
import {
badRequestError,
serverErrorByStatus,
} from "@scandic-hotels/trpc/errors"
import {
safeProtectedServiceProcedure,
serviceProcedure,
} from "@scandic-hotels/trpc/procedures"
import { getHotel } from "@scandic-hotels/trpc/routers/hotels/utils"
import { toApiLang } from "@scandic-hotels/trpc/utils"
import { createRefIdPlugin } from "@/server/plugins/refIdToConfirmationNumber"
import { getBookedHotelRoom } from "@/utils/booking"
import { encrypt } from "../../../utils/encryption"
import {
createRefIdInput,
findBookingInput,
getBookingInput,
getBookingStatusInput,
getLinkedReservationsInput,
} from "./input"
import { createBookingSchema } from "./output"
import { findBooking, getBooking } from "./utils"
const refIdPlugin = createRefIdPlugin()
export const bookingQueryRouter = router({
get: safeProtectedServiceProcedure
.input(getBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang
return next({
ctx: {
lang,
},
})
})
.query(async function ({ ctx }) {
const { confirmationNumber, lang, serviceToken } = ctx
const getBookingCounter = createCounter("trpc.booking", "get")
const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
metricsGetBooking.start()
const booking = await getBooking(confirmationNumber, lang, serviceToken)
if (!booking) {
metricsGetBooking.dataError(
`Fail to get booking data for ${confirmationNumber}`,
{ confirmationNumber }
)
return null
}
const hotelData = await getHotel(
{
hotelId: booking.hotelId,
isCardOnlyPayment: false,
language: lang,
},
serviceToken
)
if (!hotelData) {
metricsGetBooking.dataError(
`Failed to get hotel data for ${booking.hotelId}`,
{
hotelId: booking.hotelId,
}
)
throw serverErrorByStatus(404)
}
metricsGetBooking.success()
return {
...hotelData,
booking,
room: getBookedHotelRoom(
hotelData.roomCategories,
booking.roomTypeCode
),
}
}),
findBooking: safeProtectedServiceProcedure
.input(findBookingInput)
.query(async function ({
ctx,
input: { confirmationNumber, lastName, firstName, email },
}) {
const findBookingCounter = createCounter("trpc.booking", "findBooking")
const metricsFindBooking = findBookingCounter.init({ confirmationNumber })
metricsFindBooking.start()
const booking = await findBooking(
confirmationNumber,
ctx.lang,
ctx.serviceToken,
lastName,
firstName,
email
)
if (!booking) {
metricsFindBooking.dataError(
`Fail to find booking data for ${confirmationNumber}`,
{ confirmationNumber }
)
return null
}
const hotelData = await getHotel(
{
hotelId: booking.hotelId,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
if (!hotelData) {
metricsFindBooking.dataError(
`Failed to find hotel data for ${booking.hotelId}`,
{
hotelId: booking.hotelId,
}
)
throw serverErrorByStatus(404)
}
metricsFindBooking.success()
return {
...hotelData,
booking,
room: getBookedHotelRoom(
hotelData.roomCategories,
booking.roomTypeCode
),
}
}),
linkedReservations: safeProtectedServiceProcedure
.input(getLinkedReservationsInput)
.concat(refIdPlugin.toConfirmationNumber)
.use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang
return next({
ctx: {
lang,
},
})
})
.query(async function ({ ctx }) {
const { confirmationNumber, lang, serviceToken } = ctx
const getLinkedReservationsCounter = createCounter(
"trpc.booking",
"linkedReservations"
)
const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
confirmationNumber,
})
metricsGetLinkedReservations.start()
const booking = await getBooking(confirmationNumber, lang, serviceToken)
if (!booking) {
return []
}
const linkedReservationsResults = await Promise.allSettled(
booking.linkedReservations.map((linkedReservation) =>
getBooking(linkedReservation.confirmationNumber, lang, serviceToken)
)
)
const linkedReservations = []
for (const linkedReservationsResult of linkedReservationsResults) {
if (linkedReservationsResult.status === "fulfilled") {
if (linkedReservationsResult.value) {
linkedReservations.push(linkedReservationsResult.value)
} else {
metricsGetLinkedReservations.dataError(
`Unexpected value for linked reservation`
)
}
} else {
metricsGetLinkedReservations.dataError(
`Failed to get linked reservation`
)
}
}
metricsGetLinkedReservations.success()
return linkedReservations
}),
status: serviceProcedure
.input(getBookingStatusInput)
.concat(refIdPlugin.toConfirmationNumber)
.query(async function ({ ctx, input }) {
const lang = input.lang ?? ctx.lang
const { confirmationNumber } = ctx
const language = toApiLang(lang)
const getBookingStatusCounter = createCounter("trpc.booking", "status")
const metricsGetBookingStatus = getBookingStatusCounter.init({
confirmationNumber,
})
metricsGetBookingStatus.start()
const apiResponse = await api.get(
api.endpoints.v1.Booking.status(confirmationNumber),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
{
language,
}
)
if (!apiResponse.ok) {
await metricsGetBookingStatus.httpError(apiResponse)
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsGetBookingStatus.validationError(verifiedData.error)
throw badRequestError()
}
metricsGetBookingStatus.success()
const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry
return {
booking: verifiedData.data,
sig: encrypt(expire.toString()),
}
}),
createRefId: serviceProcedure
.input(createRefIdInput)
.mutation(async function ({ input }) {
const { confirmationNumber, lastName } = input
const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`)
if (!encryptedRefId) {
throw serverErrorByStatus(422, "Was not able to encrypt ref id")
}
return {
refId: encryptedRefId,
}
}),
})

View File

@@ -1,161 +0,0 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import * as api from "@scandic-hotels/trpc/api"
import {
badRequestError,
serverErrorByStatus,
} from "@scandic-hotels/trpc/errors"
import { toApiLang } from "@scandic-hotels/trpc/utils"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import type { Lang } from "@scandic-hotels/common/constants/language"
export async function getBooking(
confirmationNumber: string,
lang: Lang,
token: string
) {
const getBookingCounter = createCounter("booking", "get")
const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
metricsGetBooking.start()
const apiResponse = await api.get(
api.endpoints.v1.Booking.booking(confirmationNumber),
{
headers: {
Authorization: `Bearer ${token}`,
},
},
{ language: toApiLang(lang) }
)
if (!apiResponse.ok) {
await metricsGetBooking.httpError(apiResponse)
// If the booking is not found, return null.
// This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them.
if (apiResponse.status === 404) {
return null
}
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
}
metricsGetBooking.success()
return booking.data
}
export async function findBooking(
confirmationNumber: string,
lang: Lang,
token: string,
lastName?: string,
firstName?: string,
email?: string
) {
const findBookingCounter = createCounter("booking", "find")
const metricsGetBooking = findBookingCounter.init({
confirmationNumber,
lastName,
firstName,
email,
})
metricsGetBooking.start()
const apiResponse = await api.post(
api.endpoints.v1.Booking.find(confirmationNumber),
{
headers: {
Authorization: `Bearer ${token}`,
},
body: {
lastName,
firstName,
email,
},
},
{ language: toApiLang(lang) }
)
if (!apiResponse.ok) {
await metricsGetBooking.httpError(apiResponse)
// If the booking is not found, return null.
// This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them.
if (apiResponse.status === 400) {
return null
}
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
}
metricsGetBooking.success()
return booking.data
}
export async function cancelBooking(
confirmationNumber: string,
language: Lang,
token: string
) {
const cancelBookingCounter = createCounter("booking", "cancel")
const metricsCancelBooking = cancelBookingCounter.init({
confirmationNumber,
language,
})
metricsCancelBooking.start()
const headers = {
Authorization: `Bearer ${token}`,
}
const booking = await getBooking(confirmationNumber, language, token)
if (!booking) {
metricsCancelBooking.noDataError({ confirmationNumber })
return null
}
const { firstName, lastName, email } = booking.guest
const apiResponse = await api.remove(
api.endpoints.v1.Booking.cancel(confirmationNumber),
{
headers,
body: { firstName, lastName, email },
},
{ language: toApiLang(language) }
)
if (!apiResponse.ok) {
await metricsCancelBooking.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsCancelBooking.validationError(verifiedData.error)
return null
}
metricsCancelBooking.success()
return verifiedData.data
}

View File

@@ -4,8 +4,7 @@ import { z } from "zod"
import { Lang } from "@scandic-hotels/common/constants/language"
import { safeProtectedProcedure } from "@scandic-hotels/trpc/procedures"
import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils"
import { isValidSession } from "@/utils/session"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { getPrimaryLinks } from "./getPrimaryLinks"
import { getSecondaryLinks } from "./getSecondaryLinks"

View File

@@ -9,8 +9,8 @@ import {
import { getFriendsMembership } from "@scandic-hotels/trpc/routers/user/helpers"
import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils"
import { toApiLang } from "@scandic-hotels/trpc/utils"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { isValidSession } from "@/utils/session"
import { getMembershipCards } from "@/utils/user"
import {

View File

@@ -7,14 +7,13 @@ import { getFriendsMembership } from "@scandic-hotels/trpc/routers/user/helpers"
import { creditCardsSchema } from "@scandic-hotels/trpc/routers/user/output"
import { getVerifiedUser } from "@scandic-hotels/trpc/routers/user/utils"
import { toApiLang } from "@scandic-hotels/trpc/utils"
import { encrypt } from "@scandic-hotels/trpc/utils/encryption"
import { myBookingPath } from "@/constants/myBooking"
import { env } from "@/env/server"
import { cache } from "@/utils/cache"
import { encrypt } from "@/utils/encryption"
import * as maskValue from "@/utils/maskValue"
import { isValidSession } from "@/utils/session"
import { getCurrentWebUrl } from "@/utils/url"
import { type FriendTransaction, getStaysSchema, type Stay } from "./output"
@@ -23,19 +22,6 @@ import type { Lang } from "@scandic-hotels/common/constants/language"
import type { User } from "@scandic-hotels/trpc/types/user"
import type { Session } from "next-auth"
export async function getMembershipNumber(
session: Session | null
): Promise<string | undefined> {
if (!isValidSession(session)) return undefined
const verifiedUser = await getVerifiedUser({ session })
if (!verifiedUser || "error" in verifiedUser) {
return undefined
}
return verifiedUser.data.membershipNumber
}
export async function getPreviousStays(
accessToken: string,
limit: number = 10,

View File

@@ -3,9 +3,10 @@ import { produce } from "immer"
import { useContext } from "react"
import { create, useStore } from "zustand"
import { getBookedHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails"
import { MyStayContext } from "@/contexts/MyStay"
import { getBookedHotelRoom } from "@/utils/booking"
import {
calculateTotalPoints,

View File

@@ -1,4 +1,4 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface ManageBookingProps
extends Pick<BookingConfirmation, "booking"> {}

View File

@@ -1,8 +1,8 @@
import type { Room } from "@scandic-hotels/trpc/types/hotel"
import type {
BookingConfirmation,
BookingConfirmationSchema,
} from "@/types/trpc/routers/booking/confirmation"
} from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
export interface BookingConfirmationProps {
refId: string

View File

@@ -1,7 +1,6 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { MutableRefObject } from "react"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationHeaderProps
extends Pick<BookingConfirmation, "booking" | "hotel"> {
mainRef: MutableRefObject<HTMLElement | null>

View File

@@ -1,4 +1,4 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface BookingConfirmationHotelDetailsProps {
hotel: BookingConfirmation["hotel"]

View File

@@ -1,3 +1,3 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface PromosProps extends Pick<BookingConfirmation, "booking"> {}

View File

@@ -1,5 +1,5 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export interface BookingConfirmationRoomsProps
extends Pick<BookingConfirmation, "booking"> {

View File

@@ -1,4 +1,4 @@
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
export interface RoomProps {
booking: BookingConfirmation["booking"]

View File

@@ -1,5 +1,6 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { Ancillaries } from "../../myPages/myStay/ancillaries"
export interface SpecificationProps {

View File

@@ -3,7 +3,6 @@ import type { VariantProps } from "class-variance-authority"
import type { PropGetters } from "downshift"
import type { dialogVariants } from "@/components/Forms/BookingWidget/FormContent/Search/SearchList/Dialog/variants"
// import type { AutoCompleteLocation } from "@/server/routers/autocomplete/schema"
export interface SearchProps {
handlePressEnter: () => void

View File

@@ -1,11 +1,10 @@
import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import type { ChildBedTypeEnum } from "@scandic-hotels/trpc/enums/childBedTypeEnum"
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
import type {
BookingConfirmation,
PackageSchema,
} from "../trpc/routers/booking/confirmation"
} from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
export interface ChildBedPreference {
quantity: number

View File

@@ -1,3 +1,4 @@
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type {
Hotel,
@@ -12,7 +13,6 @@ import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDet
import type { RoomPrice } from "@/types/components/hotelReservation/enterDetails/details"
import type { PriceTypeEnum } from "@/types/components/hotelReservation/myStay/myStay"
import type { Packages } from "@/types/components/myPages/myStay/ancillaries"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export type Room = Omit<
BookingConfirmation["booking"],

View File

@@ -1,21 +0,0 @@
import type { HotelData, Room } from "@scandic-hotels/trpc/types/hotel"
import type { z } from "zod"
import type {
bookingConfirmationSchema,
packageSchema,
} from "@/server/routers/booking/output"
export interface BookingConfirmationSchema
extends z.output<typeof bookingConfirmationSchema> {}
export interface PackageSchema extends z.output<typeof packageSchema> {}
export interface BookingConfirmation extends HotelData {
booking: BookingConfirmationSchema
room:
| (Room & {
bedType: Room["roomTypes"][number]
})
| null
}

View File

@@ -1,27 +0,0 @@
import type { Room } from "@scandic-hotels/trpc/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getBookedHotelRoom(
rooms: Room[],
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
) {
if (!rooms.length || !roomTypeCode) {
return null
}
const room = rooms.find((r) => {
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
})
if (!room) {
return null
}
const bedType = room.roomTypes.find(
(roomType) => roomType.code === roomTypeCode
)
if (!bedType) {
return null
}
return {
...room,
bedType,
}
}

View File

@@ -1,52 +0,0 @@
import "server-only"
import crypto from "crypto"
import { env } from "@/env/server"
const algorithm = "DES-ECB"
const encryptionKey = env.BOOKING_ENCRYPTION_KEY
const bufferKey = Buffer.from(encryptionKey, "utf8")
export function encrypt(originalString: string) {
try {
const cipher = crypto.createCipheriv(algorithm, bufferKey, null)
cipher.setAutoPadding(false)
const bufferString = Buffer.from(originalString, "utf8")
const paddingSize =
bufferKey.length - (bufferString.length % bufferKey.length)
const paddedStr = Buffer.concat([
bufferString,
Buffer.alloc(paddingSize, 0),
])
const buffers: Buffer[] = []
buffers.push(cipher.update(paddedStr))
buffers.push(cipher.final())
const result = Buffer.concat(buffers).toString("base64")
return result
} catch (e) {
console.log(e)
return ""
}
}
export function decrypt(encryptedString: string) {
try {
const decipher = crypto.createDecipheriv(algorithm, bufferKey, null)
decipher.setAutoPadding(false)
const buffers: Buffer[] = []
buffers.push(decipher.update(encryptedString, "base64"))
buffers.push(decipher.final())
const result = Buffer.concat(buffers)
.toString("utf8")
/*
* Hexadecimal byte (null byte) replace. These occur when decrypting because
* we're disabling the auto padding for historical/compatibility reasons.
*/
.replace(/(\x00)*/g, "")
return result
} catch (e) {
console.log(e)
return ""
}
}

View File

@@ -2,9 +2,9 @@ import "server-only"
import { cookies } from "next/headers"
import { auth } from "@/auth"
import { isValidSession } from "@scandic-hotels/trpc/utils/session"
import { isValidSession } from "./session"
import { auth } from "@/auth"
export async function isLoggedInUser(): Promise<boolean> {
const session = await auth()

View File

@@ -1,34 +0,0 @@
import "server-only"
import { decrypt, encrypt } from "./encryption"
export function calculateRefId(confirmationNumber: string, lastName: string) {
const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`)
return encryptedRefId
}
export function parseRefId(refId: string) {
// RefId is DES-ECB encryption + Base64 encoding. For legacy reasons we have
// to do some manual handling here to get a proper Base64 string.
//
// - Use case: Current web replaced plus sign with hyphens when generating RefIds.
// Handling: We replace hyphens with plus signs.
//
// - Use case: Incoming links in the wild do not encode the RefId properly.
// Handling: We replace spaces with plus signs. Effectively, reversing the
// decoding of plus signs into spaces that Next.js does for us for incoming
// search params.
// Slash and equal sign are not decoded into anything, so no action needed.
// We only need to cater for those three (plus, slash, equals) as RefId is
// Base64 encoded which only has these three special characters.
const data = decrypt(refId.replace(/ |-/g, "+"))
const parts = data.split(",")
if (parts.length !== 2) {
throw new Error("Invalid refId format")
}
return {
confirmationNumber: parts[0],
lastName: parts[1],
}
}

View File

@@ -1,26 +0,0 @@
import "server-only"
import type { Session } from "next-auth"
export function isValidSession(session: Session | null): session is Session {
if (!session) {
return false
}
if (session.error) {
console.log(`Session error: ${session.error}`)
return false
}
const token = session.token
if (token?.error) {
console.log(`Session token error: ${token.error}`)
return false
}
if (token?.expires_at && token.expires_at < Date.now()) {
console.log(`Session expired: ${session.token.expires_at}`)
return false
}
return true
}

View File

@@ -1,9 +1,8 @@
import { RoomPackageCodeEnum } from "@scandic-hotels/trpc/enums/roomFilter"
import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { Packages } from "@scandic-hotels/trpc/types/packages"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getSpecialRoomType(
packages: BookingConfirmation["booking"]["packages"] | Packages | null
) {

View File

@@ -3,11 +3,11 @@ import { BreakfastPackageEnum } from "@scandic-hotels/trpc/enums/breakfast"
import { trackEvent } from "./base"
import type { PackageSchema } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { SelectedAncillary } from "@/types/components/myPages/myStay/ancillaries"
import type { Room } from "@/types/stores/my-stay"
import type { PackageSchema } from "@/types/trpc/routers/booking/confirmation"
import type { BreakfastData } from "@/stores/my-stay/add-ancillary-flow"
export function trackCancelStay(hotelId: string, bnr: string) {