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