Merge branch 'master' into feature/tracking

This commit is contained in:
Linus Flood
2024-11-18 12:20:13 +01:00
240 changed files with 5429 additions and 2717 deletions

View File

@@ -14,6 +14,7 @@ import { removeMultipleSlashes } from "@/utils/url"
import { systemSchema } from "../schemas/system"
import { IconName } from "@/types/components/icon"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { Image } from "@/types/image"
@@ -514,6 +515,11 @@ const menuItemsRefsSchema = z.intersection(
})
)
const topLinkRefsSchema = z.object({
logged_in: linkRefsSchema.nullable(),
logged_out: linkRefsSchema.nullable(),
})
export const headerRefsSchema = z
.object({
all_header: z.object({
@@ -522,7 +528,7 @@ export const headerRefsSchema = z
z.object({
menu_items: z.array(menuItemsRefsSchema),
system: systemSchema,
top_link: linkRefsSchema,
top_link: topLinkRefsSchema,
})
)
.max(1),
@@ -636,6 +642,32 @@ export const menuItemSchema = z
}
})
const topLinkItemSchema = z.intersection(
linkAndTitleSchema,
z.object({
icon: z
.enum(["loyalty", "info", "offer"])
.nullable()
.transform((icon) => {
switch (icon) {
case "loyalty":
return IconName.Gift
case "info":
return IconName.InfoCircle
case "offer":
return IconName.PriceTag
default:
return null
}
}),
})
)
export const topLinkSchema = z.object({
logged_in: topLinkItemSchema.nullable(),
logged_out: topLinkItemSchema.nullable(),
})
export const headerSchema = z
.object({
all_header: z.object({
@@ -643,7 +675,7 @@ export const headerSchema = z
.array(
z.object({
menu_items: z.array(menuItemSchema),
top_link: linkAndTitleSchema,
top_link: topLinkSchema,
})
)
.max(1),

View File

@@ -14,8 +14,13 @@ import type { ContactConfig } from "./output"
export function getConnections({ header }: HeaderRefs) {
const connections: System["system"][] = [header.system]
if (header.top_link?.link) {
connections.push(header.top_link.link)
if (header.top_link) {
if (header.top_link.logged_in?.link) {
connections.push(header.top_link.logged_in.link)
}
if (header.top_link.logged_out?.link) {
connections.push(header.top_link.logged_out.link)
}
}
if (header.menu_items.length) {

View File

@@ -122,3 +122,61 @@ export type SurpriseReward = z.output<typeof SurpriseReward>
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]
// New endpoint related types and schemas.
const BenefitReward = z.object({
title: z.string().optional(),
id: z.string().optional(),
status: z.string().optional(),
rewardId: z.string().optional(),
rewardType: z.string().optional(),
rewardTierLevel: z.string().optional(),
})
const CouponState = z.enum(["claimed", "redeemed", "viewed"])
const CouponData = z.object({
couponCode: z.string().optional(),
unwrapped: z.boolean().default(false),
state: CouponState,
expiresAt: z.string().datetime({ offset: true }).optional(),
})
const CouponReward = z.object({
title: z.string().optional(),
id: z.string().optional(),
rewardId: z.string().optional(),
rewardType: z.string().optional(),
status: z.string().optional(),
coupon: z.array(CouponData).optional(),
})
/**
* Schema for the new /profile/v1/Reward endpoint.
*
* TODO: Once we fully migrate to the new endpoint:
* 1. Remove the data transform and use the categorized structure directly.
* 2. Simplify surprise filtering in the query.
*/
export const validateCategorizedRewardsSchema = z
.object({
benefits: z.array(BenefitReward),
coupons: z.array(CouponReward),
})
.transform((data) => [
...data.benefits.map((benefit) => ({
...benefit,
type: "custom" as const, // Added for legacy compatibility.
})),
...data.coupons.map((coupon) => ({
...coupon,
type: "coupon" as const, // Added for legacy compatibility.
})),
])
export const validateApiAllTiersSchema = z.record(
z.nativeEnum(TierKey).transform((data) => {
return TierKey[data as unknown as Key]
}),
z.array(BenefitReward)
)

View File

@@ -1,10 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { unstable_cache } from "next/cache"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import {
contentStackBaseWithProtectedProcedure,
@@ -12,8 +7,6 @@ import {
router,
} from "@/server/trpc"
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import {
rewardsAllInput,
@@ -22,321 +15,40 @@ import {
rewardsUpdateInput,
} from "./input"
import {
CmsRewardsResponse,
Reward,
SurpriseReward,
validateApiRewardSchema,
validateApiTierRewardsSchema,
validateCmsRewardsSchema,
validateCategorizedRewardsSchema,
} from "./output"
import {
getAllCachedApiRewards,
getAllRewardCounter,
getAllRewardFailCounter,
getAllRewardSuccessCounter,
getByLevelRewardCounter,
getByLevelRewardFailCounter,
getByLevelRewardSuccessCounter,
getCachedAllTierRewards,
getCmsRewards,
getCurrentRewardCounter,
getCurrentRewardFailCounter,
getCurrentRewardSuccessCounter,
getUniqueRewardIds,
} from "./utils"
import { Surprise } from "@/types/components/blocks/surprises"
const meter = metrics.getMeter("trpc.reward")
// OpenTelemetry metrics: Reward
const getCurrentRewardCounter = meter.createCounter(
"trpc.contentstack.reward.current"
)
const getCurrentRewardSuccessCounter = meter.createCounter(
"trpc.contentstack.reward.current-success"
)
const getCurrentRewardFailCounter = meter.createCounter(
"trpc.contentstack.reward.current-fail"
)
const getByLevelRewardCounter = meter.createCounter(
"trpc.contentstack.reward.byLevel"
)
const getByLevelRewardSuccessCounter = meter.createCounter(
"trpc.contentstack.reward.byLevel-success"
)
const getByLevelRewardFailCounter = meter.createCounter(
"trpc.contentstack.reward.byLevel-fail"
)
const getAllRewardCounter = meter.createCounter("trpc.contentstack.reward.all")
const getAllRewardSuccessCounter = meter.createCounter(
"trpc.contentstack.reward.all-success"
)
const getAllRewardFailCounter = meter.createCounter(
"trpc.contentstack.reward.all-fail"
)
const ONE_HOUR = 60 * 60
function getUniqueRewardIds(rewardIds: string[]) {
const uniqueRewardIds = new Set(rewardIds)
return Array.from(uniqueRewardIds)
}
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()
getCurrentRewardFailCounter.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 }
)
async function getCmsRewards(locale: Lang, rewardIds: string[]) {
const tags = rewardIds.map((id) =>
generateLoyaltyConfigTag(locale, "reward", id)
)
const cmsRewardsResponse = await request<CmsRewardsResponse>(
GetRewards,
{
locale: locale,
rewardIds,
},
{ next: { tags }, cache: "force-cache" }
)
if (!cmsRewardsResponse.data) {
getAllRewardFailCounter.add(1, {
lang: locale,
error_type: "validation_error",
error: JSON.stringify(cmsRewardsResponse.data),
})
const notFoundError = notFound(cmsRewardsResponse)
console.error(
"contentstack.rewards not found error",
JSON.stringify({
query: {
locale,
rewardIds,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedCmsRewards =
validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
if (!validatedCmsRewards.success) {
getAllRewardFailCounter.add(1, {
locale,
rewardIds,
error_type: "validation_error",
error: JSON.stringify(validatedCmsRewards.error),
})
console.error(validatedCmsRewards.error)
console.error(
"contentstack.rewards validation error",
JSON.stringify({
query: { locale, rewardIds },
error: validatedCmsRewards.error,
})
)
return null
}
return validatedCmsRewards.data
}
export const rewardQueryRouter = router({
current: contentStackBaseWithProtectedProcedure
.input(rewardsCurrentInput)
.query(async function ({ input, ctx }) {
getCurrentRewardCounter.add(1)
const { limit, cursor } = input
const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
cache: undefined, // override defaultOptions
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
next: { revalidate: 60 * 60 },
})
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 = validateApiRewardSchema.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 rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const slicedData = rewardIds.slice(cursor, limit + cursor)
const cmsRewards = await getCmsRewards(ctx.lang, slicedData)
if (!cmsRewards) {
return null
}
const nextCursor =
limit + cursor < rewardIds.length ? limit + cursor : undefined
const surprisesIds = validatedApiRewards.data
.filter(
({ type, rewardType }) =>
type === "coupon" && rewardType === "Surprise"
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards.filter(
(reward) => !surprisesIds.includes(reward.reward_id)
)
getCurrentRewardSuccessCounter.add(1)
return {
rewards,
nextCursor,
}
}),
byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput)
.query(async function ({ input, ctx }) {
getByLevelRewardCounter.add(1)
const { level_id } = input
const allUpcomingApiRewards = await getAllCachedApiRewards(
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 = await getCmsRewards(ctx.lang, rewardIds)
if (!contentStackRewards) {
return null
}
const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
const levelsWithRewards = apiRewards
.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 Reward => Boolean(reward))
getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}),
all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
.query(async function ({ input, ctx }) {
getAllRewardCounter.add(1)
const allApiRewards = await getAllCachedApiRewards(ctx.serviceToken)
const allApiRewards = env.USE_NEW_REWARDS_ENDPOINT
? await getCachedAllTierRewards(ctx.serviceToken)
: await getAllCachedApiRewards(ctx.serviceToken)
if (!allApiRewards) {
return []
@@ -390,15 +102,171 @@ export const rewardQueryRouter = router({
getAllRewardSuccessCounter.add(1)
return levelsWithRewards
}),
byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput)
.query(async function ({ input, ctx }) {
getByLevelRewardCounter.add(1)
const { level_id } = input
const allUpcomingApiRewards = env.USE_NEW_REWARDS_ENDPOINT
? await getCachedAllTierRewards(ctx.serviceToken)
: await getAllCachedApiRewards(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 = await getCmsRewards(ctx.lang, rewardIds)
if (!contentStackRewards) {
return null
}
const loyaltyLevelsConfig = await getLoyaltyLevel(ctx, input.level_id)
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 Reward => Boolean(reward))
getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}),
current: contentStackBaseWithProtectedProcedure
.input(rewardsCurrentInput)
.query(async function ({ input, ctx }) {
getCurrentRewardCounter.add(1)
const { limit, cursor } = input
const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT
const endpoint = isNewEndpoint
? api.endpoints.v1.Profile.Reward.reward
: api.endpoints.v1.Profile.reward
const apiResponse = await api.get(endpoint, {
cache: undefined, // override defaultOptions
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
next: { revalidate: ONE_HOUR },
})
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 = isNewEndpoint
? validateCategorizedRewardsSchema.safeParse(data)
: validateApiRewardSchema.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 rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const slicedData = rewardIds.slice(cursor, limit + cursor)
const cmsRewards = await getCmsRewards(ctx.lang, slicedData)
if (!cmsRewards) {
return null
}
const nextCursor =
limit + cursor < rewardIds.length ? limit + cursor : undefined
const surprisesIds = validatedApiRewards.data
.filter(
({ type, rewardType }) =>
type === "coupon" && rewardType === "Surprise"
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards.filter(
(reward) => !surprisesIds.includes(reward.reward_id)
)
getCurrentRewardSuccessCounter.add(1)
return {
rewards,
nextCursor,
}
}),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
const apiResponse = await api.get(api.endpoints.v1.Profile.reward, {
cache: undefined, // override defaultOptions
const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT
const endpoint = isNewEndpoint
? api.endpoints.v1.Profile.Reward.reward
: api.endpoints.v1.Profile.reward
const apiResponse = await api.get(endpoint, {
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
next: { revalidate: 60 * 60 },
next: { revalidate: ONE_HOUR },
})
if (!apiResponse.ok) {
@@ -425,8 +293,9 @@ export const rewardQueryRouter = router({
}
const data = await apiResponse.json()
const validatedApiRewards = validateApiRewardSchema.safeParse(data)
const validatedApiRewards = isNewEndpoint
? validateCategorizedRewardsSchema.safeParse(data)
: validateApiRewardSchema.safeParse(data)
if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, {

View File

@@ -0,0 +1,232 @@
import { metrics } from "@opentelemetry/api"
import { unstable_cache } from "next/cache"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api"
import { GetRewards } from "@/lib/graphql/Query/Rewards.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
import {
CmsRewardsResponse,
validateApiAllTiersSchema,
validateApiTierRewardsSchema,
validateCmsRewardsSchema,
} from "./output"
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"
)
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(locale: Lang, rewardIds: string[]) {
const tags = rewardIds.map((id) =>
generateLoyaltyConfigTag(locale, "reward", id)
)
const cmsRewardsResponse = await request<CmsRewardsResponse>(
GetRewards,
{
locale: locale,
rewardIds,
},
{ next: { tags }, cache: "force-cache" }
)
if (!cmsRewardsResponse.data) {
getAllRewardFailCounter.add(1, {
lang: locale,
error_type: "validation_error",
error: JSON.stringify(cmsRewardsResponse.data),
})
const notFoundError = notFound(cmsRewardsResponse)
console.error(
"contentstack.rewards not found error",
JSON.stringify({
query: {
locale,
rewardIds,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedCmsRewards =
validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
if (!validatedCmsRewards.success) {
getAllRewardFailCounter.add(1, {
locale,
rewardIds,
error_type: "validation_error",
error: JSON.stringify(validatedCmsRewards.error),
})
console.error(validatedCmsRewards.error)
console.error(
"contentstack.rewards validation error",
JSON.stringify({
query: { locale, rewardIds },
error: validatedCmsRewards.error,
})
)
return null
}
return validatedCmsRewards.data
}