Merge remote-tracking branch 'origin' into feature/tracking

This commit is contained in:
Linus Flood
2024-12-13 09:02:37 +01:00
329 changed files with 4494 additions and 1910 deletions

View File

@@ -1,6 +1,5 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
export const rewardsByLevelInput = z.object({
@@ -12,15 +11,14 @@ export const rewardsAllInput = z
.object({ unique: z.boolean() })
.default({ unique: false })
export const rewardsCurrentInput = z.object({
limit: z.number().min(1).default(3),
cursor: z.number().optional().default(0),
lang: z.nativeEnum(Lang).optional(),
})
export const rewardsUpdateInput = z.array(
z.object({
rewardId: z.string(),
couponCode: z.string(),
})
)
export const rewardsRedeemInput = z.object({
rewardId: z.string(),
couponCode: z.string().optional(),
})

View File

@@ -115,13 +115,57 @@ export const validateCmsRewardsSchema = z
})
.transform((data) => data.data.all_reward.items)
export const validateCmsRewardsWithRedeemSchema = z
.object({
data: z.object({
all_reward: z.object({
items: z.array(
z.object({
taxonomies: z.array(
z.object({
term_uid: z.string().optional(),
})
),
label: z.string().optional(),
reward_id: z.string(),
grouped_label: z.string().optional(),
description: z.string().optional(),
redeem_description: z
.string()
.nullable()
.transform((val) => val || ""),
grouped_description: z.string().optional(),
value: z.string().optional(),
})
),
}),
}),
})
.transform((data) => data.data.all_reward.items)
export type ApiReward = z.output<typeof validateApiRewardSchema>[0]
export type SurpriseReward = z.output<typeof SurpriseReward>
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]
export type CmsRewardsWithRedeemResponse = z.input<
typeof validateCmsRewardsWithRedeemSchema
>
export type CMSReward = z.output<typeof validateCmsRewardsSchema>[0]
export type CMSRewardWithRedeem = z.output<
typeof validateCmsRewardsWithRedeemSchema
>[0]
export type Reward = CMSReward & {
id: string | undefined
}
export type RewardWithRedeem = CMSRewardWithRedeem & {
id: string | undefined
}
// New endpoint related types and schemas.

View File

@@ -12,14 +12,13 @@ import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import {
rewardsAllInput,
rewardsByLevelInput,
rewardsCurrentInput,
rewardsRedeemInput,
rewardsUpdateInput,
} from "./input"
import {
Reward,
validateApiRewardSchema,
validateCategorizedRewardsSchema,
} from "./output"
type
Reward, validateApiRewardSchema,
validateCategorizedRewardsSchema} from "./output"
import {
getAllCachedApiRewards,
getAllRewardCounter,
@@ -33,12 +32,16 @@ import {
getCurrentRewardCounter,
getCurrentRewardFailCounter,
getCurrentRewardSuccessCounter,
getRedeemCounter,
getRedeemFailCounter,
getRedeemSuccessCounter,
getUniqueRewardIds,
getUnwrapSurpriseCounter,
getUnwrapSurpriseFailCounter,
getUnwrapSurpriseSuccessCounter,
} from "./utils"
const ONE_HOUR = 60 * 60
export const rewardQueryRouter = router({
@@ -157,108 +160,105 @@ export const rewardQueryRouter = router({
getByLevelRewardSuccessCounter.add(1)
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}),
current: contentStackBaseWithProtectedProcedure
.input(rewardsCurrentInput)
.query(async function ({ input, ctx }) {
getCurrentRewardCounter.add(1)
current: contentStackBaseWithProtectedProcedure.query(async function ({
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 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 },
})
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,
}),
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
console.error(
"api.reward error ",
JSON.stringify({
error: {
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 wrappedSurprisesIds = validatedApiRewards.data
.filter(
(reward) =>
reward.type === "coupon" &&
reward.rewardType === "Surprise" &&
"coupon" in reward &&
reward.coupon?.some(({ unwrapped }) => !unwrapped)
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards.filter(
(reward) => !wrappedSurprisesIds.includes(reward.reward_id)
)
return null
}
getCurrentRewardSuccessCounter.add(1)
const data = await apiResponse.json()
const validatedApiRewards = isNewEndpoint
? validateCategorizedRewardsSchema.safeParse(data)
: validateApiRewardSchema.safeParse(data)
return {
rewards,
nextCursor,
}
}),
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 cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
const wrappedSurprisesIds = validatedApiRewards.data
.filter(
(reward) =>
reward.type === "coupon" &&
reward.rewardType === "Surprise" &&
"coupon" in reward &&
reward.coupon?.some(({ unwrapped }) => !unwrapped)
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards
.filter((reward) => !wrappedSurprisesIds.includes(reward.reward_id))
.map((reward) => {
return {
...reward,
id: validatedApiRewards.data.find(
({ rewardId }) => rewardId === reward.reward_id
)?.id,
}
})
getCurrentRewardSuccessCounter.add(1)
return { rewards }
}),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
@@ -427,6 +427,53 @@ export const rewardQueryRouter = router({
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
}),
})

View File

@@ -1,20 +1,24 @@
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 { GetRewards as GetRewardsWithReedem } from "@/lib/graphql/Query/RewardsWithRedeem.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
import {
CmsRewardsResponse,
validateApiAllTiersSchema,
type
CmsRewardsResponse,type
CmsRewardsWithRedeemResponse, validateApiAllTiersSchema,
validateApiTierRewardsSchema,
validateCmsRewardsSchema,
} from "./output"
validateCmsRewardsWithRedeemSchema} from "./output"
import type { Lang } from "@/constants/languages"
const meter = metrics.getMeter("trpc.reward")
export const getAllRewardCounter = meter.createCounter(
@@ -53,6 +57,15 @@ export const getUnwrapSurpriseFailCounter = meter.createCounter(
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"
)
const ONE_HOUR = 60 * 60
@@ -187,14 +200,24 @@ 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" }
)
const cmsRewardsResponse = env.USE_NEW_REWARD_MODEL
? await request<CmsRewardsWithRedeemResponse>(
GetRewardsWithReedem,
{
locale: locale,
rewardIds,
},
{ next: { tags }, cache: "force-cache" }
)
: await request<CmsRewardsResponse>(
GetRewards,
{
locale: locale,
rewardIds,
},
{ next: { tags }, cache: "force-cache" }
)
if (!cmsRewardsResponse.data) {
getAllRewardFailCounter.add(1, {
@@ -216,8 +239,9 @@ export async function getCmsRewards(locale: Lang, rewardIds: string[]) {
throw notFoundError
}
const validatedCmsRewards =
validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
const validatedCmsRewards = env.USE_NEW_REWARD_MODEL
? validateCmsRewardsWithRedeemSchema.safeParse(cmsRewardsResponse)
: validateCmsRewardsSchema.safeParse(cmsRewardsResponse)
if (!validatedCmsRewards.success) {
getAllRewardFailCounter.add(1, {

View File

@@ -6,6 +6,7 @@ export const buttonSchema = z
.object({
cta_text: z.string().optional().default(""),
open_in_new_tab: z.boolean().default(false),
is_contentstack_link: z.boolean().default(false),
external_link: z
.object({
href: z.string().optional().default(""),
@@ -34,7 +35,7 @@ export const buttonSchema = z
}),
})
.transform((data) => {
if (data.linkConnection?.edges?.length) {
if (data.linkConnection?.edges?.length && data.is_contentstack_link) {
const link = data.linkConnection.edges[0].node
return {
href: link.url,