This is just some dummy text describing the gift and
should be replaced.
-
Valid through DD M YYYY
+
+ Valid through{" "}
+ {dt(surprise.endsAt)
+ .locale(lang)
+ .format("DD MMM YYYY")}
+
{intl.formatMessage({ id: "Membership ID" })}{" "}
{membershipNumber}
diff --git a/components/MyPages/Surprises/index.tsx b/components/MyPages/Surprises/index.tsx
new file mode 100644
index 000000000..131ae2b28
--- /dev/null
+++ b/components/MyPages/Surprises/index.tsx
@@ -0,0 +1,25 @@
+import { getProfile } from "@/lib/trpc/memoizedRequests"
+import { serverClient } from "@/lib/trpc/server"
+
+import SurprisesNotification from "./SurprisesNotification"
+
+export default async function Surprises() {
+ const user = await getProfile()
+
+ if (!user || "error" in user) {
+ return null
+ }
+
+ const surprises = await serverClient().contentstack.rewards.surprises()
+
+ if (!surprises) {
+ return null
+ }
+
+ return (
+
+ )
+}
diff --git a/components/Blocks/DynamicContent/Rewards/Surprises/surprises.module.css b/components/MyPages/Surprises/surprises.module.css
similarity index 100%
rename from components/Blocks/DynamicContent/Rewards/Surprises/surprises.module.css
rename to components/MyPages/Surprises/surprises.module.css
diff --git a/components/Webviews/AccountPage/index.tsx b/components/Webviews/AccountPage/index.tsx
index 572ea485e..b7b651627 100644
--- a/components/Webviews/AccountPage/index.tsx
+++ b/components/Webviews/AccountPage/index.tsx
@@ -30,7 +30,6 @@ export default async function AccountPage() {
{linkToOverview ? : null}
-
>
)
diff --git a/server/routers/contentstack/reward/output.ts b/server/routers/contentstack/reward/output.ts
index 8fe45c5c9..236b29c62 100644
--- a/server/routers/contentstack/reward/output.ts
+++ b/server/routers/contentstack/reward/output.ts
@@ -2,20 +2,52 @@ import { z } from "zod"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
+const Coupon = z.object({
+ code: z.string().optional(),
+ status: z.string().optional(),
+ createdAt: z.string().datetime({ offset: true }).optional(),
+ customer: z.object({
+ id: z.string().optional(),
+ }),
+ name: z.string().optional(),
+ claimedAt: z.string().datetime({ offset: true }).optional(),
+ // redeemedAt: z.string().datetime({ offset: true }).optional(),
+ type: z.string().optional(),
+ value: z.number().optional(),
+ pool: z.string().optional(),
+ cfUnwrapped: z.boolean().default(false),
+})
+
+const SurpriseReward = z.object({
+ title: z.string().optional(),
+ id: z.string().optional(),
+ type: z.literal("coupon"),
+ status: z.string().optional(),
+ rewardId: z.string().optional(),
+ redeemLocation: z.string().optional(),
+ autoApplyReward: z.boolean().default(false),
+ rewardType: z.string().optional(),
+ endsAt: z.string().datetime({ offset: true }).optional(),
+ coupons: z.array(Coupon).optional(),
+})
+
export const validateApiRewardSchema = z
.object({
data: z.array(
- z.object({
- title: z.string().optional(),
- id: z.string().optional(),
- type: z.string().optional(),
- status: z.string().optional(),
- rewardId: z.string().optional(),
- redeemLocation: z.string().optional(),
- autoApplyReward: z.boolean().default(false),
- rewardType: z.string().optional(),
- rewardTierLevel: z.string().optional(),
- })
+ z.discriminatedUnion("type", [
+ z.object({
+ title: z.string().optional(),
+ id: z.string().optional(),
+ type: z.literal("custom"),
+ status: z.string().optional(),
+ rewardId: z.string().optional(),
+ redeemLocation: z.string().optional(),
+ autoApplyReward: z.boolean().default(false),
+ rewardType: z.string().optional(),
+ rewardTierLevel: z.string().optional(),
+ }),
+ SurpriseReward,
+ ])
),
})
.transform((data) => data.data)
@@ -77,6 +109,8 @@ export const validateCmsRewardsSchema = z
export type ApiReward = z.output[0]
+export type SurpriseReward = z.output
+
export type CmsRewardsResponse = z.input
export type Reward = z.output[0]
diff --git a/server/routers/contentstack/reward/query.ts b/server/routers/contentstack/reward/query.ts
index 8f5ff7667..419e0a2c2 100644
--- a/server/routers/contentstack/reward/query.ts
+++ b/server/routers/contentstack/reward/query.ts
@@ -24,11 +24,14 @@ import {
import {
CmsRewardsResponse,
Reward,
+ SurpriseReward,
validateApiRewardSchema,
validateApiTierRewardsSchema,
validateCmsRewardsSchema,
} from "./output"
+import { Surprise } from "@/types/components/blocks/surprises"
+
const meter = metrics.getMeter("trpc.reward")
// OpenTelemetry metrics: Reward
@@ -259,36 +262,21 @@ export const rewardQueryRouter = router({
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: cmsRewards,
- apiRewards: validatedApiRewards.data
- // FIXME: Remove these mocks before merging
- .concat([
- {
- autoApplyReward: false,
- title: "Free kids drink when staying",
- id: "fake-id",
- type: "surprise",
- status: "active",
- rewardId: "tier_free_kids_drink",
- redeemLocation: "On-site",
- rewardType: "Tier",
- rewardTierLevel: "L1",
- },
- {
- autoApplyReward: false,
- title: "Free kanelbulle",
- id: "fake-id-2",
- type: "surprise",
- status: "active",
- rewardId: "tier_free_kanelbulle",
- redeemLocation: "On-site",
- rewardType: "Tier",
- rewardTierLevel: "L1",
- },
- ]),
+ rewards,
nextCursor,
}
}),
@@ -402,6 +390,99 @@ export const rewardQueryRouter = router({
getAllRewardSuccessCounter.add(1)
return levelsWithRewards
}),
+ surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
+ getCurrentRewardCounter.add(1)
+
+ const apiResponse = await api.get(api.endpoints.v1.rewards, {
+ 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.surprises 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
+ }
+
+ getCurrentRewardSuccessCounter.add(1)
+
+ const surprises =
+ validatedApiRewards.data
+ .filter(
+ (reward): reward is SurpriseReward =>
+ reward?.type === "coupon" && reward?.rewardType === "Surprise"
+ )
+ .map((surprise) => {
+ const reward = cmsRewards.find(
+ ({ reward_id }) => surprise.rewardId === reward_id
+ )
+
+ if (!reward) {
+ return null
+ }
+
+ return {
+ ...reward,
+ id: surprise.id,
+ endsAt: surprise.endsAt,
+ }
+ })
+ .filter((surprise): surprise is Surprise => !!surprise) ?? []
+
+ return surprises
+ }),
update: contentStackBaseWithProtectedProcedure
.input(rewardsUpdateInput)
.mutation(async ({ input, ctx }) => {
diff --git a/types/components/blocks/currentRewards.ts b/types/components/blocks/currentRewards.ts
index ff4fb9ee3..7e6d8561c 100644
--- a/types/components/blocks/currentRewards.ts
+++ b/types/components/blocks/currentRewards.ts
@@ -5,8 +5,6 @@ import { SafeUser } from "@/types/user"
export type CurrentRewardsClientProps = {
initialCurrentRewards: {
rewards: Reward[]
- apiRewards: ApiReward[]
nextCursor: number | undefined
}
- membershipNumber?: string
}
diff --git a/types/components/blocks/surprises.ts b/types/components/blocks/surprises.ts
index d5aadfd9e..00c8fadfa 100644
--- a/types/components/blocks/surprises.ts
+++ b/types/components/blocks/surprises.ts
@@ -1,6 +1,14 @@
-import { ApiReward } from "@/server/routers/contentstack/reward/output"
+import {
+ Reward,
+ SurpriseReward,
+} from "@/server/routers/contentstack/reward/output"
+
+export interface Surprise extends Reward {
+ endsAt: SurpriseReward["endsAt"]
+ id: SurpriseReward["id"]
+}
export interface SurprisesProps {
- surprises: ApiReward[]
+ surprises: Surprise[]
membershipNumber?: string
}