chore: remove unused filter modal

remove old cms model

refactor reward types
This commit is contained in:
Christian Andolf
2025-03-18 09:19:05 +01:00
parent 45d57a9c89
commit f272dde1ef
23 changed files with 345 additions and 891 deletions

View File

@@ -1,7 +1,6 @@
import { z } from "zod"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
import { COUPON_REWARD_TYPES, REWARD_CATEGORIES } from "@/constants/rewards"
import {
linkRefsUnionSchema,
@@ -11,67 +10,18 @@ import {
import { systemSchema } from "../schemas/system"
export {
type ApiReward,
type CMSReward,
type CMSRewardsResponse,
type CMSRewardsWithRedeemResponse,
type CMSRewardWithRedeem,
type Coupon,
type GetRewardWithRedeemRefsSchema,
type RedeemableCoupon,
type RedeemLocation,
rewardWithRedeemRefsSchema,
type Surprise,
type SurpriseReward,
BenefitReward,
CouponData,
CouponReward,
REDEEM_LOCATIONS,
REWARD_TYPES,
rewardRefsSchema,
validateApiAllTiersSchema,
validateCategorizedRewardsSchema,
validateCmsRewardsSchema,
validateCmsRewardsWithRedeemSchema,
}
enum TierKey {
tier1 = MembershipLevelEnum.L1,
tier2 = MembershipLevelEnum.L2,
tier3 = MembershipLevelEnum.L3,
tier4 = MembershipLevelEnum.L4,
tier5 = MembershipLevelEnum.L5,
tier6 = MembershipLevelEnum.L6,
tier7 = MembershipLevelEnum.L7,
}
type Key = keyof typeof TierKey
/*
* TODO: Remove this once we start using the new CMS model with redeem entirely
*/
const validateCmsRewardsSchema = z
.object({
data: z.object({
all_reward: z.object({
items: z.array(
z.object({
taxonomies: z.array(
z.object({
term_uid: z.string().optional().default(""),
})
),
label: z.string().optional(),
reward_id: z.string(),
grouped_label: z.string().optional(),
description: z.string().optional(),
grouped_description: z.string().optional(),
value: z.string().optional(),
})
),
}),
}),
})
.transform((data) => data.data.all_reward.items)
type CMSRewardsResponse = z.input<typeof validateCmsRewardsSchema>
type CMSReward = z.output<typeof validateCmsRewardsSchema>[number]
const validateCmsRewardsWithRedeemSchema = z
.object({
data: z.object({
all_reward: z.object({
@@ -111,14 +61,7 @@ const validateCmsRewardsWithRedeemSchema = z
})
.transform((data) => data.data.all_reward.items)
type CMSRewardsWithRedeemResponse = z.input<
typeof validateCmsRewardsWithRedeemSchema
>
type CMSRewardWithRedeem = z.output<
typeof validateCmsRewardsWithRedeemSchema
>[number]
const rewardWithRedeemRefsSchema = z.object({
const rewardRefsSchema = z.object({
data: z.object({
all_reward: z.object({
items: z.array(
@@ -139,22 +82,42 @@ const rewardWithRedeemRefsSchema = z.object({
}),
})
type GetRewardWithRedeemRefsSchema = z.input<typeof rewardWithRedeemRefsSchema>
const REDEEM_LOCATIONS = ["Non-redeemable", "On-site", "Online"] as const
type RedeemLocation = (typeof REDEEM_LOCATIONS)[number]
const REWARD_CATEGORIES = [
"Restaurants",
"Bar",
"Voucher",
"Services and rooms",
"Spa and gym",
] as const
const BaseReward = z.object({
title: z.string().optional(),
id: z.string(),
categories: z
.array(z.enum(REWARD_CATEGORIES).or(z.literal("")))
.optional()
// we sometimes receive empty categories, this filters them out
.transform((categories = []) =>
categories.filter(
(c): c is (typeof REWARD_CATEGORIES)[number] => c !== ""
)
),
rewardId: z.string(),
redeemLocation: z.enum(REDEEM_LOCATIONS),
status: z.enum(["active", "expired"]),
})
const REWARD_TYPES = {
Surprise: "Surprise",
Campaign: "Campaign",
MemberVoucher: "Member-voucher",
Tier: "Tier",
} as const
const BenefitReward = BaseReward.merge(
z.object({
rewardType: z.enum(["Tier"]),
rewardType: z.enum([REWARD_TYPES.Tier]),
rewardTierLevel: z.string().optional(),
})
)
@@ -165,16 +128,15 @@ const CouponData = z.object({
state: z.enum(["claimed", "redeemed", "viewed"]),
expiresAt: z.string().datetime({ offset: true }).optional(),
})
type Coupon = z.output<typeof CouponData>
type RedeemableCoupon = Coupon & {
state: Exclude<Coupon["state"], "redeemed">
}
const CouponReward = BaseReward.merge(
z.object({
rewardType: z.enum(COUPON_REWARD_TYPES),
rewardType: z.enum([
REWARD_TYPES.Surprise,
REWARD_TYPES.Campaign,
REWARD_TYPES.MemberVoucher,
]),
operaRewardId: z.string().default(""),
categories: z.array(z.enum(REWARD_CATEGORIES)).optional(),
coupon: z
.array(CouponData)
.optional()
@@ -182,26 +144,24 @@ const CouponReward = BaseReward.merge(
})
)
type SurpriseReward = z.output<typeof CouponReward> & {
rewardType: "Surprise"
}
const validateCategorizedRewardsSchema = z.object({
benefits: z.array(BenefitReward),
coupons: z.array(CouponReward),
})
interface Surprise extends CMSReward {
data: SurpriseReward
}
const TierKeyMapping = {
tier1: MembershipLevelEnum.L1,
tier2: MembershipLevelEnum.L2,
tier3: MembershipLevelEnum.L3,
tier4: MembershipLevelEnum.L4,
tier5: MembershipLevelEnum.L5,
tier6: MembershipLevelEnum.L6,
tier7: MembershipLevelEnum.L7,
} as const
const validateCategorizedRewardsSchema = z
.object({
benefits: z.array(BenefitReward),
coupons: z.array(CouponReward),
})
.transform((data) => [...data.benefits, ...data.coupons])
type ApiReward = z.output<typeof validateCategorizedRewardsSchema>[number]
const TierKeys = Object.keys(TierKeyMapping) as [keyof typeof TierKeyMapping]
const validateApiAllTiersSchema = z.record(
z.nativeEnum(TierKey).transform((data) => {
return TierKey[data as unknown as Key]
}),
z.enum(TierKeys).transform((data) => TierKeyMapping[data]),
z.array(BenefitReward)
)

View File

@@ -8,7 +8,10 @@ import {
} from "@/server/trpc"
import { langInput } from "@/server/utils"
import { getReedemableCoupons } from "@/utils/rewards"
import {
getRedeemableRewards,
getUnwrappedSurpriseRewards,
} from "@/utils/rewards"
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import {
@@ -17,7 +20,7 @@ import {
rewardsRedeemInput,
rewardsUpdateInput,
} from "./input"
import { type Surprise, validateCategorizedRewardsSchema } from "./output"
import { validateCategorizedRewardsSchema } from "./output"
import {
getAllRewardCounter,
getAllRewardFailCounter,
@@ -37,15 +40,11 @@ import {
getUnwrapSurpriseCounter,
getUnwrapSurpriseFailCounter,
getUnwrapSurpriseSuccessCounter,
isSurpriseReward,
} from "./utils"
import type {
Reward,
RewardWithRedeem,
} from "@/types/components/myPages/rewards"
const ONE_HOUR = 60 * 60
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
@@ -88,7 +87,7 @@ export const rewardQueryRouter = router({
console.error("No contentStackReward found", reward?.rewardId)
}
})
.filter((reward): reward is Reward => Boolean(reward))
.filter((reward): reward is CMSReward => Boolean(reward))
const levelConfig = loyaltyLevelsConfig.find(
(l) => l.level_id === level
@@ -100,7 +99,11 @@ export const rewardQueryRouter = router({
console.error("contentstack.loyaltyLevels level not found")
throw notFound()
}
return { ...levelConfig, rewards: combinedRewards }
const result: LevelWithRewards = {
...levelConfig,
rewards: combinedRewards,
}
return result
}
)
@@ -156,7 +159,7 @@ export const rewardQueryRouter = router({
console.info("No contentStackReward found", reward?.rewardId)
}
})
.filter((reward): reward is Reward => Boolean(reward))
.filter((reward): reward is CMSReward => Boolean(reward))
getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
@@ -166,13 +169,14 @@ export const rewardQueryRouter = router({
.query(async function ({ ctx }) {
getCurrentRewardCounter.add(1)
const endpoint = api.endpoints.v1.Profile.Reward.reward
const apiResponse = await api.get(endpoint, {
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
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()
@@ -219,39 +223,25 @@ export const rewardQueryRouter = router({
return null
}
const rewardIds = validatedApiRewards.data
.map((reward) => reward.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
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: Array<Reward | RewardWithRedeem> = cmsRewards
.filter(
(cmsReward) =>
// filters out any rewards tied to wrapped surprises
!validatedApiRewards.data
.filter(isSurpriseReward)
.filter((reward) =>
reward.coupon.some(({ unwrapped }) => !unwrapped)
)
.map(({ rewardId }) => rewardId)
.includes(cmsReward.reward_id)
)
.map((cmsReward) => {
// Non-null assertion is used here because we know our reward exist
const apiReward = validatedApiRewards.data.find(
({ rewardId }) => rewardId === cmsReward.reward_id
)!
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 {
...cmsReward,
data: apiReward,
}
})
return {
...apiReward,
...cmsReward,
}
})
getCurrentRewardSuccessCounter.add(1)
@@ -315,10 +305,11 @@ export const rewardQueryRouter = router({
return null
}
const rewardIds = validatedApiRewards.data
.filter((reward) => getReedemableCoupons(reward).length)
.map((reward) => reward.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
const unwrappedSurpriseRewards = getUnwrappedSurpriseRewards(
validatedApiRewards.data.coupons
)
const rewardIds = unwrappedSurpriseRewards
.map(({ rewardId }) => rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
@@ -327,26 +318,20 @@ export const rewardQueryRouter = router({
getCurrentRewardSuccessCounter.add(1)
const surprises: Surprise[] = validatedApiRewards.data
.filter(isSurpriseReward)
.filter((reward) => {
const unwrappedCoupons =
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
return unwrappedCoupons.length
})
.map((surprise) => {
const cmsReward = cmsRewards.find(
({ reward_id }) => surprise.rewardId === reward_id
)
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,
data: surprise,
}
})
.flatMap((surprises) => (surprises ? [surprises] : []))

View File

@@ -1,11 +1,9 @@
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,
GetRewards as GetRewards,
GetRewardsRef as GetRewardsRef,
} from "@/lib/graphql/Query/RewardsWithRedeem.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
@@ -17,21 +15,17 @@ import {
} from "@/utils/generateTag"
import {
type ApiReward,
type CMSRewardsResponse,
type CMSRewardsWithRedeemResponse,
type GetRewardWithRedeemRefsSchema,
rewardWithRedeemRefsSchema,
type SurpriseReward,
rewardRefsSchema,
validateApiAllTiersSchema,
validateCmsRewardsSchema,
validateCmsRewardsWithRedeemSchema,
} from "./output"
import type {
CMSRewardsResponse,
GetRewardRefsSchema,
} from "@/types/trpc/routers/contentstack/reward"
import type { Lang } from "@/constants/languages"
export { isSurpriseReward }
const meter = metrics.getMeter("trpc.reward")
export const getAllRewardCounter = meter.createCounter(
"trpc.contentstack.reward.all"
@@ -166,95 +160,81 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
generateLoyaltyConfigTag(lang, "reward", id)
)
let cmsRewardsResponse
if (env.USE_NEW_REWARD_MODEL) {
getAllCMSRewardRefsCounter.add(1, { lang, rewardIds })
console.info(
"contentstack.reward.refs start",
getAllCMSRewardRefsCounter.add(1, { lang, rewardIds })
console.info(
"contentstack.reward.refs start",
JSON.stringify({
query: { lang, rewardIds },
})
)
const refsResponse = await request<GetRewardRefsSchema>(
GetRewardsRef,
{
locale: lang,
rewardIds,
},
{
key: rewardIds.map((rewardId) => generateRefsResponseTag(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 },
})
)
const refsResponse = await request<GetRewardWithRedeemRefsSchema>(
GetRewardsWithRedeemRef,
{
locale: lang,
rewardIds,
},
{
key: rewardIds.map((rewardId) =>
generateRefsResponseTag(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" }
)
throw notFoundError
}
const validatedRefsData = rewardRefsSchema.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 },
})
)
const cmsRewardsResponse = await request<CMSRewardsResponse>(
GetRewards,
{
locale: lang,
rewardIds,
},
{
key: tags,
ttl: "max",
}
)
if (!cmsRewardsResponse.data) {
getAllRewardFailCounter.add(1, {
lang,
@@ -275,9 +255,8 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
throw notFoundError
}
const validatedCmsRewards = env.USE_NEW_REWARD_MODEL
? validateCmsRewardsWithRedeemSchema.safeParse(cmsRewardsResponse)
: validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
const validatedCmsRewards =
validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
if (!validatedCmsRewards.success) {
getAllRewardFailCounter.add(1, {
@@ -299,7 +278,3 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
return validatedCmsRewards.data
}
function isSurpriseReward(reward: ApiReward): reward is SurpriseReward {
return reward.rewardType === "Surprise"
}