import * as api from "@/lib/api" import { notFound } from "@/server/errors/trpc" 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 { getAllRewardCounter, getAllRewardFailCounter, getAllRewardSuccessCounter, getByLevelRewardCounter, getByLevelRewardFailCounter, getByLevelRewardSuccessCounter, getCachedAllTierRewards, getCmsRewards, getCurrentRewardCounter, getCurrentRewardFailCounter, getCurrentRewardSuccessCounter, getRedeemCounter, getRedeemFailCounter, getRedeemSuccessCounter, getUniqueRewardIds, getUnwrapSurpriseCounter, getUnwrapSurpriseFailCounter, getUnwrapSurpriseSuccessCounter, } 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 }) { getAllRewardCounter.add(1) 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((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 CMSReward => 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() } const result: LevelWithRewards = { ...levelConfig, rewards: combinedRewards, } return result } ) getAllRewardSuccessCounter.add(1) return levelsWithRewards }), byLevel: contentStackBaseWithServiceProcedure .input(rewardsByLevelInput) .query(async function ({ input, ctx }) { getByLevelRewardCounter.add(1) const { level_id } = input const allUpcomingApiRewards = await getCachedAllTierRewards( 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, 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 { console.info("No contentStackReward found", reward?.rewardId) } }) .filter((reward): reward is CMSReward => Boolean(reward)) getByLevelRewardSuccessCounter.add(1) return { level: loyaltyLevelsConfig, rewards: levelsWithRewards } }), current: contentStackBaseWithProtectedProcedure .input(langInput.optional()) // lang is required for client, but not for server .query(async function ({ ctx }) { getCurrentRewardCounter.add(1) const apiResponse = await api.get( api.endpoints.v1.Profile.Reward.reward, { headers: { Authorization: `Bearer ${ctx.session.token.access_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.reward error ", JSON.stringify({ error: { status: apiResponse.status, statusText: apiResponse.statusText, text, }, }) ) return null } const data = await apiResponse.json() const validatedApiRewards = validateCategorizedRewardsSchema.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 { 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, } }) getCurrentRewardSuccessCounter.add(1) return { rewards } }), surprises: contentStackBaseWithProtectedProcedure .input(langInput.optional()) // lang is required for client, but not for server .query(async ({ ctx }) => { getCurrentRewardCounter.add(1) 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) { 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 = validateCategorizedRewardsSchema.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 unwrappedSurpriseRewards = getUnwrappedSurpriseRewards( validatedApiRewards.data.coupons ) const rewardIds = unwrappedSurpriseRewards .map(({ rewardId }) => rewardId) .sort() const cmsRewards = await getCmsRewards(ctx.lang, rewardIds) if (!cmsRewards) { return null } getCurrentRewardSuccessCounter.add(1) 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] : [])) return surprises }), unwrap: protectedProcedure .input(rewardsUpdateInput) .mutation(async ({ input, ctx }) => { getUnwrapSurpriseCounter.add(1) const promises = input.map(({ rewardId, couponCode }) => { return api.post(api.endpoints.v1.Profile.Reward.unwrap, { body: { rewardId, couponCode, }, headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, }) }) const responses = await Promise.all(promises) const errors = await Promise.all( responses.map(async (apiResponse) => { if (!apiResponse.ok) { const text = await apiResponse.text() getUnwrapSurpriseFailCounter.add(1, { error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, statusText: apiResponse.statusText, text, }), }) console.error( "contentstack.unwrap API error", JSON.stringify({ error: { status: apiResponse.status, statusText: apiResponse.statusText, text, }, query: {}, }) ) return false } return true }) ) if (errors.filter((ok) => !ok).length > 0) { return null } getUnwrapSurpriseSuccessCounter.add(1) return true }), redeem: protectedProcedure .input(rewardsRedeemInput) .mutation(async ({ input, ctx }) => { getRedeemCounter.add(1) const { rewardId, couponCode } = input 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) { const text = await apiResponse.text() getRedeemFailCounter.add(1, { error_type: "http_error", error: JSON.stringify({ status: apiResponse.status, statusText: apiResponse.statusText, text, }), }) console.error( "api.redeem error ", JSON.stringify({ error: { status: apiResponse.status, statusText: apiResponse.statusText, text, }, }) ) return null } getRedeemSuccessCounter.add(1) return true }), })