Merged in feat/booking-flow-submit (pull request #580)
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
This commit is contained in:
@@ -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 <div className={styles.wrapper}>Payment TBI</div>
|
||||
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 (
|
||||
<Button
|
||||
onClick={() =>
|
||||
// TODO: Use real values
|
||||
initiateBooking.mutate({
|
||||
hotelId: "811",
|
||||
checkInDate: "2024-12-10",
|
||||
checkOutDate: "2024-12-11",
|
||||
rooms: [
|
||||
{
|
||||
adults: 1,
|
||||
children: 0,
|
||||
rateCode: "SAVEEU",
|
||||
roomTypeCode: "QC",
|
||||
guest: {
|
||||
title: "Mr",
|
||||
firstName: "Test",
|
||||
lastName: "User",
|
||||
email: "test.user@scandichotels.com",
|
||||
phoneCountryCodePrefix: "string",
|
||||
phoneNumber: "string",
|
||||
countryCode: "string",
|
||||
},
|
||||
smsConfirmationRequested: true,
|
||||
},
|
||||
],
|
||||
payment: {
|
||||
cardHolder: {
|
||||
Email: "test.user@scandichotels.com",
|
||||
Name: "Test User",
|
||||
PhoneCountryCode: "",
|
||||
PhoneSubscriber: "",
|
||||
},
|
||||
success: "success/handle",
|
||||
error: "error/handle",
|
||||
cancel: "cancel/handle",
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
Create booking
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
.wrapper {
|
||||
}
|
||||
@@ -16,6 +16,7 @@ export namespace endpoints {
|
||||
hotels = "hotel/v1/Hotels",
|
||||
intiateSaveCard = `${creditCards}/initiateSaveCard`,
|
||||
deleteCreditCard = `${profile}/creditCards`,
|
||||
booking = "booking/v1/Bookings",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
5
server/routers/booking/index.ts
Normal file
5
server/routers/booking/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { bookingMutationRouter } from "./mutation"
|
||||
|
||||
export const bookingRouter = mergeRouters(bookingMutationRouter)
|
||||
38
server/routers/booking/input.ts
Normal file
38
server/routers/booking/input.ts
Normal file
@@ -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(),
|
||||
}),
|
||||
})
|
||||
129
server/routers/booking/mutation.ts
Normal file
129
server/routers/booking/mutation.ts
Normal file
@@ -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<string | undefined> {
|
||||
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
|
||||
}),
|
||||
}),
|
||||
})
|
||||
34
server/routers/booking/output.ts
Normal file
34
server/routers/booking/output.ts
Normal file
@@ -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<typeof createBookingSchema>
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user