diff --git a/components/HotelReservation/SelectRate/Payment/index.tsx b/components/HotelReservation/SelectRate/Payment/index.tsx index a9915a310..ed8068076 100644 --- a/components/HotelReservation/SelectRate/Payment/index.tsx +++ b/components/HotelReservation/SelectRate/Payment/index.tsx @@ -1,6 +1,62 @@ "use client" -import styles from "./payment.module.css" + +import { trpc } from "@/lib/trpc/client" + +import Button from "@/components/TempDesignSystem/Button" export default function Payment() { - return
Payment TBI
+ const initiateBooking = trpc.booking.booking.create.useMutation({ + onSuccess: (result) => { + // TODO: Handle success, poll for payment link and redirect the user to the payment + console.log("Res", result) + }, + onError: () => { + // TODO: Handle error + console.log("Error") + }, + }) + + return ( + + ) } diff --git a/components/HotelReservation/SelectRate/Payment/payment.module.css b/components/HotelReservation/SelectRate/Payment/payment.module.css deleted file mode 100644 index ec81ef8e9..000000000 --- a/components/HotelReservation/SelectRate/Payment/payment.module.css +++ /dev/null @@ -1,2 +0,0 @@ -.wrapper { -} diff --git a/lib/api/endpoints.ts b/lib/api/endpoints.ts index 03af495fc..14460c1a9 100644 --- a/lib/api/endpoints.ts +++ b/lib/api/endpoints.ts @@ -16,6 +16,7 @@ export namespace endpoints { hotels = "hotel/v1/Hotels", intiateSaveCard = `${creditCards}/initiateSaveCard`, deleteCreditCard = `${profile}/creditCards`, + booking = "booking/v1/Bookings", } } diff --git a/server/index.ts b/server/index.ts index 9b4f7ca5c..6f41f4391 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,10 +1,12 @@ /** Routers */ +import { bookingRouter } from "./routers/booking" import { contentstackRouter } from "./routers/contentstack" import { hotelsRouter } from "./routers/hotels" import { userRouter } from "./routers/user" import { router } from "./trpc" export const appRouter = router({ + booking: bookingRouter, contentstack: contentstackRouter, hotel: hotelsRouter, user: userRouter, diff --git a/server/routers/booking/index.ts b/server/routers/booking/index.ts new file mode 100644 index 000000000..65b968733 --- /dev/null +++ b/server/routers/booking/index.ts @@ -0,0 +1,5 @@ +import { mergeRouters } from "@/server/trpc" + +import { bookingMutationRouter } from "./mutation" + +export const bookingRouter = mergeRouters(bookingMutationRouter) diff --git a/server/routers/booking/input.ts b/server/routers/booking/input.ts new file mode 100644 index 000000000..46a88110e --- /dev/null +++ b/server/routers/booking/input.ts @@ -0,0 +1,38 @@ +import { z } from "zod" + +// Query +// Mutation +export const createBookingInput = z.object({ + hotelId: z.string(), + checkInDate: z.string(), + checkOutDate: z.string(), + rooms: z.array( + z.object({ + adults: z.number().int().nonnegative(), + children: z.number().int().nonnegative(), + rateCode: z.string(), + roomTypeCode: z.string(), + guest: z.object({ + title: z.string(), + firstName: z.string(), + lastName: z.string(), + email: z.string().email(), + phoneCountryCodePrefix: z.string(), + phoneNumber: z.string(), + countryCode: z.string(), + }), + smsConfirmationRequested: z.boolean(), + }) + ), + payment: z.object({ + cardHolder: z.object({ + Email: z.string().email(), + Name: z.string(), + PhoneCountryCode: z.string(), + PhoneSubscriber: z.string(), + }), + success: z.string(), + error: z.string(), + cancel: z.string(), + }), +}) diff --git a/server/routers/booking/mutation.ts b/server/routers/booking/mutation.ts new file mode 100644 index 000000000..2b35f56d4 --- /dev/null +++ b/server/routers/booking/mutation.ts @@ -0,0 +1,129 @@ +import { metrics } from "@opentelemetry/api" + +import * as api from "@/lib/api" +import { getVerifiedUser } from "@/server/routers/user/query" +import { router, safeProtectedProcedure } from "@/server/trpc" + +import { getMembership } from "@/utils/user" + +import { createBookingInput } from "./input" +import { createBookingSchema } from "./output" + +import type { Session } from "next-auth" + +const meter = metrics.getMeter("trpc.bookings") +const createBookingCounter = meter.createCounter("trpc.bookings.create") +const createBookingSuccessCounter = meter.createCounter( + "trpc.bookings.create-success" +) +const createBookingFailCounter = meter.createCounter( + "trpc.bookings.create-fail" +) + +async function getMembershipNumber( + session: Session | null +): Promise { + if (!session) return undefined + + const verifiedUser = await getVerifiedUser({ session }) + if (!verifiedUser || "error" in verifiedUser) { + return undefined + } + + const membership = getMembership(verifiedUser.data.memberships) + return membership?.membershipNumber +} + +export const bookingMutationRouter = router({ + booking: router({ + create: safeProtectedProcedure + .input(createBookingInput) + .mutation(async function ({ ctx, input }) { + const { checkInDate, checkOutDate, hotelId } = input + + const loggingAttributes = { + membershipNumber: await getMembershipNumber(ctx.session), + checkInDate, + checkOutDate, + hotelId, + } + + createBookingCounter.add(1, { hotelId, checkInDate, checkOutDate }) + + console.info( + "api.booking.booking.create start", + JSON.stringify({ + query: loggingAttributes, + }) + ) + const headers = ctx.session + ? { + Authorization: `Bearer ${ctx.session?.token.access_token}`, + } + : undefined + const apiResponse = await api.post(api.endpoints.v1.booking, { + headers, + body: input, + }) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + }), + }) + console.error( + "api.booking.booking.create error", + JSON.stringify({ + query: loggingAttributes, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + error: text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const verifiedData = createBookingSchema.safeParse(apiJson) + if (!verifiedData.success) { + createBookingFailCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + error_type: "validation_error", + }) + + console.error( + "api.booking.booking.create validation error", + JSON.stringify({ + query: loggingAttributes, + error: verifiedData.error, + }) + ) + return null + } + + createBookingSuccessCounter.add(1, { + hotelId, + checkInDate, + checkOutDate, + }) + + console.info( + "api.booking.booking.create success", + JSON.stringify({ + query: loggingAttributes, + }) + ) + return verifiedData.data + }), + }), +}) diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts new file mode 100644 index 000000000..8fedd8716 --- /dev/null +++ b/server/routers/booking/output.ts @@ -0,0 +1,34 @@ +import { z } from "zod" + +export const createBookingSchema = z + .object({ + data: z.object({ + attributes: z.object({ + confirmationNumber: z.string(), + cancellationNumber: z.string().nullable(), + reservationStatus: z.string(), + paymentUrl: z.string().nullable(), + }), + 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, + confirmationNumber: d.data.attributes.confirmationNumber, + cancellationNumber: d.data.attributes.cancellationNumber, + reservationStatus: d.data.attributes.reservationStatus, + paymentUrl: d.data.attributes.paymentUrl, + })) + +type CreateBookingData = z.infer diff --git a/server/routers/user/query.ts b/server/routers/user/query.ts index 94312edc4..a482f9787 100644 --- a/server/routers/user/query.ts +++ b/server/routers/user/query.ts @@ -84,7 +84,7 @@ const getCreditCardsFailCounter = meter.createCounter( "trpc.user.creditCards-fail" ) -async function getVerifiedUser({ session }: { session: Session }) { +export async function getVerifiedUser({ session }: { session: Session }) { const now = Date.now() if (session.token.expires_at && session.token.expires_at < now) {