Merge develop
This commit is contained in:
@@ -19,7 +19,12 @@ import {
|
||||
shortcutsSchema,
|
||||
} from "../schemas/blocks/shortcuts"
|
||||
import { textColsRefsSchema, textColsSchema } from "../schemas/blocks/textCols"
|
||||
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
|
||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||
import {
|
||||
linkAndTitleSchema,
|
||||
linkConnectionRefs,
|
||||
} from "../schemas/linkConnection"
|
||||
import {
|
||||
contentRefsSchema as sidebarContentRefsSchema,
|
||||
contentSchema as sidebarContentSchema,
|
||||
@@ -64,12 +69,19 @@ export const contentPageTextCols = z
|
||||
})
|
||||
.merge(textColsSchema)
|
||||
|
||||
export const contentPageUspGrid = z
|
||||
.object({
|
||||
__typename: z.literal(ContentPageEnum.ContentStack.blocks.UspGrid),
|
||||
})
|
||||
.merge(uspGridSchema)
|
||||
|
||||
export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||
contentPageCards,
|
||||
contentPageContent,
|
||||
contentPageDynamicContent,
|
||||
contentPageShortcuts,
|
||||
contentPageTextCols,
|
||||
contentPageUspGrid,
|
||||
])
|
||||
|
||||
export const contentPageSidebarContent = z
|
||||
@@ -98,6 +110,22 @@ export const sidebarSchema = z.discriminatedUnion("__typename", [
|
||||
contentPageJoinLoyaltyContact,
|
||||
])
|
||||
|
||||
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 contentPageSchema = z.object({
|
||||
content_page: z.object({
|
||||
@@ -108,6 +136,7 @@ export const contentPageSchema = z.object({
|
||||
header: z.object({
|
||||
heading: z.string(),
|
||||
preamble: z.string(),
|
||||
navigation_links: navigationLinksSchema,
|
||||
}),
|
||||
system: systemSchema.merge(
|
||||
z.object({
|
||||
@@ -152,12 +181,19 @@ const contentPageTextColsRefs = z
|
||||
})
|
||||
.merge(textColsRefsSchema)
|
||||
|
||||
const contentPageUspGridRefs = z
|
||||
.object({
|
||||
__typename: z.literal(ContentPageEnum.ContentStack.blocks.UspGrid),
|
||||
})
|
||||
.merge(uspGridRefsSchema)
|
||||
|
||||
const contentPageBlockRefsItem = z.discriminatedUnion("__typename", [
|
||||
contentPageBlockContentRefs,
|
||||
contentPageShortcutsRefs,
|
||||
contentPageCardsRefs,
|
||||
contentPageDynamicContentRefs,
|
||||
contentPageTextColsRefs,
|
||||
contentPageUspGridRefs,
|
||||
])
|
||||
|
||||
const contentPageSidebarContentRef = z
|
||||
@@ -179,8 +215,13 @@ const contentPageSidebarRefsItem = z.discriminatedUnion("__typename", [
|
||||
contentPageSidebarJoinLoyaltyContactRef,
|
||||
])
|
||||
|
||||
const contentPageHeaderRefs = z.object({
|
||||
navigation_links: z.array(linkConnectionRefs),
|
||||
})
|
||||
|
||||
export const contentPageRefsSchema = z.object({
|
||||
content_page: z.object({
|
||||
header: contentPageHeaderRefs,
|
||||
blocks: discriminatedUnionArray(
|
||||
contentPageBlockRefsItem.options
|
||||
).nullable(),
|
||||
|
||||
@@ -147,6 +147,12 @@ export function getConnections({ content_page }: ContentPageRefs) {
|
||||
}
|
||||
break
|
||||
}
|
||||
case ContentPageEnum.ContentStack.blocks.UspGrid: {
|
||||
if (block.usp_grid.length) {
|
||||
connections.push(...block.usp_grid)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@ import { breadcrumbsRouter } from "./breadcrumbs"
|
||||
import { contentPageRouter } from "./contentPage"
|
||||
import { hotelPageRouter } from "./hotelPage"
|
||||
import { languageSwitcherRouter } from "./languageSwitcher"
|
||||
import { loyaltyLevelRouter } from "./loyaltyLevel"
|
||||
import { loyaltyPageRouter } from "./loyaltyPage"
|
||||
import { metaDataRouter } from "./metadata"
|
||||
import { myPagesRouter } from "./myPages"
|
||||
import { rewardRouter } from "./reward"
|
||||
|
||||
export const contentstackRouter = router({
|
||||
accountPage: accountPageRouter,
|
||||
@@ -22,4 +24,6 @@ export const contentstackRouter = router({
|
||||
contentPage: contentPageRouter,
|
||||
myPages: myPagesRouter,
|
||||
metaData: metaDataRouter,
|
||||
rewards: rewardRouter,
|
||||
loyaltyLevels: loyaltyLevelRouter,
|
||||
})
|
||||
|
||||
5
server/routers/contentstack/loyaltyLevel/index.ts
Normal file
5
server/routers/contentstack/loyaltyLevel/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { loyaltyLevelQueryRouter } from "./query"
|
||||
|
||||
export const loyaltyLevelRouter = mergeRouters(loyaltyLevelQueryRouter)
|
||||
7
server/routers/contentstack/loyaltyLevel/input.ts
Normal file
7
server/routers/contentstack/loyaltyLevel/input.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||
|
||||
export const loyaltyLevelInput = z.object({
|
||||
level: z.nativeEnum(MembershipLevelEnum),
|
||||
})
|
||||
24
server/routers/contentstack/loyaltyLevel/output.ts
Normal file
24
server/routers/contentstack/loyaltyLevel/output.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||
|
||||
export const validateLoyaltyLevelsSchema = z
|
||||
.object({
|
||||
all_loyalty_level: z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
level_id: z.nativeEnum(MembershipLevelEnum),
|
||||
name: z.string(),
|
||||
user_facing_tag: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
required_nights: z.number().optional().nullable(),
|
||||
required_points: z.number(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((data) => data.all_loyalty_level.items)
|
||||
|
||||
export type LoyaltyLevelsResponse = z.input<typeof validateLoyaltyLevelsSchema>
|
||||
|
||||
export type LoyaltyLevel = z.output<typeof validateLoyaltyLevelsSchema>[0]
|
||||
147
server/routers/contentstack/loyaltyLevel/query.ts
Normal file
147
server/routers/contentstack/loyaltyLevel/query.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import {
|
||||
MembershipLevel,
|
||||
MembershipLevelEnum,
|
||||
} from "@/constants/membershipLevels"
|
||||
import {
|
||||
GetAllLoyaltyLevels,
|
||||
GetLoyaltyLevel,
|
||||
} from "@/lib/graphql/Query/LoyaltyLevels.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { Context } from "@/server/context"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
||||
|
||||
import { loyaltyLevelInput } from "./input"
|
||||
import { LoyaltyLevelsResponse, validateLoyaltyLevelsSchema } from "./output"
|
||||
|
||||
const meter = metrics.getMeter("trpc.loyaltyLevel")
|
||||
// OpenTelemetry metrics: Loyalty Level
|
||||
const getAllLoyaltyLevelCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.all"
|
||||
)
|
||||
|
||||
const getAllLoyaltyLevelSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.all-success"
|
||||
)
|
||||
const getAllLoyaltyLevelFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.loyaltyLevel.all-fail"
|
||||
)
|
||||
|
||||
export async function getAllLoyaltyLevels(ctx: Context) {
|
||||
getAllLoyaltyLevelCounter.add(1)
|
||||
|
||||
// Ideally we should fetch all available tiers from API, but since they
|
||||
// are static, we can just use the enum values. We want to know which
|
||||
// levels we are fetching so that we can use tags to cache them
|
||||
const allLevelIds = Object.values(MembershipLevelEnum)
|
||||
|
||||
const tags = allLevelIds.map((levelId) =>
|
||||
generateLoyaltyConfigTag(ctx.lang, "loyalty_level", levelId)
|
||||
)
|
||||
|
||||
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
||||
GetAllLoyaltyLevels,
|
||||
{ lang: ctx.lang, level_ids: allLevelIds },
|
||||
{ next: { tags }, cache: "force-cache" }
|
||||
)
|
||||
|
||||
if (!loyaltyLevelsConfigResponse.data) {
|
||||
getAllLoyaltyLevelFailCounter.add(1)
|
||||
const notFoundError = notFound(loyaltyLevelsConfigResponse)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevels not found error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang: ctx.lang,
|
||||
},
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedLoyaltyLevels = validateLoyaltyLevelsSchema.safeParse(
|
||||
loyaltyLevelsConfigResponse.data
|
||||
)
|
||||
if (!validatedLoyaltyLevels.success) {
|
||||
getAllLoyaltyLevelFailCounter.add(1)
|
||||
console.error(validatedLoyaltyLevels.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang: ctx.lang,
|
||||
},
|
||||
error: validatedLoyaltyLevels.error,
|
||||
})
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
getAllLoyaltyLevelSuccessCounter.add(1)
|
||||
return validatedLoyaltyLevels.data
|
||||
}
|
||||
|
||||
export async function getLoyaltyLevel(ctx: Context, level_id: MembershipLevel) {
|
||||
getAllLoyaltyLevelCounter.add(1)
|
||||
|
||||
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
|
||||
) {
|
||||
getAllLoyaltyLevelFailCounter.add(1)
|
||||
const notFoundError = notFound(loyaltyLevelsConfigResponse)
|
||||
console.error(
|
||||
"contentstack.loyaltyLevels 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) {
|
||||
getAllLoyaltyLevelFailCounter.add(1)
|
||||
console.error(validatedLoyaltyLevels.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
JSON.stringify({
|
||||
query: { lang: ctx.lang, level_id },
|
||||
error: validatedLoyaltyLevels.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getAllLoyaltyLevelSuccessCounter.add(1)
|
||||
return validatedLoyaltyLevels.data[0]
|
||||
}
|
||||
|
||||
export const loyaltyLevelQueryRouter = router({
|
||||
byLevel: contentstackBaseProcedure
|
||||
.input(loyaltyLevelInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
return getLoyaltyLevel(ctx, input.level)
|
||||
}),
|
||||
all: contentstackBaseProcedure.query(async function ({ ctx }) {
|
||||
return getAllLoyaltyLevels(ctx)
|
||||
}),
|
||||
})
|
||||
5
server/routers/contentstack/reward/index.ts
Normal file
5
server/routers/contentstack/reward/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { mergeRouters } from "@/server/trpc"
|
||||
|
||||
import { rewardQueryRouter } from "./query"
|
||||
|
||||
export const rewardRouter = mergeRouters(rewardQueryRouter)
|
||||
19
server/routers/contentstack/reward/input.ts
Normal file
19
server/routers/contentstack/reward/input.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||
|
||||
export const rewardsByLevelInput = z.object({
|
||||
level_id: z.nativeEnum(MembershipLevelEnum),
|
||||
unique: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export const rewardsAllInput = z
|
||||
.object({ unique: z.boolean() })
|
||||
.default({ unique: false })
|
||||
|
||||
export const rewardsCurrentInput = z.object({
|
||||
limit: z.number().min(1).default(3),
|
||||
cursor: z.number().optional().default(0),
|
||||
lang: z.nativeEnum(Lang).optional(),
|
||||
})
|
||||
82
server/routers/contentstack/reward/output.ts
Normal file
82
server/routers/contentstack/reward/output.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||
|
||||
export const validateApiRewardSchema = z.object({
|
||||
data: z.array(
|
||||
z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
rewardId: z.string().optional(),
|
||||
redeemLocation: z.string().optional(),
|
||||
autoApplyReward: z.boolean().default(false),
|
||||
rewardType: z.string().optional(),
|
||||
rewardTierLevel: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
),
|
||||
})
|
||||
|
||||
enum TierKey {
|
||||
tier1 = MembershipLevelEnum.L1,
|
||||
tier2 = MembershipLevelEnum.L2,
|
||||
tier3 = MembershipLevelEnum.L3,
|
||||
tier4 = MembershipLevelEnum.L4,
|
||||
tier5 = MembershipLevelEnum.L5,
|
||||
tier6 = MembershipLevelEnum.L6,
|
||||
tier7 = MembershipLevelEnum.L7,
|
||||
}
|
||||
|
||||
type Key = keyof typeof TierKey
|
||||
|
||||
export const validateApiTierRewardsSchema = z.record(
|
||||
z.nativeEnum(TierKey).transform((data) => {
|
||||
return TierKey[data as unknown as Key]
|
||||
}),
|
||||
z.array(
|
||||
z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
id: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
status: z.string().optional(),
|
||||
rewardId: z.string().optional(),
|
||||
redeemLocation: z.string().optional(),
|
||||
autoApplyReward: z.boolean().default(false),
|
||||
rewardType: z.string().optional(),
|
||||
rewardTierLevel: z.string().optional(),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
)
|
||||
|
||||
export const validateCmsRewardsSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
all_reward: z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
taxonomies: z.array(
|
||||
z.object({
|
||||
term_uid: z.string().optional(),
|
||||
})
|
||||
),
|
||||
label: z.string().optional(),
|
||||
reward_id: z.string(),
|
||||
grouped_label: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
grouped_description: z.string().optional(),
|
||||
value: z.string().optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.transform((data) => data.data.all_reward.items)
|
||||
|
||||
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
|
||||
|
||||
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]
|
||||
371
server/routers/contentstack/reward/query.ts
Normal file
371
server/routers/contentstack/reward/query.ts
Normal file
@@ -0,0 +1,371 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import * as api from "@/lib/api"
|
||||
import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { Context } from "@/server/context"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import {
|
||||
contentStackBaseWithProfileServiceProcedure,
|
||||
contentStackBaseWithProtectedProcedure,
|
||||
router,
|
||||
} from "@/server/trpc"
|
||||
|
||||
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
||||
|
||||
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
|
||||
import {
|
||||
rewardsAllInput,
|
||||
rewardsByLevelInput,
|
||||
rewardsCurrentInput,
|
||||
} from "./input"
|
||||
import {
|
||||
CmsRewardsResponse,
|
||||
Reward,
|
||||
validateApiRewardSchema,
|
||||
validateApiTierRewardsSchema,
|
||||
validateCmsRewardsSchema,
|
||||
} from "./output"
|
||||
|
||||
const meter = metrics.getMeter("trpc.reward")
|
||||
// OpenTelemetry metrics: Reward
|
||||
|
||||
const getCurrentRewardCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.current"
|
||||
)
|
||||
const getCurrentRewardSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.current-success"
|
||||
)
|
||||
|
||||
const getCurrentRewardFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.current-fail"
|
||||
)
|
||||
|
||||
const getByLevelRewardCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.byLevel"
|
||||
)
|
||||
const getByLevelRewardSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.byLevel-success"
|
||||
)
|
||||
|
||||
const getByLevelRewardFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.byLevel-fail"
|
||||
)
|
||||
|
||||
const getAllRewardCounter = meter.createCounter("trpc.contentstack.reward.all")
|
||||
|
||||
const getAllRewardSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.all-success"
|
||||
)
|
||||
const getAllRewardFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.all-fail"
|
||||
)
|
||||
|
||||
function getUniqueRewardIds(rewardIds: string[]) {
|
||||
const uniqueRewardIds = new Set(rewardIds)
|
||||
return Array.from(uniqueRewardIds)
|
||||
}
|
||||
|
||||
async function getAllApiRewards(ctx: Context & { serviceToken: string }) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.tierRewards, {
|
||||
cache: undefined, // override defaultOptions
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
// One hour. Since the service token is refreshed every hour, this is the longest cache we can have.
|
||||
next: { revalidate: 60 * 60 },
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.rewards.tierRewards error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiTierRewards.error),
|
||||
})
|
||||
console.error(validatedApiTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiTierRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return validatedApiTierRewards.data
|
||||
}
|
||||
|
||||
async function getCmsRewards(locale: Lang, rewardIds: string[]) {
|
||||
const tags = rewardIds.map((id) =>
|
||||
generateLoyaltyConfigTag(locale, "reward", id)
|
||||
)
|
||||
|
||||
const cmsRewardsResponse = await request<CmsRewardsResponse>(
|
||||
GetRewards,
|
||||
{
|
||||
locale: locale,
|
||||
rewardIds,
|
||||
},
|
||||
{ next: { tags }, cache: "force-cache" }
|
||||
)
|
||||
|
||||
if (!cmsRewardsResponse.data) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
lang: locale,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(cmsRewardsResponse.data),
|
||||
})
|
||||
const notFoundError = notFound(cmsRewardsResponse)
|
||||
console.error(
|
||||
"contentstack.rewards not found error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
locale,
|
||||
rewardIds,
|
||||
},
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedCmsRewards =
|
||||
validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
|
||||
|
||||
if (!validatedCmsRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
locale,
|
||||
rewardIds,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedCmsRewards.error),
|
||||
})
|
||||
console.error(validatedCmsRewards.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
JSON.stringify({
|
||||
query: { locale, rewardIds },
|
||||
error: validatedCmsRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return validatedCmsRewards.data
|
||||
}
|
||||
|
||||
export const rewardQueryRouter = router({
|
||||
current: contentStackBaseWithProtectedProcedure
|
||||
.input(rewardsCurrentInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
getCurrentRewardCounter.add(1)
|
||||
|
||||
const { limit, cursor } = input
|
||||
|
||||
const apiResponse = await api.get(api.endpoints.v1.rewards, {
|
||||
cache: undefined, // override defaultOptions
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
next: { revalidate: 60 * 60 },
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.reward error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
|
||||
const validatedApiRewards = validateApiRewardSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiRewards.success) {
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
locale: ctx.lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiRewards.error),
|
||||
})
|
||||
console.error(validatedApiRewards.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
JSON.stringify({
|
||||
query: { locale: ctx.lang },
|
||||
error: validatedApiRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const rewardIds = validatedApiRewards.data.data
|
||||
.map((reward) => reward?.rewardId)
|
||||
.filter(Boolean)
|
||||
.sort() as string[]
|
||||
|
||||
const slicedData = rewardIds.slice(cursor, limit + cursor)
|
||||
|
||||
const cmsRewards = await getCmsRewards(ctx.lang, slicedData)
|
||||
|
||||
if (!cmsRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
const nextCursor =
|
||||
limit + cursor < rewardIds.length ? limit + cursor : undefined
|
||||
|
||||
getCurrentRewardSuccessCounter.add(1)
|
||||
return {
|
||||
rewards: cmsRewards,
|
||||
nextCursor,
|
||||
}
|
||||
}),
|
||||
byLevel: contentStackBaseWithProfileServiceProcedure
|
||||
.input(rewardsByLevelInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
getByLevelRewardCounter.add(1)
|
||||
const { level_id } = input
|
||||
|
||||
const allUpcomingApiRewards = await getAllApiRewards(ctx)
|
||||
|
||||
if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
|
||||
getByLevelRewardFailCounter.add(1)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
let apiRewards = allUpcomingApiRewards[level_id]!
|
||||
|
||||
if (input.unique) {
|
||||
apiRewards = allUpcomingApiRewards[level_id]!.filter(
|
||||
(reward) => reward?.rewardTierLevel === level_id
|
||||
)
|
||||
}
|
||||
|
||||
const rewardIds = apiRewards
|
||||
.map((reward) => reward?.rewardId)
|
||||
.filter((id): id is string => Boolean(id))
|
||||
|
||||
const contentStackRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||
if (!contentStackRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
|
||||
|
||||
const levelsWithRewards = apiRewards
|
||||
.map((reward) => {
|
||||
const contentStackReward = contentStackRewards.find((r) => {
|
||||
return r.reward_id === reward?.rewardId
|
||||
})
|
||||
|
||||
if (contentStackReward) {
|
||||
return contentStackReward
|
||||
} else {
|
||||
console.error("No contentStackReward found", reward?.rewardId)
|
||||
}
|
||||
})
|
||||
.filter((reward): reward is Reward => Boolean(reward))
|
||||
|
||||
getByLevelRewardSuccessCounter.add(1)
|
||||
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
|
||||
}),
|
||||
all: contentStackBaseWithProfileServiceProcedure
|
||||
.input(rewardsAllInput)
|
||||
.query(async function ({ input, ctx }) {
|
||||
getAllRewardCounter.add(1)
|
||||
const allApiRewards = await getAllApiRewards(ctx)
|
||||
|
||||
if (!allApiRewards) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rewardIds = Object.values(allApiRewards)
|
||||
.flatMap((level) => level.map((reward) => reward?.rewardId))
|
||||
.filter((id): id is string => Boolean(id))
|
||||
|
||||
const contentStackRewards = await getCmsRewards(
|
||||
ctx.lang,
|
||||
getUniqueRewardIds(rewardIds)
|
||||
)
|
||||
|
||||
if (!contentStackRewards) {
|
||||
return []
|
||||
}
|
||||
|
||||
const loyaltyLevelsConfig = await getAllLoyaltyLevels(ctx)
|
||||
const levelsWithRewards = Object.entries(allApiRewards).map(
|
||||
([level, rewards]) => {
|
||||
const combinedRewards = rewards
|
||||
.filter((r) => (input.unique ? r?.rewardTierLevel === level : true))
|
||||
.map((reward) => {
|
||||
const contentStackReward = contentStackRewards.find((r) => {
|
||||
return r.reward_id === reward?.rewardId
|
||||
})
|
||||
|
||||
if (contentStackReward) {
|
||||
return contentStackReward
|
||||
} else {
|
||||
console.error("No contentStackReward found", reward?.rewardId)
|
||||
}
|
||||
})
|
||||
.filter((reward): reward is Reward => Boolean(reward))
|
||||
|
||||
const levelConfig = loyaltyLevelsConfig.find(
|
||||
(l) => l.level_id === level
|
||||
)
|
||||
|
||||
if (!levelConfig) {
|
||||
getAllRewardFailCounter.add(1)
|
||||
|
||||
console.error("contentstack.loyaltyLevels level not found")
|
||||
throw notFound()
|
||||
}
|
||||
return { ...levelConfig, rewards: combinedRewards }
|
||||
}
|
||||
)
|
||||
|
||||
getAllRewardSuccessCounter.add(1)
|
||||
return levelsWithRewards
|
||||
}),
|
||||
})
|
||||
@@ -15,17 +15,10 @@ export const cardBlockSchema = z.object({
|
||||
body_text: z.string().optional().default(""),
|
||||
has_primary_button: z.boolean().default(false),
|
||||
has_secondary_button: z.boolean().default(false),
|
||||
has_sidepeek_button: z.boolean().optional().default(false),
|
||||
heading: z.string().optional().default(""),
|
||||
is_content_card: z.boolean().optional().default(false),
|
||||
primary_button: buttonSchema,
|
||||
scripted_top_title: z.string().optional(),
|
||||
secondary_button: buttonSchema,
|
||||
sidepeek_button: z
|
||||
.object({
|
||||
call_to_action_text: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
system: systemSchema,
|
||||
title: z.string().optional(),
|
||||
})
|
||||
@@ -36,23 +29,53 @@ export function transformCardBlock(card: typeof cardBlockSchema._type) {
|
||||
backgroundImage: card.background_image,
|
||||
body_text: card.body_text,
|
||||
heading: card.heading,
|
||||
isContentCard: card.is_content_card,
|
||||
primaryButton: card.has_primary_button ? card.primary_button : undefined,
|
||||
scripted_top_title: card.scripted_top_title,
|
||||
secondaryButton: card.has_secondary_button
|
||||
? card.secondary_button
|
||||
: undefined,
|
||||
sidePeekButton:
|
||||
card.has_sidepeek_button && card.sidepeek_button?.call_to_action_text
|
||||
? {
|
||||
title: card.sidepeek_button.call_to_action_text,
|
||||
}
|
||||
: undefined,
|
||||
system: card.system,
|
||||
title: card.title,
|
||||
}
|
||||
}
|
||||
|
||||
export const teaserCardBlockSchema = z.object({
|
||||
__typename: z.literal(CardsGridEnum.cards.TeaserCard),
|
||||
heading: z.string().default(""),
|
||||
body_text: z.string().default(""),
|
||||
image: tempImageVaultAssetSchema,
|
||||
primary_button: buttonSchema,
|
||||
secondary_button: buttonSchema,
|
||||
has_primary_button: z.boolean().default(false),
|
||||
has_secondary_button: z.boolean().default(false),
|
||||
has_sidepeek_button: z.boolean().default(false),
|
||||
side_peek_button: z
|
||||
.object({
|
||||
title: z.string().optional().default(""),
|
||||
})
|
||||
.optional(),
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
export function transformTeaserCardBlock(
|
||||
card: typeof teaserCardBlockSchema._type
|
||||
) {
|
||||
return {
|
||||
__typename: card.__typename,
|
||||
body_text: card.body_text,
|
||||
heading: card.heading,
|
||||
primaryButton: card.has_primary_button ? card.primary_button : undefined,
|
||||
secondaryButton: card.has_secondary_button
|
||||
? card.secondary_button
|
||||
: undefined,
|
||||
sidePeekButton: card.has_sidepeek_button
|
||||
? card.side_peek_button
|
||||
: undefined,
|
||||
image: card.image,
|
||||
system: card.system,
|
||||
}
|
||||
}
|
||||
|
||||
const loyaltyCardBlockSchema = z.object({
|
||||
__typename: z.literal(CardsGridEnum.cards.LoyaltyCard),
|
||||
body_text: z.string().optional(),
|
||||
@@ -77,6 +100,7 @@ export const cardsGridSchema = z.object({
|
||||
node: z.discriminatedUnion("__typename", [
|
||||
cardBlockSchema,
|
||||
loyaltyCardBlockSchema,
|
||||
teaserCardBlockSchema,
|
||||
]),
|
||||
})
|
||||
),
|
||||
@@ -95,6 +119,8 @@ export const cardsGridSchema = z.object({
|
||||
cards: data.cardConnection.edges.map((card) => {
|
||||
if (card.node.__typename === CardsGridEnum.cards.Card) {
|
||||
return transformCardBlock(card.node)
|
||||
} else if (card.node.__typename === CardsGridEnum.cards.TeaserCard) {
|
||||
return transformTeaserCardBlock(card.node)
|
||||
} else {
|
||||
return {
|
||||
__typename: card.node.__typename,
|
||||
@@ -118,7 +144,11 @@ export const cardBlockRefsSchema = z.object({
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
export function transformCardBlockRefs(card: typeof cardBlockRefsSchema._type) {
|
||||
export function transformCardBlockRefs(
|
||||
card:
|
||||
| typeof cardBlockRefsSchema._type
|
||||
| typeof teaserCardBlockRefsSchema._type
|
||||
) {
|
||||
const cards = [card.system]
|
||||
if (card.primary_button) {
|
||||
cards.push(card.primary_button)
|
||||
@@ -135,6 +165,13 @@ const loyaltyCardBlockRefsSchema = z.object({
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
export const teaserCardBlockRefsSchema = z.object({
|
||||
__typename: z.literal(CardsGridEnum.cards.TeaserCard),
|
||||
primary_button: linkConnectionRefsSchema,
|
||||
secondary_button: linkConnectionRefsSchema,
|
||||
system: systemSchema,
|
||||
})
|
||||
|
||||
export const cardGridRefsSchema = z.object({
|
||||
cards_grid: z
|
||||
.object({
|
||||
@@ -144,6 +181,7 @@ export const cardGridRefsSchema = z.object({
|
||||
node: z.discriminatedUnion("__typename", [
|
||||
cardBlockRefsSchema,
|
||||
loyaltyCardBlockRefsSchema,
|
||||
teaserCardBlockRefsSchema,
|
||||
]),
|
||||
})
|
||||
),
|
||||
@@ -152,7 +190,10 @@ export const cardGridRefsSchema = z.object({
|
||||
.transform((data) => {
|
||||
return data.cardConnection.edges
|
||||
.map(({ node }) => {
|
||||
if (node.__typename === CardsGridEnum.cards.Card) {
|
||||
if (
|
||||
node.__typename === CardsGridEnum.cards.Card ||
|
||||
node.__typename === CardsGridEnum.cards.TeaserCard
|
||||
) {
|
||||
return transformCardBlockRefs(node)
|
||||
} else {
|
||||
const loyaltyCards = [node.system]
|
||||
|
||||
@@ -1,47 +1,48 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
|
||||
|
||||
import { imageRefsSchema, imageSchema } from "./image"
|
||||
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
import { ContentEnum } from "@/types/enums/content"
|
||||
|
||||
export const textColsSchema = z
|
||||
.object({
|
||||
typename: z
|
||||
.literal(BlocksEnums.block.TextCols)
|
||||
.optional()
|
||||
.default(BlocksEnums.block.TextCols),
|
||||
text_cols: z.object({
|
||||
columns: z.array(
|
||||
z.object({
|
||||
title: z.string().optional().default(""),
|
||||
text: z.object({
|
||||
json: z.any(), // JSON
|
||||
embedded_itemsConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: z.discriminatedUnion("__typename", [
|
||||
export const textColsSchema = z.object({
|
||||
typename: z
|
||||
.literal(BlocksEnums.block.TextCols)
|
||||
.optional()
|
||||
.default(BlocksEnums.block.TextCols),
|
||||
text_cols: z.object({
|
||||
columns: z.array(
|
||||
z.object({
|
||||
title: z.string().optional().default(""),
|
||||
text: z.object({
|
||||
json: z.any(), // JSON
|
||||
embedded_itemsConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: z
|
||||
.discriminatedUnion("__typename", [
|
||||
imageSchema,
|
||||
pageLinks.contentPageSchema,
|
||||
pageLinks.hotelPageSchema,
|
||||
pageLinks.loyaltyPageSchema,
|
||||
])
|
||||
.transform((data) => {
|
||||
const link = pageLinks.transform(data)
|
||||
if (link) {
|
||||
return link
|
||||
}
|
||||
return data
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
.transform((data) => {
|
||||
const link = pageLinks.transform(data)
|
||||
if (link) {
|
||||
return link
|
||||
}
|
||||
return data
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
|
||||
const actualRefs = z.discriminatedUnion("__typename", [
|
||||
pageLinks.contentPageRefSchema,
|
||||
@@ -53,9 +54,9 @@ type Refs = {
|
||||
node: z.TypeOf<typeof actualRefs>
|
||||
}
|
||||
|
||||
export const textColsRefsSchema = z
|
||||
.object({
|
||||
text_cols: z.object({
|
||||
export const textColsRefsSchema = z.object({
|
||||
text_cols: z
|
||||
.object({
|
||||
columns: z.array(
|
||||
z.object({
|
||||
text: z.object({
|
||||
@@ -65,20 +66,22 @@ export const textColsRefsSchema = z
|
||||
node: z.discriminatedUnion("__typename", [
|
||||
imageRefsSchema,
|
||||
...actualRefs.options,
|
||||
])
|
||||
]),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}).transform(data => {
|
||||
return data.columns.map(column => {
|
||||
const filtered = column.text.embedded_itemsConnection.edges
|
||||
.filter(
|
||||
block => block.node.__typename !== ContentEnum.blocks.SysAsset
|
||||
})
|
||||
.transform((data) => {
|
||||
return data.columns
|
||||
.map((column) => {
|
||||
const filtered = column.text.embedded_itemsConnection.edges.filter(
|
||||
(block) => block.node.__typename !== ContentEnum.blocks.SysAsset
|
||||
) as unknown as Refs[] // TS issue with filtered out types
|
||||
return filtered.map(({ node }) => node.system)
|
||||
}).flat()
|
||||
return filtered.map(({ node }) => node.system)
|
||||
})
|
||||
.flat()
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
102
server/routers/contentstack/schemas/blocks/uspGrid.ts
Normal file
102
server/routers/contentstack/schemas/blocks/uspGrid.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
|
||||
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
import { UspGridEnum } from "@/types/enums/uspGrid"
|
||||
|
||||
const uspCardSchema = z.object({
|
||||
icon: UspGridEnum.uspIcons,
|
||||
text: z.object({
|
||||
json: z.any(), // JSON
|
||||
embedded_itemsConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: z
|
||||
.discriminatedUnion("__typename", [
|
||||
pageLinks.accountPageSchema,
|
||||
pageLinks.contentPageSchema,
|
||||
pageLinks.hotelPageSchema,
|
||||
pageLinks.loyaltyPageSchema,
|
||||
])
|
||||
.transform((data) => {
|
||||
const link = pageLinks.transform(data)
|
||||
if (link) {
|
||||
return link
|
||||
}
|
||||
return data
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export const uspGridSchema = z.object({
|
||||
typename: z
|
||||
.literal(BlocksEnums.block.UspGrid)
|
||||
.optional()
|
||||
.default(BlocksEnums.block.UspGrid),
|
||||
usp_grid: z
|
||||
.object({
|
||||
cardsConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: z.object({
|
||||
usp_card: z.array(uspCardSchema),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
return {
|
||||
usp_card: data.cardsConnection.edges.flatMap(
|
||||
(edge) => edge.node.usp_card
|
||||
),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const actualRefs = z.discriminatedUnion("__typename", [
|
||||
pageLinks.accountPageRefSchema,
|
||||
pageLinks.contentPageRefSchema,
|
||||
pageLinks.hotelPageRefSchema,
|
||||
pageLinks.loyaltyPageRefSchema,
|
||||
])
|
||||
|
||||
export const uspGridRefsSchema = z.object({
|
||||
usp_grid: z
|
||||
.object({
|
||||
cardsConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: z.object({
|
||||
usp_card: z.array(
|
||||
z.object({
|
||||
text: z.object({
|
||||
embedded_itemsConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: actualRefs,
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
return data.cardsConnection.edges.flatMap(({ node }) =>
|
||||
node.usp_card.flatMap((card) =>
|
||||
card.text.embedded_itemsConnection.edges.map(
|
||||
({ node }) => node.system
|
||||
)
|
||||
)
|
||||
)
|
||||
}),
|
||||
})
|
||||
72
server/routers/contentstack/schemas/linkConnection.ts
Normal file
72
server/routers/contentstack/schemas/linkConnection.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { discriminatedUnion } from "@/lib/discriminatedUnion"
|
||||
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
|
||||
|
||||
const linkUnionSchema = z.discriminatedUnion("__typename", [
|
||||
pageLinks.contentPageSchema,
|
||||
pageLinks.hotelPageSchema,
|
||||
pageLinks.loyaltyPageSchema,
|
||||
])
|
||||
|
||||
const titleSchema = z.object({
|
||||
title: z.string().optional().default(""),
|
||||
})
|
||||
|
||||
export const linkConnectionSchema = z
|
||||
.object({
|
||||
linkConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: discriminatedUnion(linkUnionSchema.options),
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.linkConnection.edges.length) {
|
||||
const link = pageLinks.transform(data.linkConnection.edges[0].node)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
link: null,
|
||||
}
|
||||
})
|
||||
|
||||
export const linkAndTitleSchema = z.intersection(
|
||||
linkConnectionSchema,
|
||||
titleSchema
|
||||
)
|
||||
|
||||
const linkRefsUnionSchema = z.discriminatedUnion("__typename", [
|
||||
pageLinks.contentPageRefSchema,
|
||||
pageLinks.hotelPageRefSchema,
|
||||
pageLinks.loyaltyPageRefSchema,
|
||||
])
|
||||
|
||||
export const linkConnectionRefs = z
|
||||
.object({
|
||||
linkConnection: z.object({
|
||||
edges: z.array(
|
||||
z.object({
|
||||
node: linkRefsUnionSchema,
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
if (data.linkConnection.edges.length) {
|
||||
const link = pageLinks.transformRef(data.linkConnection.edges[0].node)
|
||||
if (link) {
|
||||
return {
|
||||
link,
|
||||
}
|
||||
}
|
||||
}
|
||||
return { link: null }
|
||||
})
|
||||
@@ -2,6 +2,13 @@ import { z } from "zod"
|
||||
|
||||
import { toLang } from "@/server/utils"
|
||||
|
||||
import { getPoiGroupByCategoryName } from "./utils"
|
||||
|
||||
import {
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
|
||||
const ratingsSchema = z
|
||||
.object({
|
||||
tripAdvisor: z.object({
|
||||
@@ -152,12 +159,9 @@ const hotelContentSchema = z.object({
|
||||
const detailedFacilitySchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
code: z.string().optional(),
|
||||
applyToAllHotels: z.boolean(),
|
||||
public: z.boolean(),
|
||||
icon: z.string(),
|
||||
iconName: z.string().optional(),
|
||||
sortOrder: z.number(),
|
||||
filter: z.string().optional(),
|
||||
})
|
||||
|
||||
const healthFacilitySchema = z.object({
|
||||
@@ -216,32 +220,15 @@ const rewardNightSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
const poiCategories = z.enum([
|
||||
"Airport",
|
||||
"Amusement park",
|
||||
"Bus terminal",
|
||||
"Fair",
|
||||
"Hospital",
|
||||
"Hotel",
|
||||
"Marketing city",
|
||||
"Museum",
|
||||
"Nearby companies",
|
||||
"Parking / Garage",
|
||||
"Restaurant",
|
||||
"Shopping",
|
||||
"Sports",
|
||||
"Theatre",
|
||||
"Tourist",
|
||||
"Transportations",
|
||||
"Zoo",
|
||||
])
|
||||
const poiGroups = z.nativeEnum(PointOfInterestGroupEnum)
|
||||
const poiCategoryNames = z.nativeEnum(PointOfInterestCategoryNameEnum)
|
||||
|
||||
export const pointOfInterestSchema = z
|
||||
.object({
|
||||
name: z.string(),
|
||||
distance: z.number(),
|
||||
category: z.object({
|
||||
name: poiCategories,
|
||||
name: poiCategoryNames,
|
||||
group: z.string(),
|
||||
}),
|
||||
location: locationSchema,
|
||||
@@ -250,7 +237,8 @@ export const pointOfInterestSchema = z
|
||||
.transform((poi) => ({
|
||||
name: poi.name,
|
||||
distance: poi.distance,
|
||||
category: poi.category.name,
|
||||
categoryName: poi.category.name,
|
||||
group: getPoiGroupByCategoryName(poi.category.name),
|
||||
coordinates: {
|
||||
lat: poi.location.latitude,
|
||||
lng: poi.location.longitude,
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
} from "@/server/errors/trpc"
|
||||
import { extractHotelImages } from "@/server/routers/utils/hotels"
|
||||
import {
|
||||
contentStackUidWithServiceProcedure,
|
||||
contentStackUidWithHotelServiceProcedure,
|
||||
hotelServiceProcedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
serviceProcedure,
|
||||
} from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
@@ -83,7 +83,7 @@ async function getContentstackData(
|
||||
}
|
||||
|
||||
export const hotelQueryRouter = router({
|
||||
get: contentStackUidWithServiceProcedure
|
||||
get: contentStackUidWithHotelServiceProcedure
|
||||
.input(getHotelInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { lang, uid } = ctx
|
||||
@@ -178,34 +178,34 @@ export const hotelQueryRouter = router({
|
||||
|
||||
const roomCategories = included
|
||||
? included
|
||||
.filter((item) => item.type === "roomcategories")
|
||||
.map((roomCategory) => {
|
||||
const validatedRoom = roomSchema.safeParse(roomCategory)
|
||||
if (!validatedRoom.success) {
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
lang,
|
||||
include,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(
|
||||
validatedRoom.error.issues.map(({ code, message }) => ({
|
||||
code,
|
||||
message,
|
||||
}))
|
||||
),
|
||||
})
|
||||
console.error(
|
||||
"api.hotels.hotel validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedRoom.error,
|
||||
.filter((item) => item.type === "roomcategories")
|
||||
.map((roomCategory) => {
|
||||
const validatedRoom = roomSchema.safeParse(roomCategory)
|
||||
if (!validatedRoom.success) {
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
lang,
|
||||
include,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(
|
||||
validatedRoom.error.issues.map(({ code, message }) => ({
|
||||
code,
|
||||
message,
|
||||
}))
|
||||
),
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
console.error(
|
||||
"api.hotels.hotel validation error",
|
||||
JSON.stringify({
|
||||
query: { hotelId, params },
|
||||
error: validatedRoom.error,
|
||||
})
|
||||
)
|
||||
throw badRequestError()
|
||||
}
|
||||
|
||||
return validatedRoom.data
|
||||
})
|
||||
return validatedRoom.data
|
||||
})
|
||||
: []
|
||||
|
||||
const activities = contentstackData?.content
|
||||
@@ -233,7 +233,7 @@ export const hotelQueryRouter = router({
|
||||
}
|
||||
}),
|
||||
availability: router({
|
||||
get: serviceProcedure
|
||||
get: hotelServiceProcedure
|
||||
.input(getAvailabilityInputSchema)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const {
|
||||
@@ -395,7 +395,7 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
hotelData: router({
|
||||
get: serviceProcedure
|
||||
get: hotelServiceProcedure
|
||||
.input(getlHotelDataInputSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { hotelId, language, include } = input
|
||||
@@ -493,7 +493,7 @@ export const hotelQueryRouter = router({
|
||||
}),
|
||||
}),
|
||||
locations: router({
|
||||
get: serviceProcedure.query(async function ({ ctx }) {
|
||||
get: hotelServiceProcedure.query(async function ({ ctx }) {
|
||||
const searchParams = new URLSearchParams()
|
||||
searchParams.set("language", toApiLang(ctx.lang))
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { IconName } from "@/types/components/icon"
|
||||
|
||||
import deepmerge from "deepmerge"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
@@ -15,23 +13,39 @@ import {
|
||||
} from "./output"
|
||||
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import {
|
||||
PointOfInterestCategoryNameEnum,
|
||||
PointOfInterestGroupEnum,
|
||||
} from "@/types/hotel"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
|
||||
export function getIconByPoiCategory(category: string) {
|
||||
export function getPoiGroupByCategoryName(
|
||||
category: PointOfInterestCategoryNameEnum
|
||||
) {
|
||||
switch (category) {
|
||||
case "Transportations":
|
||||
return IconName.Train
|
||||
case "Shopping":
|
||||
return IconName.Shopping
|
||||
case "Museum":
|
||||
return IconName.Museum
|
||||
case "Tourist":
|
||||
return IconName.Cultural
|
||||
case "Restaurant":
|
||||
return IconName.Restaurant
|
||||
case PointOfInterestCategoryNameEnum.AIRPORT:
|
||||
case PointOfInterestCategoryNameEnum.BUS_TERMINAL:
|
||||
case PointOfInterestCategoryNameEnum.TRANSPORTATIONS:
|
||||
return PointOfInterestGroupEnum.PUBLIC_TRANSPORT
|
||||
case PointOfInterestCategoryNameEnum.AMUSEMENT_PARK:
|
||||
case PointOfInterestCategoryNameEnum.MUSEUM:
|
||||
case PointOfInterestCategoryNameEnum.SPORTS:
|
||||
case PointOfInterestCategoryNameEnum.THEATRE:
|
||||
case PointOfInterestCategoryNameEnum.TOURIST:
|
||||
case PointOfInterestCategoryNameEnum.ZOO:
|
||||
return PointOfInterestGroupEnum.ATTRACTIONS
|
||||
case PointOfInterestCategoryNameEnum.NEARBY_COMPANIES:
|
||||
case PointOfInterestCategoryNameEnum.FAIR:
|
||||
return PointOfInterestGroupEnum.BUSINESS
|
||||
case PointOfInterestCategoryNameEnum.PARKING_GARAGE:
|
||||
return PointOfInterestGroupEnum.PARKING
|
||||
case PointOfInterestCategoryNameEnum.SHOPPING:
|
||||
case PointOfInterestCategoryNameEnum.RESTAURANT:
|
||||
return PointOfInterestGroupEnum.SHOPPING_DINING
|
||||
case PointOfInterestCategoryNameEnum.HOSPITAL:
|
||||
default:
|
||||
return null
|
||||
return PointOfInterestGroupEnum.LOCATION
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user