From 16cc26632e5282275c5c22af09c4bcfb14b086af Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Joakim=20J=C3=A4derberg?=
Date: Mon, 2 Feb 2026 14:28:14 +0000
Subject: [PATCH] Merged in chore/refactor-trpc-booking-routes (pull request
#3510)
feat(BOOK-750): refactor booking endpoints
* WIP
* wip
* wip
* parse dates in UTC
* wip
* no more errors
* Merge branch 'master' of bitbucket.org:scandic-swap/web into chore/refactor-trpc-booking-routes
* .
* cleanup
* import named z from zod
* fix(BOOK-750): updateBooking api endpoint expects dateOnly, we passed ISO date
Approved-by: Anton Gunnarsson
---
.../MyStay/GuestDetails/index.tsx | 10 +-
.../MyStay/accessBooking.test.ts | 6 +-
.../HotelReservation/MyStay/accessBooking.ts | 4 +-
apps/scandic-web/providers/MyStay.tsx | 8 +-
.../EnterDetails/Payment/PaymentClient.tsx | 80 ++---
.../lib/pages/PaymentCallbackPage.tsx | 8 +-
packages/trpc/lib/errors.ts | 7 +
packages/trpc/lib/routers/booking/input.ts | 21 +-
.../booking/mutation/addPackagesRoute.ts | 42 +++
.../booking/mutation/cancelBookingRoute.ts | 51 +++
.../routers/booking/mutation/create/index.ts | 95 ------
.../mutation/createBookingRoute/index.ts | 46 +++
.../{create => createBookingRoute}/schema.ts | 118 ++-----
.../booking/mutation/createRefIdRoute.ts | 19 ++
.../booking/mutation/guaranteeBookingRoute.ts | 43 +++
.../lib/routers/booking/mutation/index.ts | 264 ++------------
.../booking/mutation/priceChangeRoute.ts | 24 ++
.../mutation/validatePartnerPayment.ts | 2 +-
packages/trpc/lib/routers/booking/query.ts | 323 +-----------------
.../routers/booking/query/findBookingRoute.ts | 87 +++++
.../routers/booking/query/getBookingRoute.ts | 84 +++++
.../booking/query/getBookingStatusRoute.ts | 33 ++
.../query/getLinkedReservationsRoute.ts | 34 ++
packages/trpc/lib/routers/booking/utils.ts | 171 ----------
.../lib/routers/contentstack/schemas/alert.ts | 2 +-
.../booking/addPackageToBooking/index.ts | 60 ++++
.../services/booking/cancelBooking/index.ts | 76 +++++
.../services/booking/cancelBooking/schema.ts | 56 +++
.../services/booking/createBooking/index.ts | 88 +++++
.../services/booking/createBooking/schema.ts | 84 +++++
.../lib/services/booking/findBooking/index.ts | 81 +++++
.../lib/services/booking/getBooking/index.ts | 59 ++++
.../booking/getBooking/schema.ts} | 90 +++--
.../booking/getBookingStatus/index.ts | 65 ++++
.../booking/getBookingStatus/schema.ts | 61 ++++
.../booking/guaranteeBooking/index.ts | 63 ++++
.../booking/linkedReservations/index.ts | 64 ++++
.../lib/services/booking/priceChange/index.ts | 41 +++
packages/trpc/lib/services/booking/schema.ts | 84 +++++
.../schema/bookingReservationStatusSchema.ts | 22 ++
.../lib/services/booking/updateBooking.ts | 80 +++++
.../trpc/lib/types/bookingConfirmation.ts | 3 +-
packages/trpc/lib/types/entry.ts | 2 +-
packages/trpc/package.json | 1 +
44 files changed, 1621 insertions(+), 1041 deletions(-)
create mode 100644 packages/trpc/lib/routers/booking/mutation/addPackagesRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/mutation/cancelBookingRoute.ts
delete mode 100644 packages/trpc/lib/routers/booking/mutation/create/index.ts
create mode 100644 packages/trpc/lib/routers/booking/mutation/createBookingRoute/index.ts
rename packages/trpc/lib/routers/booking/mutation/{create => createBookingRoute}/schema.ts (55%)
create mode 100644 packages/trpc/lib/routers/booking/mutation/createRefIdRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/mutation/guaranteeBookingRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/mutation/priceChangeRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/query/findBookingRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/query/getBookingRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/query/getBookingStatusRoute.ts
create mode 100644 packages/trpc/lib/routers/booking/query/getLinkedReservationsRoute.ts
delete mode 100644 packages/trpc/lib/routers/booking/utils.ts
create mode 100644 packages/trpc/lib/services/booking/addPackageToBooking/index.ts
create mode 100644 packages/trpc/lib/services/booking/cancelBooking/index.ts
create mode 100644 packages/trpc/lib/services/booking/cancelBooking/schema.ts
create mode 100644 packages/trpc/lib/services/booking/createBooking/index.ts
create mode 100644 packages/trpc/lib/services/booking/createBooking/schema.ts
create mode 100644 packages/trpc/lib/services/booking/findBooking/index.ts
create mode 100644 packages/trpc/lib/services/booking/getBooking/index.ts
rename packages/trpc/lib/{routers/booking/output.ts => services/booking/getBooking/schema.ts} (93%)
create mode 100644 packages/trpc/lib/services/booking/getBookingStatus/index.ts
create mode 100644 packages/trpc/lib/services/booking/getBookingStatus/schema.ts
create mode 100644 packages/trpc/lib/services/booking/guaranteeBooking/index.ts
create mode 100644 packages/trpc/lib/services/booking/linkedReservations/index.ts
create mode 100644 packages/trpc/lib/services/booking/priceChange/index.ts
create mode 100644 packages/trpc/lib/services/booking/schema.ts
create mode 100644 packages/trpc/lib/services/booking/schema/bookingReservationStatusSchema.ts
create mode 100644 packages/trpc/lib/services/booking/updateBooking.ts
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx
index eed717ecc..0f0809224 100644
--- a/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx
+++ b/apps/scandic-web/components/HotelReservation/MyStay/GuestDetails/index.tsx
@@ -1,4 +1,5 @@
"use client"
+
import { zodResolver } from "@hookform/resolvers/zod"
import { usePathname, useRouter } from "next/navigation"
import { useState } from "react"
@@ -25,7 +26,7 @@ import ModifyContact from "../ModifyContact"
import styles from "./guestDetails.module.css"
-import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
+import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import {
type ModifyContactSchema,
@@ -34,9 +35,9 @@ import {
import { MODAL_STEPS } from "@/types/components/hotelReservation/myStay/myStay"
import type { SafeUser } from "@/types/user"
-interface GuestDetailsProps {
+type GuestDetailsProps = {
refId: string
- guest: Guest
+ guest: BookingConfirmation["booking"]["guest"]
isCancelled: boolean
user: SafeUser
}
@@ -76,6 +77,7 @@ export default function GuestDetails({
const isFirstStep = currentStep === MODAL_STEPS.INITIAL
const isMemberBooking =
+ !!user?.membership?.membershipNumber &&
guest.membershipNumber === user?.membership?.membershipNumber
const updateGuest = trpc.booking.update.useMutation({
@@ -196,7 +198,7 @@ export default function GuestDetails({
{guest.firstName} {guest.lastName}
- {isMemberBooking && user.membership && (
+ {isMemberBooking && user?.membership && (
{intl.formatMessage(
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts
index bb99926cf..477acd4ce 100644
--- a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts
+++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts
@@ -8,7 +8,7 @@ import accessBooking, {
} from "./accessBooking"
import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue"
-import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
+import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { SafeUser } from "@/types/user"
@@ -201,7 +201,7 @@ const badAuthenticatedUser: SafeUser = {
profilingConsentUpdateDate: undefined,
}
-const loggedOutGuest: Guest = {
+const loggedOutGuest: BookingConfirmation["booking"]["guest"] = {
email: "logged+out@scandichotels.com",
firstName: "Anonymous",
lastName: "Booking",
@@ -210,7 +210,7 @@ const loggedOutGuest: Guest = {
countryCode: "SE",
}
-const loggedInGuest: Guest = {
+const loggedInGuest: BookingConfirmation["booking"]["guest"] = {
email: "logged+in@scandichotels.com",
firstName: "Authenticated",
lastName: "Booking",
diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts
index 7ad9f7d7e..bd8b8f31d 100644
--- a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts
+++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.ts
@@ -1,5 +1,5 @@
import type { AdditionalInfoCookieValue } from "@scandic-hotels/booking-flow/types/components/findMyBooking/additionalInfoCookieValue"
-import type { Guest } from "@scandic-hotels/trpc/routers/booking/output"
+import type { BookingConfirmation } from "@scandic-hotels/trpc/types/bookingConfirmation"
import type { SafeUser } from "@/types/user"
@@ -15,7 +15,7 @@ export {
* Whether a request can access a confirmed booking or not.
*/
function accessBooking(
- guest: Guest,
+ guest: BookingConfirmation["booking"]["guest"],
lastName: string,
user: SafeUser | null,
cookie: string = ""
diff --git a/apps/scandic-web/providers/MyStay.tsx b/apps/scandic-web/providers/MyStay.tsx
index 0bb923739..0e3369f0c 100644
--- a/apps/scandic-web/providers/MyStay.tsx
+++ b/apps/scandic-web/providers/MyStay.tsx
@@ -13,22 +13,20 @@ 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 { BookingConfirmation } 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 { getLinkedReservations } from "@/lib/trpc/memoizedRequests"
interface MyStayProviderProps {
bookingConfirmation: BookingConfirmation
breakfastPackages: Packages | null
isLoggedIn?: boolean
lang: Lang
- linkedReservationsPromise: Promise
+ linkedReservationsPromise: ReturnType
refId: string
roomCategories: RoomCategories
savedCreditCards: CreditCard[] | null
diff --git a/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx b/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx
index 32d1655d8..b4f30e6e5 100644
--- a/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx
+++ b/packages/booking-flow/lib/components/EnterDetails/Payment/PaymentClient.tsx
@@ -52,7 +52,7 @@ import { getPaymentHeadingConfig } from "./utils"
import styles from "./payment.module.css"
-import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
+import type { CreateBookingInput } from "@scandic-hotels/trpc/routers/booking/input"
import type { CreditCard } from "@scandic-hotels/trpc/types/user"
import type { PriceChangeData } from "../PriceChangeData"
@@ -128,45 +128,46 @@ export default function PaymentClient({
const initiateBooking = trpc.booking.create.useMutation({
onSuccess: (result) => {
- if (result) {
- if ("error" in result) {
- const queryParams = new URLSearchParams(searchParams.toString())
- queryParams.set("errorCode", result.cause)
- window.history.replaceState(
- {},
- "",
- `${pathname}?${queryParams.toString()}`
- )
- handlePaymentError(result.cause)
- return
- }
-
- const { booking } = result
- const mainRoom = booking.rooms[0]
-
- if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) {
- clearBookingWidgetState()
- // Cookie is used by Booking Confirmation page to validate that the user came from payment callback
- // eslint-disable-next-line react-hooks/immutability
- document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
- const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
- router.push(confirmationUrl)
- return
- }
-
- setRefId(mainRoom.refId)
-
- const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
- if (hasPriceChange) {
- const priceChangeData = booking.rooms
- .map((room) => room.priceChangedMetadata || null)
- .filter(isNotNull)
- setPriceChangeData(priceChangeData)
- } else {
- setIsPollingForBookingStatus(true)
- }
- } else {
+ if (!result) {
handlePaymentError("No confirmation number")
+ return
+ }
+
+ if ("error" in result) {
+ const queryParams = new URLSearchParams(searchParams.toString())
+ queryParams.set("errorCode", result.cause)
+ window.history.replaceState(
+ {},
+ "",
+ `${pathname}?${queryParams.toString()}`
+ )
+ handlePaymentError(result.cause)
+ return
+ }
+
+ const { booking } = result
+ const mainRoom = booking.rooms[0]
+
+ if (booking.reservationStatus == BookingStatusEnum.BookingCompleted) {
+ clearBookingWidgetState()
+ // Cookie is used by Booking Confirmation page to validate that the user came from payment callback
+ // eslint-disable-next-line react-hooks/immutability
+ document.cookie = `bcsig=${result.sig}; Path=/; Max-Age=60; Secure; SameSite=Strict`
+ const confirmationUrl = `${bookingConfirmation(lang)}?RefId=${encodeURIComponent(mainRoom.refId)}`
+ router.push(confirmationUrl)
+ return
+ }
+
+ setRefId(mainRoom.refId)
+
+ const hasPriceChange = booking.rooms.some((r) => r.priceChangedMetadata)
+ if (hasPriceChange) {
+ const priceChangeData = booking.rooms
+ .map((room) => room.priceChangedMetadata || null)
+ .filter(isNotNull)
+ setPriceChangeData(priceChangeData)
+ } else {
+ setIsPollingForBookingStatus(true)
}
},
onError: (error) => {
@@ -419,6 +420,7 @@ export default function PaymentClient({
}
),
}
+
initiateBooking.mutate(payload)
},
[
diff --git a/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx b/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx
index f120850db..b5651cd26 100644
--- a/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx
+++ b/packages/booking-flow/lib/pages/PaymentCallbackPage.tsx
@@ -9,7 +9,7 @@ import {
import { logger } from "@scandic-hotels/common/logger"
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 { getBooking } from "@scandic-hotels/trpc/services/booking/getBooking"
import { encrypt } from "@scandic-hotels/trpc/utils/encryption"
import { BookingFlowConfig } from "../bookingFlowConfig/bookingFlowConfig"
@@ -18,7 +18,7 @@ import { HandleSuccessCallback } from "../components/EnterDetails/Payment/Paymen
import { serverClient } from "../trpc"
import type { Lang } from "@scandic-hotels/common/constants/language"
-import type { CreateBookingSchema } from "@scandic-hotels/trpc/routers/booking/mutation/create/schema"
+import type { BookingStatus } from "@scandic-hotels/trpc/services/booking/getBookingStatus"
import type { NextSearchParams } from "../types"
@@ -99,7 +99,7 @@ export async function PaymentCallbackPage({
notFound()
}
- const booking = await getBooking(confirmationNumber, lang, token)
+ const booking = await getBooking({ confirmationNumber, lang }, token)
const refId = booking?.refId
const caller = await serverClient()
@@ -156,7 +156,7 @@ function HandleBookingStatusError({
config,
status,
}: {
- booking: CreateBookingSchema | null
+ booking: BookingStatus | null
confirmationNumber?: string
returnUrl: string
config: BookingFlowConfig
diff --git a/packages/trpc/lib/errors.ts b/packages/trpc/lib/errors.ts
index fc1acf724..c75f811e6 100644
--- a/packages/trpc/lib/errors.ts
+++ b/packages/trpc/lib/errors.ts
@@ -79,6 +79,13 @@ export function unprocessableContent(cause?: TRPCCause, message: string = "") {
})
}
+export function badGatewayError(cause?: TRPCCause, message: string = "") {
+ return new TRPCError({
+ code: "BAD_GATEWAY",
+ cause: harmonizeCause(cause, message),
+ })
+}
+
export function internalServerError(cause?: TRPCCause, message: string = "") {
return new TRPCError({
code: "INTERNAL_SERVER_ERROR",
diff --git a/packages/trpc/lib/routers/booking/input.ts b/packages/trpc/lib/routers/booking/input.ts
index 95906c893..dba05e9d3 100644
--- a/packages/trpc/lib/routers/booking/input.ts
+++ b/packages/trpc/lib/routers/booking/input.ts
@@ -30,20 +30,6 @@ 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()
@@ -63,7 +49,7 @@ export const updateBookingInput = z.object({
countryCode: z.string().optional(),
})
.optional(),
- language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
+ language: z.nativeEnum(Lang),
})
// Query
@@ -85,7 +71,4 @@ export const findBookingInput = z.object({
})
export type LinkedReservationsInput = z.input
-
-export const getBookingStatusInput = z.object({
- lang: z.nativeEnum(Lang).optional(),
-})
+export type { CreateBookingInput } from "./mutation/createBookingRoute/schema"
diff --git a/packages/trpc/lib/routers/booking/mutation/addPackagesRoute.ts b/packages/trpc/lib/routers/booking/mutation/addPackagesRoute.ts
new file mode 100644
index 000000000..531185219
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/mutation/addPackagesRoute.ts
@@ -0,0 +1,42 @@
+import { z } from "zod"
+
+import { Lang } from "@scandic-hotels/common/constants/language"
+
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { addPackageToBooking } from "../../../services/booking/addPackageToBooking"
+
+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),
+})
+const refIdPlugin = createRefIdPlugin()
+
+export const addPackagesRoute = safeProtectedServiceProcedure
+ .input(addPackageInput)
+ .concat(refIdPlugin.toConfirmationNumber)
+ .use(async ({ ctx, next }) => {
+ const token = await ctx.getScandicUserToken()
+ return next({
+ ctx: {
+ token,
+ },
+ })
+ })
+ .mutation(async function ({ ctx, input }) {
+ const { confirmationNumber } = ctx
+ const { language, refId, ...body } = input
+
+ return await addPackageToBooking(
+ { confirmationNumber, lang: language, ...body },
+ ctx.token ?? ctx.serviceToken
+ )
+ })
diff --git a/packages/trpc/lib/routers/booking/mutation/cancelBookingRoute.ts b/packages/trpc/lib/routers/booking/mutation/cancelBookingRoute.ts
new file mode 100644
index 000000000..e619674d3
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/mutation/cancelBookingRoute.ts
@@ -0,0 +1,51 @@
+import { createLogger } from "@scandic-hotels/common/logger/createLogger"
+
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { cancelBooking } from "../../../services/booking/cancelBooking"
+import { cancelBookingsInput } from "../input"
+
+const bookingLogger = createLogger("trpc.booking.cancelBooking")
+const refIdPlugin = createRefIdPlugin()
+export const cancelBookingRoute = safeProtectedServiceProcedure
+ .input(cancelBookingsInput)
+ .concat(refIdPlugin.toConfirmationNumbers)
+ .use(async ({ ctx, next }) => {
+ const token = await ctx.getScandicUserToken()
+
+ return next({
+ ctx: {
+ token,
+ },
+ })
+ })
+ .mutation(async function ({ ctx, input }) {
+ const { confirmationNumbers } = ctx
+ const { language } = input
+
+ const token = ctx.token ?? ctx.serviceToken
+ 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 {
+ bookingLogger.error(
+ `Cancelling booking failed for confirmationNumber: ${confirmationNumbers[idx]}`,
+ response.reason
+ )
+ }
+
+ cancelledRoomsSuccessfully.push(null)
+ }
+
+ return cancelledRoomsSuccessfully
+ })
diff --git a/packages/trpc/lib/routers/booking/mutation/create/index.ts b/packages/trpc/lib/routers/booking/mutation/create/index.ts
deleted file mode 100644
index 38ae73558..000000000
--- a/packages/trpc/lib/routers/booking/mutation/create/index.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import "server-only"
-
-import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
-import { createCounter } from "@scandic-hotels/common/telemetry"
-
-import * as api from "../../../../api"
-import { safeProtectedServiceProcedure } from "../../../../procedures"
-import { encrypt } from "../../../../utils/encryption"
-import { createBookingInput, createBookingSchema } from "./schema"
-
-export const create = safeProtectedServiceProcedure
- .input(createBookingInput)
- .use(async ({ ctx, next }) => {
- const token = await ctx.getScandicUserToken()
-
- 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({
- 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 ?? ctx.serviceToken}`,
- }
-
- const includePartnerSpecific =
- inputWithoutLang.payment?.paymentMethod ===
- PaymentMethodEnum.PartnerPoints
- if (includePartnerSpecific) {
- const session = await ctx.auth()
- const token = session?.token.access_token
- if (!token) {
- throw new Error(
- "Cannot create booking with partner points without partner token"
- )
- }
-
- inputWithoutLang.partnerSpecific = {
- eurobonusAccessToken: session?.token.access_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()),
- }
- })
diff --git a/packages/trpc/lib/routers/booking/mutation/createBookingRoute/index.ts b/packages/trpc/lib/routers/booking/mutation/createBookingRoute/index.ts
new file mode 100644
index 000000000..49c2ba60c
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/mutation/createBookingRoute/index.ts
@@ -0,0 +1,46 @@
+import "server-only"
+
+import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
+
+import { safeProtectedServiceProcedure } from "../../../../procedures"
+import { createBooking } from "../../../../services/booking/createBooking"
+import { encrypt } from "../../../../utils/encryption"
+import { createBookingInput } from "./schema"
+export const createBookingRoute = safeProtectedServiceProcedure
+ .input(createBookingInput)
+ .use(async ({ ctx, next }) => {
+ const token = await ctx.getScandicUserToken()
+
+ return next({
+ ctx: {
+ token,
+ },
+ })
+ })
+ .mutation(async function ({ ctx, input }) {
+ if (input.payment?.paymentMethod === PaymentMethodEnum.PartnerPoints) {
+ const session = await ctx.auth()
+ const token = session?.token.access_token
+ if (!token) {
+ throw new Error(
+ "Cannot create booking with partner points without partner token"
+ )
+ }
+
+ input.partnerSpecific = {
+ eurobonusAccessToken: session?.token.access_token,
+ }
+ }
+
+ const booking = await createBooking(input, ctx.token ?? ctx.serviceToken)
+ if ("error" in booking) {
+ return { ...booking }
+ }
+
+ const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry
+
+ return {
+ booking,
+ sig: encrypt(expire.toString()),
+ }
+ })
diff --git a/packages/trpc/lib/routers/booking/mutation/create/schema.ts b/packages/trpc/lib/routers/booking/mutation/createBookingRoute/schema.ts
similarity index 55%
rename from packages/trpc/lib/routers/booking/mutation/create/schema.ts
rename to packages/trpc/lib/routers/booking/mutation/createBookingRoute/schema.ts
index 4165d0ee1..ed37db4bd 100644
--- a/packages/trpc/lib/routers/booking/mutation/create/schema.ts
+++ b/packages/trpc/lib/routers/booking/mutation/createBookingRoute/schema.ts
@@ -1,11 +1,31 @@
import { z } from "zod"
import { Lang } from "@scandic-hotels/common/constants/language"
+import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
-import { langToApiLang } from "../../../../constants/apiLang"
import { ChildBedTypeEnum } from "../../../../enums/childBedTypeEnum"
-import { calculateRefId } from "../../../../utils/refId"
-import { guestSchema } from "../../output"
+
+const paymentSchema = z.object({
+ paymentMethod: z.nativeEnum(PaymentMethodEnum),
+ 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(),
+})
const roomsSchema = z
.array(
@@ -75,28 +95,6 @@ const roomsSchema = z
})
})
-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(),
-})
-
export type CreateBookingInput = z.input
export const createBookingInput = z.object({
hotelId: z.string(),
@@ -104,78 +102,10 @@ export const createBookingInput = z.object({
checkOutDate: z.string(),
rooms: roomsSchema,
payment: paymentSchema.optional(),
- language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
+ language: z.nativeEnum(Lang),
partnerSpecific: z
.object({
eurobonusAccessToken: z.string(),
})
.optional(),
})
-
-export const createBookingSchema = z
- .object({
- data: z.object({
- attributes: z.object({
- reservationStatus: z.string(),
- guest: guestSchema.optional(),
- paymentUrl: z.string().nullable().optional(),
- paymentMethod: 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,
- paymentMethod: d.data.attributes.paymentMethod,
- 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,
- }))
-export type CreateBookingSchema = z.infer
diff --git a/packages/trpc/lib/routers/booking/mutation/createRefIdRoute.ts b/packages/trpc/lib/routers/booking/mutation/createRefIdRoute.ts
new file mode 100644
index 000000000..76fbb9663
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/mutation/createRefIdRoute.ts
@@ -0,0 +1,19 @@
+import { serverErrorByStatus } from "../../../errors"
+import { serviceProcedure } from "../../../procedures"
+import { encrypt } from "../../../utils/encryption"
+import { createRefIdInput } from "../input"
+
+export const createRefIdRoute = 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,
+ }
+ })
diff --git a/packages/trpc/lib/routers/booking/mutation/guaranteeBookingRoute.ts b/packages/trpc/lib/routers/booking/mutation/guaranteeBookingRoute.ts
new file mode 100644
index 000000000..9f1bebdc3
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/mutation/guaranteeBookingRoute.ts
@@ -0,0 +1,43 @@
+import { z } from "zod"
+
+import { Lang } from "@scandic-hotels/common/constants/language"
+
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { guaranteeBooking } from "../../../services/booking/guaranteeBooking"
+
+const guaranteeBookingInput = z.object({
+ card: z
+ .object({
+ alias: z.string(),
+ expiryDate: z.string(),
+ cardType: z.string(),
+ })
+ .optional(),
+ language: z.nativeEnum(Lang),
+ success: z.string().nullable(),
+ error: z.string().nullable(),
+ cancel: z.string().nullable(),
+})
+
+const refIdPlugin = createRefIdPlugin()
+export const guaranteeBookingRoute = safeProtectedServiceProcedure
+ .input(guaranteeBookingInput)
+ .concat(refIdPlugin.toConfirmationNumber)
+ .use(async ({ ctx, next }) => {
+ const token = await ctx.getScandicUserToken()
+
+ return next({
+ ctx: {
+ token,
+ },
+ })
+ })
+ .mutation(async function ({ ctx, input }) {
+ const { confirmationNumber } = ctx
+ const { language, refId, ...body } = input
+
+ const token = ctx.token ?? ctx.serviceToken
+
+ return guaranteeBooking({ confirmationNumber, language, ...body }, token)
+ })
diff --git a/packages/trpc/lib/routers/booking/mutation/index.ts b/packages/trpc/lib/routers/booking/mutation/index.ts
index 2d7ba4ab7..a418389d0 100644
--- a/packages/trpc/lib/routers/booking/mutation/index.ts
+++ b/packages/trpc/lib/routers/booking/mutation/index.ts
@@ -1,226 +1,34 @@
-import { createLogger } from "@scandic-hotels/common/logger/createLogger"
+import { dt } from "@scandic-hotels/common/dt"
import { createCounter } from "@scandic-hotels/common/telemetry"
import { router } from "../../.."
import * as api from "../../../api"
import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
import { safeProtectedServiceProcedure } from "../../../procedures"
+import { updateBooking } from "../../../services/booking/updateBooking"
import {
- addPackageInput,
- cancelBookingsInput,
- guaranteeBookingInput,
removePackageInput,
resendConfirmationInput,
updateBookingInput,
} from "../input"
-import { bookingConfirmationSchema } from "../output"
-import { cancelBooking } from "../utils"
-import { createBookingSchema } from "./create/schema"
-import { create } from "./create"
+import { addPackagesRoute } from "./addPackagesRoute"
+import { cancelBookingRoute } from "./cancelBookingRoute"
+import { createBookingRoute } from "./createBookingRoute"
+import { createRefIdRoute } from "./createRefIdRoute"
+import { guaranteeBookingRoute } from "./guaranteeBookingRoute"
+import { priceChangeRoute } from "./priceChangeRoute"
import { validatePartnerPayment } from "./validatePartnerPayment"
const refIdPlugin = createRefIdPlugin()
-const bookingLogger = createLogger("trpc.booking")
export const bookingMutationRouter = router({
- create,
+ create: createBookingRoute,
+ createRefId: createRefIdRoute,
validatePartnerPayment,
- priceChange: safeProtectedServiceProcedure
- .concat(refIdPlugin.toConfirmationNumber)
- .use(async ({ ctx, next }) => {
- const token = await ctx.getScandicUserToken()
-
- return next({
- ctx: {
- token,
- },
- })
- })
- .mutation(async function ({ ctx }) {
- const { confirmationNumber } = ctx
-
- const priceChangeCounter = createCounter("trpc.booking.price-change")
- const metricsPriceChange = priceChangeCounter.init({ confirmationNumber })
-
- metricsPriceChange.start()
-
- const token = ctx.token ?? ctx.serviceToken
- 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 = await ctx.getScandicUserToken()
-
- return next({
- ctx: {
- token,
- },
- })
- })
- .mutation(async function ({ ctx, input }) {
- const { confirmationNumbers } = ctx
- const { language } = input
-
- const token = ctx.token ?? ctx.serviceToken
- 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 {
- bookingLogger.error(
- `Cancelling booking failed for confirmationNumber: ${confirmationNumbers[idx]}`,
- response.reason
- )
- }
-
- cancelledRoomsSuccessfully.push(null)
- }
-
- return cancelledRoomsSuccessfully
- }),
- packages: safeProtectedServiceProcedure
- .input(addPackageInput)
- .concat(refIdPlugin.toConfirmationNumber)
- .use(async ({ ctx, next }) => {
- const token = await ctx.getScandicUserToken()
- return next({
- ctx: {
- token,
- },
- })
- })
- .mutation(async function ({ ctx, input }) {
- const { confirmationNumber } = ctx
- const { language, refId, ...body } = input
-
- const addPackageCounter = createCounter("trpc.booking.package.add")
- const metricsAddPackage = addPackageCounter.init({
- confirmationNumber,
- language,
- })
-
- metricsAddPackage.start()
-
- const token = ctx.token ?? ctx.serviceToken
- 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 = await ctx.getScandicUserToken()
-
- return next({
- ctx: {
- token,
- },
- })
- })
- .mutation(async function ({ ctx, input }) {
- const { confirmationNumber } = ctx
- const { language, refId, ...body } = input
-
- const guaranteeBookingCounter = createCounter("trpc.booking.guarantee")
- const metricsGuaranteeBooking = guaranteeBookingCounter.init({
- confirmationNumber,
- language,
- })
-
- metricsGuaranteeBooking.start()
-
- const token = ctx.token ?? ctx.serviceToken
- 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
- }),
+ priceChange: priceChangeRoute,
+ cancel: cancelBookingRoute,
+ packages: addPackagesRoute,
+ guarantee: guaranteeBookingRoute,
update: safeProtectedServiceProcedure
.input(updateBookingInput)
.concat(refIdPlugin.toConfirmationNumber)
@@ -235,43 +43,21 @@ export const bookingMutationRouter = router({
})
.mutation(async function ({ ctx, input }) {
const { confirmationNumber } = ctx
- const { language, refId, ...body } = input
-
- const updateBookingCounter = createCounter("trpc.booking.update")
- const metricsUpdateBooking = updateBookingCounter.init({
- confirmationNumber,
- language,
- })
-
- metricsUpdateBooking.start()
+ const { language, refId, ...rest } = input
const token = ctx.token ?? ctx.serviceToken
- const apiResponse = await api.put(
- api.endpoints.v1.Booking.booking(confirmationNumber),
+
+ return updateBooking(
{
- body,
- headers: {
- Authorization: `Bearer ${token}`,
- },
+ confirmationNumber,
+ lang: language,
+ checkInDate: rest.checkInDate ? dt.utc(rest.checkInDate) : undefined,
+ checkOutDate: rest.checkOutDate
+ ? dt.utc(rest.checkOutDate)
+ : undefined,
+ guest: rest.guest,
},
- { language }
+ token
)
-
- 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)
diff --git a/packages/trpc/lib/routers/booking/mutation/priceChangeRoute.ts b/packages/trpc/lib/routers/booking/mutation/priceChangeRoute.ts
new file mode 100644
index 000000000..3effe6e64
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/mutation/priceChangeRoute.ts
@@ -0,0 +1,24 @@
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { priceChange } from "../../../services/booking/priceChange"
+const refIdPlugin = createRefIdPlugin()
+
+export const priceChangeRoute = safeProtectedServiceProcedure
+ .concat(refIdPlugin.toConfirmationNumber)
+ .use(async ({ ctx, next }) => {
+ const token = await ctx.getScandicUserToken()
+
+ return next({
+ ctx: {
+ token,
+ },
+ })
+ })
+ .mutation(async function ({ ctx }) {
+ const { confirmationNumber } = ctx
+
+ return await priceChange(
+ { confirmationNumber },
+ ctx.token ?? ctx.serviceToken
+ )
+ })
diff --git a/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts b/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts
index 6e2824efc..e75ec9b3a 100644
--- a/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts
+++ b/packages/trpc/lib/routers/booking/mutation/validatePartnerPayment.ts
@@ -1,6 +1,6 @@
import "server-only"
-import z from "zod"
+import { z } from "zod"
import { createCounter } from "@scandic-hotels/common/telemetry"
diff --git a/packages/trpc/lib/routers/booking/query.ts b/packages/trpc/lib/routers/booking/query.ts
index 1628c4370..4b01defec 100644
--- a/packages/trpc/lib/routers/booking/query.ts
+++ b/packages/trpc/lib/routers/booking/query.ts
@@ -1,319 +1,12 @@
-import { createCounter } from "@scandic-hotels/common/telemetry"
-
import { router } from "../.."
-import * as api from "../../api"
-import {
- badRequestError,
- extractResponseDetails,
- notFoundError,
- serverErrorByStatus,
-} from "../../errors"
-import { createRefIdPlugin } from "../../plugins/refIdToConfirmationNumber"
-import {
- safeProtectedServiceProcedure,
- serviceProcedure,
-} from "../../procedures"
-import { toApiLang } from "../../utils"
-import { encrypt } from "../../utils/encryption"
-import { isValidSession } from "../../utils/session"
-import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
-import { getHotel } from "../hotels/services/getHotel"
-import { createBookingSchema } from "./mutation/create/schema"
-import { getHotelRoom } from "./helpers"
-import {
- createRefIdInput,
- findBookingInput,
- getBookingInput,
- getBookingStatusInput,
- getLinkedReservationsInput,
-} from "./input"
-import { findBooking, getBooking } from "./utils"
-
-const refIdPlugin = createRefIdPlugin()
+import { findBookingRoute } from "./query/findBookingRoute"
+import { getBookingRoute } from "./query/getBookingRoute"
+import { getBookingStatusRoute } from "./query/getBookingStatusRoute"
+import { getLinkedReservationsRoute } from "./query/getLinkedReservationsRoute"
export const bookingQueryRouter = router({
- get: safeProtectedServiceProcedure
- .input(getBookingInput)
- .concat(refIdPlugin.toConfirmationNumber)
- .use(async ({ ctx, input, next }) => {
- const lang = input.lang ?? ctx.lang
- const token = await ctx.getScandicUserToken()
-
- return next({
- ctx: {
- lang,
- token,
- },
- })
- })
- .query(async function ({ ctx }) {
- const { confirmationNumber, lang, token, serviceToken } = ctx
-
- const getBookingCounter = createCounter("trpc.booking.get")
- const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
-
- metricsGetBooking.start()
-
- const booking = await getBooking(
- confirmationNumber,
- lang,
- token ?? serviceToken
- )
-
- if (!booking) {
- metricsGetBooking.dataError(
- `Fail to get booking data for ${confirmationNumber}`,
- { confirmationNumber }
- )
- return null
- }
-
- const [hotelData, hotelPages] = await Promise.all([
- getHotel(
- {
- hotelId: booking.hotelId,
- isCardOnlyPayment: false,
- language: lang,
- },
- serviceToken
- ),
- getHotelPageUrls(lang),
- ])
- const hotelPage = hotelPages.find(
- (page) => page.hotelId === booking.hotelId
- )
-
- if (!hotelData) {
- metricsGetBooking.dataError(
- `Failed to get hotel data for ${booking.hotelId}`,
- {
- hotelId: booking.hotelId,
- }
- )
- throw notFoundError({
- message: "Hotel data not found",
- errorDetails: { hotelId: booking.hotelId },
- })
- }
-
- metricsGetBooking.success()
-
- return {
- ...hotelData,
- url: hotelPage?.url || null,
- booking,
- room: getHotelRoom(hotelData.roomCategories, booking.roomTypeCode),
- }
- }),
- findBooking: safeProtectedServiceProcedure
- .input(findBookingInput)
- .use(async ({ ctx, input, next }) => {
- const lang = input.lang ?? ctx.lang
- const token = isValidSession(ctx.session)
- ? ctx.session.token.access_token
- : ctx.serviceToken
-
- return next({
- ctx: {
- lang,
- token,
- },
- })
- })
- .query(async function ({
- ctx,
- input: { confirmationNumber, lastName, firstName, email },
- }) {
- const { lang, token, serviceToken } = ctx
- const findBookingCounter = createCounter("trpc.booking.findBooking")
- const metricsFindBooking = findBookingCounter.init({ confirmationNumber })
-
- metricsFindBooking.start()
-
- const booking = await findBooking(
- confirmationNumber,
- lang,
- token,
- lastName,
- firstName,
- email
- )
-
- if (!booking) {
- metricsFindBooking.dataError(
- `Fail to find booking data for ${confirmationNumber}`,
- { confirmationNumber }
- )
- return null
- }
-
- const [hotelData, hotelPages] = await Promise.all([
- getHotel(
- {
- hotelId: booking.hotelId,
- isCardOnlyPayment: false,
- language: lang,
- },
- serviceToken
- ),
- getHotelPageUrls(lang),
- ])
- const hotelPage = hotelPages.find(
- (page) => page.hotelId === booking.hotelId
- )
-
- if (!hotelData) {
- metricsFindBooking.dataError(
- `Failed to find hotel data for ${booking.hotelId}`,
- {
- hotelId: booking.hotelId,
- }
- )
-
- throw notFoundError({
- message: "Hotel data not found",
- errorDetails: { hotelId: booking.hotelId },
- })
- }
-
- metricsFindBooking.success()
-
- return {
- ...hotelData,
- url: hotelPage?.url || null,
- booking,
- room: getHotelRoom(hotelData.roomCategories, booking.roomTypeCode),
- }
- }),
- linkedReservations: safeProtectedServiceProcedure
- .input(getLinkedReservationsInput)
- .concat(refIdPlugin.toConfirmationNumber)
- .use(async ({ ctx, input, next }) => {
- const lang = input.lang ?? ctx.lang
- const token = isValidSession(ctx.session)
- ? ctx.session.token.access_token
- : ctx.serviceToken
-
- return next({
- ctx: {
- lang,
- token,
- },
- })
- })
- .query(async function ({ ctx }) {
- const { confirmationNumber, lang, token } = ctx
-
- const getLinkedReservationsCounter = createCounter(
- "trpc.booking.linkedReservations"
- )
- const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
- confirmationNumber,
- })
-
- metricsGetLinkedReservations.start()
-
- const booking = await getBooking(confirmationNumber, lang, token)
-
- if (!booking) {
- return []
- }
-
- const linkedReservationsResults = await Promise.allSettled(
- booking.linkedReservations.map((linkedReservation) =>
- getBooking(linkedReservation.confirmationNumber, lang, token)
- )
- )
-
- 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,
- await extractResponseDetails(apiResponse),
- "getBookingStatus failed"
- )
- }
-
- const apiJson = await apiResponse.json()
- const verifiedData = createBookingSchema.safeParse(apiJson)
- if (!verifiedData.success) {
- metricsGetBookingStatus.validationError(verifiedData.error)
-
- throw badRequestError({
- message: "Invalid booking data",
- errorDetails: verifiedData.error.formErrors,
- })
- }
-
- 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,
- }
- }),
+ get: getBookingRoute,
+ findBooking: findBookingRoute,
+ linkedReservations: getLinkedReservationsRoute,
+ status: getBookingStatusRoute,
})
diff --git a/packages/trpc/lib/routers/booking/query/findBookingRoute.ts b/packages/trpc/lib/routers/booking/query/findBookingRoute.ts
new file mode 100644
index 000000000..5518819a3
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/query/findBookingRoute.ts
@@ -0,0 +1,87 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import { notFoundError } from "../../../errors"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { findBooking } from "../../../services/booking/findBooking"
+import { isValidSession } from "../../../utils/session"
+import { getHotelPageUrls } from "../../contentstack/hotelPage/utils"
+import { getHotel } from "../../hotels/services/getHotel"
+import { getHotelRoom } from "../helpers"
+import { findBookingInput } from "../input"
+
+export const findBookingRoute = safeProtectedServiceProcedure
+ .input(findBookingInput)
+ .use(async ({ ctx, input, next }) => {
+ const lang = input.lang ?? ctx.lang
+ const token = isValidSession(ctx.session)
+ ? ctx.session.token.access_token
+ : ctx.serviceToken
+
+ return next({
+ ctx: {
+ lang,
+ token,
+ },
+ })
+ })
+ .query(async function ({
+ ctx,
+ input: { confirmationNumber, lastName, firstName, email },
+ }) {
+ const { lang, token, serviceToken } = ctx
+ const findBookingCounter = createCounter("trpc.booking.findBooking")
+ const metricsFindBooking = findBookingCounter.init({ confirmationNumber })
+
+ metricsFindBooking.start()
+
+ const booking = await findBooking(
+ { confirmationNumber, lang, lastName, firstName, email },
+ token
+ )
+
+ if (!booking) {
+ metricsFindBooking.dataError(
+ `Fail to find booking data for ${confirmationNumber}`,
+ { confirmationNumber }
+ )
+ return null
+ }
+
+ const [hotelData, hotelPages] = await Promise.all([
+ getHotel(
+ {
+ hotelId: booking.hotelId,
+ isCardOnlyPayment: false,
+ language: lang,
+ },
+ serviceToken
+ ),
+ getHotelPageUrls(lang),
+ ])
+ const hotelPage = hotelPages.find(
+ (page) => page.hotelId === booking.hotelId
+ )
+
+ if (!hotelData) {
+ metricsFindBooking.dataError(
+ `Failed to find hotel data for ${booking.hotelId}`,
+ {
+ hotelId: booking.hotelId,
+ }
+ )
+
+ throw notFoundError({
+ message: "Hotel data not found",
+ errorDetails: { hotelId: booking.hotelId },
+ })
+ }
+
+ metricsFindBooking.success()
+
+ return {
+ ...hotelData,
+ url: hotelPage?.url || null,
+ booking,
+ room: getHotelRoom(hotelData.roomCategories, booking.roomTypeCode),
+ }
+ })
diff --git a/packages/trpc/lib/routers/booking/query/getBookingRoute.ts b/packages/trpc/lib/routers/booking/query/getBookingRoute.ts
new file mode 100644
index 000000000..b59dec601
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/query/getBookingRoute.ts
@@ -0,0 +1,84 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import { notFoundError } from "../../../errors"
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { getBooking } from "../../../services/booking/getBooking"
+import { getHotelPageUrls } from "../../contentstack/hotelPage/utils"
+import { getHotel } from "../../hotels/services/getHotel"
+import { getHotelRoom } from "../helpers"
+import { getBookingInput } from "../input"
+
+const refIdPlugin = createRefIdPlugin()
+export const getBookingRoute = safeProtectedServiceProcedure
+ .input(getBookingInput)
+ .concat(refIdPlugin.toConfirmationNumber)
+ .use(async ({ ctx, input, next }) => {
+ const lang = input.lang ?? ctx.lang
+ const token = await ctx.getScandicUserToken()
+
+ return next({
+ ctx: {
+ lang,
+ token,
+ },
+ })
+ })
+ .query(async function ({ ctx }) {
+ const { confirmationNumber, lang, token, serviceToken } = ctx
+
+ const getBookingCounter = createCounter("trpc.booking.get")
+ const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
+
+ metricsGetBooking.start()
+
+ const booking = await getBooking(
+ { confirmationNumber, lang },
+ token ?? serviceToken
+ )
+
+ if (!booking) {
+ metricsGetBooking.dataError(
+ `Fail to get booking data for ${confirmationNumber}`,
+ { confirmationNumber }
+ )
+ return null
+ }
+
+ const [hotelData, hotelPages] = await Promise.all([
+ getHotel(
+ {
+ hotelId: booking.hotelId,
+ isCardOnlyPayment: false,
+ language: lang,
+ },
+ serviceToken
+ ),
+ getHotelPageUrls(lang),
+ ])
+ const hotelPage = hotelPages.find(
+ (page) => page.hotelId === booking.hotelId
+ )
+
+ if (!hotelData) {
+ metricsGetBooking.dataError(
+ `Failed to get hotel data for ${booking.hotelId}`,
+ {
+ hotelId: booking.hotelId,
+ }
+ )
+ throw notFoundError({
+ message: "Hotel data not found",
+ errorDetails: { hotelId: booking.hotelId },
+ })
+ }
+
+ metricsGetBooking.success()
+
+ return {
+ ...hotelData,
+ url: hotelPage?.url || null,
+ booking,
+ room: getHotelRoom(hotelData.roomCategories, booking.roomTypeCode),
+ }
+ })
diff --git a/packages/trpc/lib/routers/booking/query/getBookingStatusRoute.ts b/packages/trpc/lib/routers/booking/query/getBookingStatusRoute.ts
new file mode 100644
index 000000000..5a7191654
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/query/getBookingStatusRoute.ts
@@ -0,0 +1,33 @@
+import { z } from "zod"
+
+import { Lang } from "@scandic-hotels/common/constants/language"
+
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { getBookingStatus } from "../../../services/booking/getBookingStatus"
+import { encrypt } from "../../../utils/encryption"
+
+const getBookingStatusInput = z.object({
+ lang: z.nativeEnum(Lang).optional(),
+})
+
+const refIdPlugin = createRefIdPlugin()
+export const getBookingStatusRoute = safeProtectedServiceProcedure
+ .input(getBookingStatusInput)
+ .concat(refIdPlugin.toConfirmationNumber)
+ .query(async function ({ ctx, input }) {
+ const lang = input.lang ?? ctx.lang
+ const { confirmationNumber } = ctx
+
+ const booking = await getBookingStatus(
+ { confirmationNumber, lang },
+ ctx.serviceToken
+ )
+
+ const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry
+
+ return {
+ booking,
+ sig: encrypt(expire.toString()),
+ }
+ })
diff --git a/packages/trpc/lib/routers/booking/query/getLinkedReservationsRoute.ts b/packages/trpc/lib/routers/booking/query/getLinkedReservationsRoute.ts
new file mode 100644
index 000000000..d6d00f82d
--- /dev/null
+++ b/packages/trpc/lib/routers/booking/query/getLinkedReservationsRoute.ts
@@ -0,0 +1,34 @@
+import { createRefIdPlugin } from "../../../plugins/refIdToConfirmationNumber"
+import { safeProtectedServiceProcedure } from "../../../procedures"
+import { getLinkedReservations } from "../../../services/booking/linkedReservations"
+import { isValidSession } from "../../../utils/session"
+import { getLinkedReservationsInput } from "../input"
+
+const refIdPlugin = createRefIdPlugin()
+export const getLinkedReservationsRoute = safeProtectedServiceProcedure
+ .input(getLinkedReservationsInput)
+ .concat(refIdPlugin.toConfirmationNumber)
+ .use(async ({ ctx, input, next }) => {
+ const lang = input.lang ?? ctx.lang
+ const token = isValidSession(ctx.session)
+ ? ctx.session.token.access_token
+ : ctx.serviceToken
+
+ return next({
+ ctx: {
+ lang,
+ token,
+ },
+ })
+ })
+ .query(async function ({ ctx }) {
+ const { confirmationNumber, lang, token } = ctx
+
+ return getLinkedReservations(
+ {
+ confirmationNumber,
+ lang,
+ },
+ token
+ )
+ })
diff --git a/packages/trpc/lib/routers/booking/utils.ts b/packages/trpc/lib/routers/booking/utils.ts
deleted file mode 100644
index 11b922652..000000000
--- a/packages/trpc/lib/routers/booking/utils.ts
+++ /dev/null
@@ -1,171 +0,0 @@
-import { createCounter } from "@scandic-hotels/common/telemetry"
-
-import * as api from "../../api"
-import {
- badRequestError,
- extractResponseDetails,
- serverErrorByStatus,
-} from "../../errors"
-import { toApiLang } from "../../utils"
-import { createBookingSchema } from "./mutation/create/schema"
-import { bookingConfirmationSchema } 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,
- await extractResponseDetails(apiResponse),
- "getBooking failed"
- )
- }
-
- 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 || apiResponse.status === 404) {
- return null
- }
-
- throw serverErrorByStatus(
- apiResponse.status,
- await extractResponseDetails(apiResponse),
- "findBooking failed"
- )
- }
-
- 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
-}
diff --git a/packages/trpc/lib/routers/contentstack/schemas/alert.ts b/packages/trpc/lib/routers/contentstack/schemas/alert.ts
index ebe083549..15c20aee7 100644
--- a/packages/trpc/lib/routers/contentstack/schemas/alert.ts
+++ b/packages/trpc/lib/routers/contentstack/schemas/alert.ts
@@ -1,4 +1,4 @@
-import z from "zod"
+import { z } from "zod"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
diff --git a/packages/trpc/lib/services/booking/addPackageToBooking/index.ts b/packages/trpc/lib/services/booking/addPackageToBooking/index.ts
new file mode 100644
index 000000000..4f97f19a9
--- /dev/null
+++ b/packages/trpc/lib/services/booking/addPackageToBooking/index.ts
@@ -0,0 +1,60 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import { toApiLang } from "../../../utils"
+import { bookingConfirmationSchema } from "../getBooking/schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+export async function addPackageToBooking(
+ {
+ lang,
+ ...input
+ }: {
+ confirmationNumber: string
+ lang: Lang
+ packages: {
+ code: string
+ quantity: number
+ comment?: string | undefined
+ }[]
+ ancillaryComment: string
+ ancillaryDeliveryTime?: string | null | undefined
+ },
+ token: string
+) {
+ const addPackageCounter = createCounter("trpc.booking.package.add")
+ const metricsAddPackage = addPackageCounter.init({
+ confirmationNumber: input.confirmationNumber,
+ language: lang,
+ })
+
+ metricsAddPackage.start()
+
+ const apiResponse = await api.post(
+ api.endpoints.v1.Booking.packages(input.confirmationNumber),
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ body: input,
+ },
+ { language: toApiLang(lang) }
+ )
+
+ if (!apiResponse.ok) {
+ await metricsAddPackage.httpError(apiResponse)
+ return null
+ }
+
+ const apiJson = await apiResponse.json()
+ const verifiedData = bookingConfirmationSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsAddPackage.validationError(verifiedData.error)
+ return null
+ }
+
+ metricsAddPackage.success()
+
+ return verifiedData.data
+}
diff --git a/packages/trpc/lib/services/booking/cancelBooking/index.ts b/packages/trpc/lib/services/booking/cancelBooking/index.ts
new file mode 100644
index 000000000..ff40d52cd
--- /dev/null
+++ b/packages/trpc/lib/services/booking/cancelBooking/index.ts
@@ -0,0 +1,76 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import {
+ badGatewayError,
+ extractResponseDetails,
+ serverErrorByStatus,
+} from "../../../errors"
+import { toApiLang } from "../../../utils"
+import { getBooking } from "../getBooking"
+import { cancelBookingSchema } from "./schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+import type { CancelBooking } from "./schema"
+
+export async function cancelBooking(
+ {
+ confirmationNumber,
+ language,
+ }: { confirmationNumber: string; language: Lang },
+ token: string
+): Promise {
+ const cancelBookingCounter = createCounter("booking.cancel")
+ const metricsCancelBooking = cancelBookingCounter.init({
+ confirmationNumber,
+ language,
+ })
+
+ metricsCancelBooking.start()
+
+ const headers = {
+ Authorization: `Bearer ${token}`,
+ }
+
+ const booking = await getBooking(
+ { confirmationNumber, lang: 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)
+ throw serverErrorByStatus(
+ apiResponse.status,
+ await extractResponseDetails(apiResponse),
+ `cancelBooking failed for ${confirmationNumber}`
+ )
+ }
+
+ const apiJson = await apiResponse.json()
+ const verifiedData = cancelBookingSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsCancelBooking.validationError(verifiedData.error)
+ throw badGatewayError({
+ message: "Invalid response from cancelBooking",
+ errorDetails: { validationError: verifiedData.error },
+ })
+ }
+
+ metricsCancelBooking.success()
+
+ return verifiedData.data
+}
diff --git a/packages/trpc/lib/services/booking/cancelBooking/schema.ts b/packages/trpc/lib/services/booking/cancelBooking/schema.ts
new file mode 100644
index 000000000..04b1d9927
--- /dev/null
+++ b/packages/trpc/lib/services/booking/cancelBooking/schema.ts
@@ -0,0 +1,56 @@
+import { z } from "zod"
+
+import { bookingReservationStatusSchema } from "../schema/bookingReservationStatusSchema"
+
+export type CancelBooking = z.infer
+export const cancelBookingSchema = z
+ .object({
+ data: z.object({
+ attributes: z.object({
+ reservationStatus: bookingReservationStatusSchema,
+ paymentUrl: z.string().nullable().optional(),
+ paymentMethod: 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(),
+ }),
+ })
+ .transform((apiResponse) => {
+ return {
+ id: apiResponse.data.id,
+ type: apiResponse.data.type,
+ reservationStatus: apiResponse.data.attributes.reservationStatus,
+ paymentUrl: apiResponse.data.attributes.paymentUrl,
+ paymentMethod: apiResponse.data.attributes.paymentMethod,
+ rooms: apiResponse.data.attributes.rooms,
+ errors: apiResponse.data.attributes.errors,
+ }
+ })
diff --git a/packages/trpc/lib/services/booking/createBooking/index.ts b/packages/trpc/lib/services/booking/createBooking/index.ts
new file mode 100644
index 000000000..7722374c9
--- /dev/null
+++ b/packages/trpc/lib/services/booking/createBooking/index.ts
@@ -0,0 +1,88 @@
+import "server-only"
+
+import { PaymentMethodEnum } from "@scandic-hotels/common/constants/paymentMethod"
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import {
+ badGatewayError,
+ extractResponseDetails,
+ serverErrorByStatus,
+} from "../../../errors"
+import { toApiLang } from "../../../utils"
+import { createBookingSchema } from "./schema"
+
+import type { CreateBookingInput } from "../../../routers/booking/mutation/createBookingRoute/schema"
+
+export async function createBooking(input: CreateBookingInput, token: string) {
+ validateInputData(input)
+
+ const createBookingCounter = createCounter("trpc.booking.create")
+
+ const metricsCreateBooking = createBookingCounter.init({
+ ...input,
+ rooms: input.rooms.map(({ guest, ...room }) => {
+ const { becomeMember, membershipNumber } = guest
+ return { ...room, guest: { becomeMember, membershipNumber } }
+ }),
+ })
+
+ metricsCreateBooking.start()
+
+ const apiResponse = await api.post(
+ api.endpoints.v1.Booking.bookings,
+ {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ body: input,
+ },
+ { language: toApiLang(input.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
+ }
+
+ throw serverErrorByStatus(
+ apiResponse.status,
+ await extractResponseDetails(apiResponse),
+ "createBooking failed"
+ )
+ }
+
+ const apiJson = await apiResponse.json()
+ const verifiedData = createBookingSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsCreateBooking.validationError(verifiedData.error)
+ throw badGatewayError({
+ message: "Invalid response from createBooking",
+ errorDetails: { validationError: verifiedData.error },
+ })
+ }
+
+ metricsCreateBooking.success()
+
+ return verifiedData.data
+}
+
+function validateInputData(input: CreateBookingInput) {
+ if (!input.payment) {
+ return
+ }
+
+ if (input.payment.paymentMethod !== PaymentMethodEnum.PartnerPoints) {
+ return
+ }
+
+ if (!input.partnerSpecific?.eurobonusAccessToken) {
+ throw new Error(
+ "Missing partnerSpecific data for PartnerPoints payment method"
+ )
+ }
+}
diff --git a/packages/trpc/lib/services/booking/createBooking/schema.ts b/packages/trpc/lib/services/booking/createBooking/schema.ts
new file mode 100644
index 000000000..4394bd965
--- /dev/null
+++ b/packages/trpc/lib/services/booking/createBooking/schema.ts
@@ -0,0 +1,84 @@
+import { z } from "zod"
+
+import {
+ nullableStringEmailValidator,
+ nullableStringValidator,
+} from "@scandic-hotels/common/utils/zod/stringValidator"
+
+import { calculateRefId } from "../../../utils/refId"
+
+const guestSchema = z.object({
+ email: nullableStringEmailValidator,
+ firstName: nullableStringValidator,
+ lastName: nullableStringValidator,
+ membershipNumber: nullableStringValidator,
+ phoneNumber: nullableStringValidator,
+ countryCode: nullableStringValidator,
+})
+
+export const createBookingSchema = z
+ .object({
+ data: z.object({
+ attributes: z.object({
+ reservationStatus: z.string(),
+ guest: guestSchema.optional(),
+ paymentUrl: z.string().nullable().optional(),
+ paymentMethod: 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,
+ paymentMethod: d.data.attributes.paymentMethod,
+ 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,
+ }))
diff --git a/packages/trpc/lib/services/booking/findBooking/index.ts b/packages/trpc/lib/services/booking/findBooking/index.ts
new file mode 100644
index 000000000..2756671a9
--- /dev/null
+++ b/packages/trpc/lib/services/booking/findBooking/index.ts
@@ -0,0 +1,81 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import {
+ badRequestError,
+ extractResponseDetails,
+ serverErrorByStatus,
+} from "../../../errors"
+import { toApiLang } from "../../../utils"
+import { bookingConfirmationSchema } from "../getBooking/schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+export async function findBooking(
+ {
+ confirmationNumber,
+ lang,
+ lastName,
+ firstName,
+ email,
+ }: {
+ confirmationNumber: string
+ lang: Lang
+ lastName?: string
+ firstName?: string
+ email?: string
+ },
+ token: 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 || apiResponse.status === 404) {
+ return null
+ }
+
+ throw serverErrorByStatus(
+ apiResponse.status,
+ await extractResponseDetails(apiResponse),
+ "findBooking failed"
+ )
+ }
+
+ const apiJson = await apiResponse.json()
+ const booking = bookingConfirmationSchema.safeParse(apiJson)
+ if (!booking.success) {
+ metricsGetBooking.validationError(booking.error)
+ throw badRequestError()
+ }
+
+ metricsGetBooking.success()
+
+ return booking.data
+}
diff --git a/packages/trpc/lib/services/booking/getBooking/index.ts b/packages/trpc/lib/services/booking/getBooking/index.ts
new file mode 100644
index 000000000..c1935754a
--- /dev/null
+++ b/packages/trpc/lib/services/booking/getBooking/index.ts
@@ -0,0 +1,59 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import {
+ badRequestError,
+ extractResponseDetails,
+ serverErrorByStatus,
+} from "../../../errors"
+import { toApiLang } from "../../../utils"
+import { bookingConfirmationSchema } from "./schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+export async function getBooking(
+ { confirmationNumber, lang }: { 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,
+ await extractResponseDetails(apiResponse),
+ "getBooking failed"
+ )
+ }
+
+ const apiJson = await apiResponse.json()
+ const booking = bookingConfirmationSchema.safeParse(apiJson)
+ if (!booking.success) {
+ metricsGetBooking.validationError(booking.error)
+ throw badRequestError()
+ }
+
+ metricsGetBooking.success()
+
+ return booking.data
+}
diff --git a/packages/trpc/lib/routers/booking/output.ts b/packages/trpc/lib/services/booking/getBooking/schema.ts
similarity index 93%
rename from packages/trpc/lib/routers/booking/output.ts
rename to packages/trpc/lib/services/booking/getBooking/schema.ts
index ef8674fc1..10ccba680 100644
--- a/packages/trpc/lib/routers/booking/output.ts
+++ b/packages/trpc/lib/services/booking/getBooking/schema.ts
@@ -8,57 +8,18 @@ import {
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"
+import { BookingStatusEnum } from "../../../enums/bookingStatus"
+import { BreakfastPackageEnum } from "../../../enums/breakfast"
+import { ChildBedTypeEnum } from "../../../enums/childBedTypeEnum"
+import { calculateRefId } from "../../../utils/refId"
+import { bookingReservationStatusSchema } from "../schema/bookingReservationStatusSchema"
-export const guestSchema = z.object({
- email: nullableStringEmailValidator,
- firstName: nullableStringValidator,
- lastName: nullableStringValidator,
- membershipNumber: nullableStringValidator,
- phoneNumber: nullableStringValidator,
- countryCode: nullableStringValidator,
-})
-
-export type Guest = z.output
-
-// 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(""),
@@ -82,7 +43,16 @@ const rateDefinitionSchema = z.object({
isCampaignRate: z.boolean().default(false),
})
-export const linkedReservationSchema = z.object({
+const guestSchema = z.object({
+ email: nullableStringEmailValidator,
+ firstName: nullableStringValidator,
+ lastName: nullableStringValidator,
+ membershipNumber: nullableStringValidator,
+ phoneNumber: nullableStringValidator,
+ countryCode: nullableStringValidator,
+})
+
+const linkedReservationSchema = z.object({
confirmationNumber: z.string().default(""),
hotelId: z.string().default(""),
checkinDate: z.string(),
@@ -137,6 +107,34 @@ const linksSchema = z.object({
.nullable(),
})
+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,
+ }))
+
export const bookingConfirmationSchema = z
.object({
data: z.object({
@@ -174,7 +172,7 @@ export const bookingConfirmationSchema = z
multiRoom: z.boolean(),
packages: z.array(packageSchema).default([]),
rateDefinition: rateDefinitionSchema,
- reservationStatus: z.string().nullable().default(""),
+ reservationStatus: bookingReservationStatusSchema,
roomPoints: z.number(),
roomPointType: z
.enum(["Scandic", "EuroBonus"])
diff --git a/packages/trpc/lib/services/booking/getBookingStatus/index.ts b/packages/trpc/lib/services/booking/getBookingStatus/index.ts
new file mode 100644
index 000000000..a71e434fe
--- /dev/null
+++ b/packages/trpc/lib/services/booking/getBookingStatus/index.ts
@@ -0,0 +1,65 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import {
+ badRequestError,
+ extractResponseDetails,
+ serverErrorByStatus,
+} from "../../../errors"
+import { toApiLang } from "../../../utils"
+import { bookingStatusSchema } from "./schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+import type { BookingStatus } from "./schema"
+export type { BookingStatus } from "./schema"
+
+export async function getBookingStatus(
+ { confirmationNumber, lang }: { confirmationNumber: string; lang: Lang },
+ serviceToken: string
+): Promise {
+ 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 ${serviceToken}`,
+ },
+ },
+ {
+ language,
+ }
+ )
+
+ if (!apiResponse.ok) {
+ await metricsGetBookingStatus.httpError(apiResponse)
+ throw serverErrorByStatus(
+ apiResponse.status,
+ await extractResponseDetails(apiResponse),
+ "getBookingStatus failed"
+ )
+ }
+
+ const apiJson = await apiResponse.json()
+ const verifiedData = bookingStatusSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsGetBookingStatus.validationError(verifiedData.error)
+
+ throw badRequestError({
+ message: "Invalid booking data",
+ errorDetails: verifiedData.error.formErrors,
+ })
+ }
+
+ metricsGetBookingStatus.success()
+
+ return verifiedData.data
+}
diff --git a/packages/trpc/lib/services/booking/getBookingStatus/schema.ts b/packages/trpc/lib/services/booking/getBookingStatus/schema.ts
new file mode 100644
index 000000000..e0508b598
--- /dev/null
+++ b/packages/trpc/lib/services/booking/getBookingStatus/schema.ts
@@ -0,0 +1,61 @@
+import { z } from "zod"
+
+import { calculateRefId } from "../../../utils/refId"
+import { bookingReservationStatusSchema } from "../schema/bookingReservationStatusSchema"
+
+export type BookingStatus = z.infer
+export const bookingStatusSchema = z
+ .object({
+ data: z.object({
+ attributes: z.object({
+ reservationStatus: bookingReservationStatusSchema,
+ paymentUrl: z.string().nullable().optional(),
+ paymentMethod: 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(),
+ }),
+ })
+ .transform((d) => ({
+ id: d.data.id,
+ type: d.data.type,
+ reservationStatus: d.data.attributes.reservationStatus,
+ paymentUrl: d.data.attributes.paymentUrl,
+ paymentMethod: d.data.attributes.paymentMethod,
+ errors: d.data.attributes.errors,
+ rooms: d.data.attributes.rooms.map((room) => {
+ const lastName = ""
+ return {
+ ...room,
+ refId: calculateRefId(room.confirmationNumber, lastName),
+ }
+ }),
+ }))
diff --git a/packages/trpc/lib/services/booking/guaranteeBooking/index.ts b/packages/trpc/lib/services/booking/guaranteeBooking/index.ts
new file mode 100644
index 000000000..ad2154d86
--- /dev/null
+++ b/packages/trpc/lib/services/booking/guaranteeBooking/index.ts
@@ -0,0 +1,63 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import { langToApiLang } from "../../../constants/apiLang"
+import { slimBookingSchema } from "../schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+export async function guaranteeBooking(
+ {
+ confirmationNumber,
+ language,
+ success,
+ error,
+ cancel,
+ card,
+ }: {
+ confirmationNumber: string
+ language: Lang
+ success: string | null
+ error: string | null
+ cancel: string | null
+ card?: { alias: string; expiryDate: string; cardType: string }
+ },
+ token: string
+) {
+ 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: { success, error, cancel, card },
+ },
+ { language: langToApiLang[language] }
+ )
+
+ if (!apiResponse.ok) {
+ await metricsGuaranteeBooking.httpError(apiResponse)
+ return null
+ }
+
+ const apiJson = await apiResponse.json()
+ const verifiedData = slimBookingSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsGuaranteeBooking.validationError(verifiedData.error)
+ return null
+ }
+
+ metricsGuaranteeBooking.success()
+
+ return verifiedData.data
+}
diff --git a/packages/trpc/lib/services/booking/linkedReservations/index.ts b/packages/trpc/lib/services/booking/linkedReservations/index.ts
new file mode 100644
index 000000000..f59135181
--- /dev/null
+++ b/packages/trpc/lib/services/booking/linkedReservations/index.ts
@@ -0,0 +1,64 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import { getBooking } from "../getBooking"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+export async function getLinkedReservations(
+ {
+ confirmationNumber,
+ lang,
+ }: {
+ confirmationNumber: string
+ lang: Lang
+ },
+ token: string
+): Promise>>[]> {
+ const getLinkedReservationsCounter = createCounter(
+ "booking.linkedReservations"
+ )
+
+ const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
+ confirmationNumber,
+ })
+
+ metricsGetLinkedReservations.start()
+
+ const booking = await getBooking({ confirmationNumber, lang }, token)
+
+ if (!booking) {
+ return []
+ }
+
+ const linkedReservationsResults = await Promise.allSettled(
+ booking.linkedReservations.map((linkedReservation) =>
+ getBooking(
+ { confirmationNumber: linkedReservation.confirmationNumber, lang },
+ token
+ )
+ )
+ )
+
+ const linkedReservations: NonNullable<
+ Awaited>
+ >[] = []
+ for (const linkedReservationsResult of linkedReservationsResults) {
+ if (linkedReservationsResult.status !== "fulfilled") {
+ metricsGetLinkedReservations.dataError(`Failed to get linked reservation`)
+ continue
+ }
+
+ if (!linkedReservationsResult.value) {
+ metricsGetLinkedReservations.dataError(
+ `Unexpected value for linked reservation`
+ )
+ continue
+ }
+
+ linkedReservations.push(linkedReservationsResult.value)
+ }
+
+ metricsGetLinkedReservations.success()
+
+ return linkedReservations
+}
diff --git a/packages/trpc/lib/services/booking/priceChange/index.ts b/packages/trpc/lib/services/booking/priceChange/index.ts
new file mode 100644
index 000000000..69d1404d8
--- /dev/null
+++ b/packages/trpc/lib/services/booking/priceChange/index.ts
@@ -0,0 +1,41 @@
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../../api"
+import { slimBookingSchema } from "../schema"
+
+export async function priceChange(
+ { confirmationNumber }: { confirmationNumber: string },
+ token: string
+) {
+ 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 = slimBookingSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsPriceChange.validationError(verifiedData.error)
+ return null
+ }
+
+ metricsPriceChange.success()
+
+ return verifiedData.data
+}
diff --git a/packages/trpc/lib/services/booking/schema.ts b/packages/trpc/lib/services/booking/schema.ts
new file mode 100644
index 000000000..2686b1be7
--- /dev/null
+++ b/packages/trpc/lib/services/booking/schema.ts
@@ -0,0 +1,84 @@
+import { z } from "zod"
+
+import {
+ nullableStringEmailValidator,
+ nullableStringValidator,
+} from "@scandic-hotels/common/utils/zod/stringValidator"
+
+import { calculateRefId } from "../../utils/refId"
+
+const guestSchema = z.object({
+ email: nullableStringEmailValidator,
+ firstName: nullableStringValidator,
+ lastName: nullableStringValidator,
+ membershipNumber: nullableStringValidator,
+ phoneNumber: nullableStringValidator,
+ countryCode: nullableStringValidator,
+})
+
+export const slimBookingSchema = z
+ .object({
+ data: z.object({
+ attributes: z.object({
+ reservationStatus: z.string(),
+ guest: guestSchema.optional(),
+ paymentUrl: z.string().nullable().optional(),
+ paymentMethod: 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,
+ paymentMethod: d.data.attributes.paymentMethod,
+ 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,
+ }))
diff --git a/packages/trpc/lib/services/booking/schema/bookingReservationStatusSchema.ts b/packages/trpc/lib/services/booking/schema/bookingReservationStatusSchema.ts
new file mode 100644
index 000000000..6613e2c55
--- /dev/null
+++ b/packages/trpc/lib/services/booking/schema/bookingReservationStatusSchema.ts
@@ -0,0 +1,22 @@
+import { z } from "zod"
+
+export const bookingReservationStatusSchema = z
+ .enum([
+ "CreatedInOhip",
+ "PendingAcceptPriceChange",
+ "PendingGuarantee",
+ "PendingPayment",
+ "PaymentRegistered",
+ "PaymentAuthorized",
+ "ConfirmedInOhip",
+ "PaymentSucceeded",
+ "PaymentFailed",
+ "PaymentError",
+ "PaymentCancelled",
+ "BookingCompleted",
+ "Cancelled",
+ "CheckedOut",
+ "PendingMembership",
+ "Unknown",
+ ])
+ .catch("Unknown")
diff --git a/packages/trpc/lib/services/booking/updateBooking.ts b/packages/trpc/lib/services/booking/updateBooking.ts
new file mode 100644
index 000000000..0d91dd7cc
--- /dev/null
+++ b/packages/trpc/lib/services/booking/updateBooking.ts
@@ -0,0 +1,80 @@
+import { type Dayjs, dt } from "@scandic-hotels/common/dt"
+import { createCounter } from "@scandic-hotels/common/telemetry"
+
+import * as api from "../../api"
+import { langToApiLang } from "../../constants/apiLang"
+import {
+ badGatewayError,
+ extractResponseDetails,
+ serverErrorByStatus,
+} from "../../errors"
+import { bookingConfirmationSchema } from "./getBooking/schema"
+
+import type { Lang } from "@scandic-hotels/common/constants/language"
+
+export async function updateBooking(
+ input: {
+ confirmationNumber: string
+ lang: Lang
+ checkInDate: Dayjs | Date | undefined
+ checkOutDate: Dayjs | Date | undefined
+ guest?: {
+ email?: string | undefined
+ phoneNumber?: string | undefined
+ countryCode?: string | undefined
+ }
+ },
+ token: string
+) {
+ const updateBookingCounter = createCounter("booking.update")
+ const metricsUpdateBooking = updateBookingCounter.init({
+ confirmationNumber: input.confirmationNumber,
+ language: input.lang,
+ })
+
+ metricsUpdateBooking.start()
+ const body = {
+ checkInDate: input.checkInDate
+ ? dt(input.checkInDate).format("YYYY-MM-DD")
+ : undefined,
+ checkOutDate: input.checkOutDate
+ ? dt(input.checkOutDate).format("YYYY-MM-DD")
+ : undefined,
+ guest: input.guest,
+ }
+
+ const apiResponse = await api.put(
+ api.endpoints.v1.Booking.booking(input.confirmationNumber),
+ {
+ body,
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ },
+ { language: langToApiLang[input.lang] }
+ )
+
+ if (!apiResponse.ok) {
+ await metricsUpdateBooking.httpError(apiResponse)
+ throw serverErrorByStatus(
+ apiResponse.status,
+ await extractResponseDetails(apiResponse),
+ "updateBooking failed for " + input.confirmationNumber
+ )
+ }
+
+ const apiJson = await apiResponse.json()
+
+ const verifiedData = bookingConfirmationSchema.safeParse(apiJson)
+ if (!verifiedData.success) {
+ metricsUpdateBooking.validationError(verifiedData.error)
+ throw badGatewayError({
+ message: "Invalid response from updateBooking",
+ errorDetails: { validationError: verifiedData.error },
+ })
+ }
+
+ metricsUpdateBooking.success()
+
+ return verifiedData.data
+}
diff --git a/packages/trpc/lib/types/bookingConfirmation.ts b/packages/trpc/lib/types/bookingConfirmation.ts
index dd0761bd9..9364a79a2 100644
--- a/packages/trpc/lib/types/bookingConfirmation.ts
+++ b/packages/trpc/lib/types/bookingConfirmation.ts
@@ -3,9 +3,8 @@ import type { z } from "zod"
import type {
bookingConfirmationSchema,
packageSchema,
-} from "../routers/booking/output"
+} from "../services/booking/getBooking/schema"
import type { HotelData, Room } from "./hotel"
-
export interface BookingConfirmationSchema extends z.output<
typeof bookingConfirmationSchema
> {}
diff --git a/packages/trpc/lib/types/entry.ts b/packages/trpc/lib/types/entry.ts
index f81fb221d..331206d4f 100644
--- a/packages/trpc/lib/types/entry.ts
+++ b/packages/trpc/lib/types/entry.ts
@@ -1,4 +1,4 @@
-import z from "zod"
+import { z } from "zod"
const baseResolveSchema = z.object({
system: z.object({
diff --git a/packages/trpc/package.json b/packages/trpc/package.json
index 3765ddeac..48756f373 100644
--- a/packages/trpc/package.json
+++ b/packages/trpc/package.json
@@ -33,6 +33,7 @@
"./routers/autocomplete/*": "./lib/routers/autocomplete/*.ts",
"./routers/navigation/*": "./lib/routers/navigation/*.ts",
"./routers/appRouter": "./lib/routers/appRouter.ts",
+ "./services/*": "./lib/services/*/index.ts",
"./enums/*": "./lib/enums/*.ts",
"./types/*": "./lib/types/*.ts",
"./constants/*": "./lib/constants/*.ts",