Merge master

This commit is contained in:
Linus Flood
2025-05-02 14:01:50 +02:00
116 changed files with 2531 additions and 1682 deletions

View File

@@ -1,81 +1,163 @@
import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { createCounter } from "@/server/telemetry"
import { getUserOrServiceToken } from "@/server/tokenManager"
import { toApiLang } from "@/server/utils"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { getCacheClient } from "@/services/dataCache"
import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
import { bookingSchema, createBookingSchema } from "./output"
import type { BookingSchema } from "@/types/trpc/routers/booking/confirmation"
import type { Lang } from "@/constants/languages"
export function getBookedHotelRoom(
rooms: Room[] | undefined,
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
) {
if (!rooms?.length || !roomTypeCode) {
return null
}
const room = rooms?.find((r) => {
return r.roomTypes.find((roomType) => roomType.code === roomTypeCode)
})
if (!room) {
return null
}
const bedType = room.roomTypes.find(
(roomType) => roomType.code === roomTypeCode
)
if (!bedType) {
return null
}
return {
...room,
bedType,
}
}
export async function getBooking(
confirmationNumber: string,
lang: Lang,
token: string
) {
export async function getBooking(confirmationNumber: string, lang: Lang) {
const getBookingCounter = createCounter("booking", "get")
const metricsGetBooking = getBookingCounter.init({ confirmationNumber })
metricsGetBooking.start()
const apiResponse = await api.get(
api.endpoints.v1.Booking.booking(confirmationNumber),
const cacheKey = `${lang}:booking:${confirmationNumber}`
const cache = await getCacheClient()
const result: BookingSchema | null = await cache.cacheOrGet(
cacheKey,
async () => {
const token = getUserOrServiceToken()
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 = bookingSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
}
return booking.data
},
"1h"
)
metricsGetBooking.success()
return result
}
export async function getBookings(confirmationNumbers: string[], lang: Lang) {
const results = await Promise.allSettled(
confirmationNumbers.map((confirmationNumber) => {
return getBooking(confirmationNumber, lang)
})
)
return results.map((result) => {
if (result.status === "fulfilled" && result.value) {
return result.value
}
return null
})
}
export async function getLinkedReservations(
confirmationNumber: string,
lang: Lang
) {
const booking = await getBooking(confirmationNumber, lang)
if (!booking) {
return null
}
if (booking.linkedReservations.length > 0) {
const confirmationNumbers = booking.linkedReservations.map(
(linkedReservation) => {
return linkedReservation.confirmationNumber
}
)
const bookings = await getBookings(confirmationNumbers, lang)
const linkedReservations = bookings.map((booking, i) => {
if (booking === null) {
return {
confirmationNumber: confirmationNumbers[i],
error: true,
} as const
}
return booking
})
return linkedReservations
}
return []
}
export async function cancelBooking(confirmationNumber: string, lang: Lang) {
const cancelBookingCounter = createCounter("booking", "cancel")
const metricsCancelBooking = cancelBookingCounter.init({
confirmationNumber,
lang,
})
metricsCancelBooking.start()
const token = getUserOrServiceToken()
const headers = {
Authorization: `Bearer ${token}`,
}
const booking = await getBooking(confirmationNumber, lang)
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: {
Authorization: `Bearer ${token}`,
},
headers,
body: { firstName, lastName, email },
},
{ language: toApiLang(lang) }
)
if (!apiResponse.ok) {
await metricsGetBooking.httpError(apiResponse)
// If the booking is not found, return null.
// This scenario is expected to happen when a logged in user trying to access a booking that doesn't belong to them.
if (apiResponse.status === 400) {
return null
}
throw serverErrorByStatus(apiResponse.status, apiResponse)
await metricsCancelBooking.httpError(apiResponse)
return null
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
const verifiedData = createBookingSchema.safeParse(apiJson)
if (!verifiedData.success) {
metricsCancelBooking.validationError(verifiedData.error)
return null
}
metricsGetBooking.success()
metricsCancelBooking.success()
return booking.data
return verifiedData.data
}
export async function findBooking(
@@ -124,7 +206,7 @@ export async function findBooking(
}
const apiJson = await apiResponse.json()
const booking = bookingConfirmationSchema.safeParse(apiJson)
const booking = bookingSchema.safeParse(apiJson)
if (!booking.success) {
metricsGetBooking.validationError(booking.error)
throw badRequestError()
@@ -134,52 +216,3 @@ export async function findBooking(
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
}