import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../.." import * as api from "../../api" import { badRequestError, 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() 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 serverErrorByStatus(404) } 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 serverErrorByStatus(404) } 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, apiResponse) } const apiJson = await apiResponse.json() const verifiedData = createBookingSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetBookingStatus.validationError(verifiedData.error) throw badRequestError() } metricsGetBookingStatus.success() const expire = Math.floor(Date.now() / 1000) + 60 // 1 minute expiry return { booking: verifiedData.data, sig: encrypt(expire.toString()), } }), createRefId: serviceProcedure .input(createRefIdInput) .mutation(async function ({ input }) { const { confirmationNumber, lastName } = input const encryptedRefId = encrypt(`${confirmationNumber},${lastName}`) if (!encryptedRefId) { throw serverErrorByStatus(422, "Was not able to encrypt ref id") } return { refId: encryptedRefId, } }), })