From e6db1b17c693e3610503f6d6ffd8129878a13e1d Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Mon, 21 Oct 2024 17:11:15 +0200 Subject: [PATCH] fix(SW-556): now fetching surprises separately in component. also showing surprises on any account page --- .../(live)/(protected)/my-pages/layout.tsx | 2 + .../Rewards/CurrentLevel/Client.tsx | 21 +-- .../Rewards/CurrentLevel/index.tsx | 12 +- .../Surprises/SurprisesNotification.tsx} | 16 ++- components/MyPages/Surprises/index.tsx | 25 ++++ .../Surprises/surprises.module.css | 0 components/Webviews/AccountPage/index.tsx | 1 - server/routers/contentstack/reward/output.ts | 56 ++++++-- server/routers/contentstack/reward/query.ts | 135 ++++++++++++++---- types/components/blocks/currentRewards.ts | 2 - types/components/blocks/surprises.ts | 12 +- 11 files changed, 209 insertions(+), 73 deletions(-) rename components/{Blocks/DynamicContent/Rewards/Surprises/index.tsx => MyPages/Surprises/SurprisesNotification.tsx} (94%) create mode 100644 components/MyPages/Surprises/index.tsx rename components/{Blocks/DynamicContent/Rewards => MyPages}/Surprises/surprises.module.css (100%) diff --git a/app/[lang]/(live)/(protected)/my-pages/layout.tsx b/app/[lang]/(live)/(protected)/my-pages/layout.tsx index f5b84dbd7..e1ca6c45a 100644 --- a/app/[lang]/(live)/(protected)/my-pages/layout.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/layout.tsx @@ -1,4 +1,5 @@ import Sidebar from "@/components/MyPages/Sidebar" +import Surprises from "@/components/MyPages/Surprises" import styles from "./layout.module.css" @@ -17,6 +18,7 @@ export default async function MyPagesLayout({ {children} + ) } diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx index 204534cfe..edfef9782 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/Client.tsx @@ -1,7 +1,10 @@ "use client" import { trpc } from "@/lib/trpc/client" -import { ApiReward, Reward } from "@/server/routers/contentstack/reward/output" +import { + Reward, + SurpriseReward, +} from "@/server/routers/contentstack/reward/output" import LoadingSpinner from "@/components/LoadingSpinner" import Grids from "@/components/TempDesignSystem/Grids" @@ -9,15 +12,12 @@ import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton" import Title from "@/components/TempDesignSystem/Text/Title" import useLang from "@/hooks/useLang" -import Surprises from "../Surprises" - import styles from "./current.module.css" import type { CurrentRewardsClientProps } from "@/types/components/blocks/currentRewards" export default function ClientCurrentRewards({ initialCurrentRewards, - membershipNumber, }: CurrentRewardsClientProps) { const lang = useLang() const { data, isFetching, fetchNextPage, hasNextPage, isLoading } = @@ -39,18 +39,12 @@ export default function ClientCurrentRewards({ return } - const rewards = + const cmsRewards = data?.pages .flatMap((page) => page?.rewards) .filter((reward): reward is Reward => !!reward) ?? [] - const surprises = - data?.pages - .flatMap((page) => page?.apiRewards) - .filter((reward): reward is ApiReward => reward?.type === "surprise") ?? - [] - - if (!rewards.length && !surprises.length) { + if (!cmsRewards.length) { return null } @@ -63,7 +57,7 @@ export default function ClientCurrentRewards({ return ( <> - {rewards.map((reward, idx) => ( + {cmsRewards.map((reward, idx) => (
))} - <Surprises surprises={surprises} membershipNumber={membershipNumber} /> </> ) } diff --git a/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx b/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx index 9ab0f5d28..cb970e5e7 100644 --- a/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx +++ b/components/Blocks/DynamicContent/Rewards/CurrentLevel/index.tsx @@ -1,4 +1,3 @@ -import { getProfile } from "@/lib/trpc/memoizedRequests" import { serverClient } from "@/lib/trpc/server" import SectionContainer from "@/components/Section/Container" @@ -23,19 +22,10 @@ export default async function CurrentRewardsBlock({ return null } - const user = await getProfile() - - if (!user || "error" in user) { - return null - } - return ( <SectionContainer> <SectionHeader title={title} link={link} preamble={subtitle} /> - <ClientCurrentRewards - initialCurrentRewards={initialCurrentRewards} - membershipNumber={user.membership?.membershipNumber} - /> + <ClientCurrentRewards initialCurrentRewards={initialCurrentRewards} /> <SectionLink link={link} variant="mobile" /> </SectionContainer> ) diff --git a/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx b/components/MyPages/Surprises/SurprisesNotification.tsx similarity index 94% rename from components/Blocks/DynamicContent/Rewards/Surprises/index.tsx rename to components/MyPages/Surprises/SurprisesNotification.tsx index 66abfaa45..b8d8ac111 100644 --- a/components/Blocks/DynamicContent/Rewards/Surprises/index.tsx +++ b/components/MyPages/Surprises/SurprisesNotification.tsx @@ -6,6 +6,7 @@ import { Dialog, Modal, ModalOverlay } from "react-aria-components" import { useIntl } from "react-intl" import { benefits } from "@/constants/routes/myPages" +import { dt } from "@/lib/dt" import { trpc } from "@/lib/trpc/client" import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons" @@ -22,7 +23,7 @@ import styles from "./surprises.module.css" import type { SurprisesProps } from "@/types/components/blocks/surprises" -export default function Surprises({ +export default function SurprisesNotification({ surprises, membershipNumber, }: SurprisesProps) { @@ -45,8 +46,8 @@ export default function Surprises({ } function viewRewards() { - if (surprise.id) { - update.mutate({ id: surprise.id }) + if (surprise.reward_id) { + update.mutate({ id: surprise.reward_id }) } } @@ -105,13 +106,18 @@ export default function Surprises({ {showSurprises ? ( <> <div className={styles.content}> - <Surprise title={surprise.title}> + <Surprise title={surprise.label}> <Body textAlign="center"> This is just some dummy text describing the gift and should be replaced. </Body> <div className={styles.badge}> - <Caption>Valid through DD M YYYY</Caption> + <Caption> + Valid through{" "} + {dt(surprise.endsAt) + .locale(lang) + .format("DD MMM YYYY")} + </Caption> <Caption> {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 ( + <SurprisesNotification + surprises={surprises} + membershipNumber={user.membership?.membershipNumber} + /> + ) +} 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 ? <LinkToOverview /> : null} <Blocks content={accountPage.content} /> </MaxWidth> - <TrackingSDK pageData={tracking} /> </> ) 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<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] 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 }