Merge branch 'develop' into feature/tracking
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { bookingMutationRouter } from "./mutation"
|
||||
import { bookingQueryRouter } from "./query"
|
||||
|
||||
export const bookingRouter = mergeRouters(bookingMutationRouter)
|
||||
export const bookingRouter = mergeRouters(
|
||||
bookingMutationRouter,
|
||||
bookingQueryRouter
|
||||
)
|
||||
|
||||
@@ -1,38 +1,68 @@
|
||||
import { z } from "zod"
|
||||
|
||||
// Query
|
||||
const roomsSchema = z.array(
|
||||
z.object({
|
||||
adults: z.number().int().nonnegative(),
|
||||
childrenAges: z
|
||||
.array(
|
||||
z.object({
|
||||
age: z.number().int().nonnegative(),
|
||||
bedType: z.string(),
|
||||
})
|
||||
)
|
||||
.default([]),
|
||||
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(),
|
||||
membershipNumber: z.string().optional(),
|
||||
}),
|
||||
smsConfirmationRequested: z.boolean(),
|
||||
packages: z.object({
|
||||
breakfast: z.boolean(),
|
||||
allergyFriendly: z.boolean(),
|
||||
petFriendly: z.boolean(),
|
||||
accessibility: z.boolean(),
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
const paymentSchema = z.object({
|
||||
paymentMethod: z.string(),
|
||||
card: z
|
||||
.object({
|
||||
alias: z.string(),
|
||||
expiryDate: z.string(),
|
||||
cardType: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
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(),
|
||||
})
|
||||
|
||||
// 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(),
|
||||
}),
|
||||
rooms: roomsSchema,
|
||||
payment: paymentSchema,
|
||||
})
|
||||
|
||||
// Query
|
||||
export const getBookingStatusInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@ 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 { bookingServiceProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
@@ -36,13 +36,15 @@ async function getMembershipNumber(
|
||||
|
||||
export const bookingMutationRouter = router({
|
||||
booking: router({
|
||||
create: safeProtectedProcedure
|
||||
create: bookingServiceProcedure
|
||||
.input(createBookingInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const { checkInDate, checkOutDate, hotelId } = input
|
||||
|
||||
// TODO: add support for user token OR service token in procedure
|
||||
// then we can fetch membership number if user token exists
|
||||
const loggingAttributes = {
|
||||
membershipNumber: await getMembershipNumber(ctx.session),
|
||||
// membershipNumber: await getMembershipNumber(ctx.session),
|
||||
checkInDate,
|
||||
checkOutDate,
|
||||
hotelId,
|
||||
@@ -56,11 +58,10 @@ export const bookingMutationRouter = router({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
const headers = ctx.session
|
||||
? {
|
||||
Authorization: `Bearer ${ctx.session?.token.access_token}`,
|
||||
}
|
||||
: undefined
|
||||
const headers = {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
}
|
||||
|
||||
const apiResponse = await api.post(api.endpoints.v1.booking, {
|
||||
headers,
|
||||
body: input,
|
||||
|
||||
@@ -5,9 +5,9 @@ export const createBookingSchema = z
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
confirmationNumber: z.string(),
|
||||
cancellationNumber: z.string().nullable(),
|
||||
cancellationNumber: z.string().optional(),
|
||||
reservationStatus: z.string(),
|
||||
paymentUrl: z.string().nullable(),
|
||||
paymentUrl: z.string().optional(),
|
||||
}),
|
||||
type: z.string(),
|
||||
id: z.string(),
|
||||
|
||||
85
server/routers/booking/query.ts
Normal file
85
server/routers/booking/query.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { bookingServiceProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { getBookingStatusInput } from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
|
||||
const meter = metrics.getMeter("trpc.booking")
|
||||
const getBookingStatusCounter = meter.createCounter("trpc.booking.status")
|
||||
const getBookingStatusSuccessCounter = meter.createCounter(
|
||||
"trpc.booking.status-success"
|
||||
)
|
||||
const getBookingStatusFailCounter = meter.createCounter(
|
||||
"trpc.booking.status-fail"
|
||||
)
|
||||
|
||||
export const bookingQueryRouter = router({
|
||||
status: bookingServiceProcedure
|
||||
.input(getBookingStatusInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
const { confirmationNumber } = input
|
||||
getBookingStatusCounter.add(1, { confirmationNumber })
|
||||
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.booking}/${confirmationNumber}/status`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
getBookingStatusFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.booking.status error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: responseMessage,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
getBookingStatusFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(verifiedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.status validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
getBookingStatusSuccessCounter.add(1, { confirmationNumber })
|
||||
console.info(
|
||||
"api.booking.status success",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
})
|
||||
)
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
})
|
||||
@@ -551,10 +551,13 @@ const linkSchema = z
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.linkConnection.edges.length) {
|
||||
const link = pageLinks.transform(data.linkConnection.edges[0].node)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
const linkNode = data.linkConnection.edges[0].node
|
||||
if (linkNode) {
|
||||
const link = pageLinks.transform(linkNode)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,17 @@ const getAllLoyaltyLevelFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.all-fail"
|
||||
)
|
||||
|
||||
const getByLevelLoyaltyLevelCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.byLevel"
|
||||
)
|
||||
|
||||
const getByLevelLoyaltyLevelSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.byLevel-success"
|
||||
)
|
||||
const getByLevelLoyaltyLevelFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.byLevel-fail"
|
||||
)
|
||||
|
||||
export async function getAllLoyaltyLevels(ctx: Context) {
|
||||
getAllLoyaltyLevelCounter.add(1)
|
||||
|
||||
@@ -87,7 +98,9 @@ export async function getAllLoyaltyLevels(ctx: Context) {
|
||||
}
|
||||
|
||||
export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
|
||||
getAllLoyaltyLevelCounter.add(1)
|
||||
getByLevelLoyaltyLevelCounter.add(1, {
|
||||
query: JSON.stringify({ lang: ctx.lang, level_id }),
|
||||
})
|
||||
|
||||
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
||||
GetLoyaltyLevel,
|
||||
@@ -103,10 +116,10 @@ export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
|
||||
!loyaltyLevelsConfigResponse.data ||
|
||||
!loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length
|
||||
) {
|
||||
getAllLoyaltyLevelFailCounter.add(1)
|
||||
getByLevelLoyaltyLevelFailCounter.add(1)
|
||||
const notFoundError = notFound(loyaltyLevelsConfigResponse)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevels not found error",
|
||||
"contentstack.loyaltyLevel not found error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: { code: notFoundError.code },
|
||||
@@ -119,10 +132,10 @@ export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
|
||||
loyaltyLevelsConfigResponse.data
|
||||
)
|
||||
if (!validatedLoyaltyLevels.success) {
|
||||
getAllLoyaltyLevelFailCounter.add(1)
|
||||
getByLevelLoyaltyLevelFailCounter.add(1)
|
||||
console.error(validatedLoyaltyLevels.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
"contentstack.loyaltyLevel validation error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: validatedLoyaltyLevels.error,
|
||||
@@ -131,7 +144,7 @@ export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
|
||||
return null
|
||||
}
|
||||
|
||||
getAllLoyaltyLevelSuccessCounter.add(1)
|
||||
getByLevelLoyaltyLevelSuccessCounter.add(1)
|
||||
return validatedLoyaltyLevels.data[0]
|
||||
}
|
||||
|
||||
|
||||
@@ -47,13 +47,13 @@ export const activitiesCard = z.object({
|
||||
}
|
||||
}
|
||||
return {
|
||||
background_image: data.background_image,
|
||||
body_text: data.body_text,
|
||||
backgroundImage: data.background_image,
|
||||
bodyText: data.body_text,
|
||||
contentPage,
|
||||
cta_text: data.cta_text,
|
||||
ctaText: data.cta_text,
|
||||
heading: data.heading,
|
||||
open_in_new_tab: !!data.open_in_new_tab,
|
||||
scripted_title: data.scripted_title,
|
||||
openInNewTab: !!data.open_in_new_tab,
|
||||
scriptedTopTitle: data.scripted_title,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -25,10 +25,13 @@ export const linkConnectionSchema = z
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.linkConnection.edges.length) {
|
||||
const link = pageLinks.transform(data.linkConnection.edges[0].node)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
const linkNode = data.linkConnection.edges[0].node
|
||||
if (linkNode) {
|
||||
const link = pageLinks.transform(linkNode)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,17 +57,20 @@ export const linkConnectionRefs = z
|
||||
linkConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: linkRefsUnionSchema,
|
||||
node: discriminatedUnion(linkRefsUnionSchema.options),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.linkConnection.edges.length) {
|
||||
const link = pageLinks.transformRef(data.linkConnection.edges[0].node)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
const linkNode = data.linkConnection.edges[0].node
|
||||
if (linkNode) {
|
||||
const link = pageLinks.transformRef(linkNode)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ export const getHotelInputSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const getAvailabilityInputSchema = z.object({
|
||||
export const getHotelsAvailabilityInputSchema = z.object({
|
||||
cityId: z.string(),
|
||||
roomStayStartDate: z.string(),
|
||||
roomStayEndDate: z.string(),
|
||||
|
||||
@@ -164,6 +164,16 @@ const detailedFacilitySchema = z.object({
|
||||
filter: z.string().optional(),
|
||||
})
|
||||
|
||||
export const facilitySchema = z.object({
|
||||
headingText: z.string(),
|
||||
heroImages: z.array(
|
||||
z.object({
|
||||
metaData: imageMetaDataSchema,
|
||||
imageSizes: imageSizesSchema,
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
const healthFacilitySchema = z.object({
|
||||
type: z.string(),
|
||||
content: z.object({
|
||||
@@ -436,6 +446,22 @@ export const roomSchema = z.object({
|
||||
type: z.enum(["roomcategories"]),
|
||||
})
|
||||
|
||||
const merchantInformationSchema = z.object({
|
||||
webMerchantId: z.string(),
|
||||
cards: z.record(z.string(), z.boolean()).transform((val) => {
|
||||
return Object.entries(val)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([key]) => key)
|
||||
}),
|
||||
alternatePaymentOptions: z
|
||||
.record(z.string(), z.boolean())
|
||||
.transform((val) => {
|
||||
return Object.entries(val)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([key]) => key)
|
||||
}),
|
||||
})
|
||||
|
||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||
export const getHotelDataSchema = z.object({
|
||||
data: z.object({
|
||||
@@ -471,6 +497,7 @@ export const getHotelDataSchema = z.object({
|
||||
hotelContent: hotelContentSchema,
|
||||
detailedFacilities: z.array(detailedFacilitySchema),
|
||||
healthFacilities: z.array(healthFacilitySchema),
|
||||
merchantInformationData: merchantInformationSchema,
|
||||
rewardNight: rewardNightSchema,
|
||||
pointsOfInterest: z
|
||||
.array(pointOfInterestSchema)
|
||||
@@ -480,6 +507,9 @@ export const getHotelDataSchema = z.object({
|
||||
socialMedia: socialMediaSchema,
|
||||
meta: metaSchema.optional(),
|
||||
isActive: z.boolean(),
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
}),
|
||||
relationships: relationshipsSchema,
|
||||
}),
|
||||
@@ -495,26 +525,18 @@ const occupancySchema = z.object({
|
||||
|
||||
const bestPricePerStaySchema = z.object({
|
||||
currency: z.string(),
|
||||
amount: z.number(),
|
||||
regularAmount: z.number(),
|
||||
memberAmount: z.number(),
|
||||
discountRate: z.number(),
|
||||
discountAmount: z.number(),
|
||||
points: z.number(),
|
||||
numberOfVouchers: z.number(),
|
||||
numberOfBonusCheques: z.number(),
|
||||
// TODO: remove optional when API is ready
|
||||
regularAmount: z.string().optional(),
|
||||
// TODO: remove optional when API is ready
|
||||
memberAmount: z.string().optional(),
|
||||
})
|
||||
|
||||
const bestPricePerNightSchema = z.object({
|
||||
currency: z.string(),
|
||||
amount: z.number(),
|
||||
regularAmount: z.number(),
|
||||
memberAmount: z.number(),
|
||||
discountRate: z.number(),
|
||||
discountAmount: z.number(),
|
||||
points: z.number(),
|
||||
numberOfVouchers: z.number(),
|
||||
numberOfBonusCheques: z.number(),
|
||||
// TODO: remove optional when API is ready
|
||||
regularAmount: z.string().optional(),
|
||||
// TODO: remove optional when API is ready
|
||||
memberAmount: z.string().optional(),
|
||||
})
|
||||
|
||||
const linksSchema = z.object({
|
||||
@@ -526,7 +548,7 @@ const linksSchema = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
const availabilitySchema = z.object({
|
||||
const hotelsAvailabilitySchema = z.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
attributes: z.object({
|
||||
@@ -545,10 +567,10 @@ const availabilitySchema = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
export const getAvailabilitySchema = availabilitySchema
|
||||
export type Availability = z.infer<typeof availabilitySchema>
|
||||
export type AvailabilityPrices =
|
||||
Availability["data"][number]["attributes"]["bestPricePerNight"]
|
||||
export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema
|
||||
export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
|
||||
export type HotelsAvailabilityPrices =
|
||||
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
|
||||
|
||||
const flexibilityPrice = z.object({
|
||||
standard: z.number(),
|
||||
|
||||
@@ -20,14 +20,14 @@ import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { hotelPageSchema } from "../contentstack/hotelPage/output"
|
||||
import {
|
||||
getAvailabilityInputSchema,
|
||||
getHotelInputSchema,
|
||||
getHotelsAvailabilityInputSchema,
|
||||
getlHotelDataInputSchema,
|
||||
getRatesInputSchema,
|
||||
} from "./input"
|
||||
import {
|
||||
getAvailabilitySchema,
|
||||
getHotelDataSchema,
|
||||
getHotelsAvailabilitySchema,
|
||||
getRatesSchema,
|
||||
roomSchema,
|
||||
} from "./output"
|
||||
@@ -40,8 +40,10 @@ import {
|
||||
TWENTYFOUR_HOURS,
|
||||
} from "./utils"
|
||||
|
||||
import { FacilityEnum } from "@/types/components/hotelPage/facilities"
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type { Facility } from "@/types/hotel"
|
||||
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
|
||||
|
||||
const meter = metrics.getMeter("trpc.hotels")
|
||||
@@ -49,12 +51,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
||||
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
|
||||
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
|
||||
|
||||
const availabilityCounter = meter.createCounter("trpc.hotel.availability")
|
||||
const availabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability-success"
|
||||
const hotelsAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels"
|
||||
)
|
||||
const availabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability-fail"
|
||||
const hotelsAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels-success"
|
||||
)
|
||||
const hotelsAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels-fail"
|
||||
)
|
||||
|
||||
async function getContentstackData(
|
||||
@@ -173,7 +177,6 @@ export const hotelQueryRouter = router({
|
||||
const included = validatedHotelData.data.included || []
|
||||
|
||||
const hotelAttributes = validatedHotelData.data.data.attributes
|
||||
|
||||
const images = extractHotelImages(hotelAttributes)
|
||||
|
||||
const roomCategories = included
|
||||
@@ -212,6 +215,21 @@ export const hotelQueryRouter = router({
|
||||
? contentstackData?.content[0]
|
||||
: null
|
||||
|
||||
const facilities: Facility[] = [
|
||||
{
|
||||
...apiJson.data.attributes.restaurantImages,
|
||||
id: FacilityEnum.restaurant,
|
||||
},
|
||||
{
|
||||
...apiJson.data.attributes.conferencesAndMeetings,
|
||||
id: FacilityEnum.conference,
|
||||
},
|
||||
{
|
||||
...apiJson.data.attributes.healthAndWellness,
|
||||
id: FacilityEnum.wellness,
|
||||
},
|
||||
]
|
||||
|
||||
getHotelSuccessCounter.add(1, { hotelId, lang, include })
|
||||
console.info(
|
||||
"api.hotels.hotel success",
|
||||
@@ -230,11 +248,12 @@ export const hotelQueryRouter = router({
|
||||
pointsOfInterest: hotelAttributes.pointsOfInterest,
|
||||
roomCategories,
|
||||
activitiesCard: activities?.upcoming_activities_card,
|
||||
facilities,
|
||||
}
|
||||
}),
|
||||
availability: router({
|
||||
get: hotelServiceProcedure
|
||||
.input(getAvailabilityInputSchema)
|
||||
hotels: hotelServiceProcedure
|
||||
.input(getHotelsAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const {
|
||||
cityId,
|
||||
@@ -257,7 +276,7 @@ export const hotelQueryRouter = router({
|
||||
attachedProfileId,
|
||||
}
|
||||
|
||||
availabilityCounter.add(1, {
|
||||
hotelsAvailabilityCounter.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
@@ -267,11 +286,11 @@ export const hotelQueryRouter = router({
|
||||
reservationProfileType,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.availability start",
|
||||
"api.hotels.hotelsAvailability start",
|
||||
JSON.stringify({ query: { cityId, params } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
`${api.endpoints.v1.availability}/${cityId}`,
|
||||
`${api.endpoints.v1.hotelsAvailability}/${cityId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
@@ -281,7 +300,7 @@ export const hotelQueryRouter = router({
|
||||
)
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
availabilityFailCounter.add(1, {
|
||||
hotelsAvailabilityFailCounter.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
@@ -297,7 +316,7 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.availability error",
|
||||
"api.hotels.hotelsAvailability error",
|
||||
JSON.stringify({
|
||||
query: { cityId, params },
|
||||
error: {
|
||||
@@ -311,9 +330,9 @@ export const hotelQueryRouter = router({
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateAvailabilityData =
|
||||
getAvailabilitySchema.safeParse(apiJson)
|
||||
getHotelsAvailabilitySchema.safeParse(apiJson)
|
||||
if (!validateAvailabilityData.success) {
|
||||
availabilityFailCounter.add(1, {
|
||||
hotelsAvailabilityFailCounter.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
@@ -325,7 +344,7 @@ export const hotelQueryRouter = router({
|
||||
error: JSON.stringify(validateAvailabilityData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.availability validation error",
|
||||
"api.hotels.hotelsAvailability validation error",
|
||||
JSON.stringify({
|
||||
query: { cityId, params },
|
||||
error: validateAvailabilityData.error,
|
||||
@@ -333,7 +352,7 @@ export const hotelQueryRouter = router({
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
availabilitySuccessCounter.add(1, {
|
||||
hotelsAvailabilitySuccessCounter.add(1, {
|
||||
cityId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
@@ -343,7 +362,7 @@ export const hotelQueryRouter = router({
|
||||
reservationProfileType,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.availability success",
|
||||
"api.hotels.hotelsAvailability success",
|
||||
JSON.stringify({
|
||||
query: { cityId, params: params },
|
||||
})
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { initiateSaveCardSchema } from "@/server/routers/user/output"
|
||||
import {
|
||||
initiateSaveCardSchema,
|
||||
subscriberIdSchema,
|
||||
} from "@/server/routers/user/output"
|
||||
import { protectedProcedure, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
@@ -8,6 +14,17 @@ import {
|
||||
saveCreditCardInput,
|
||||
} from "./input"
|
||||
|
||||
const meter = metrics.getMeter("trpc.user")
|
||||
const generatePreferencesLinkCounter = meter.createCounter(
|
||||
"trpc.user.generatePreferencesLink"
|
||||
)
|
||||
const generatePreferencesLinkSuccessCounter = meter.createCounter(
|
||||
"trpc.user.generatePreferencesLink-success"
|
||||
)
|
||||
const generatePreferencesLinkFailCounter = meter.createCounter(
|
||||
"trpc.user.generatePreferencesLink-fail"
|
||||
)
|
||||
|
||||
export const userMutationRouter = router({
|
||||
creditCard: router({
|
||||
add: protectedProcedure.input(addCreditCardInput).mutation(async function ({
|
||||
@@ -128,4 +145,62 @@ export const userMutationRouter = router({
|
||||
return true
|
||||
}),
|
||||
}),
|
||||
generatePreferencesLink: protectedProcedure.mutation(async function ({
|
||||
ctx,
|
||||
}) {
|
||||
generatePreferencesLinkCounter.add(1)
|
||||
const apiResponse = await api.get(api.endpoints.v1.subscriberId, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
generatePreferencesLinkFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.user.subscriberId error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
|
||||
const validatedData = subscriberIdSchema.safeParse(data)
|
||||
|
||||
if (!validatedData.success) {
|
||||
generatePreferencesLinkSuccessCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.user.generatePreferencesLink validation error",
|
||||
JSON.stringify({
|
||||
error: validatedData.error,
|
||||
})
|
||||
)
|
||||
console.error(validatedData.error.format())
|
||||
|
||||
return null
|
||||
}
|
||||
const preferencesLink = new URL(env.SALESFORCE_PREFERENCE_BASE_URL)
|
||||
preferencesLink.searchParams.set("subKey", validatedData.data.subscriberId)
|
||||
|
||||
generatePreferencesLinkSuccessCounter.add(1)
|
||||
return preferencesLink.toString()
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -234,3 +234,7 @@ export const initiateSaveCardSchema = z.object({
|
||||
type: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const subscriberIdSchema = z.object({
|
||||
subscriberId: z.string(),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { SafeParseSuccess } from "zod"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import {
|
||||
@@ -27,8 +28,8 @@ import type {
|
||||
LoginType,
|
||||
TrackingSDKUserData,
|
||||
} from "@/types/components/tracking"
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
import { Transactions } from "@/types/enums/transactions"
|
||||
import { User } from "@/types/user"
|
||||
import type { MembershipLevel } from "@/constants/membershipLevels"
|
||||
|
||||
// OpenTelemetry metrics: User
|
||||
@@ -161,6 +162,51 @@ export async function getVerifiedUser({ session }: { session: Session }) {
|
||||
return verifiedData
|
||||
}
|
||||
|
||||
function parsedUser(data: User, isMFA: boolean) {
|
||||
const country = countries.find((c) => c.code === data.address.countryCode)
|
||||
|
||||
const user = {
|
||||
address: {
|
||||
city: data.address.city,
|
||||
country: country?.name ?? "",
|
||||
countryCode: data.address.countryCode,
|
||||
streetAddress: data.address.streetAddress,
|
||||
zipCode: data.address.zipCode,
|
||||
},
|
||||
dateOfBirth: data.dateOfBirth,
|
||||
email: data.email,
|
||||
firstName: data.firstName,
|
||||
language: data.language,
|
||||
lastName: data.lastName,
|
||||
membership: getMembership(data.memberships),
|
||||
memberships: data.memberships,
|
||||
name: `${data.firstName} ${data.lastName}`,
|
||||
phoneNumber: data.phoneNumber,
|
||||
profileId: data.profileId,
|
||||
}
|
||||
|
||||
if (!isMFA) {
|
||||
if (user.address.city) {
|
||||
user.address.city = maskValue.text(user.address.city)
|
||||
}
|
||||
if (user.address.streetAddress) {
|
||||
user.address.streetAddress = maskValue.text(user.address.streetAddress)
|
||||
}
|
||||
|
||||
user.address.zipCode = data.address?.zipCode
|
||||
? maskValue.text(data.address.zipCode)
|
||||
: ""
|
||||
|
||||
user.dateOfBirth = maskValue.all(user.dateOfBirth)
|
||||
|
||||
user.email = maskValue.email(user.email)
|
||||
|
||||
user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : ""
|
||||
}
|
||||
|
||||
return user
|
||||
}
|
||||
|
||||
export const userQueryRouter = router({
|
||||
get: protectedProcedure
|
||||
.use(async function (opts) {
|
||||
@@ -184,57 +230,25 @@ export const userQueryRouter = router({
|
||||
return data
|
||||
}
|
||||
|
||||
const verifiedData = data
|
||||
|
||||
const country = countries.find(
|
||||
(c) => c.code === verifiedData.data.address.countryCode
|
||||
)
|
||||
|
||||
const user = {
|
||||
address: {
|
||||
city: verifiedData.data.address.city,
|
||||
country: country?.name ?? "",
|
||||
countryCode: verifiedData.data.address.countryCode,
|
||||
streetAddress: verifiedData.data.address.streetAddress,
|
||||
zipCode: verifiedData.data.address.zipCode,
|
||||
},
|
||||
dateOfBirth: verifiedData.data.dateOfBirth,
|
||||
email: verifiedData.data.email,
|
||||
firstName: verifiedData.data.firstName,
|
||||
language: verifiedData.data.language,
|
||||
lastName: verifiedData.data.lastName,
|
||||
membership: getMembership(verifiedData.data.memberships),
|
||||
memberships: verifiedData.data.memberships,
|
||||
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
|
||||
phoneNumber: verifiedData.data.phoneNumber,
|
||||
profileId: verifiedData.data.profileId,
|
||||
}
|
||||
|
||||
if (!ctx.isMFA) {
|
||||
if (user.address.city) {
|
||||
user.address.city = maskValue.text(user.address.city)
|
||||
}
|
||||
if (user.address.streetAddress) {
|
||||
user.address.streetAddress = maskValue.text(
|
||||
user.address.streetAddress
|
||||
)
|
||||
}
|
||||
|
||||
user.address.zipCode = verifiedData.data.address?.zipCode
|
||||
? maskValue.text(verifiedData.data.address.zipCode)
|
||||
: ""
|
||||
|
||||
user.dateOfBirth = maskValue.all(user.dateOfBirth)
|
||||
|
||||
user.email = maskValue.email(user.email)
|
||||
|
||||
user.phoneNumber = user.phoneNumber
|
||||
? maskValue.phone(user.phoneNumber)
|
||||
: ""
|
||||
}
|
||||
|
||||
return user
|
||||
return parsedUser(data.data, ctx.isMFA)
|
||||
}),
|
||||
getSafely: safeProtectedProcedure.query(async function getUser({ ctx }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await getVerifiedUser({ session: ctx.session })
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
if ("error" in data) {
|
||||
return data
|
||||
}
|
||||
|
||||
return parsedUser(data.data, true)
|
||||
}),
|
||||
name: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
|
||||
@@ -121,29 +121,24 @@ export const safeProtectedProcedure = t.procedure.use(async function (opts) {
|
||||
})
|
||||
})
|
||||
|
||||
export const profileServiceProcedure = t.procedure.use(async (opts) => {
|
||||
const { access_token } = await fetchServiceToken(["profile"])
|
||||
if (!access_token) {
|
||||
throw internalServerError("Failed to obtain profile service token")
|
||||
}
|
||||
return opts.next({
|
||||
ctx: {
|
||||
serviceToken: access_token,
|
||||
},
|
||||
function createServiceProcedure(serviceName: string) {
|
||||
return t.procedure.use(async (opts) => {
|
||||
const { access_token } = await fetchServiceToken([serviceName])
|
||||
if (!access_token) {
|
||||
throw internalServerError(`Failed to obtain ${serviceName} service token`)
|
||||
}
|
||||
return opts.next({
|
||||
ctx: {
|
||||
serviceToken: access_token,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const bookingServiceProcedure = createServiceProcedure("booking")
|
||||
export const hotelServiceProcedure = createServiceProcedure("hotel")
|
||||
export const profileServiceProcedure = createServiceProcedure("profile")
|
||||
|
||||
export const hotelServiceProcedure = t.procedure.use(async (opts) => {
|
||||
const { access_token } = await fetchServiceToken(["hotel"])
|
||||
if (!access_token) {
|
||||
throw internalServerError("Failed to obtain hotel service token")
|
||||
}
|
||||
return opts.next({
|
||||
ctx: {
|
||||
serviceToken: access_token,
|
||||
},
|
||||
})
|
||||
})
|
||||
export const serverActionProcedure = t.procedure.experimental_caller(
|
||||
experimental_nextAppDirCaller({
|
||||
createContext,
|
||||
|
||||
Reference in New Issue
Block a user