fix: booking confirmation validation

This commit is contained in:
Christel Westerberg
2024-11-01 10:24:56 +01:00
parent cdc5652347
commit 644ce369aa
5 changed files with 145 additions and 118 deletions

View File

@@ -45,8 +45,8 @@ export default async function BookingConfirmationPage({
} }
) )
const fromDate = dt(booking.temp.fromDate).locale(params.lang) const fromDate = dt(booking.checkInDate).locale(params.lang)
const toDate = dt(booking.temp.toDate).locale(params.lang) const toDate = dt(booking.checkOutDate).locale(params.lang)
const nights = intl.formatMessage( const nights = intl.formatMessage(
{ id: "booking.nights" }, { id: "booking.nights" },
{ {
@@ -77,7 +77,7 @@ export default async function BookingConfirmationPage({
textTransform="regular" textTransform="regular"
type="h1" type="h1"
> >
{booking.hotel.name} {booking.hotel?.data.attributes.name}
</Title> </Title>
</hgroup> </hgroup>
<Body className={styles.body} textAlign="center"> <Body className={styles.body} textAlign="center">
@@ -91,7 +91,7 @@ export default async function BookingConfirmationPage({
<Subtitle color="burgundy" type="two"> <Subtitle color="burgundy" type="two">
{intl.formatMessage( {intl.formatMessage(
{ id: "Reference #{bookingNr}" }, { id: "Reference #{bookingNr}" },
{ bookingNr: "A92320VV" } { bookingNr: booking.confirmationNumber }
)} )}
</Subtitle> </Subtitle>
</header> </header>
@@ -183,11 +183,13 @@ export default async function BookingConfirmationPage({
</Caption> </Caption>
<div> <div>
<Body color="burgundy" textTransform="bold"> <Body color="burgundy" textTransform="bold">
{booking.hotel.name} {booking.hotel?.data.attributes.name}
</Body> </Body>
<Body color="uiTextHighContrast">{booking.hotel.email}</Body>
<Body color="uiTextHighContrast"> <Body color="uiTextHighContrast">
{booking.hotel.phoneNumber} {booking.hotel?.data.attributes.contactInformation.email}
</Body>
<Body color="uiTextHighContrast">
{booking.hotel?.data.attributes.contactInformation.phoneNumber}
</Body> </Body>
</div> </div>
</div> </div>
@@ -219,7 +221,16 @@ export default async function BookingConfirmationPage({
<Body color="burgundy" textTransform="bold"> <Body color="burgundy" textTransform="bold">
{intl.formatMessage({ id: "Total cost" })} {intl.formatMessage({ id: "Total cost" })}
</Body> </Body>
<Body color="uiTextHighContrast">{booking.temp.total}</Body> <Body color="uiTextHighContrast">
{" "}
{intl.formatMessage(
{ id: "{amount} {currency}" },
{
amount: intl.formatNumber(booking.totalPrice),
currency: booking.currencyCode,
}
)}
</Body>
<Caption color="uiTextPlaceholder"> <Caption color="uiTextPlaceholder">
{`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`}
</Caption> </Caption>

View File

@@ -44,14 +44,18 @@ const childrenAgesSchema = z.object({
const guestSchema = z.object({ const guestSchema = z.object({
firstName: z.string(), firstName: z.string(),
lastName: z.string(), lastName: z.string(),
email: z.string().nullable(),
phoneNumber: z.string().nullable(),
}) })
const packagesSchema = z.object({ const packagesSchema = z.array(
accessibility: z.boolean(), z.object({
allergyFriendly: z.boolean(), accessibility: z.boolean().optional(),
breakfast: z.boolean(), allergyFriendly: z.boolean().optional(),
petFriendly: z.boolean(), breakfast: z.boolean().optional(),
}) petFriendly: z.boolean().optional(),
})
)
export const bookingConfirmationSchema = z export const bookingConfirmationSchema = z
.object({ .object({
@@ -66,7 +70,7 @@ export const bookingConfirmationSchema = z
confirmationNumber: z.string(), confirmationNumber: z.string(),
currencyCode: z.string(), currencyCode: z.string(),
guest: guestSchema, guest: guestSchema,
hasPayRouting: z.boolean(), hasPayRouting: z.boolean().optional(),
hotelId: z.string(), hotelId: z.string(),
packages: packagesSchema, packages: packagesSchema,
rateCode: z.string(), rateCode: z.string(),

View File

@@ -4,6 +4,7 @@ import * as api from "@/lib/api"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc" import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { router, serviceProcedure } from "@/server/trpc" import { router, serviceProcedure } from "@/server/trpc"
import { getHotelData } from "../hotels/query"
import { bookingConfirmationInput, getBookingStatusInput } from "./input" import { bookingConfirmationInput, getBookingStatusInput } from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output" import { bookingConfirmationSchema, createBookingSchema } from "./output"
@@ -81,6 +82,11 @@ export const bookingQueryRouter = router({
throw badRequestError() throw badRequestError()
} }
const hotelData = await getHotelData(
{ hotelId: booking.data.hotelId, language: ctx.lang },
ctx.serviceToken
)
getBookingConfirmationSuccessCounter.add(1, { confirmationNumber }) getBookingConfirmationSuccessCounter.add(1, { confirmationNumber })
console.info( console.info(
"api.booking.confirmation success", "api.booking.confirmation success",
@@ -91,6 +97,7 @@ export const bookingQueryRouter = router({
return { return {
...booking.data, ...booking.data,
hotel: hotelData,
temp: { temp: {
breakfastFrom: "06:30", breakfastFrom: "06:30",
breakfastTo: "11:00", breakfastTo: "11:00",
@@ -127,11 +134,6 @@ export const bookingQueryRouter = router({
memberbershipNumber: "19822", memberbershipNumber: "19822",
phoneNumber: "+46702446688", phoneNumber: "+46702446688",
}, },
hotel: {
email: "bookings@scandichotels.com",
name: "Downtown Camper by Scandic",
phoneNumber: "+4689001350",
},
} }
}), }),
status: serviceProcedure.input(getBookingStatusInput).query(async function ({ status: serviceProcedure.input(getBookingStatusInput).query(async function ({

View File

@@ -68,6 +68,8 @@ export const getHotelDataInputSchema = z.object({
include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(), include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(),
}) })
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
export const getBreakfastPackageInputSchema = z.object({ export const getBreakfastPackageInputSchema = z.object({
adults: z.number().min(1, { message: "at least one adult is required" }), adults: z.number().min(1, { message: "at least one adult is required" }),
fromDate: z fromDate: z

View File

@@ -39,6 +39,7 @@ import {
getRatesInputSchema, getRatesInputSchema,
getRoomsAvailabilityInputSchema, getRoomsAvailabilityInputSchema,
getSelectedRoomAvailabilityInputSchema, getSelectedRoomAvailabilityInputSchema,
type HotelDataInput,
} from "./input" } from "./input"
import { import {
breakfastPackagesSchema, breakfastPackagesSchema,
@@ -162,6 +163,110 @@ async function getContentstackData(lang: Lang, uid?: string | null) {
return hotelPageData.data.hotel_page return hotelPageData.data.hotel_page
} }
export async function getHotelData(
input: HotelDataInput,
serviceToken: string
) {
const { hotelId, language, include, isCardOnlyPayment } = input
const params: Record<string, string> = {
hotelId,
language,
}
if (include) {
params.include = include.join(",")
}
getHotelCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelData error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validateHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.hotelData validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
getHotelSuccessCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData success",
JSON.stringify({
query: { hotelId, params: params },
})
)
if (isCardOnlyPayment) {
validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions =
[]
}
return validateHotelData.data
}
export const hotelQueryRouter = router({ export const hotelQueryRouter = router({
get: contentStackUidWithServiceProcedure get: contentStackUidWithServiceProcedure
.input(getHotelInputSchema) .input(getHotelInputSchema)
@@ -753,104 +858,7 @@ export const hotelQueryRouter = router({
get: serviceProcedure get: serviceProcedure
.input(getHotelDataInputSchema) .input(getHotelDataInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const { hotelId, language, include, isCardOnlyPayment } = input return getHotelData(input, ctx.serviceToken)
const params: Record<string, string> = {
hotelId,
language,
}
if (include) {
params.include = include.join(",")
}
getHotelCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponse = await api.get(
api.endpoints.v1.Hotel.Hotels.hotel(hotelId),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.hotels.hotelData error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const apiJson = await apiResponse.json()
const validateHotelData = getHotelDataSchema.safeParse(apiJson)
if (!validateHotelData.success) {
getHotelFailCounter.add(1, {
hotelId,
language,
include,
error_type: "validation_error",
error: JSON.stringify(validateHotelData.error),
})
console.error(
"api.hotels.hotelData validation error",
JSON.stringify({
query: { hotelId, params },
error: validateHotelData.error,
})
)
throw badRequestError()
}
getHotelSuccessCounter.add(1, {
hotelId,
language,
include,
})
console.info(
"api.hotels.hotelData success",
JSON.stringify({
query: { hotelId, params: params },
})
)
if (isCardOnlyPayment) {
validateHotelData.data.data.attributes.merchantInformationData.alternatePaymentOptions =
[]
}
return validateHotelData.data
}), }),
}), }),
locations: router({ locations: router({