Merge branch 'develop'
This commit is contained in:
@@ -44,14 +44,18 @@ const childrenAgesSchema = z.object({
|
||||
const guestSchema = z.object({
|
||||
firstName: z.string(),
|
||||
lastName: z.string(),
|
||||
email: z.string().nullable(),
|
||||
phoneNumber: z.string().nullable(),
|
||||
})
|
||||
|
||||
const packagesSchema = z.object({
|
||||
accessibility: z.boolean(),
|
||||
allergyFriendly: z.boolean(),
|
||||
breakfast: z.boolean(),
|
||||
petFriendly: z.boolean(),
|
||||
})
|
||||
const packagesSchema = z.array(
|
||||
z.object({
|
||||
accessibility: z.boolean().optional(),
|
||||
allergyFriendly: z.boolean().optional(),
|
||||
breakfast: z.boolean().optional(),
|
||||
petFriendly: z.boolean().optional(),
|
||||
})
|
||||
)
|
||||
|
||||
export const bookingConfirmationSchema = z
|
||||
.object({
|
||||
@@ -66,7 +70,7 @@ export const bookingConfirmationSchema = z
|
||||
confirmationNumber: z.string(),
|
||||
currencyCode: z.string(),
|
||||
guest: guestSchema,
|
||||
hasPayRouting: z.boolean(),
|
||||
hasPayRouting: z.boolean().optional(),
|
||||
hotelId: z.string(),
|
||||
packages: packagesSchema,
|
||||
rateCode: z.string(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as api from "@/lib/api"
|
||||
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
|
||||
import { router, serviceProcedure } from "@/server/trpc"
|
||||
|
||||
import { getHotelData } from "../hotels/query"
|
||||
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
|
||||
import { bookingConfirmationSchema, createBookingSchema } from "./output"
|
||||
|
||||
@@ -81,6 +82,11 @@ export const bookingQueryRouter = router({
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const hotelData = await getHotelData(
|
||||
{ hotelId: booking.data.hotelId, language: ctx.lang },
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
getBookingConfirmationSuccessCounter.add(1, { confirmationNumber })
|
||||
console.info(
|
||||
"api.booking.confirmation success",
|
||||
@@ -91,6 +97,7 @@ export const bookingQueryRouter = router({
|
||||
|
||||
return {
|
||||
...booking.data,
|
||||
hotel: hotelData,
|
||||
temp: {
|
||||
breakfastFrom: "06:30",
|
||||
breakfastTo: "11:00",
|
||||
@@ -127,11 +134,6 @@ export const bookingQueryRouter = router({
|
||||
memberbershipNumber: "19822",
|
||||
phoneNumber: "+46702446688",
|
||||
},
|
||||
hotel: {
|
||||
email: "bookings@scandichotels.com",
|
||||
name: "Downtown Camper by Scandic",
|
||||
phoneNumber: "+4689001350",
|
||||
},
|
||||
}
|
||||
}),
|
||||
status: serviceProcedure.input(getBookingStatusInput).query(async function ({
|
||||
|
||||
@@ -816,6 +816,7 @@ const alertConnectionRefSchema = z.object({
|
||||
node: z.object({
|
||||
link: linkRefsSchema,
|
||||
sidepeek_content: sidepeekContentRefSchema,
|
||||
system: systemSchema,
|
||||
}),
|
||||
})
|
||||
),
|
||||
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
getAlertPhoneContactData,
|
||||
getConnections,
|
||||
getFooterConnections,
|
||||
getSiteConfigConnections,
|
||||
} from "./utils"
|
||||
|
||||
import type {
|
||||
@@ -630,7 +631,7 @@ export const baseQueryRouter = router({
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(lang, "siteConfig")],
|
||||
tags: [generateRefsResponseTag(lang, "site_config")],
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -676,6 +677,14 @@ export const baseQueryRouter = router({
|
||||
return null
|
||||
}
|
||||
|
||||
const connections = getSiteConfigConnections(validatedSiteConfigRef.data)
|
||||
const siteConfigUid = responseRef.data.all_site_config.items[0].system.uid
|
||||
|
||||
const tags = [
|
||||
generateTagsFromSystem(lang, connections),
|
||||
generateTag(lang, siteConfigUid),
|
||||
].flat()
|
||||
|
||||
getSiteConfigRefSuccessCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig.refs success",
|
||||
@@ -695,9 +704,7 @@ export const baseQueryRouter = router({
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [`${lang}:siteConfig`],
|
||||
},
|
||||
next: { tags },
|
||||
}
|
||||
),
|
||||
getContactConfig(lang),
|
||||
@@ -739,6 +746,7 @@ export const baseQueryRouter = router({
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getSiteConfigSuccessCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig success",
|
||||
|
||||
@@ -5,7 +5,10 @@ import type { System } from "@/types/requests/system"
|
||||
import type { Edges } from "@/types/requests/utils/edges"
|
||||
import type { NodeRefs } from "@/types/requests/utils/refs"
|
||||
import type { HeaderRefs } from "@/types/trpc/routers/contentstack/header"
|
||||
import type { Alert } from "@/types/trpc/routers/contentstack/siteConfig"
|
||||
import type {
|
||||
AlertOutput,
|
||||
GetSiteConfigRefData,
|
||||
} from "@/types/trpc/routers/contentstack/siteConfig"
|
||||
import type { ContactConfig } from "./output"
|
||||
|
||||
export function getConnections({ header }: HeaderRefs) {
|
||||
@@ -70,8 +73,31 @@ export function getFooterConnections(refs: FooterRefDataRaw) {
|
||||
return connections
|
||||
}
|
||||
|
||||
export function getSiteConfigConnections(refs: GetSiteConfigRefData) {
|
||||
const siteConfigData = refs.all_site_config.items[0]
|
||||
const connections: System["system"][] = []
|
||||
|
||||
const alertConnection = siteConfigData.sitewide_alert.alertConnection
|
||||
|
||||
alertConnection.edges.forEach(({ node }) => {
|
||||
connections.push(node.system)
|
||||
|
||||
const link = node.link.link
|
||||
if (link) {
|
||||
connections.push(link)
|
||||
}
|
||||
node.sidepeek_content.content.embedded_itemsConnection.edges.forEach(
|
||||
({ node }) => {
|
||||
connections.push(node.system)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return connections
|
||||
}
|
||||
|
||||
export function getAlertPhoneContactData(
|
||||
alert: Alert,
|
||||
alert: AlertOutput,
|
||||
contactConfig: ContactConfig
|
||||
) {
|
||||
if (alert.phoneContact) {
|
||||
|
||||
@@ -11,6 +11,7 @@ const bookingWidgetToggleSchema = z
|
||||
export const validateBookingWidgetToggleSchema = z.object({
|
||||
account_page: bookingWidgetToggleSchema,
|
||||
loyalty_page: bookingWidgetToggleSchema,
|
||||
collection_page: bookingWidgetToggleSchema,
|
||||
content_page: bookingWidgetToggleSchema,
|
||||
hotel_page: bookingWidgetToggleSchema,
|
||||
current_blocks_page: bookingWidgetToggleSchema,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { ValueOf } from "next/dist/shared/lib/constants"
|
||||
|
||||
import {
|
||||
GetAccountPageSettings,
|
||||
GetCollectionPageSettings,
|
||||
GetContentPageSettings,
|
||||
GetCurrentBlocksPageSettings,
|
||||
GetHotelPageSettings,
|
||||
@@ -43,6 +44,9 @@ export const bookingwidgetQueryRouter = router({
|
||||
case ContentTypeEnum.loyaltyPage:
|
||||
GetPageSettings = GetLoyaltyPageSettings
|
||||
break
|
||||
case ContentTypeEnum.collectionPage:
|
||||
GetPageSettings = GetCollectionPageSettings
|
||||
break
|
||||
case ContentTypeEnum.contentPage:
|
||||
GetPageSettings = GetContentPageSettings
|
||||
break
|
||||
|
||||
@@ -50,6 +50,16 @@ export type GetLoyaltyPageBreadcrumbsRefsData = z.infer<
|
||||
typeof validateLoyaltyPageBreadcrumbsRefsContentstackSchema
|
||||
>
|
||||
|
||||
export const validateCollectionPageBreadcrumbsRefsContentstackSchema = z.object(
|
||||
{
|
||||
collection_page: breadcrumbsRefs,
|
||||
}
|
||||
)
|
||||
|
||||
export type GetCollectionPageBreadcrumbsRefsData = z.infer<
|
||||
typeof validateCollectionPageBreadcrumbsRefsContentstackSchema
|
||||
>
|
||||
|
||||
export const validateContentPageBreadcrumbsRefsContentstackSchema = z.object({
|
||||
content_page: breadcrumbsRefs,
|
||||
})
|
||||
@@ -107,3 +117,11 @@ export const validateContentPageBreadcrumbsContentstackSchema = z.object({
|
||||
export type GetContentPageBreadcrumbsData = z.infer<
|
||||
typeof validateContentPageBreadcrumbsContentstackSchema
|
||||
>
|
||||
|
||||
export const validateCollectionPageBreadcrumbsContentstackSchema = z.object({
|
||||
collection_page: page,
|
||||
})
|
||||
|
||||
export type GetCollectionPageBreadcrumbsData = z.infer<
|
||||
typeof validateCollectionPageBreadcrumbsContentstackSchema
|
||||
>
|
||||
|
||||
@@ -2,6 +2,10 @@ import {
|
||||
GetMyPagesBreadcrumbs,
|
||||
GetMyPagesBreadcrumbsRefs,
|
||||
} from "@/lib/graphql/Query/Breadcrumbs/AccountPage.graphql"
|
||||
import {
|
||||
GetCollectionPageBreadcrumbs,
|
||||
GetCollectionPageBreadcrumbsRefs,
|
||||
} from "@/lib/graphql/Query/Breadcrumbs/CollectionPage.graphql"
|
||||
import {
|
||||
GetContentPageBreadcrumbs,
|
||||
GetContentPageBreadcrumbsRefs,
|
||||
@@ -13,12 +17,16 @@ import {
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import {
|
||||
GetCollectionPageBreadcrumbsData,
|
||||
GetCollectionPageBreadcrumbsRefsData,
|
||||
type GetContentPageBreadcrumbsData,
|
||||
type GetContentPageBreadcrumbsRefsData,
|
||||
type GetLoyaltyPageBreadcrumbsData,
|
||||
type GetLoyaltyPageBreadcrumbsRefsData,
|
||||
type GetMyPagesBreadcrumbsData,
|
||||
type GetMyPagesBreadcrumbsRefsData,
|
||||
validateCollectionPageBreadcrumbsContentstackSchema,
|
||||
validateCollectionPageBreadcrumbsRefsContentstackSchema,
|
||||
validateContentPageBreadcrumbsContentstackSchema,
|
||||
validateContentPageBreadcrumbsRefsContentstackSchema,
|
||||
validateLoyaltyPageBreadcrumbsContentstackSchema,
|
||||
@@ -84,6 +92,48 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
|
||||
)
|
||||
}
|
||||
|
||||
async function getCollectionPageBreadcrumbs(variables: Variables) {
|
||||
const refsResponse =
|
||||
await getRefsResponse<GetCollectionPageBreadcrumbsRefsData>(
|
||||
GetCollectionPageBreadcrumbsRefs,
|
||||
variables
|
||||
)
|
||||
const validatedRefsData =
|
||||
validateCollectionPageBreadcrumbsRefsContentstackSchema.safeParse(
|
||||
refsResponse.data
|
||||
)
|
||||
|
||||
if (!validatedRefsData.success) {
|
||||
console.error(
|
||||
`Failed to validate CollectionPpage Breadcrumbs Refs - (uid: ${variables.uid})`
|
||||
)
|
||||
console.error(validatedRefsData.error)
|
||||
return null
|
||||
}
|
||||
const tags = getTags(validatedRefsData.data.collection_page, variables)
|
||||
const response = await getResponse<GetCollectionPageBreadcrumbsData>(
|
||||
GetCollectionPageBreadcrumbs,
|
||||
variables,
|
||||
tags
|
||||
)
|
||||
if (!response.data.collection_page.web?.breadcrumbs?.title) {
|
||||
return null
|
||||
}
|
||||
const validatedBreadcrumbsData =
|
||||
validateCollectionPageBreadcrumbsContentstackSchema.safeParse(response.data)
|
||||
if (!validatedBreadcrumbsData.success) {
|
||||
console.error(
|
||||
`Failed to validate Collectionpage Breadcrumbs Data - (uid: ${variables.uid})`
|
||||
)
|
||||
console.error(validatedBreadcrumbsData.error)
|
||||
return null
|
||||
}
|
||||
return getBreadcrumbs(
|
||||
validatedBreadcrumbsData.data.collection_page,
|
||||
variables.locale
|
||||
)
|
||||
}
|
||||
|
||||
async function getContentPageBreadcrumbs(variables: Variables) {
|
||||
const refsResponse = await getRefsResponse<GetContentPageBreadcrumbsRefsData>(
|
||||
GetContentPageBreadcrumbsRefs,
|
||||
@@ -189,6 +239,8 @@ export const breadcrumbsQueryRouter = router({
|
||||
switch (ctx.contentType) {
|
||||
case PageTypeEnum.accountPage:
|
||||
return await getMyPagesBreadcrumbs(variables)
|
||||
case PageTypeEnum.collectionPage:
|
||||
return await getCollectionPageBreadcrumbs(variables)
|
||||
case PageTypeEnum.contentPage:
|
||||
return await getContentPageBreadcrumbs(variables)
|
||||
case PageTypeEnum.loyaltyPage:
|
||||
|
||||
5
server/routers/contentstack/collectionPage/index.ts
Normal file
5
server/routers/contentstack/collectionPage/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { collectionPageQueryRouter } from "./query"
|
||||
|
||||
export const collectionPageRouter = mergeRouters(collectionPageQueryRouter)
|
||||
124
server/routers/contentstack/collectionPage/output.ts
Normal file
124
server/routers/contentstack/collectionPage/output.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
|
||||
|
||||
import {
|
||||
cardGridRefsSchema,
|
||||
cardsGridSchema,
|
||||
} from "../schemas/blocks/cardsGrid"
|
||||
import {
|
||||
shortcutsRefsSchema,
|
||||
shortcutsSchema,
|
||||
} from "../schemas/blocks/shortcuts"
|
||||
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
|
||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||
import {
|
||||
linkAndTitleSchema,
|
||||
linkConnectionRefs,
|
||||
} from "../schemas/linkConnection"
|
||||
import { systemSchema } from "../schemas/system"
|
||||
|
||||
import { CollectionPageEnum } from "@/types/enums/collectionPage"
|
||||
|
||||
// Block schemas
|
||||
export const collectionPageCards = z
|
||||
.object({
|
||||
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.CardsGrid),
|
||||
})
|
||||
.merge(cardsGridSchema)
|
||||
|
||||
export const collectionPageShortcuts = z
|
||||
.object({
|
||||
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Shortcuts),
|
||||
})
|
||||
.merge(shortcutsSchema)
|
||||
|
||||
export const collectionPageUspGrid = z
|
||||
.object({
|
||||
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.UspGrid),
|
||||
})
|
||||
.merge(uspGridSchema)
|
||||
|
||||
export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||
collectionPageCards,
|
||||
collectionPageShortcuts,
|
||||
collectionPageUspGrid,
|
||||
])
|
||||
|
||||
const navigationLinksSchema = z
|
||||
.array(linkAndTitleSchema)
|
||||
.nullable()
|
||||
.transform((data) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
|
||||
return data
|
||||
.filter((item) => !!item.link)
|
||||
.map((item) => ({
|
||||
url: item.link!.url,
|
||||
title: item.title || item.link!.title,
|
||||
}))
|
||||
})
|
||||
|
||||
// Content Page Schema and types
|
||||
export const collectionPageSchema = z.object({
|
||||
collection_page: z.object({
|
||||
hero_image: tempImageVaultAssetSchema,
|
||||
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
|
||||
title: z.string(),
|
||||
header: z.object({
|
||||
heading: z.string(),
|
||||
preamble: z.string(),
|
||||
navigation_links: navigationLinksSchema,
|
||||
}),
|
||||
system: systemSchema.merge(
|
||||
z.object({
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
trackingProps: z.object({
|
||||
url: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
/** REFS */
|
||||
const collectionPageCardsRefs = z
|
||||
.object({
|
||||
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.CardsGrid),
|
||||
})
|
||||
.merge(cardGridRefsSchema)
|
||||
|
||||
const collectionPageShortcutsRefs = z
|
||||
.object({
|
||||
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Shortcuts),
|
||||
})
|
||||
.merge(shortcutsRefsSchema)
|
||||
|
||||
const collectionPageUspGridRefs = z
|
||||
.object({
|
||||
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.UspGrid),
|
||||
})
|
||||
.merge(uspGridRefsSchema)
|
||||
|
||||
const collectionPageBlockRefsItem = z.discriminatedUnion("__typename", [
|
||||
collectionPageShortcutsRefs,
|
||||
collectionPageCardsRefs,
|
||||
collectionPageUspGridRefs,
|
||||
])
|
||||
|
||||
const collectionPageHeaderRefs = z.object({
|
||||
navigation_links: z.array(linkConnectionRefs),
|
||||
})
|
||||
|
||||
export const collectionPageRefsSchema = z.object({
|
||||
collection_page: z.object({
|
||||
header: collectionPageHeaderRefs,
|
||||
blocks: discriminatedUnionArray(
|
||||
collectionPageBlockRefsItem.options
|
||||
).nullable(),
|
||||
system: systemSchema,
|
||||
}),
|
||||
})
|
||||
80
server/routers/contentstack/collectionPage/query.ts
Normal file
80
server/routers/contentstack/collectionPage/query.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { GetCollectionPage } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import { collectionPageSchema } from "./output"
|
||||
import {
|
||||
fetchCollectionPageRefs,
|
||||
generatePageTags,
|
||||
getCollectionPageCounter,
|
||||
validateCollectionPageRefs,
|
||||
} from "./utils"
|
||||
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import { GetCollectionPageSchema } from "@/types/trpc/routers/contentstack/collectionPage"
|
||||
|
||||
export const collectionPageQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
const { lang, uid } = ctx
|
||||
|
||||
const collectionPageRefsData = await fetchCollectionPageRefs(lang, uid)
|
||||
|
||||
const collectionPageRefs = validateCollectionPageRefs(
|
||||
collectionPageRefsData,
|
||||
lang,
|
||||
uid
|
||||
)
|
||||
if (!collectionPageRefs) {
|
||||
return null
|
||||
}
|
||||
const tags = generatePageTags(collectionPageRefs, lang)
|
||||
|
||||
getCollectionPageCounter.add(1, { lang, uid })
|
||||
console.info(
|
||||
"contentstack.collectionPage start",
|
||||
JSON.stringify({
|
||||
query: { lang, uid },
|
||||
})
|
||||
)
|
||||
|
||||
const response = await request<GetCollectionPageSchema>(
|
||||
GetCollectionPage,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const collectionPage = collectionPageSchema.safeParse(response.data)
|
||||
if (!collectionPage.success) {
|
||||
console.error(
|
||||
`Failed to validate CollectionPage Data - (lang: ${lang}, uid: ${uid})`
|
||||
)
|
||||
console.error(collectionPage.error?.format())
|
||||
return null
|
||||
}
|
||||
|
||||
const tracking: TrackingSDKPageData = {
|
||||
pageId: collectionPage.data.collection_page.system.uid,
|
||||
domainLanguage: collectionPage.data.collection_page.system.locale as Lang,
|
||||
publishedDate: collectionPage.data.collection_page.system.updated_at,
|
||||
createdDate: collectionPage.data.collection_page.system.created_at,
|
||||
channel: TrackingChannelEnum["collection-page"],
|
||||
pageType: "collectionpage",
|
||||
pageName: collectionPage.data.trackingProps.url,
|
||||
siteSections: collectionPage.data.trackingProps.url,
|
||||
}
|
||||
|
||||
return {
|
||||
collectionPage: collectionPage.data.collection_page,
|
||||
tracking,
|
||||
}
|
||||
}),
|
||||
})
|
||||
152
server/routers/contentstack/collectionPage/utils.ts
Normal file
152
server/routers/contentstack/collectionPage/utils.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { GetCollectionPageRefs } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
|
||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||
|
||||
import { collectionPageRefsSchema } from "./output"
|
||||
|
||||
import { CollectionPageEnum } from "@/types/enums/collectionPage"
|
||||
import { System } from "@/types/requests/system"
|
||||
import {
|
||||
CollectionPageRefs,
|
||||
GetCollectionPageRefsSchema,
|
||||
} from "@/types/trpc/routers/contentstack/collectionPage"
|
||||
|
||||
const meter = metrics.getMeter("trpc.collectionPage")
|
||||
// OpenTelemetry metrics: CollectionPage
|
||||
|
||||
export const getCollectionPageCounter = meter.createCounter(
|
||||
"trpc.contentstack.collectionPage.get"
|
||||
)
|
||||
|
||||
const getCollectionPageRefsCounter = meter.createCounter(
|
||||
"trpc.contentstack.collectionPage.get"
|
||||
)
|
||||
const getCollectionPageRefsFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.collectionPage.get-fail"
|
||||
)
|
||||
const getCollectionPageRefsSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.collectionPage.get-success"
|
||||
)
|
||||
|
||||
export async function fetchCollectionPageRefs(lang: Lang, uid: string) {
|
||||
getCollectionPageRefsCounter.add(1, { lang, uid })
|
||||
console.info(
|
||||
"contentstack.collectionPage.refs start",
|
||||
JSON.stringify({
|
||||
query: { lang, uid },
|
||||
})
|
||||
)
|
||||
const refsResponse = await request<GetCollectionPageRefsSchema>(
|
||||
GetCollectionPageRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!refsResponse.data) {
|
||||
const notFoundError = notFound(refsResponse)
|
||||
getCollectionPageRefsFailCounter.add(1, {
|
||||
lang,
|
||||
uid,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
code: notFoundError.code,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.collectionPage.refs not found error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang,
|
||||
uid,
|
||||
},
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
return refsResponse.data
|
||||
}
|
||||
|
||||
export function validateCollectionPageRefs(
|
||||
data: GetCollectionPageRefsSchema,
|
||||
lang: Lang,
|
||||
uid: string
|
||||
) {
|
||||
const validatedData = collectionPageRefsSchema.safeParse(data)
|
||||
if (!validatedData.success) {
|
||||
getCollectionPageRefsFailCounter.add(1, {
|
||||
lang,
|
||||
uid,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedData.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.collectionPage.refs validation error",
|
||||
JSON.stringify({
|
||||
query: { lang, uid },
|
||||
error: validatedData.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
getCollectionPageRefsSuccessCounter.add(1, { lang, uid })
|
||||
console.info(
|
||||
"contentstack.collectionPage.refs success",
|
||||
JSON.stringify({
|
||||
query: { lang, uid },
|
||||
})
|
||||
)
|
||||
|
||||
return validatedData.data
|
||||
}
|
||||
|
||||
export function generatePageTags(
|
||||
validatedData: CollectionPageRefs,
|
||||
lang: Lang
|
||||
): string[] {
|
||||
const connections = getConnections(validatedData)
|
||||
return [
|
||||
generateTagsFromSystem(lang, connections),
|
||||
generateTag(lang, validatedData.collection_page.system.uid),
|
||||
].flat()
|
||||
}
|
||||
|
||||
export function getConnections({ collection_page }: CollectionPageRefs) {
|
||||
const connections: System["system"][] = [collection_page.system]
|
||||
if (collection_page.blocks) {
|
||||
collection_page.blocks.forEach((block) => {
|
||||
switch (block.__typename) {
|
||||
case CollectionPageEnum.ContentStack.blocks.Shortcuts: {
|
||||
if (block.shortcuts.shortcuts.length) {
|
||||
connections.push(...block.shortcuts.shortcuts)
|
||||
}
|
||||
break
|
||||
}
|
||||
case CollectionPageEnum.ContentStack.blocks.CardsGrid: {
|
||||
if (block.cards_grid.length) {
|
||||
connections.push(...block.cards_grid)
|
||||
}
|
||||
break
|
||||
}
|
||||
case CollectionPageEnum.ContentStack.blocks.UspGrid: {
|
||||
if (block.usp_grid.length) {
|
||||
connections.push(...block.usp_grid)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return connections
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { batchRequest } from "@/lib/graphql/batchRequest"
|
||||
import {
|
||||
GetContentPage,
|
||||
GetContentPageBlocksBatch1,
|
||||
GetContentPageBlocksBatch2,
|
||||
} from "@/lib/graphql/Query/ContentPage/ContentPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import { contentPageSchema } from "./output"
|
||||
@@ -12,30 +12,20 @@ import {
|
||||
fetchContentPageRefs,
|
||||
generatePageTags,
|
||||
getContentPageCounter,
|
||||
validateContentPageRefs,
|
||||
} from "./utils"
|
||||
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import { ContentPageEnum } from "@/types/enums/contentPage"
|
||||
import type {
|
||||
GetBlock,
|
||||
GetContentPageSchema,
|
||||
} from "@/types/trpc/routers/contentstack/contentPage"
|
||||
import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage"
|
||||
|
||||
export const contentPageQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
const { lang, uid } = ctx
|
||||
|
||||
const contentPageRefsData = await fetchContentPageRefs(lang, uid)
|
||||
const contentPageRefs = await fetchContentPageRefs(lang, uid)
|
||||
|
||||
const contentPageRefs = validateContentPageRefs(
|
||||
contentPageRefsData,
|
||||
lang,
|
||||
uid
|
||||
)
|
||||
if (!contentPageRefs) {
|
||||
return null
|
||||
}
|
||||
@@ -50,69 +40,42 @@ export const contentPageQueryRouter = router({
|
||||
})
|
||||
)
|
||||
|
||||
const [mainResponse, blocksResponse1, blocksResponse2] = await Promise.all([
|
||||
request<GetContentPageSchema>(
|
||||
GetContentPage,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
const contentPageRequest = await batchRequest<GetContentPageSchema>([
|
||||
{
|
||||
document: GetContentPage,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
}
|
||||
),
|
||||
request<GetContentPageSchema>(
|
||||
GetContentPageBlocksBatch1,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
document: GetContentPageBlocksBatch1,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
}
|
||||
),
|
||||
request<GetContentPageSchema>(
|
||||
GetContentPageBlocksBatch2,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
document: GetContentPageBlocksBatch2,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const blocksOrder = mainResponse.data.content_page.blocks?.map(
|
||||
(block) => block.__typename
|
||||
)
|
||||
|
||||
let sortedBlocks
|
||||
if (blocksOrder) {
|
||||
const blocks = [
|
||||
blocksResponse1.data.content_page.blocks,
|
||||
blocksResponse2.data.content_page.blocks,
|
||||
]
|
||||
.flat(2)
|
||||
.filter((obj) => !(obj && Object.keys(obj).length < 2))
|
||||
// Remove empty objects and objects with only typename
|
||||
|
||||
sortedBlocks = blocksOrder
|
||||
.map((typename: ContentPageEnum.ContentStack.blocks) =>
|
||||
blocks.find((block) => block?.__typename === typename)
|
||||
)
|
||||
.filter((block): block is GetBlock => !!block)
|
||||
}
|
||||
|
||||
const responseData = {
|
||||
...mainResponse.data,
|
||||
content_page: {
|
||||
...mainResponse.data.content_page,
|
||||
blocks: sortedBlocks,
|
||||
},
|
||||
}
|
||||
|
||||
const contentPage = contentPageSchema.safeParse(responseData)
|
||||
const contentPage = contentPageSchema.safeParse(contentPageRequest.data)
|
||||
if (!contentPage.success) {
|
||||
console.error(
|
||||
`Failed to validate Contentpage Data - (lang: ${lang}, uid: ${uid})`
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { batchRequest } from "@/lib/graphql/batchRequest"
|
||||
import {
|
||||
GetContentPageBlocksRefs,
|
||||
GetContentPageRefs,
|
||||
} from "@/lib/graphql/Query/ContentPage/ContentPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
|
||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||
@@ -44,30 +44,30 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) {
|
||||
query: { lang, uid },
|
||||
})
|
||||
)
|
||||
const [mainRefsResponse, blockRefsResponse] = await Promise.all([
|
||||
request<GetContentPageRefsSchema>(
|
||||
GetContentPageRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
const res = await batchRequest<GetContentPageRefsSchema>([
|
||||
{
|
||||
document: GetContentPageRefs,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
}
|
||||
),
|
||||
request<GetContentPageRefsSchema>(
|
||||
GetContentPageBlocksRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
},
|
||||
},
|
||||
{
|
||||
document: GetContentPageBlocksRefs,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
tags: [generateTag(lang, uid + 1)],
|
||||
},
|
||||
}
|
||||
),
|
||||
},
|
||||
},
|
||||
])
|
||||
if (!mainRefsResponse.data) {
|
||||
const notFoundError = notFound(mainRefsResponse)
|
||||
if (!res.data) {
|
||||
const notFoundError = notFound(res)
|
||||
getContentPageRefsFailCounter.add(1, {
|
||||
lang,
|
||||
uid,
|
||||
@@ -88,22 +88,7 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) {
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
const responseData = {
|
||||
...mainRefsResponse.data,
|
||||
content_page: {
|
||||
...mainRefsResponse.data.content_page,
|
||||
blocks: blockRefsResponse.data.content_page.blocks,
|
||||
},
|
||||
}
|
||||
return responseData
|
||||
}
|
||||
|
||||
export function validateContentPageRefs(
|
||||
data: GetContentPageRefsSchema,
|
||||
lang: Lang,
|
||||
uid: string
|
||||
) {
|
||||
const validatedData = contentPageRefsSchema.safeParse(data)
|
||||
const validatedData = contentPageRefsSchema.safeParse(res.data)
|
||||
if (!validatedData.success) {
|
||||
getContentPageRefsFailCounter.add(1, {
|
||||
lang,
|
||||
@@ -162,6 +147,18 @@ export function getConnections({ content_page }: ContentPageRefs) {
|
||||
}
|
||||
}
|
||||
break
|
||||
case ContentPageEnum.ContentStack.blocks.CardsGrid: {
|
||||
if (block.cards_grid.length) {
|
||||
connections.push(...block.cards_grid)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentPageEnum.ContentStack.blocks.DynamicContent: {
|
||||
if (block.dynamic_content.link) {
|
||||
connections.push(block.dynamic_content.link)
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentPageEnum.ContentStack.blocks.Shortcuts: {
|
||||
if (block.shortcuts.shortcuts.length) {
|
||||
connections.push(...block.shortcuts.shortcuts)
|
||||
@@ -180,6 +177,14 @@ export function getConnections({ content_page }: ContentPageRefs) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentPageEnum.ContentStack.blocks.CardsGrid: {
|
||||
if (block.cards_grid.length) {
|
||||
block.cards_grid.forEach((card) => {
|
||||
connections.push(card)
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { accountPageRouter } from "./accountPage"
|
||||
import { baseRouter } from "./base"
|
||||
import { bookingwidgetRouter } from "./bookingwidget"
|
||||
import { breadcrumbsRouter } from "./breadcrumbs"
|
||||
import { collectionPageRouter } from "./collectionPage"
|
||||
import { contentPageRouter } from "./contentPage"
|
||||
import { hotelPageRouter } from "./hotelPage"
|
||||
import { languageSwitcherRouter } from "./languageSwitcher"
|
||||
@@ -21,6 +22,7 @@ export const contentstackRouter = router({
|
||||
hotelPage: hotelPageRouter,
|
||||
languageSwitcher: languageSwitcherRouter,
|
||||
loyaltyPage: loyaltyPageRouter,
|
||||
collectionPage: collectionPageRouter,
|
||||
contentPage: contentPageRouter,
|
||||
myPages: myPagesRouter,
|
||||
metaData: metaDataRouter,
|
||||
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
GetDaDeEnUrlsAccountPage,
|
||||
GetFiNoSvUrlsAccountPage,
|
||||
} from "@/lib/graphql/Query/AccountPage/AccountPage.graphql"
|
||||
import {
|
||||
GetDaDeEnUrlsCollectionPage,
|
||||
GetFiNoSvUrlsCollectionPage,
|
||||
} from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
|
||||
import {
|
||||
GetDaDeEnUrlsContentPage,
|
||||
GetFiNoSvUrlsContentPage,
|
||||
@@ -88,6 +92,10 @@ async function getLanguageSwitcher(options: LanguageSwitcherVariables) {
|
||||
daDeEnDocument = GetDaDeEnUrlsContentPage
|
||||
fiNoSvDocument = GetFiNoSvUrlsContentPage
|
||||
break
|
||||
case PageTypeEnum.collectionPage:
|
||||
daDeEnDocument = GetDaDeEnUrlsCollectionPage
|
||||
fiNoSvDocument = GetFiNoSvUrlsCollectionPage
|
||||
break
|
||||
default:
|
||||
console.error(`type: [${options.contentType}]`)
|
||||
console.error(`Trying to get a content type that is not supported`)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { cache } from "react"
|
||||
|
||||
import {
|
||||
MembershipLevel,
|
||||
@@ -42,7 +43,7 @@ const getByLevelLoyaltyLevelFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.byLevel-fail"
|
||||
)
|
||||
|
||||
export async function getAllLoyaltyLevels(ctx: Context) {
|
||||
export const getAllLoyaltyLevels = cache(async (ctx: Context) => {
|
||||
getAllLoyaltyLevelCounter.add(1)
|
||||
|
||||
// Ideally we should fetch all available tiers from API, but since they
|
||||
@@ -95,58 +96,60 @@ export async function getAllLoyaltyLevels(ctx: Context) {
|
||||
|
||||
getAllLoyaltyLevelSuccessCounter.add(1)
|
||||
return validatedLoyaltyLevels.data
|
||||
}
|
||||
})
|
||||
|
||||
export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
|
||||
getByLevelLoyaltyLevelCounter.add(1, {
|
||||
query: JSON.stringify({ lang: ctx.lang, level_id }),
|
||||
})
|
||||
export const getLoyaltyLevel = cache(
|
||||
async (ctx: Context, level_id: MembershipLevel) => {
|
||||
getByLevelLoyaltyLevelCounter.add(1, {
|
||||
query: JSON.stringify({ lang: ctx.lang, level_id }),
|
||||
})
|
||||
|
||||
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
||||
GetLoyaltyLevel,
|
||||
{ lang: ctx.lang, level_id },
|
||||
{
|
||||
next: {
|
||||
tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)],
|
||||
},
|
||||
cache: "force-cache",
|
||||
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
||||
GetLoyaltyLevel,
|
||||
{ lang: ctx.lang, level_id },
|
||||
{
|
||||
next: {
|
||||
tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)],
|
||||
},
|
||||
cache: "force-cache",
|
||||
}
|
||||
)
|
||||
if (
|
||||
!loyaltyLevelsConfigResponse.data ||
|
||||
!loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length
|
||||
) {
|
||||
getByLevelLoyaltyLevelFailCounter.add(1)
|
||||
const notFoundError = notFound(loyaltyLevelsConfigResponse)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevel not found error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
)
|
||||
if (
|
||||
!loyaltyLevelsConfigResponse.data ||
|
||||
!loyaltyLevelsConfigResponse.data.all_loyalty_level.items.length
|
||||
) {
|
||||
getByLevelLoyaltyLevelFailCounter.add(1)
|
||||
const notFoundError = notFound(loyaltyLevelsConfigResponse)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevel not found error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse(
|
||||
loyaltyLevelsConfigResponse.data
|
||||
)
|
||||
if (!validatedLoyaltyLevels.success) {
|
||||
getByLevelLoyaltyLevelFailCounter.add(1)
|
||||
console.error(validatedLoyaltyLevels.error)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevel validation error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: validatedLoyaltyLevels.error,
|
||||
})
|
||||
const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse(
|
||||
loyaltyLevelsConfigResponse.data
|
||||
)
|
||||
return null
|
||||
}
|
||||
if (!validatedLoyaltyLevels.success) {
|
||||
getByLevelLoyaltyLevelFailCounter.add(1)
|
||||
console.error(validatedLoyaltyLevels.error)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevel validation error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: validatedLoyaltyLevels.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getByLevelLoyaltyLevelSuccessCounter.add(1)
|
||||
return validatedLoyaltyLevels.data[0]
|
||||
}
|
||||
getByLevelLoyaltyLevelSuccessCounter.add(1)
|
||||
return validatedLoyaltyLevels.data[0]
|
||||
}
|
||||
)
|
||||
|
||||
export const loyaltyLevelQueryRouter = router({
|
||||
byLevel: contentstackBaseProcedure
|
||||
|
||||
@@ -34,7 +34,7 @@ const SurpriseReward = z.object({
|
||||
rewardId: z.string().optional(),
|
||||
redeemLocation: z.string().optional(),
|
||||
autoApplyReward: z.boolean().default(false),
|
||||
rewardType: z.literal("Surprise"),
|
||||
rewardType: z.string().optional(),
|
||||
endsAt: z.string().datetime({ offset: true }).optional(),
|
||||
coupons: z.array(Coupon).optional(),
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import { imageContainerSchema } from "./imageContainer"
|
||||
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid"
|
||||
import { scriptedCardThemeEnum } from "@/types/enums/scriptedCard"
|
||||
|
||||
export const cardBlockSchema = z.object({
|
||||
__typename: z.literal(CardsGridEnum.cards.Card),
|
||||
@@ -91,7 +92,22 @@ export const teaserCardBlockSchema = z.object({
|
||||
has_secondary_button: z.boolean().default(false),
|
||||
secondary_button: buttonSchema,
|
||||
})
|
||||
.optional(),
|
||||
.optional()
|
||||
.transform((data) => {
|
||||
if (!data) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
...data,
|
||||
primary_button: data.has_primary_button
|
||||
? data.primary_button
|
||||
: undefined,
|
||||
secondary_button: data.has_secondary_button
|
||||
? data.secondary_button
|
||||
: undefined,
|
||||
}
|
||||
}),
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
@@ -146,7 +162,7 @@ export const cardsGridSchema = z.object({
|
||||
}),
|
||||
layout: z.nativeEnum(CardsGridLayoutEnum),
|
||||
preamble: z.string().optional().default(""),
|
||||
theme: z.enum(["one", "two", "three"]).nullable(),
|
||||
theme: z.nativeEnum(scriptedCardThemeEnum).nullable(),
|
||||
title: z.string().optional().default(""),
|
||||
})
|
||||
.transform((data) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
|
||||
|
||||
const linkUnionSchema = z.discriminatedUnion("__typename", [
|
||||
pageLinks.contentPageSchema,
|
||||
pageLinks.collectionPageSchema,
|
||||
pageLinks.hotelPageSchema,
|
||||
pageLinks.loyaltyPageSchema,
|
||||
])
|
||||
|
||||
@@ -30,6 +30,16 @@ export const extendedPageLinkSchema = pageLinkSchema.merge(
|
||||
}),
|
||||
})
|
||||
)
|
||||
export const collectionPageSchema = z
|
||||
.object({
|
||||
__typename: z.literal(ContentEnum.blocks.CollectionPage),
|
||||
})
|
||||
.merge(extendedPageLinkSchema)
|
||||
|
||||
export const collectionPageRefSchema = z.object({
|
||||
__typename: z.literal(ContentEnum.blocks.CollectionPage),
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
export const contentPageSchema = z
|
||||
.object({
|
||||
@@ -67,6 +77,7 @@ export const loyaltyPageRefSchema = z.object({
|
||||
type Data =
|
||||
| z.output<typeof accountPageSchema>
|
||||
| z.output<typeof contentPageSchema>
|
||||
| z.output<typeof collectionPageSchema>
|
||||
| z.output<typeof hotelPageSchema>
|
||||
| z.output<typeof loyaltyPageSchema>
|
||||
| Object
|
||||
@@ -83,6 +94,7 @@ export function transform(data: Data) {
|
||||
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
|
||||
}
|
||||
case ContentEnum.blocks.ContentPage:
|
||||
case ContentEnum.blocks.CollectionPage:
|
||||
case ContentEnum.blocks.LoyaltyPage:
|
||||
// TODO: Once all links use this transform
|
||||
// `web` can be removed and not to be worried
|
||||
@@ -102,6 +114,7 @@ export function transform(data: Data) {
|
||||
|
||||
type RefData =
|
||||
| z.output<typeof accountPageRefSchema>
|
||||
| z.output<typeof collectionPageRefSchema>
|
||||
| z.output<typeof contentPageRefSchema>
|
||||
| z.output<typeof hotelPageRefSchema>
|
||||
| z.output<typeof loyaltyPageRefSchema>
|
||||
@@ -112,6 +125,7 @@ export function transformRef(data: RefData) {
|
||||
switch (data.__typename) {
|
||||
case ContentEnum.blocks.AccountPage:
|
||||
case ContentEnum.blocks.ContentPage:
|
||||
case ContentEnum.blocks.CollectionPage:
|
||||
case ContentEnum.blocks.HotelPage:
|
||||
case ContentEnum.blocks.LoyaltyPage:
|
||||
return data.system
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
transformCardBlockRefs,
|
||||
} from "../blocks/cardsGrid"
|
||||
|
||||
import { scriptedCardThemeEnum } from "@/types/enums/scriptedCard"
|
||||
import { SidebarEnums } from "@/types/enums/sidebar"
|
||||
|
||||
export const scriptedCardsSchema = z.object({
|
||||
@@ -16,17 +17,7 @@ export const scriptedCardsSchema = z.object({
|
||||
.default(SidebarEnums.blocks.ScriptedCard),
|
||||
scripted_card: z
|
||||
.object({
|
||||
theme: z
|
||||
.enum([
|
||||
"one",
|
||||
"two",
|
||||
"three",
|
||||
"primaryDim",
|
||||
"primaryDark",
|
||||
"primaryInverted",
|
||||
"primaryStrong",
|
||||
])
|
||||
.nullable(),
|
||||
theme: z.nativeEnum(scriptedCardThemeEnum).nullable(),
|
||||
scripted_cardConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const getHotelInputSchema = z.object({
|
||||
include: z
|
||||
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const getHotelsAvailabilityInputSchema = z.object({
|
||||
cityId: z.string(),
|
||||
roomStayStartDate: z.string(),
|
||||
@@ -29,19 +23,48 @@ export const getRoomsAvailabilityInputSchema = z.object({
|
||||
rateCode: z.string().optional(),
|
||||
})
|
||||
|
||||
export const getSelectedRoomAvailabilityInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
roomStayStartDate: z.string(),
|
||||
roomStayEndDate: z.string(),
|
||||
adults: z.number(),
|
||||
children: z.string().optional(),
|
||||
promotionCode: z.string().optional(),
|
||||
reservationProfileType: z.string().optional().default(""),
|
||||
attachedProfileId: z.string().optional().default(""),
|
||||
rateCode: z.string(),
|
||||
roomTypeCode: z.string(),
|
||||
})
|
||||
|
||||
export type GetSelectedRoomAvailabilityInput = z.input<
|
||||
typeof getSelectedRoomAvailabilityInputSchema
|
||||
>
|
||||
|
||||
export type GetRoomsAvailabilityInput = z.input<
|
||||
typeof getRoomsAvailabilityInputSchema
|
||||
>
|
||||
|
||||
export const getRatesInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
})
|
||||
|
||||
export const getlHotelDataInputSchema = z.object({
|
||||
export const getHotelDataInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
language: z.string(),
|
||||
isCardOnlyPayment: z.boolean().optional(),
|
||||
include: z
|
||||
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const getBreakfastPackageInput = z.object({
|
||||
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
|
||||
|
||||
export const getBreakfastPackageInputSchema = z.object({
|
||||
adults: z.number().min(1, { message: "at least one adult is required" }),
|
||||
fromDate: z
|
||||
.string()
|
||||
.min(1, { message: "fromDate is required" })
|
||||
.pipe(z.coerce.date()),
|
||||
hotelId: z.string().min(1, { message: "hotelId is required" }),
|
||||
toDate: z
|
||||
.string()
|
||||
.min(1, { message: "toDate is required" })
|
||||
.pipe(z.coerce.date()),
|
||||
})
|
||||
|
||||
@@ -71,34 +71,6 @@ const ecoLabelsSchema = z.object({
|
||||
svanenEcoLabelCertificateNumber: z.string().optional(),
|
||||
})
|
||||
|
||||
const hotelFacilityDetailSchema = z.object({
|
||||
heading: z.string(),
|
||||
description: z.string(),
|
||||
})
|
||||
|
||||
const hotelFacilitySchema = z.object({
|
||||
breakfast: hotelFacilityDetailSchema,
|
||||
checkout: hotelFacilityDetailSchema,
|
||||
gym: hotelFacilityDetailSchema,
|
||||
internet: hotelFacilityDetailSchema,
|
||||
laundry: hotelFacilityDetailSchema,
|
||||
luggage: hotelFacilityDetailSchema,
|
||||
shop: hotelFacilityDetailSchema,
|
||||
telephone: hotelFacilityDetailSchema,
|
||||
})
|
||||
|
||||
const hotelInformationDetailSchema = z.object({
|
||||
heading: z.string(),
|
||||
description: z.string(),
|
||||
link: z.string().optional(),
|
||||
})
|
||||
|
||||
const hotelInformationSchema = z.object({
|
||||
accessibility: hotelInformationDetailSchema,
|
||||
safety: hotelInformationDetailSchema,
|
||||
sustainability: hotelInformationDetailSchema,
|
||||
})
|
||||
|
||||
const interiorSchema = z.object({
|
||||
numberOfBeds: z.number(),
|
||||
numberOfCribs: z.number(),
|
||||
@@ -253,64 +225,74 @@ export const pointOfInterestSchema = z
|
||||
|
||||
const parkingPricingSchema = z.object({
|
||||
freeParking: z.boolean(),
|
||||
paymentType: z.string(),
|
||||
paymentType: z.string().optional(),
|
||||
localCurrency: z.object({
|
||||
currency: z.string(),
|
||||
currency: z.string().optional(),
|
||||
range: z.object({
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
}),
|
||||
ordinary: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
weekend: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
ordinary: z
|
||||
.array(
|
||||
z.object({
|
||||
period: z.string().optional(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
weekend: z
|
||||
.array(
|
||||
z.object({
|
||||
period: z.string().optional(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
requestedCurrency: z
|
||||
.object({
|
||||
currency: z.string(),
|
||||
range: z.object({
|
||||
min: z.number(),
|
||||
max: z.number(),
|
||||
}),
|
||||
ordinary: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
currency: z.string().optional(),
|
||||
range: z
|
||||
.object({
|
||||
min: z.number().optional(),
|
||||
max: z.number().optional(),
|
||||
})
|
||||
),
|
||||
weekend: z.array(
|
||||
z.object({
|
||||
period: z.string(),
|
||||
amount: z.number(),
|
||||
startTime: z.string(),
|
||||
endTime: z.string(),
|
||||
})
|
||||
),
|
||||
.optional(),
|
||||
ordinary: z
|
||||
.array(
|
||||
z.object({
|
||||
period: z.string().optional(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
weekend: z
|
||||
.array(
|
||||
z.object({
|
||||
period: z.string().optional(),
|
||||
amount: z.number().optional(),
|
||||
startTime: z.string().optional(),
|
||||
endTime: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const parkingSchema = z.object({
|
||||
type: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
numberOfParkingSpots: z.number().optional(),
|
||||
numberOfChargingSpaces: z.number().optional(),
|
||||
distanceToHotel: z.number(),
|
||||
distanceToHotel: z.number().optional(),
|
||||
canMakeReservation: z.boolean(),
|
||||
pricing: parkingPricingSchema,
|
||||
})
|
||||
@@ -423,8 +405,6 @@ export const getHotelDataSchema = z.object({
|
||||
hotelFacts: z.object({
|
||||
checkin: checkinSchema,
|
||||
ecoLabels: ecoLabelsSchema,
|
||||
hotelFacilityDetail: hotelFacilitySchema,
|
||||
hotelInformation: hotelInformationSchema,
|
||||
interior: interiorSchema,
|
||||
receptionHours: receptionHoursSchema,
|
||||
yearBuilt: z.string(),
|
||||
@@ -518,7 +498,7 @@ export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
|
||||
export type HotelsAvailabilityPrices =
|
||||
HotelsAvailability["data"][number]["attributes"]["bestPricePerNight"]
|
||||
|
||||
const priceSchema = z.object({
|
||||
export const priceSchema = z.object({
|
||||
pricePerNight: z.string(),
|
||||
pricePerStay: z.string(),
|
||||
currency: z.string(),
|
||||
@@ -804,17 +784,27 @@ export const apiLocationsSchema = z.object({
|
||||
),
|
||||
})
|
||||
|
||||
const breakfastPackagePriceSchema = z
|
||||
.object({
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
price: z.string(),
|
||||
totalPrice: z.string(),
|
||||
})
|
||||
.default({
|
||||
currency: CurrencyEnum.SEK,
|
||||
price: "0",
|
||||
totalPrice: "0",
|
||||
}) // TODO: Remove optional and default when the API change has been deployed
|
||||
|
||||
export const breakfastPackageSchema = z.object({
|
||||
code: z.string(),
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
description: z.string(),
|
||||
originalPrice: z.number().default(0),
|
||||
packagePrice: z.number(),
|
||||
localPrice: breakfastPackagePriceSchema,
|
||||
requestedPrice: breakfastPackagePriceSchema,
|
||||
packageType: z.enum([
|
||||
PackageTypeEnum.BreakfastAdult,
|
||||
PackageTypeEnum.BreakfastChildren,
|
||||
]),
|
||||
totalPrice: z.number(),
|
||||
})
|
||||
|
||||
export const breakfastPackagesSchema = z
|
||||
|
||||
@@ -1,7 +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 {
|
||||
@@ -31,12 +33,13 @@ import {
|
||||
getRoomPackagesSchema,
|
||||
} from "./schemas/packages"
|
||||
import {
|
||||
getBreakfastPackageInput,
|
||||
getHotelInputSchema,
|
||||
getBreakfastPackageInputSchema,
|
||||
getHotelDataInputSchema,
|
||||
getHotelsAvailabilityInputSchema,
|
||||
getlHotelDataInputSchema,
|
||||
getRatesInputSchema,
|
||||
getRoomsAvailabilityInputSchema,
|
||||
getSelectedRoomAvailabilityInputSchema,
|
||||
type HotelDataInput,
|
||||
} from "./input"
|
||||
import {
|
||||
breakfastPackagesSchema,
|
||||
@@ -54,6 +57,7 @@ import {
|
||||
} from "./utils"
|
||||
|
||||
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
|
||||
import type { BedType } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
@@ -93,6 +97,16 @@ const roomsAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms-fail"
|
||||
)
|
||||
|
||||
const selectedRoomAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room"
|
||||
)
|
||||
const selectedRoomAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room-success"
|
||||
)
|
||||
const selectedRoomAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room-fail"
|
||||
)
|
||||
|
||||
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
|
||||
const breakfastPackagesSuccessCounter = meter.createCounter(
|
||||
"trpc.package.breakfast-success"
|
||||
@@ -150,147 +164,190 @@ async function getContentstackData(lang: Lang, uid?: string | null) {
|
||||
return hotelPageData.data.hotel_page
|
||||
}
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
get: contentStackUidWithServiceProcedure
|
||||
.input(getHotelInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { lang, uid } = ctx
|
||||
const { include } = input
|
||||
export const getHotelData = cache(
|
||||
async (input: HotelDataInput, serviceToken: string) => {
|
||||
const { hotelId, language, isCardOnlyPayment } = input
|
||||
|
||||
const contentstackData = await getContentstackData(lang, uid)
|
||||
const hotelId = contentstackData?.hotel_page_id
|
||||
const params: Record<string, string> = {
|
||||
hotelId,
|
||||
language,
|
||||
}
|
||||
|
||||
if (!hotelId) {
|
||||
throw notFound(`Hotel not found for uid: ${uid}`)
|
||||
}
|
||||
params.include = "RoomCategories" // "RoomCategories","NearbyHotels","Restaurants","City",
|
||||
|
||||
const apiLang = toApiLang(lang)
|
||||
const params: Record<string, string> = {
|
||||
getHotelCounter.add(1, {
|
||||
hotelId,
|
||||
language,
|
||||
})
|
||||
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}`,
|
||||
},
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
next: {
|
||||
revalidate: 60 * 30, // 30 minutes
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
language: apiLang,
|
||||
}
|
||||
|
||||
if (include) {
|
||||
params.include = include.join(",")
|
||||
}
|
||||
|
||||
getHotelCounter.add(1, { hotelId, lang, include })
|
||||
console.info(
|
||||
"api.hotels.hotel start",
|
||||
language,
|
||||
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 },
|
||||
})
|
||||
)
|
||||
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,
|
||||
lang,
|
||||
include,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.hotel error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedHotelData = getHotelDataSchema.safeParse(apiJson)
|
||||
|
||||
if (!validatedHotelData.success) {
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
lang,
|
||||
include,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedHotelData.error),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotels.hotel validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedHotelData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const included = validatedHotelData.data.included || []
|
||||
|
||||
const hotelAttributes = validatedHotelData.data.data.attributes
|
||||
const images = hotelAttributes.gallery?.smallerImages
|
||||
const hotelAlerts = hotelAttributes.meta?.specialAlerts || []
|
||||
|
||||
const roomCategories = included
|
||||
? included.filter((item) => item.type === "roomcategories")
|
||||
: []
|
||||
|
||||
const activities = contentstackData?.content
|
||||
? contentstackData?.content[0]
|
||||
: null
|
||||
|
||||
const facilities: Facility[] = [
|
||||
{
|
||||
...apiJson.data.attributes.restaurantImages,
|
||||
id: FacilityCardTypeEnum.restaurant,
|
||||
},
|
||||
{
|
||||
...apiJson.data.attributes.conferencesAndMeetings,
|
||||
id: FacilityCardTypeEnum.conference,
|
||||
},
|
||||
{
|
||||
...apiJson.data.attributes.healthAndWellness,
|
||||
id: FacilityCardTypeEnum.wellness,
|
||||
},
|
||||
]
|
||||
|
||||
getHotelSuccessCounter.add(1, { hotelId, lang, include })
|
||||
console.info(
|
||||
"api.hotels.hotel success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params: params },
|
||||
},
|
||||
})
|
||||
)
|
||||
return {
|
||||
hotelName: hotelAttributes.name,
|
||||
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
|
||||
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,
|
||||
}
|
||||
}),
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validateHotelData = getHotelDataSchema.safeParse(apiJson)
|
||||
|
||||
if (!validateHotelData.success) {
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
language,
|
||||
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,
|
||||
})
|
||||
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({
|
||||
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.meta?.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,
|
||||
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
|
||||
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,
|
||||
}
|
||||
}),
|
||||
availability: router({
|
||||
hotels: serviceProcedure
|
||||
.input(getHotelsAvailabilityInputSchema)
|
||||
@@ -545,6 +602,196 @@ export const hotelQueryRouter = router({
|
||||
|
||||
return validateAvailabilityData.data
|
||||
}),
|
||||
room: serviceProcedure
|
||||
.input(getSelectedRoomAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const {
|
||||
hotelId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
promotionCode,
|
||||
reservationProfileType,
|
||||
attachedProfileId,
|
||||
rateCode,
|
||||
roomTypeCode,
|
||||
} = input
|
||||
|
||||
const params: Record<string, string | number | undefined> = {
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
...(children && { children }),
|
||||
promotionCode,
|
||||
reservationProfileType,
|
||||
attachedProfileId,
|
||||
language: toApiLang(ctx.lang),
|
||||
}
|
||||
|
||||
selectedRoomAvailabilityCounter.add(1, {
|
||||
hotelId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
promotionCode,
|
||||
reservationProfileType,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.selectedRoomAvailability start",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
const apiResponseAvailability = await api.get(
|
||||
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponseAvailability.ok) {
|
||||
const text = await apiResponseAvailability.text()
|
||||
selectedRoomAvailabilityFailCounter.add(1, {
|
||||
hotelId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
promotionCode,
|
||||
reservationProfileType,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponseAvailability.status,
|
||||
statusText: apiResponseAvailability.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.selectedRoomAvailability error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: {
|
||||
status: apiResponseAvailability.status,
|
||||
statusText: apiResponseAvailability.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
const apiJsonAvailability = await apiResponseAvailability.json()
|
||||
const validateAvailabilityData =
|
||||
getRoomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
||||
if (!validateAvailabilityData.success) {
|
||||
selectedRoomAvailabilityFailCounter.add(1, {
|
||||
hotelId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
promotionCode,
|
||||
reservationProfileType,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validateAvailabilityData.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.selectedRoomAvailability validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validateAvailabilityData.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
const hotelData = await getHotelData(
|
||||
{
|
||||
hotelId,
|
||||
language: ctx.lang,
|
||||
},
|
||||
ctx.serviceToken
|
||||
)
|
||||
|
||||
const selectedRoom = validateAvailabilityData.data.roomConfigurations
|
||||
.filter((room) => room.status === "Available")
|
||||
.find((room) => room.roomTypeCode === roomTypeCode)
|
||||
|
||||
const availableRoomsInCategory =
|
||||
validateAvailabilityData.data.roomConfigurations.filter(
|
||||
(room) =>
|
||||
room.status === "Available" &&
|
||||
room.roomType === selectedRoom?.roomType
|
||||
)
|
||||
|
||||
if (!selectedRoom) {
|
||||
console.error("No matching room found")
|
||||
return null
|
||||
}
|
||||
|
||||
const memberRate = selectedRoom.products.find(
|
||||
(rate) => rate.productType.member?.rateCode === rateCode
|
||||
)?.productType.member
|
||||
|
||||
const publicRate = selectedRoom.products.find(
|
||||
(rate) => rate.productType.public?.rateCode === rateCode
|
||||
)?.productType.public
|
||||
|
||||
const mustBeGuaranteed =
|
||||
validateAvailabilityData.data.rateDefinitions.filter(
|
||||
(rate) => rate.rateCode === rateCode
|
||||
)[0].mustBeGuaranteed
|
||||
|
||||
const cancellationText =
|
||||
validateAvailabilityData.data.rateDefinitions.find(
|
||||
(rate) => rate.rateCode === rateCode
|
||||
)?.cancellationText ?? ""
|
||||
|
||||
const bedTypes = availableRoomsInCategory
|
||||
.map((availRoom) => {
|
||||
const matchingRoom = hotelData?.included
|
||||
?.find((room) => room.name === availRoom.roomType)
|
||||
?.roomTypes.find(
|
||||
(roomType) => roomType.code === availRoom.roomTypeCode
|
||||
)
|
||||
|
||||
if (matchingRoom) {
|
||||
return {
|
||||
description: matchingRoom.mainBed.description,
|
||||
size: matchingRoom.mainBed.widthRange,
|
||||
value: matchingRoom.code,
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter((bed): bed is BedType => Boolean(bed))
|
||||
|
||||
selectedRoomAvailabilitySuccessCounter.add(1, {
|
||||
hotelId,
|
||||
roomStayStartDate,
|
||||
roomStayEndDate,
|
||||
adults,
|
||||
children,
|
||||
promotionCode,
|
||||
reservationProfileType,
|
||||
})
|
||||
console.info(
|
||||
"api.hotels.selectedRoomAvailability success",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params: params },
|
||||
})
|
||||
)
|
||||
|
||||
return {
|
||||
selectedRoom,
|
||||
mustBeGuaranteed,
|
||||
cancellationText,
|
||||
memberRate,
|
||||
publicRate,
|
||||
bedTypes,
|
||||
}
|
||||
}),
|
||||
}),
|
||||
rates: router({
|
||||
get: publicProcedure
|
||||
@@ -584,106 +831,9 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
hotelData: router({
|
||||
get: serviceProcedure
|
||||
.input(getlHotelDataInputSchema)
|
||||
.input(getHotelDataInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
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 ${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
|
||||
return getHotelData(input, ctx.serviceToken)
|
||||
}),
|
||||
}),
|
||||
locations: router({
|
||||
@@ -738,11 +888,16 @@ export const hotelQueryRouter = router({
|
||||
const { hotelId, startDate, endDate, adults, children, packageCodes } =
|
||||
input
|
||||
|
||||
const { lang } = ctx
|
||||
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const searchParams = new URLSearchParams({
|
||||
startDate,
|
||||
endDate,
|
||||
adults: adults.toString(),
|
||||
children: children.toString(),
|
||||
language: apiLang,
|
||||
})
|
||||
|
||||
packageCodes.forEach((code) => {
|
||||
@@ -816,14 +971,20 @@ export const hotelQueryRouter = router({
|
||||
return validatedPackagesData.data
|
||||
}),
|
||||
breakfast: safeProtectedServiceProcedure
|
||||
.input(getBreakfastPackageInput)
|
||||
.input(getBreakfastPackageInputSchema)
|
||||
.query(async function ({ ctx, input }) {
|
||||
const { lang } = ctx
|
||||
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
const params = {
|
||||
Adults: 2,
|
||||
EndDate: "2024-10-28",
|
||||
StartDate: "2024-10-25",
|
||||
Adults: input.adults,
|
||||
EndDate: dt(input.toDate).format("YYYY-MM-DD"),
|
||||
StartDate: dt(input.fromDate).format("YYYY-MM-DD"),
|
||||
language: apiLang,
|
||||
}
|
||||
const metricsData = { ...input, ...params }
|
||||
|
||||
const metricsData = { ...params, hotelId: input.hotelId }
|
||||
breakfastPackagesCounter.add(1, metricsData)
|
||||
console.info(
|
||||
"api.package.breakfast start",
|
||||
@@ -909,10 +1070,13 @@ export const hotelQueryRouter = router({
|
||||
const freeBreakfastPackage = breakfastPackages.data.find(
|
||||
(pkg) => pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||
)
|
||||
if (freeBreakfastPackage) {
|
||||
if (originalBreakfastPackage) {
|
||||
freeBreakfastPackage.originalPrice =
|
||||
originalBreakfastPackage.packagePrice
|
||||
if (freeBreakfastPackage && freeBreakfastPackage.localPrice) {
|
||||
if (
|
||||
originalBreakfastPackage &&
|
||||
originalBreakfastPackage.localPrice
|
||||
) {
|
||||
freeBreakfastPackage.localPrice.price =
|
||||
originalBreakfastPackage.localPrice.price
|
||||
}
|
||||
return [freeBreakfastPackage]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { CurrencyEnum } from "@/types/enums/currency"
|
||||
|
||||
export const getRoomPackagesInputSchema = z.object({
|
||||
hotelId: z.string(),
|
||||
@@ -11,33 +12,40 @@ export const getRoomPackagesInputSchema = z.object({
|
||||
packageCodes: z.array(z.string()).optional().default([]),
|
||||
})
|
||||
|
||||
const packagesSchema = z.array(
|
||||
z.object({
|
||||
code: z.enum([
|
||||
RoomPackageCodeEnum.PET_ROOM,
|
||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
]),
|
||||
itemCode: z.string(),
|
||||
description: z.string(),
|
||||
currency: z.string(),
|
||||
calculatedPrice: z.number(),
|
||||
inventories: z.array(
|
||||
z.object({
|
||||
date: z.string(),
|
||||
total: z.number(),
|
||||
available: z.number(),
|
||||
})
|
||||
),
|
||||
export const packagePriceSchema = z
|
||||
.object({
|
||||
currency: z.nativeEnum(CurrencyEnum),
|
||||
price: z.string(),
|
||||
totalPrice: z.string(),
|
||||
})
|
||||
)
|
||||
.optional()
|
||||
.default({
|
||||
currency: CurrencyEnum.SEK,
|
||||
price: "0",
|
||||
totalPrice: "0",
|
||||
}) // TODO: Remove optional and default when the API change has been deployed
|
||||
|
||||
export const packagesSchema = z.object({
|
||||
code: z.nativeEnum(RoomPackageCodeEnum),
|
||||
itemCode: z.string(),
|
||||
description: z.string(),
|
||||
localPrice: packagePriceSchema,
|
||||
requestedPrice: packagePriceSchema,
|
||||
inventories: z.array(
|
||||
z.object({
|
||||
date: z.string(),
|
||||
total: z.number(),
|
||||
available: z.number(),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
||||
export const getRoomPackagesSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
hotelId: z.number(),
|
||||
packages: packagesSchema,
|
||||
packages: z.array(packagesSchema),
|
||||
}),
|
||||
relationships: z
|
||||
.object({
|
||||
|
||||
@@ -87,8 +87,11 @@ export const roomSchema = z
|
||||
name: data.attributes.name,
|
||||
occupancy: data.attributes.occupancy,
|
||||
roomSize: data.attributes.roomSize,
|
||||
roomTypes: data.attributes.roomTypes,
|
||||
sortOrder: data.attributes.sortOrder,
|
||||
type: data.type,
|
||||
roomFacilities: data.attributes.roomFacilities,
|
||||
}
|
||||
})
|
||||
|
||||
export type RoomType = Pick<z.output<typeof roomSchema>, "roomTypes" | "name">
|
||||
|
||||
Reference in New Issue
Block a user