Migrate to a monorepo setup - step 1 * Move web to subfolder /apps/scandic-web * Yarn + transitive deps - Move to yarn - design-system package removed for now since yarn doesn't support the parameter for token (ie project currently broken) - Add missing transitive dependencies as Yarn otherwise prevents these imports - VS Code doesn't pick up TS path aliases unless you open /apps/scandic-web instead of root (will be fixed with monorepo) * Pin framer-motion to temporarily fix typing issue https://github.com/adobe/react-spectrum/issues/7494 * Pin zod to avoid typ error There seems to have been a breaking change in the types returned by zod where error is now returned as undefined instead of missing in the type. We should just handle this but to avoid merge conflicts just pin the dependency for now. * Pin react-intl version Pin version of react-intl to avoid tiny type issue where formatMessage does not accept a generic any more. This will be fixed in a future commit, but to avoid merge conflicts just pin for now. * Pin typescript version Temporarily pin version as newer versions as stricter and results in a type error. Will be fixed in future commit after merge. * Setup workspaces * Add design-system as a monorepo package * Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN * Fix husky for monorepo setup * Update netlify.toml * Add lint script to root package.json * Add stub readme * Fix react-intl formatMessage types * Test netlify.toml in root * Remove root toml * Update netlify.toml publish path * Remove package-lock.json * Update build for branch/preview builds Approved-by: Linus Flood
369 lines
9.9 KiB
TypeScript
369 lines
9.9 KiB
TypeScript
import { metrics } from "@opentelemetry/api"
|
|
import { unstable_cache } from "next/cache"
|
|
|
|
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 { 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"
|
|
)
|
|
|
|
const ONE_HOUR = 60 * 60
|
|
|
|
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 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()
|
|
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
|
|
},
|
|
["getAllApiRewards"],
|
|
{ revalidate: ONE_HOUR }
|
|
)
|
|
|
|
/**
|
|
* Cached for 1 hour.
|
|
*/
|
|
export const getCachedAllTierRewards = unstable_cache(
|
|
async function (token) {
|
|
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
|
|
},
|
|
["getApiAllTierRewards"],
|
|
{ revalidate: ONE_HOUR }
|
|
)
|
|
|
|
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,
|
|
},
|
|
{
|
|
cache: "force-cache",
|
|
next: {
|
|
tags: rewardIds.map((rewardId) => generateTag(lang, rewardId)),
|
|
},
|
|
}
|
|
)
|
|
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,
|
|
},
|
|
{ next: { tags }, cache: "force-cache" }
|
|
)
|
|
} else {
|
|
cmsRewardsResponse = await request<CmsRewardsResponse>(
|
|
GetRewards,
|
|
{
|
|
locale: lang,
|
|
rewardIds,
|
|
},
|
|
{ next: { tags }, cache: "force-cache" }
|
|
)
|
|
}
|
|
|
|
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()
|
|
}
|