Distributed cache * cache deleteKey now uses an options object instead of a lonely argument variable fuzzy * merge * remove debug logs and cleanup * cleanup * add fault handling * add fault handling * add pid when logging redis client creation * add identifier when logging redis client creation * cleanup * feat: add redis-api as it's own app * feature: use http wrapper for redis * feat: add the possibility to fallback to unstable_cache * Add error handling if redis cache is unresponsive * add logging for unstable_cache * merge * don't cache errors * fix: metadatabase on branchdeploys * Handle when /en/destinations throws add ErrorBoundary * Add sentry-logging when ErrorBoundary catches exception * Fix error handling for distributed cache * cleanup code * Added Application Insights back * Update generateApiKeys script and remove duplicate * Merge branch 'feature/redis' of bitbucket.org:scandic-swap/web into feature/redis * merge Approved-by: Linus Flood
378 lines
10 KiB
TypeScript
378 lines
10 KiB
TypeScript
import { metrics } from "@opentelemetry/api"
|
|
|
|
import { env } from "@/env/server"
|
|
import * as api from "@/lib/api"
|
|
import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
|
|
import {
|
|
GetRewards as GetRewardsWithReedem,
|
|
GetRewardsRef as GetRewardsWithRedeemRef,
|
|
} from "@/lib/graphql/Query/RewardsWithRedeem.graphql"
|
|
import { request } from "@/lib/graphql/request"
|
|
import { notFound } from "@/server/errors/trpc"
|
|
|
|
import { getCacheClient } from "@/services/dataCache"
|
|
import { generateLoyaltyConfigTag, generateTag } from "@/utils/generateTag"
|
|
|
|
import {
|
|
type ApiReward,
|
|
type CategorizedApiReward,
|
|
type CmsRewardsResponse,
|
|
type CmsRewardsWithRedeemResponse,
|
|
type GetRewardWithRedeemRefsSchema,
|
|
rewardWithRedeemRefsSchema,
|
|
validateApiAllTiersSchema,
|
|
validateApiTierRewardsSchema,
|
|
validateCmsRewardsSchema,
|
|
validateCmsRewardsWithRedeemSchema,
|
|
} from "./output"
|
|
|
|
import type { Lang } from "@/constants/languages"
|
|
|
|
const meter = metrics.getMeter("trpc.reward")
|
|
export const getAllRewardCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.all"
|
|
)
|
|
export const getAllRewardFailCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.all-fail"
|
|
)
|
|
export const getAllRewardSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.all-success"
|
|
)
|
|
export const getCurrentRewardCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.current"
|
|
)
|
|
export const getCurrentRewardFailCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.current-fail"
|
|
)
|
|
export const getCurrentRewardSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.current-success"
|
|
)
|
|
export const getByLevelRewardCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.byLevel"
|
|
)
|
|
export const getByLevelRewardFailCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.byLevel-fail"
|
|
)
|
|
export const getByLevelRewardSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.byLevel-success"
|
|
)
|
|
export const getUnwrapSurpriseCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.unwrap"
|
|
)
|
|
export const getUnwrapSurpriseFailCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.unwrap-fail"
|
|
)
|
|
export const getUnwrapSurpriseSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.unwrap-success"
|
|
)
|
|
export const getRedeemCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.redeem"
|
|
)
|
|
export const getRedeemFailCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.redeem-fail"
|
|
)
|
|
export const getRedeemSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.redeem-success"
|
|
)
|
|
|
|
export const getAllCMSRewardRefsCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.all"
|
|
)
|
|
export const getAllCMSRewardRefsFailCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.all-fail"
|
|
)
|
|
export const getAllCMSRewardRefsSuccessCounter = meter.createCounter(
|
|
"trpc.contentstack.reward.all-success"
|
|
)
|
|
|
|
export function getUniqueRewardIds(rewardIds: string[]) {
|
|
const uniqueRewardIds = new Set(rewardIds)
|
|
return Array.from(uniqueRewardIds)
|
|
}
|
|
|
|
/**
|
|
* Uses the legacy profile/v1/Profile/tierRewards endpoint.
|
|
* TODO: Delete when the new endpoint is out in production.
|
|
*/
|
|
export async function getAllCachedApiRewards(token: string) {
|
|
const cacheClient = await getCacheClient()
|
|
|
|
return await cacheClient.cacheOrGet(
|
|
"getAllApiRewards",
|
|
async () => {
|
|
const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
})
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getAllRewardFailCounter.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
|
|
},
|
|
"1h"
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Cached for 1 hour.
|
|
*/
|
|
export async function getCachedAllTierRewards(token: string) {
|
|
const cacheClient = await getCacheClient()
|
|
|
|
return await cacheClient.cacheOrGet(
|
|
"getAllTierRewards",
|
|
async () => {
|
|
const apiResponse = await api.get(
|
|
api.endpoints.v1.Profile.Reward.allTiers,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${token}`,
|
|
},
|
|
}
|
|
)
|
|
|
|
if (!apiResponse.ok) {
|
|
const text = await apiResponse.text()
|
|
getAllRewardFailCounter.add(1, {
|
|
error_type: "http_error",
|
|
error: JSON.stringify({
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
}),
|
|
})
|
|
console.error(
|
|
"api.rewards.allTiers error ",
|
|
JSON.stringify({
|
|
error: {
|
|
status: apiResponse.status,
|
|
statusText: apiResponse.statusText,
|
|
text,
|
|
},
|
|
})
|
|
)
|
|
|
|
throw apiResponse
|
|
}
|
|
|
|
const data = await apiResponse.json()
|
|
const validatedApiAllTierRewards =
|
|
validateApiAllTiersSchema.safeParse(data)
|
|
|
|
if (!validatedApiAllTierRewards.success) {
|
|
getAllRewardFailCounter.add(1, {
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validatedApiAllTierRewards.error),
|
|
})
|
|
console.error(validatedApiAllTierRewards.error)
|
|
console.error(
|
|
"api.rewards validation error",
|
|
JSON.stringify({
|
|
error: validatedApiAllTierRewards.error,
|
|
})
|
|
)
|
|
throw validatedApiAllTierRewards.error
|
|
}
|
|
|
|
return validatedApiAllTierRewards.data
|
|
},
|
|
"1h"
|
|
)
|
|
}
|
|
|
|
export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
|
const tags = rewardIds.map((id) =>
|
|
generateLoyaltyConfigTag(lang, "reward", id)
|
|
)
|
|
|
|
let cmsRewardsResponse
|
|
if (env.USE_NEW_REWARD_MODEL) {
|
|
getAllCMSRewardRefsCounter.add(1, { lang, rewardIds })
|
|
console.info(
|
|
"contentstack.reward.refs start",
|
|
JSON.stringify({
|
|
query: { lang, rewardIds },
|
|
})
|
|
)
|
|
const refsResponse = await request<GetRewardWithRedeemRefsSchema>(
|
|
GetRewardsWithRedeemRef,
|
|
{
|
|
locale: lang,
|
|
rewardIds,
|
|
},
|
|
{
|
|
key: rewardIds.map((rewardId) => generateTag(lang, rewardId)),
|
|
ttl: "max",
|
|
}
|
|
)
|
|
if (!refsResponse.data) {
|
|
const notFoundError = notFound(refsResponse)
|
|
getAllCMSRewardRefsFailCounter.add(1, {
|
|
lang,
|
|
rewardIds,
|
|
error_type: "not_found",
|
|
error: JSON.stringify({ code: notFoundError.code }),
|
|
})
|
|
console.error(
|
|
"contentstack.reward.refs not found error",
|
|
JSON.stringify({
|
|
query: { lang, rewardIds },
|
|
error: { code: notFoundError.code },
|
|
})
|
|
)
|
|
throw notFoundError
|
|
}
|
|
|
|
const validatedRefsData = rewardWithRedeemRefsSchema.safeParse(refsResponse)
|
|
|
|
if (!validatedRefsData.success) {
|
|
getAllCMSRewardRefsFailCounter.add(1, {
|
|
lang,
|
|
rewardIds,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validatedRefsData.error),
|
|
})
|
|
console.error(
|
|
"contentstack.reward.refs validation error",
|
|
JSON.stringify({
|
|
query: { lang, rewardIds },
|
|
error: validatedRefsData.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
getAllCMSRewardRefsSuccessCounter.add(1, { lang, rewardIds })
|
|
console.info(
|
|
"contentstack.startPage.refs success",
|
|
JSON.stringify({
|
|
query: { lang, rewardIds },
|
|
})
|
|
)
|
|
|
|
cmsRewardsResponse = await request<CmsRewardsWithRedeemResponse>(
|
|
GetRewardsWithReedem,
|
|
{
|
|
locale: lang,
|
|
rewardIds,
|
|
},
|
|
{
|
|
key: tags,
|
|
ttl: "max",
|
|
}
|
|
)
|
|
} else {
|
|
cmsRewardsResponse = await request<CmsRewardsResponse>(
|
|
GetRewards,
|
|
{
|
|
locale: lang,
|
|
rewardIds,
|
|
},
|
|
{ key: tags, ttl: "max" }
|
|
)
|
|
}
|
|
|
|
if (!cmsRewardsResponse.data) {
|
|
getAllRewardFailCounter.add(1, {
|
|
lang,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(cmsRewardsResponse.data),
|
|
})
|
|
const notFoundError = notFound(cmsRewardsResponse)
|
|
console.error(
|
|
"contentstack.rewards not found error",
|
|
JSON.stringify({
|
|
query: {
|
|
locale: lang,
|
|
rewardIds,
|
|
},
|
|
error: { code: notFoundError.code },
|
|
})
|
|
)
|
|
throw notFoundError
|
|
}
|
|
|
|
const validatedCmsRewards = env.USE_NEW_REWARD_MODEL
|
|
? validateCmsRewardsWithRedeemSchema.safeParse(cmsRewardsResponse)
|
|
: validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
|
|
|
|
if (!validatedCmsRewards.success) {
|
|
getAllRewardFailCounter.add(1, {
|
|
locale: lang,
|
|
rewardIds,
|
|
error_type: "validation_error",
|
|
error: JSON.stringify(validatedCmsRewards.error),
|
|
})
|
|
console.error(validatedCmsRewards.error)
|
|
console.error(
|
|
"contentstack.rewards validation error",
|
|
JSON.stringify({
|
|
query: { locale: lang, rewardIds },
|
|
error: validatedCmsRewards.error,
|
|
})
|
|
)
|
|
return null
|
|
}
|
|
|
|
return validatedCmsRewards.data
|
|
}
|
|
|
|
export function getNonRedeemedRewardIds(
|
|
rewards: Array<ApiReward | CategorizedApiReward>
|
|
) {
|
|
return rewards
|
|
.filter((reward) => {
|
|
if ("coupon" in reward && reward.coupon.length > 0) {
|
|
if (reward.coupon.every((coupon) => coupon.state === "redeemed")) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
.map((reward) => reward?.rewardId)
|
|
.filter((rewardId): rewardId is string => !!rewardId)
|
|
.sort()
|
|
}
|