feat: refactor of my stay

This commit is contained in:
Simon Emanuelsson
2025-04-25 14:08:14 +02:00
committed by Simon.Emanuelsson
parent b5deb84b33
commit ec087a3d15
208 changed files with 5458 additions and 4569 deletions

View File

@@ -131,6 +131,11 @@ export const cancelBookingInput = z.object({
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const cancelManyBookingsInput = z.object({
confirmationNumbers: z.array(z.string()),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const guaranteeBookingInput = z.object({
confirmationNumber: z.string(),
card: z
@@ -175,5 +180,15 @@ const confirmationNumberInput = z.object({
})
export const getBookingInput = confirmationNumberInput
export const getLinkedReservationsInput = z.object({
lang: z.nativeEnum(Lang).optional(),
rooms: z.array(
z.object({
confirmationNumber: z.string(),
})
),
})
export type LinkedReservationsInput = z.input<typeof getLinkedReservationsInput>
export const getBookingStatusInput = confirmationNumberInput

View File

@@ -6,6 +6,7 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc"
import {
addPackageInput,
cancelBookingInput,
cancelManyBookingsInput,
createBookingInput,
guaranteeBookingInput,
priceChangeInput,
@@ -13,6 +14,7 @@ import {
updateBookingInput,
} from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { cancelBooking } from "./utils"
export const bookingMutationRouter = router({
create: safeProtectedServiceProcedure
@@ -113,52 +115,40 @@ export const bookingMutationRouter = router({
cancel: safeProtectedServiceProcedure
.input(cancelBookingInput)
.mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
const token = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumber, language } = input
return await cancelBooking(confirmationNumber, language, token)
}),
cancelMany: safeProtectedServiceProcedure
.input(cancelManyBookingsInput)
.mutation(async function ({ ctx, input }) {
const token = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumbers, language } = input
const cancelBookingCounter = createCounter("trpc.booking", "cancel")
const metricsCancelBooking = cancelBookingCounter.init({
confirmationNumber,
language,
})
metricsCancelBooking.start()
const headers = {
Authorization: `Bearer ${accessToken}`,
}
const cancellationReason = {
reasonCode: "WEB-CANCEL",
reason: "WEB-CANCEL",
}
const apiResponse = await api.remove(
api.endpoints.v1.Booking.cancel(confirmationNumber),
{
headers,
body: JSON.stringify(cancellationReason),
} as RequestInit,
{ language }
const responses = await Promise.allSettled(
confirmationNumbers.map((confirmationNumber) =>
cancelBooking(confirmationNumber, language, token)
)
)
if (!apiResponse.ok) {
await metricsCancelBooking.httpError(apiResponse)
return false
const cancelledRoomsSuccessfully = []
for (const [idx, response] of responses.entries()) {
if (response.status === "fulfilled") {
if (response.value) {
cancelledRoomsSuccessfully.push(true)
continue
}
} else {
console.info(
`Cancelling booking failed for confirmationNumber: ${confirmationNumbers[idx]}`
)
console.error(response.reason)
}
cancelledRoomsSuccessfully.push(false)
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsCancelBooking.validationError(verifiedData.error)
return null
}
metricsCancelBooking.success()
return verifiedData.data
return cancelledRoomsSuccessfully
}),
packages: safeProtectedServiceProcedure
.input(addPackageInput)

View File

@@ -201,7 +201,7 @@ export const bookingConfirmationSchema = z
attributes: z.object({
adults: z.number().int(),
ancillary: ancillarySchema,
cancellationNumber: z.string().nullable().default(""),
cancelationNumber: z.string().nullable().default(""),
checkInDate: z.date({ coerce: true }),
checkOutDate: z.date({ coerce: true }),
childBedPreferences: z.array(childBedPreferencesSchema).default([]),
@@ -263,4 +263,6 @@ export const bookingConfirmationSchema = z
isCancelable: !!data.links.cancel,
isModifiable: !!data.links.modify,
canModifyAncillaries: !!data.links.addAncillary,
// Typo from API
cancellationNumber: data.attributes.cancelationNumber,
}))

View File

@@ -13,68 +13,54 @@ import {
createRefIdInput,
getBookingInput,
getBookingStatusInput,
getLinkedReservationsInput,
} from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { getBookedHotelRoom } from "./utils"
import { createBookingSchema } from "./output"
import { getBookedHotelRoom, getBooking } from "./utils"
export const bookingQueryRouter = router({
get: safeProtectedServiceProcedure
.input(getBookingInput)
.query(async function ({
ctx,
input: { confirmationNumber, lang: inputLang },
}) {
.use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang
const token = ctx.session?.token.access_token ?? ctx.serviceToken
return next({
ctx: {
lang,
token,
},
})
})
.query(async function ({ ctx, input: { confirmationNumber } }) {
const getBookingCounter = createCounter("trpc.booking", "get")
const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
metricsGetBooking.start()
let lang = ctx.lang ?? inputLang
const booking = await getBooking(confirmationNumber, ctx.lang, ctx.token)
const token = ctx.session?.token.access_token ?? ctx.serviceToken
const apiResponse = await api.get(
api.endpoints.v1.Booking.booking(confirmationNumber),
{
headers: {
Authorization: `Bearer ${token}`,
},
}
)
if (!apiResponse.ok) {
await metricsGetBooking.httpError(apiResponse)
// If the booking is not found, return null.
// This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them.
if (apiResponse.status === 400) {
return null
}
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
if (!booking) {
metricsGetBooking.dataError(
`Fail to get booking data for ${confirmationNumber}`,
{ confirmationNumber }
)
return null
}
const hotelData = await getHotel(
{
hotelId: booking.data.hotelId,
hotelId: booking.hotelId,
isCardOnlyPayment: false,
language: lang,
language: ctx.lang,
},
ctx.serviceToken
)
if (!hotelData) {
metricsGetBooking.dataError(
`Failed to get hotel data for ${booking.data.hotelId}`,
`Failed to get hotel data for ${booking.hotelId}`,
{
hotelId: booking.data.hotelId,
hotelId: booking.hotelId,
}
)
@@ -85,13 +71,62 @@ export const bookingQueryRouter = router({
return {
...hotelData,
booking: booking.data,
booking,
room: getBookedHotelRoom(
hotelData.roomCategories,
booking.data.roomTypeCode
booking.roomTypeCode
),
}
}),
linkedReservations: safeProtectedServiceProcedure
.input(getLinkedReservationsInput)
.use(async ({ ctx, input, next }) => {
const lang = input.lang ?? ctx.lang
const token = ctx.session?.token.access_token ?? ctx.serviceToken
return next({
ctx: {
lang,
token,
},
})
})
.query(async function ({ ctx, input: { rooms } }) {
const getLinkedReservationsCounter = createCounter(
"trpc.booking",
"linkedReservations"
)
const metricsGetLinkedReservations = getLinkedReservationsCounter.init({
confirmationNumbers: rooms,
})
metricsGetLinkedReservations.start()
const linkedReservationsResult = await Promise.allSettled(
rooms.map((room) =>
getBooking(room.confirmationNumber, ctx.lang, ctx.token)
)
)
const linkedReservations = []
for (const booking of linkedReservationsResult) {
if (booking.status === "fulfilled") {
if (booking.value) {
linkedReservations.push(booking.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).query(async function ({
ctx,
input,

View File

@@ -1,5 +1,13 @@
import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { createCounter } from "@/server/telemetry"
import { toApiLang } from "@/server/utils"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import type { Lang } from "@/constants/languages"
export function getBookedHotelRoom(
rooms: Room[] | undefined,
@@ -25,3 +33,96 @@ export function getBookedHotelRoom(
bedType,
}
}
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 === 400) {
return null
}
throw serverErrorByStatus(apiResponse.status, apiResponse)
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
}
metricsGetBooking.success()
return booking.data
}
export async function cancelBooking(
confirmationNumber: string,
language: string,
token: string
) {
const cancellationReason = {
reasonCode: "WEB-CANCEL",
reason: "WEB-CANCEL",
}
const cancelBookingCounter = createCounter("booking", "cancel")
const metricsCancelBooking = cancelBookingCounter.init({
cancellationReason,
confirmationNumber,
language,
})
metricsCancelBooking.start()
const headers = {
Authorization: `Bearer ${token}`,
}
const apiResponse = await api.remove(
api.endpoints.v1.Booking.cancel(confirmationNumber),
{
headers,
body: JSON.stringify(cancellationReason),
} as RequestInit,
{ language }
)
if (!apiResponse.ok) {
await metricsCancelBooking.httpError(apiResponse)
return false
}
const apiJson = await apiResponse.json()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsCancelBooking.validationError(verifiedData.error)
return null
}
metricsCancelBooking.success()
return verifiedData.data
}

View File

@@ -1031,6 +1031,7 @@ export async function getRoomsAvailability(
const apiResponse = await api.get(
api.endpoints.v1.Availability.hotel(hotelId),
{
cache: undefined, // overwrite default
headers: {
Authorization: `Bearer ${token}`,
},