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( 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 }), })