import * as api from "@/lib/api" import { notFound } from "@/server/errors/trpc" import { createCounter } from "@/server/telemetry" import { contentStackBaseWithProtectedProcedure, contentStackBaseWithServiceProcedure, protectedProcedure, router, } from "@/server/trpc" import { langInput } from "@/server/utils" import { getRedeemableRewards, getUnwrappedSurpriseRewards, } from "@/utils/rewards" import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query" import { rewardsAllInput, rewardsByLevelInput, rewardsRedeemInput, rewardsUpdateInput, } from "./input" import { validateCategorizedRewardsSchema } from "./output" import { getCachedAllTierRewards, getCmsRewards, getUniqueRewardIds, } from "./utils" import type { BaseReward, Surprise } from "@/types/components/myPages/rewards" import type { LevelWithRewards } from "@/types/components/overviewTable" import type { CMSReward } from "@/types/trpc/routers/contentstack/reward" export const rewardQueryRouter = router({ all: contentStackBaseWithServiceProcedure .input(rewardsAllInput) .query(async function ({ input, ctx }) { const getContentstackRewardAllCounter = createCounter( "trpc.contentstack", "reward.all" ) const metricsGetContentstackRewardAll = getContentstackRewardAllCounter.init() metricsGetContentstackRewardAll.start() const allApiRewards = await getCachedAllTierRewards(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((reward) => input.unique ? reward.rewardTierLevel === level : true ) .map((reward) => { const contentStackReward = contentStackRewards.find((r) => { return r.reward_id === reward.rewardId }) if (contentStackReward) { return contentStackReward } else { metricsGetContentstackRewardAll.dataError( `Failed to find reward in CMS for reward ${reward.rewardId} `, { rewardId: reward.rewardId, } ) } }) .filter((reward): reward is CMSReward => Boolean(reward)) const levelConfig = loyaltyLevelsConfig.find( (l) => l.level_id === level ) if (!levelConfig) { metricsGetContentstackRewardAll.dataError( `Failed to matched loyalty level between API and CMS for level ${level}` ) throw notFound() } const result: LevelWithRewards = { ...levelConfig, rewards: combinedRewards, } return result } ) metricsGetContentstackRewardAll.success() return levelsWithRewards }), byLevel: contentStackBaseWithServiceProcedure .input(rewardsByLevelInput) .query(async function ({ input, ctx }) { const { level_id } = input const getRewardByLevelCounter = createCounter( "trpc.contentstack", "reward.byLevel" ) const metricsGetRewardByLevel = getRewardByLevelCounter.init({ level_id, }) metricsGetRewardByLevel.start() const allUpcomingApiRewards = await getCachedAllTierRewards( ctx.serviceToken ) if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { metricsGetRewardByLevel.noDataError() 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, loyaltyLevelsConfig] = await Promise.all([ getCmsRewards(ctx.lang, rewardIds), getLoyaltyLevel(ctx, input.level_id), ]) if (!contentStackRewards) { return null } const levelsWithRewards = apiRewards .map((reward) => { const contentStackReward = contentStackRewards.find((r) => { return r.reward_id === reward.rewardId }) if (contentStackReward) { return contentStackReward } else { metricsGetRewardByLevel.dataError( `Failed to find reward in Contentstack with rewardId: ${reward.rewardId}`, { rewardId: reward.rewardId, } ) } }) .filter((reward): reward is CMSReward => Boolean(reward)) metricsGetRewardByLevel.success() return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } }), current: contentStackBaseWithProtectedProcedure .input(langInput.optional()) // lang is required for client, but not for server .query(async function ({ ctx }) { const getCurrentRewardCounter = createCounter( "trpc.contentstack", "reward.current" ) const metricsGetCurrentReward = getCurrentRewardCounter.init() metricsGetCurrentReward.start() const apiResponse = await api.get( api.endpoints.v1.Profile.Reward.reward, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, } ) if (!apiResponse.ok) { await metricsGetCurrentReward.httpError(apiResponse) return null } const data = await apiResponse.json() const validatedApiRewards = validateCategorizedRewardsSchema.safeParse(data) if (!validatedApiRewards.success) { metricsGetCurrentReward.validationError(validatedApiRewards.error) return null } const { benefits, coupons } = validatedApiRewards.data const redeemableRewards = getRedeemableRewards([...benefits, ...coupons]) const rewardIds = redeemableRewards.map(({ rewardId }) => rewardId).sort() const cmsRewards = await getCmsRewards(ctx.lang, rewardIds) if (!cmsRewards) { return null } const rewards: BaseReward[] = cmsRewards.map((cmsReward) => { // Non-null assertion is used here because we know our reward exist const apiReward = redeemableRewards.find( ({ rewardId }) => rewardId === cmsReward.reward_id )! return { ...apiReward, ...cmsReward, } }) metricsGetCurrentReward.success() return { rewards } }), surprises: contentStackBaseWithProtectedProcedure .input(langInput.optional()) // lang is required for client, but not for server .query(async ({ ctx }) => { const getSurprisesCounter = createCounter( "trpc.contentstack", "surprises" ) const metricsGetSurprises = getSurprisesCounter.init() metricsGetSurprises.start() const endpoint = api.endpoints.v1.Profile.Reward.reward const apiResponse = await api.get(endpoint, { cache: undefined, headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, }) if (!apiResponse.ok) { await metricsGetSurprises.httpError(apiResponse) return null } const data = await apiResponse.json() const validatedApiRewards = validateCategorizedRewardsSchema.safeParse(data) if (!validatedApiRewards.success) { metricsGetSurprises.validationError(validatedApiRewards.error) return null } const unwrappedSurpriseRewards = getUnwrappedSurpriseRewards( validatedApiRewards.data.coupons ) const rewardIds = unwrappedSurpriseRewards .map(({ rewardId }) => rewardId) .sort() const cmsRewards = await getCmsRewards(ctx.lang, rewardIds) if (!cmsRewards) { return null } const surprises: Surprise[] = cmsRewards .map((cmsReward) => { // Non-null assertion is used here because we know our reward exist const apiReward = unwrappedSurpriseRewards.find( ({ rewardId }) => rewardId === cmsReward.reward_id )! if (!cmsReward) { return null } return { ...apiReward, ...cmsReward, } }) .flatMap((surprises) => (surprises ? [surprises] : [])) metricsGetSurprises.success() return surprises }), unwrap: protectedProcedure .input(rewardsUpdateInput) .mutation(async ({ input, ctx }) => { const results = await Promise.allSettled( // Execute each unwrap individually input.map(({ rewardId, couponCode }) => { async function handleUnwrap() { const getUnwrapSurpriseCounter = createCounter( "trpc.contentstack", "reward.unwrap" ) const metricsGetUnwrapSurprise = getUnwrapSurpriseCounter.init({ rewardId, couponCode, }) metricsGetUnwrapSurprise.start() const apiResponse = await api.post( api.endpoints.v1.Profile.Reward.unwrap, { body: { rewardId, couponCode, }, headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, } ) if (!apiResponse.ok) { metricsGetUnwrapSurprise.httpError(apiResponse) return false } metricsGetUnwrapSurprise.success() return true } return handleUnwrap() }) ) if ( results.some( (result) => result.status === "rejected" || result.value === false ) ) { return null } return true }), redeem: protectedProcedure .input(rewardsRedeemInput) .mutation(async ({ input, ctx }) => { const { rewardId, couponCode } = input const getRedeemCounter = createCounter( "trpc.contentstack", "reward.redeem" ) const metricGetRedeem = getRedeemCounter.init({ rewardId, couponCode }) metricGetRedeem.start() const apiResponse = await api.post( api.endpoints.v1.Profile.Reward.redeem, { body: { rewardId, couponCode, }, headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, } ) if (!apiResponse.ok) { metricGetRedeem.httpError(apiResponse) return null } metricGetRedeem.success() return true }), })