import { BookingStatusEnum } from "@/constants/booking" import { bookingConfirmation } from "@/constants/routes/hotelReservation" import * as api from "@/lib/api" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { createCounter } from "@/server/telemetry" import { router, safeProtectedServiceProcedure, serviceProcedure, } from "@/server/trpc" import { getBookedHotelRoom } from "@/stores/my-stay" import { calculateRefId, parseRefId } from "@/utils/refId" import { getHotel } from "../hotels/utils" import { bookingConfirmationInput, createRefIdInput, getBookingConfirmationErrorInput, getBookingStatusInput, getConfirmationCompletedInput, getLinkedReservationsInput, } from "./input" import { createBookingSchema } from "./output" import { getBooking, getLinkedReservations } from "./utils" import type { BookingSchema } from "@/types/trpc/routers/booking/confirmation" export const bookingQueryRouter = router({ confirmation: safeProtectedServiceProcedure .input(bookingConfirmationInput) .use(async ({ ctx, input, next }) => { const lang = input.lang ?? ctx.lang const { confirmationNumber } = parseRefId(input.refId) return next({ ctx: { lang, confirmationNumber, }, }) }) .query(async function ({ ctx: { confirmationNumber, lang, serviceToken }, }) { const getBookingCounter = createCounter("trpc.booking", "get") const metricsGetBooking = getBookingCounter.init({ confirmationNumber }) metricsGetBooking.start() const booking = await getBooking(confirmationNumber, lang) if (!booking) { metricsGetBooking.dataError( `Fail to get booking data for ${confirmationNumber}`, { confirmationNumber } ) return null } const hotelData = await getHotel( { hotelId: booking.hotelId, isCardOnlyPayment: false, language: lang, }, serviceToken ) if (!hotelData) { metricsGetBooking.dataError( `Failed to get hotel data for ${booking.hotelId}`, { hotelId: booking.hotelId, } ) throw serverErrorByStatus(404) } const room = getBookedHotelRoom( hotelData.roomCategories, booking.roomTypeCode ) if (!room) { metricsGetBooking.dataError( `Failed to extract booked room ${booking.roomTypeCode} from room categories for ${booking.hotelId}`, { roomTypeCode: booking.roomTypeCode, hotelId: booking.hotelId, } ) throw serverErrorByStatus(404) } metricsGetBooking.success() return { hotelData, booking, room, } }), linkedReservations: safeProtectedServiceProcedure .input(getLinkedReservationsInput) .use(async ({ ctx, input, next }) => { const lang = input.lang ?? ctx.lang const { confirmationNumber } = parseRefId(input.refId) return next({ ctx: { lang, confirmationNumber, }, }) }) .query(async function ({ ctx: { confirmationNumber, lang } }) { const linkedReservationsCounter = createCounter( "trpc.booking", "linkedReservations" ) const metricsLinkedReservations = linkedReservationsCounter.init({ confirmationNumber, }) metricsLinkedReservations.start() const linkedReservations = await getLinkedReservations( confirmationNumber, lang ) if (!linkedReservations) { metricsLinkedReservations.noDataError() return null } const validLinkedReservations = linkedReservations.reduce< BookingSchema[] >((acc, linkedReservation) => { if ("error" in linkedReservation) { metricsLinkedReservations.dataError( `Failed to get linked reservations ${linkedReservation.confirmationNumber}`, { linkedReservationConfirmationNumber: linkedReservation.confirmationNumber, } ) return acc } acc.push(linkedReservation) return acc }, []) metricsLinkedReservations.success() return validLinkedReservations }), status: serviceProcedure .input(getBookingStatusInput) .use(async ({ input, next }) => { const { confirmationNumber } = parseRefId(input.refId) return next({ ctx: { confirmationNumber, }, }) }) .query(async function ({ ctx: { confirmationNumber, serviceToken } }) { 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}`, }, } ) if (!apiResponse.ok) { await metricsGetBookingStatus.httpError(apiResponse) throw serverErrorByStatus(apiResponse.status, apiResponse) } const apiJson = await apiResponse.json() const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetBookingStatus.validationError(verifiedData.error) throw badRequestError() } metricsGetBookingStatus.success() return verifiedData.data }), confirmationCompleted: serviceProcedure .input(getConfirmationCompletedInput) .use(async ({ input, next }) => { const { confirmationNumber } = parseRefId(input.refId) return next({ ctx: { confirmationNumber, }, }) }) .query(async function ({ ctx, input }) { const { confirmationNumber } = ctx const confirmationCompletedCounter = createCounter( "trpc.booking", "confirmationCompleted" ) const metricsConfirmationCompleted = confirmationCompletedCounter.init({ confirmationNumber, }) metricsConfirmationCompleted.start() const apiResponse = await api.get( api.endpoints.v1.Booking.status(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, }, } ) if (!apiResponse.ok) { await metricsConfirmationCompleted.httpError(apiResponse) throw serverErrorByStatus(apiResponse.status, apiResponse) } const apiJson = await apiResponse.json() const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { metricsConfirmationCompleted.validationError(verifiedData.error) throw badRequestError() } const confirmationUrl = verifiedData.data.reservationStatus === BookingStatusEnum.BookingCompleted ? `${bookingConfirmation(input.lang)}?RefId=${verifiedData.data.rooms[0].refId}` : "" const result = { ...verifiedData.data, redirectUrl: verifiedData.data.paymentUrl || confirmationUrl, } metricsConfirmationCompleted.success() return result }), confirmationError: serviceProcedure .input(getBookingConfirmationErrorInput) .use(async ({ input, next }) => { const { confirmationNumber } = parseRefId(input.refId) return next({ ctx: { confirmationNumber, }, }) }) .query(async function ({ ctx }) { const { confirmationNumber } = ctx const confirmationErrorCounter = createCounter( "trpc.booking", "confirmationError" ) const metricsConfirmationError = confirmationErrorCounter.init({ confirmationNumber, }) metricsConfirmationError.start() const apiResponse = await api.get( api.endpoints.v1.Booking.status(confirmationNumber), { headers: { Authorization: `Bearer ${ctx.serviceToken}`, }, } ) if (!apiResponse.ok) { await metricsConfirmationError.httpError(apiResponse) throw serverErrorByStatus(apiResponse.status, apiResponse) } const apiJson = await apiResponse.json() const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { metricsConfirmationError.validationError(verifiedData.error) throw badRequestError() } metricsConfirmationError.success() return verifiedData.data }), createRefId: serviceProcedure .input(createRefIdInput) .mutation(async function ({ input }) { const { confirmationNumber, lastName } = input const encryptedRefId = calculateRefId(confirmationNumber, lastName) if (!encryptedRefId) { throw serverErrorByStatus(422, "Was not able to encrypt ref id") } return { refId: encryptedRefId, } }), })