From b9dbcf7d90ff77825eecc9e045ead81df59aac5a Mon Sep 17 00:00:00 2001 From: Niclas Edenvin Date: Fri, 20 Sep 2024 13:05:23 +0000 Subject: [PATCH] Merged in feat/booking-flow-submit (pull request #580) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This implements the actual call to the API to create a booking. That’s the only thing it does, it doesn’t handle the response in any way. This PR is just to get it there and the new booking sub team will handle it further, with payment etc. Approved-by: Michael Zetterberg Approved-by: Fredrik Thorsson Approved-by: Simon.Emanuelsson --- .../SelectRate/Payment/index.tsx | 60 +++++++- .../SelectRate/Payment/payment.module.css | 2 - lib/api/endpoints.ts | 1 + server/index.ts | 2 + server/routers/booking/index.ts | 5 + server/routers/booking/input.ts | 38 ++++++ server/routers/booking/mutation.ts | 129 ++++++++++++++++++ server/routers/booking/output.ts | 34 +++++ server/routers/user/query.ts | 2 +- 9 files changed, 268 insertions(+), 5 deletions(-) delete mode 100644 components/HotelReservation/SelectRate/Payment/payment.module.css create mode 100644 server/routers/booking/index.ts create mode 100644 server/routers/booking/input.ts create mode 100644 server/routers/booking/mutation.ts create mode 100644 server/routers/booking/output.ts 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) {