Files
web/server/routers/contentstack/reward/query.ts
2024-10-29 15:39:21 +01:00

502 lines
14 KiB
TypeScript

import { metrics } from "@opentelemetry/api"
import { unstable_cache } from "next/cache"
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 { notFound } from "@/server/errors/trpc"
import {
contentStackBaseWithProtectedProcedure,
contentStackBaseWithServiceProcedure,
router,
} from "@/server/trpc"
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import {
rewardsAllInput,
rewardsByLevelInput,
rewardsCurrentInput,
rewardsUpdateInput,
} from "./input"
import {
CmsRewardsResponse,
Reward,
SurpriseReward,
validateApiRewardSchema,
validateApiTierRewardsSchema,
validateCmsRewardsSchema,
} from "./output"
import { Surprise } from "@/types/components/blocks/surprises"
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"
)
const ONE_HOUR = 60 * 60
function getUniqueRewardIds(rewardIds: string[]) {
const uniqueRewardIds = new Set(rewardIds)
return Array.from(uniqueRewardIds)
}
const getAllCachedApiRewards = unstable_cache(
async function (token) {
const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
headers: {
Authorization: `Bearer ${token}`,
},
})
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,
},
})
)
throw apiResponse
}
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,
})
)
throw validatedApiTierRewards.error
}
return validatedApiTierRewards.data
},
["getAllApiRewards"],
{ revalidate: ONE_HOUR }
)
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.Profile.reward, {
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
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
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
const surprisesIds = validatedApiRewards.data
.filter(
({ type, rewardType }) =>
type === "coupon" && rewardType === "Surprise"
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards.filter(
(reward) => !surprisesIds.includes(reward.reward_id)
)
getCurrentRewardSuccessCounter.add(1)
return {
rewards,
nextCursor,
}
}),
byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput)
.query(async function ({ input, ctx }) {
getByLevelRewardCounter.add(1)
const { level_id } = input
const allUpcomingApiRewards = await getAllCachedApiRewards(
ctx.serviceToken
)
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: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
.query(async function ({ input, ctx }) {
getAllRewardCounter.add(1)
const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken)
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
}),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
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.surprises validation error",
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
return null
}
const rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
getCurrentRewardSuccessCounter.add(1)
const surprises =
validatedApiRewards.data
.filter(
(reward): reward is SurpriseReward =>
reward?.type === "coupon" && reward?.rewardType === "Surprise"
)
.map((surprise) => {
const reward = cmsRewards.find(
({ reward_id }) => surprise.rewardId === reward_id
)
if (!reward) {
return null
}
return {
...reward,
id: surprise.id,
endsAt: surprise.endsAt,
}
})
.filter((surprise): surprise is Surprise => !!surprise) ?? []
return surprises
}),
update: contentStackBaseWithProtectedProcedure
.input(rewardsUpdateInput)
.mutation(async ({ input, ctx }) => {
const response = await Promise.resolve({ ok: true })
// const response = await api.post(api.endpoints.v1.rewards, {
// body: {
// ids: [input.id],
// },
// })
if (!response.ok) {
return false
}
return true
}),
})