Merge master
This commit is contained in:
@@ -83,6 +83,10 @@ export const createBookingInput = z.object({
|
||||
payment: paymentSchema,
|
||||
})
|
||||
|
||||
export const priceChangeInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
})
|
||||
|
||||
// Query
|
||||
const confirmationNumberInput = z.object({
|
||||
confirmationNumber: z.string(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc"
|
||||
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
import { createBookingInput } from "./input"
|
||||
import { createBookingInput, priceChangeInput } from "./input"
|
||||
import { createBookingSchema } from "./output"
|
||||
|
||||
import type { Session } from "next-auth"
|
||||
@@ -20,6 +20,14 @@ const createBookingFailCounter = meter.createCounter(
|
||||
"trpc.bookings.create-fail"
|
||||
)
|
||||
|
||||
const priceChangeCounter = meter.createCounter("trpc.bookings.price-change")
|
||||
const priceChangeSuccessCounter = meter.createCounter(
|
||||
"trpc.bookings.price-change-success"
|
||||
)
|
||||
const priceChangeFailCounter = meter.createCounter(
|
||||
"trpc.bookings.price-change-fail"
|
||||
)
|
||||
|
||||
async function getMembershipNumber(
|
||||
session: Session | null
|
||||
): Promise<string | undefined> {
|
||||
@@ -122,6 +130,71 @@ export const bookingMutationRouter = router({
|
||||
query: loggingAttributes,
|
||||
})
|
||||
)
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
priceChange: safeProtectedServiceProcedure
|
||||
.input(priceChangeInput)
|
||||
.mutation(async function ({ ctx, input }) {
|
||||
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
|
||||
const { confirmationNumber } = input
|
||||
|
||||
priceChangeCounter.add(1, { confirmationNumber })
|
||||
|
||||
const headers = {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
}
|
||||
|
||||
const apiResponse = await api.put(
|
||||
api.endpoints.v1.Booking.priceChange(confirmationNumber),
|
||||
{
|
||||
headers,
|
||||
body: input,
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
priceChangeFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.booking.priceChange error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
error: text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedData = createBookingSchema.safeParse(apiJson)
|
||||
if (!verifiedData.success) {
|
||||
priceChangeFailCounter.add(1, {
|
||||
confirmationNumber,
|
||||
error_type: "validation_error",
|
||||
})
|
||||
console.error(
|
||||
"api.booking.priceChange validation error",
|
||||
JSON.stringify({
|
||||
query: { confirmationNumber },
|
||||
error: verifiedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
priceChangeSuccessCounter.add(1, { confirmationNumber })
|
||||
|
||||
return verifiedData.data
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -21,8 +21,8 @@ export const createBookingSchema = z
|
||||
errorMessage: z.string().nullable().optional(),
|
||||
priceChangedMetadata: z
|
||||
.object({
|
||||
roomPrice: z.number().nullable().optional(),
|
||||
totalPrice: z.number().nullable().optional(),
|
||||
roomPrice: z.number(),
|
||||
totalPrice: z.number(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
@@ -54,19 +54,20 @@ export const createBookingSchema = z
|
||||
|
||||
// QUERY
|
||||
const extraBedTypesSchema = z.object({
|
||||
quantity: z.number(),
|
||||
bedType: z.nativeEnum(ChildBedTypeEnum),
|
||||
quantity: z.number().int(),
|
||||
})
|
||||
|
||||
const guestSchema = z.object({
|
||||
email: z.string().email().nullable().default(""),
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
firstName: z.string().nullable().default(""),
|
||||
lastName: z.string().nullable().default(""),
|
||||
membershipNumber: z.string().nullable().default(""),
|
||||
phoneNumber: phoneValidator().nullable().default(""),
|
||||
})
|
||||
|
||||
const packageSchema = z.object({
|
||||
code: z.string().default(""),
|
||||
code: z.string().nullable().default(""),
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
quantity: z.number().int(),
|
||||
totalPrice: z.number(),
|
||||
@@ -74,35 +75,37 @@ const packageSchema = z.object({
|
||||
unitPrice: z.number(),
|
||||
})
|
||||
|
||||
const rateDefinitionSchema = z.object({
|
||||
breakfastIncluded: z.boolean().default(false),
|
||||
cancellationRule: z.string().nullable().default(""),
|
||||
cancellationText: z.string().nullable().default(""),
|
||||
generalTerms: z.array(z.string()).default([]),
|
||||
isMemberRate: z.boolean().default(false),
|
||||
mustBeGuaranteed: z.boolean().default(false),
|
||||
rateCode: z.string().nullable().default(""),
|
||||
title: z.string().nullable().default(""),
|
||||
})
|
||||
|
||||
export const bookingConfirmationSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
adults: z.number(),
|
||||
adults: z.number().int(),
|
||||
checkInDate: z.date({ coerce: true }),
|
||||
checkOutDate: z.date({ coerce: true }),
|
||||
createDateTime: z.date({ coerce: true }),
|
||||
childrenAges: z.array(z.number()),
|
||||
childrenAges: z.array(z.number().int()).default([]),
|
||||
extraBedTypes: z.array(extraBedTypesSchema).default([]),
|
||||
computedReservationStatus: z.string(),
|
||||
confirmationNumber: z.string(),
|
||||
computedReservationStatus: z.string().nullable().default(""),
|
||||
confirmationNumber: z.string().nullable().default(""),
|
||||
currencyCode: z.nativeEnum(CurrencyEnum),
|
||||
guest: guestSchema,
|
||||
hotelId: z.string(),
|
||||
packages: z.array(packageSchema),
|
||||
rateDefinition: z.object({
|
||||
rateCode: z.string(),
|
||||
title: z.string().nullable(),
|
||||
breakfastIncluded: z.boolean(),
|
||||
isMemberRate: z.boolean(),
|
||||
generalTerms: z.array(z.string()).optional(),
|
||||
cancellationRule: z.string().optional(),
|
||||
cancellationText: z.string().optional(),
|
||||
mustBeGuaranteed: z.boolean(),
|
||||
}),
|
||||
reservationStatus: z.string(),
|
||||
roomPrice: z.number().int(),
|
||||
roomTypeCode: z.string(),
|
||||
packages: z.array(packageSchema).default([]),
|
||||
rateDefinition: rateDefinitionSchema,
|
||||
reservationStatus: z.string().nullable().default(""),
|
||||
roomPrice: z.number(),
|
||||
roomTypeCode: z.string().nullable().default(""),
|
||||
totalPrice: z.number(),
|
||||
totalPriceExVat: z.number(),
|
||||
vatAmount: z.number(),
|
||||
|
||||
@@ -422,6 +422,7 @@ export const baseQueryRouter = router({
|
||||
locale: input.lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(input.lang, currentFooterUID)],
|
||||
},
|
||||
|
||||
@@ -70,6 +70,7 @@ export const bookingwidgetQueryRouter = router({
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid, bookingwidgetAffix)],
|
||||
},
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { hotelAttributesSchema } from "../../hotels/output"
|
||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||
import { getDescription, getImages, getTitle } from "./utils"
|
||||
import { getDescription, getImage, getTitle } from "./utils"
|
||||
|
||||
import type { Metadata } from "next"
|
||||
|
||||
@@ -71,16 +72,21 @@ export const rawMetadataSchema = z.object({
|
||||
.nullable(),
|
||||
hero_image: tempImageVaultAssetSchema.nullable(),
|
||||
blocks: metaDataBlocksSchema,
|
||||
hotel_page_id: z.string().optional().nullable(),
|
||||
hotelData: hotelAttributesSchema
|
||||
.pick({ name: true, address: true, hotelContent: true, gallery: true })
|
||||
.optional()
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
export const metadataSchema = rawMetadataSchema.transform((data) => {
|
||||
export const metadataSchema = rawMetadataSchema.transform(async (data) => {
|
||||
const noIndex = !!data.web?.seo_metadata?.noindex
|
||||
|
||||
const metadata: Metadata = {
|
||||
title: getTitle(data),
|
||||
title: await getTitle(data),
|
||||
description: getDescription(data),
|
||||
openGraph: {
|
||||
images: getImages(data),
|
||||
images: getImage(data),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,15 @@ import { cache } from "react"
|
||||
import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
|
||||
import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
|
||||
import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
|
||||
import { GetHotelPageMetadata } from "@/lib/graphql/Query/HotelPage/Metadata.graphql"
|
||||
import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
|
||||
import { getHotelData } from "../../hotels/query"
|
||||
import { metadataSchema } from "./output"
|
||||
import { affix } from "./utils"
|
||||
|
||||
@@ -86,10 +88,10 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
|
||||
return response.data
|
||||
})
|
||||
|
||||
function getTransformedMetadata(data: unknown) {
|
||||
async function getTransformedMetadata(data: unknown) {
|
||||
transformMetadataCounter.add(1)
|
||||
console.info("contentstack.metadata transform start")
|
||||
const validatedMetadata = metadataSchema.safeParse(data)
|
||||
const validatedMetadata = await metadataSchema.safeParseAsync(data)
|
||||
|
||||
if (!validatedMetadata.success) {
|
||||
transformMetadataFailCounter.add(1, {
|
||||
@@ -112,7 +114,7 @@ function getTransformedMetadata(data: unknown) {
|
||||
}
|
||||
|
||||
export const metadataQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
|
||||
const variables = {
|
||||
lang: ctx.lang,
|
||||
uid: ctx.uid,
|
||||
@@ -139,6 +141,22 @@ export const metadataQueryRouter = router({
|
||||
loyalty_page: RawMetadataSchema
|
||||
}>(GetLoyaltyPageMetadata, variables)
|
||||
return getTransformedMetadata(loyaltyPageResponse.loyalty_page)
|
||||
case PageTypeEnum.hotelPage:
|
||||
const hotelPageResponse = await fetchMetadata<{
|
||||
hotel_page: RawMetadataSchema
|
||||
}>(GetHotelPageMetadata, variables)
|
||||
const hotelPageData = hotelPageResponse.hotel_page
|
||||
const hotelData = hotelPageData.hotel_page_id
|
||||
? await getHotelData(
|
||||
{ hotelId: hotelPageData.hotel_page_id, language: ctx.lang },
|
||||
ctx.serviceToken
|
||||
)
|
||||
: null
|
||||
|
||||
return getTransformedMetadata({
|
||||
...hotelPageData,
|
||||
hotelData: hotelData?.data.attributes,
|
||||
})
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
import type { RawMetadataSchema } from "@/types/trpc/routers/contentstack/metadata"
|
||||
|
||||
@@ -58,11 +60,21 @@ function truncateTextAfterLastPeriod(
|
||||
return `${maxLengthText}...`
|
||||
}
|
||||
|
||||
export function getTitle(data: RawMetadataSchema) {
|
||||
export async function getTitle(data: RawMetadataSchema) {
|
||||
const intl = await getIntl()
|
||||
const metadata = data.web?.seo_metadata
|
||||
if (metadata?.title) {
|
||||
return metadata.title
|
||||
}
|
||||
if (data.hotelData) {
|
||||
return intl.formatMessage(
|
||||
{ id: "Stay at HOTEL_NAME | Hotel in DESTINATION" },
|
||||
{
|
||||
hotelName: data.hotelData.name,
|
||||
destination: data.hotelData.address.city,
|
||||
}
|
||||
)
|
||||
}
|
||||
if (data.web?.breadcrumbs?.title) {
|
||||
return data.web.breadcrumbs.title
|
||||
}
|
||||
@@ -80,6 +92,9 @@ export function getDescription(data: RawMetadataSchema) {
|
||||
if (metadata?.description) {
|
||||
return metadata.description
|
||||
}
|
||||
if (data.hotelData) {
|
||||
return data.hotelData.hotelContent.texts.descriptions.short
|
||||
}
|
||||
if (data.preamble) {
|
||||
return truncateTextAfterLastPeriod(data.preamble)
|
||||
}
|
||||
@@ -102,28 +117,35 @@ export function getDescription(data: RawMetadataSchema) {
|
||||
return ""
|
||||
}
|
||||
|
||||
export function getImages(data: RawMetadataSchema) {
|
||||
export function getImage(data: RawMetadataSchema) {
|
||||
const metadataImage = data.web?.seo_metadata?.seo_image
|
||||
const heroImage = data.hero_image
|
||||
const hotelImage =
|
||||
data.hotelData?.gallery?.heroImages?.[0] ||
|
||||
data.hotelData?.gallery?.smallerImages?.[0]
|
||||
|
||||
// Currently we don't have the possibility to get smaller images from ImageVault (2024-11-15)
|
||||
if (metadataImage) {
|
||||
return [
|
||||
{
|
||||
url: metadataImage.url,
|
||||
width: metadataImage.dimensions.width,
|
||||
height: metadataImage.dimensions.height,
|
||||
},
|
||||
]
|
||||
return {
|
||||
url: metadataImage.url,
|
||||
alt: metadataImage.meta.alt || undefined,
|
||||
width: metadataImage.dimensions.width,
|
||||
height: metadataImage.dimensions.height,
|
||||
}
|
||||
}
|
||||
if (hotelImage) {
|
||||
return {
|
||||
url: hotelImage.imageSizes.small,
|
||||
alt: hotelImage.metaData.altText || undefined,
|
||||
}
|
||||
}
|
||||
if (heroImage) {
|
||||
return [
|
||||
{
|
||||
url: heroImage.url,
|
||||
width: heroImage.dimensions.width,
|
||||
height: heroImage.dimensions.height,
|
||||
},
|
||||
]
|
||||
return {
|
||||
url: heroImage.url,
|
||||
alt: heroImage.meta.alt || undefined,
|
||||
width: heroImage.dimensions.width,
|
||||
height: heroImage.dimensions.height,
|
||||
}
|
||||
}
|
||||
return []
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -131,13 +131,15 @@ export const rewardQueryRouter = router({
|
||||
.map((reward) => reward?.rewardId)
|
||||
.filter((id): id is string => Boolean(id))
|
||||
|
||||
const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||
const [contentStackRewards, loyaltyLevelsConfig] = await Promise.all([
|
||||
getCmsRewards(ctx.lang, rewardIds),
|
||||
getLoyaltyLevel(ctx, input.level_id),
|
||||
])
|
||||
|
||||
if (!contentStackRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
|
||||
|
||||
const levelsWithRewards = apiRewards
|
||||
.map((reward) => {
|
||||
const contentStackReward = contentStackRewards.find((r) => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export const tableSchema = z.object({
|
||||
data: z.array(z.object({}).catchall(z.string())),
|
||||
skipReset: z.boolean(),
|
||||
tableActionEnabled: z.boolean(),
|
||||
headerRowAdded: z.boolean(),
|
||||
headerRowAdded: z.boolean().optional().default(false),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -76,4 +76,7 @@ export const getRoomPackagesInputSchema = z.object({
|
||||
})
|
||||
export const getCityCoordinatesInputSchema = z.object({
|
||||
city: z.string(),
|
||||
hotel: z.object({
|
||||
address: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { ChildBedTypeEnum } from "@/constants/booking"
|
||||
import { ChildBedTypeEnum, PaymentMethodEnum } from "@/constants/booking"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { toLang } from "@/server/utils"
|
||||
|
||||
@@ -369,6 +369,7 @@ const merchantInformationSchema = z.object({
|
||||
return Object.entries(val)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([key]) => key)
|
||||
.filter((key): key is PaymentMethodEnum => !!key)
|
||||
}),
|
||||
alternatePaymentOptions: z
|
||||
.record(z.string(), z.boolean())
|
||||
@@ -376,6 +377,7 @@ const merchantInformationSchema = z.object({
|
||||
return Object.entries(val)
|
||||
.filter(([_, enabled]) => enabled)
|
||||
.map(([key]) => key)
|
||||
.filter((key): key is PaymentMethodEnum => !!key)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -422,6 +424,47 @@ const hotelFactsSchema = z.object({
|
||||
yearBuilt: z.string(),
|
||||
})
|
||||
|
||||
export const hotelAttributesSchema = z.object({
|
||||
accessibilityElevatorPitchText: z.string().optional(),
|
||||
address: addressSchema,
|
||||
cityId: z.string(),
|
||||
cityName: z.string(),
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
contactInformation: contactInformationSchema,
|
||||
detailedFacilities: z
|
||||
.array(detailedFacilitySchema)
|
||||
.transform((facilities) =>
|
||||
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
),
|
||||
gallery: gallerySchema.optional(),
|
||||
galleryImages: z.array(imageSchema).optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
healthFacilities: z.array(healthFacilitySchema),
|
||||
hotelContent: hotelContentSchema,
|
||||
hotelFacts: hotelFactsSchema,
|
||||
hotelRoomElevatorPitchText: z.string().optional(),
|
||||
hotelType: z.string().optional(),
|
||||
isActive: z.boolean(),
|
||||
isPublished: z.boolean(),
|
||||
keywords: z.array(z.string()),
|
||||
location: locationSchema,
|
||||
merchantInformationData: merchantInformationSchema,
|
||||
name: z.string(),
|
||||
operaId: z.string(),
|
||||
parking: z.array(parkingSchema),
|
||||
pointsOfInterest: z
|
||||
.array(pointOfInterestSchema)
|
||||
.transform((pois) =>
|
||||
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
|
||||
),
|
||||
ratings: ratingsSchema,
|
||||
rewardNight: rewardNightSchema,
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
socialMedia: socialMediaSchema,
|
||||
specialAlerts: specialAlertsSchema,
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
})
|
||||
|
||||
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
|
||||
export const getHotelDataSchema = z.object({
|
||||
data: z.object({
|
||||
@@ -434,46 +477,7 @@ export const getHotelDataSchema = z.object({
|
||||
}
|
||||
return lang
|
||||
}),
|
||||
attributes: z.object({
|
||||
accessibilityElevatorPitchText: z.string().optional(),
|
||||
address: addressSchema,
|
||||
cityId: z.string(),
|
||||
cityName: z.string(),
|
||||
conferencesAndMeetings: facilitySchema.optional(),
|
||||
contactInformation: contactInformationSchema,
|
||||
detailedFacilities: z
|
||||
.array(detailedFacilitySchema)
|
||||
.transform((facilities) =>
|
||||
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
),
|
||||
gallery: gallerySchema.optional(),
|
||||
galleryImages: z.array(imageSchema).optional(),
|
||||
healthAndWellness: facilitySchema.optional(),
|
||||
healthFacilities: z.array(healthFacilitySchema),
|
||||
hotelContent: hotelContentSchema,
|
||||
hotelFacts: hotelFactsSchema,
|
||||
hotelRoomElevatorPitchText: z.string().optional(),
|
||||
hotelType: z.string().optional(),
|
||||
isActive: z.boolean(),
|
||||
isPublished: z.boolean(),
|
||||
keywords: z.array(z.string()),
|
||||
location: locationSchema,
|
||||
merchantInformationData: merchantInformationSchema,
|
||||
name: z.string(),
|
||||
operaId: z.string(),
|
||||
parking: z.array(parkingSchema),
|
||||
pointsOfInterest: z
|
||||
.array(pointOfInterestSchema)
|
||||
.transform((pois) =>
|
||||
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
|
||||
),
|
||||
ratings: ratingsSchema,
|
||||
rewardNight: rewardNightSchema,
|
||||
restaurantImages: facilitySchema.optional(),
|
||||
socialMedia: socialMediaSchema,
|
||||
specialAlerts: specialAlertsSchema,
|
||||
specialNeedGroups: z.array(specialNeedGroupSchema),
|
||||
}),
|
||||
attributes: hotelAttributesSchema,
|
||||
relationships: relationshipsSchema,
|
||||
}),
|
||||
// NOTE: We can pass an "include" param to the hotel API to retrieve
|
||||
|
||||
@@ -1,18 +1,9 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { cache } from "react"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import * as api from "@/lib/api"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { badRequestError } from "@/server/errors/trpc"
|
||||
import {
|
||||
badRequestError,
|
||||
notFound,
|
||||
serverErrorByStatus,
|
||||
} from "@/server/errors/trpc"
|
||||
import {
|
||||
contentStackUidWithServiceProcedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
safeProtectedServiceProcedure,
|
||||
@@ -20,13 +11,8 @@ import {
|
||||
} from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { hotelPageSchema } from "../contentstack/hotelPage/output"
|
||||
import {
|
||||
fetchHotelPageRefs,
|
||||
generatePageTags,
|
||||
getHotelPageCounter,
|
||||
validateHotelPageRefs,
|
||||
} from "../contentstack/hotelPage/utils"
|
||||
import { cache } from "@/utils/cache"
|
||||
|
||||
import { getVerifiedUser, parsedUser } from "../user/query"
|
||||
import {
|
||||
getBreakfastPackageInputSchema,
|
||||
@@ -55,14 +41,10 @@ import {
|
||||
TWENTYFOUR_HOURS,
|
||||
} from "./utils"
|
||||
|
||||
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
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")
|
||||
const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
||||
@@ -115,55 +97,6 @@ const breakfastPackagesFailCounter = meter.createCounter(
|
||||
"trpc.package.breakfast-fail"
|
||||
)
|
||||
|
||||
async function getContentstackData(lang: Lang, uid?: string | null) {
|
||||
if (!uid) {
|
||||
return null
|
||||
}
|
||||
const contentPageRefsData = await fetchHotelPageRefs(lang, uid)
|
||||
const contentPageRefs = validateHotelPageRefs(contentPageRefsData, lang, uid)
|
||||
if (!contentPageRefs) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tags = generatePageTags(contentPageRefs, lang)
|
||||
|
||||
getHotelPageCounter.add(1, { lang, uid })
|
||||
console.info(
|
||||
"contentstack.hotelPage start",
|
||||
JSON.stringify({
|
||||
query: { lang, uid },
|
||||
})
|
||||
)
|
||||
const response = await request<GetHotelPageData>(
|
||||
GetHotelPage,
|
||||
{
|
||||
locale: lang,
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
throw notFound(response)
|
||||
}
|
||||
|
||||
const hotelPageData = hotelPageSchema.safeParse(response.data)
|
||||
if (!hotelPageData.success) {
|
||||
console.error(
|
||||
`Failed to validate Hotel Page - (uid: ${uid}, lang: ${lang})`
|
||||
)
|
||||
console.error(hotelPageData.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return hotelPageData.data.hotel_page
|
||||
}
|
||||
|
||||
export const getHotelData = cache(
|
||||
async (input: HotelDataInput, serviceToken: string) => {
|
||||
const { hotelId, language, isCardOnlyPayment } = input
|
||||
@@ -277,90 +210,6 @@ export const getHotelData = cache(
|
||||
)
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
|
||||
const { lang, uid } = ctx
|
||||
|
||||
const contentstackData = await getContentstackData(lang, uid)
|
||||
const hotelId = contentstackData?.hotel_page_id
|
||||
|
||||
if (!hotelId) {
|
||||
throw notFound(`Hotel not found for uid: ${uid}`)
|
||||
}
|
||||
|
||||
const hotelData = await getHotelData(
|
||||
{
|
||||
hotelId,
|
||||
language: ctx.lang,
|
||||
},
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
if (!hotelData) {
|
||||
throw notFound()
|
||||
}
|
||||
|
||||
const included = hotelData.included || []
|
||||
|
||||
const hotelAttributes = hotelData.data.attributes
|
||||
const images = hotelAttributes.gallery?.smallerImages
|
||||
const hotelAlerts = hotelAttributes.specialAlerts
|
||||
|
||||
const roomCategories = included
|
||||
? included.filter((item) => item.type === "roomcategories")
|
||||
: []
|
||||
|
||||
const activities = contentstackData?.content
|
||||
? contentstackData?.content[0]
|
||||
: null
|
||||
|
||||
const facilities: Facility[] = [
|
||||
{
|
||||
...hotelData.data.attributes.restaurantImages,
|
||||
id: FacilityCardTypeEnum.restaurant,
|
||||
headingText:
|
||||
hotelData?.data.attributes.restaurantImages?.headingText ?? "",
|
||||
heroImages:
|
||||
hotelData?.data.attributes.restaurantImages?.heroImages ?? [],
|
||||
},
|
||||
{
|
||||
...hotelData.data.attributes.conferencesAndMeetings,
|
||||
id: FacilityCardTypeEnum.conference,
|
||||
headingText:
|
||||
hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "",
|
||||
heroImages:
|
||||
hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [],
|
||||
},
|
||||
{
|
||||
...hotelData.data.attributes.healthAndWellness,
|
||||
id: FacilityCardTypeEnum.wellness,
|
||||
headingText:
|
||||
hotelData?.data.attributes.healthAndWellness?.headingText ?? "",
|
||||
heroImages:
|
||||
hotelData?.data.attributes.healthAndWellness?.heroImages ?? [],
|
||||
},
|
||||
]
|
||||
|
||||
return {
|
||||
hotelId,
|
||||
hotelName: hotelAttributes.name,
|
||||
hotelDescriptions: hotelAttributes.hotelContent.texts,
|
||||
hotelLocation: hotelAttributes.location,
|
||||
hotelAddress: hotelAttributes.address,
|
||||
hotelRatings: hotelAttributes.ratings,
|
||||
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
|
||||
hotelImages: images,
|
||||
pointsOfInterest: hotelAttributes.pointsOfInterest,
|
||||
roomCategories,
|
||||
activitiesCard: activities?.upcoming_activities_card,
|
||||
facilities,
|
||||
alerts: hotelAlerts,
|
||||
faq: contentstackData?.faq,
|
||||
healthFacilities: hotelAttributes.healthFacilities,
|
||||
contact: hotelAttributes.contactInformation,
|
||||
socials: hotelAttributes.socialMedia,
|
||||
ecoLabels: hotelAttributes.hotelFacts.ecoLabels,
|
||||
}
|
||||
}),
|
||||
availability: router({
|
||||
hotels: serviceProcedure
|
||||
.input(getHotelsAvailabilityInputSchema)
|
||||
@@ -1087,14 +936,37 @@ export const hotelQueryRouter = router({
|
||||
.input(getCityCoordinatesInputSchema)
|
||||
.query(async function ({ input }) {
|
||||
const apiKey = process.env.GOOGLE_STATIC_MAP_KEY
|
||||
const { city } = input
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(city)}&key=${apiKey}`
|
||||
const { city, hotel } = input
|
||||
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
const { lat, lng } = data.results[0].geometry.location
|
||||
async function fetchCoordinates(address: string) {
|
||||
const url = `https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${apiKey}`
|
||||
const response = await fetch(url)
|
||||
const data = await response.json()
|
||||
|
||||
return { lat, lng }
|
||||
if (data.status !== "OK") {
|
||||
console.error(`Geocode error: ${data.status}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const location = data.results[0]?.geometry?.location
|
||||
if (!location) {
|
||||
console.error("No location found in geocode response")
|
||||
return null
|
||||
}
|
||||
|
||||
return location
|
||||
}
|
||||
|
||||
let location = await fetchCoordinates(city)
|
||||
if (!location) {
|
||||
location = await fetchCoordinates(`${city}, ${hotel.address}`)
|
||||
}
|
||||
|
||||
if (!location) {
|
||||
throw new Error("Unable to fetch coordinates")
|
||||
}
|
||||
|
||||
return location
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -55,3 +55,11 @@ export const signupInput = signUpSchema
|
||||
streetAddress: "",
|
||||
},
|
||||
}))
|
||||
|
||||
export const getSavedPaymentCardsInput = z.object({
|
||||
supportedCards: z.array(z.string()),
|
||||
})
|
||||
|
||||
export type GetSavedPaymentCardsInput = z.input<
|
||||
typeof getSavedPaymentCardsInput
|
||||
>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import { passwordValidator } from "@/utils/passwordValidator"
|
||||
import { phoneValidator } from "@/utils/phoneValidator"
|
||||
import { getMembership } from "@/utils/user"
|
||||
|
||||
export const membershipSchema = z.object({
|
||||
|
||||
@@ -13,7 +13,11 @@ import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||
import * as maskValue from "@/utils/maskValue"
|
||||
import { getMembership, getMembershipCards } from "@/utils/user"
|
||||
|
||||
import { friendTransactionsInput, staysInput } from "./input"
|
||||
import {
|
||||
friendTransactionsInput,
|
||||
getSavedPaymentCardsInput,
|
||||
staysInput,
|
||||
} from "./input"
|
||||
import {
|
||||
creditCardsSchema,
|
||||
getFriendTransactionsSchema,
|
||||
@@ -154,6 +158,7 @@ export const getVerifiedUser = cache(
|
||||
"api.user.profile validation error",
|
||||
JSON.stringify({
|
||||
errors: verifiedData.error,
|
||||
apiResponse: apiJson,
|
||||
})
|
||||
)
|
||||
return null
|
||||
@@ -751,13 +756,26 @@ export const userQueryRouter = router({
|
||||
creditCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
return await getCreditCards({ session: ctx.session })
|
||||
}),
|
||||
safeCreditCards: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
safePaymentCards: safeProtectedProcedure
|
||||
.input(getSavedPaymentCardsInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
if (!ctx.session) {
|
||||
return null
|
||||
}
|
||||
|
||||
return await getCreditCards({ session: ctx.session, onlyNonExpired: true })
|
||||
}),
|
||||
const savedCards = await getCreditCards({
|
||||
session: ctx.session,
|
||||
onlyNonExpired: true,
|
||||
})
|
||||
|
||||
if (!savedCards) {
|
||||
return null
|
||||
}
|
||||
|
||||
return savedCards.filter((card) =>
|
||||
input.supportedCards.includes(card.type)
|
||||
)
|
||||
}),
|
||||
|
||||
membershipCards: protectedProcedure.query(async function ({ ctx }) {
|
||||
getProfileCounter.add(1)
|
||||
|
||||
@@ -71,11 +71,12 @@ async function fetchServiceToken(scopes: string[]) {
|
||||
|
||||
export async function getServiceToken() {
|
||||
let scopes: string[] = []
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
scopes = ["profile"]
|
||||
} else {
|
||||
if (env.ENABLE_BOOKING_FLOW) {
|
||||
scopes = ["profile", "hotel", "booking", "package"]
|
||||
} else {
|
||||
scopes = ["profile"]
|
||||
}
|
||||
|
||||
const tag = generateServiceTokenTag(scopes)
|
||||
const getCachedJwt = unstable_cache(
|
||||
async (scopes) => {
|
||||
|
||||
Reference in New Issue
Block a user