From f272dde1efaa52a86f8c8c67c461fd8093fd4b3f Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 18 Mar 2025 09:19:05 +0100 Subject: [PATCH] chore: remove unused filter modal remove old cms model refactor reward types --- apps/scandic-web/.env.test | 2 +- .../Rewards/CurrentRewards/Client.tsx | 9 +- .../filterRewardsModal.module.css | 175 ------------- .../FilterRewardsModal/index.tsx | 247 ------------------ .../Rewards/Redeem/Flows/Campaign.tsx | 20 +- .../Rewards/Redeem/Flows/Tier.tsx | 21 +- .../DynamicContent/Rewards/Redeem/index.tsx | 25 +- .../Rewards/Redeem/useRedeemFlow.ts | 20 +- .../Rewards/ScriptedRewardText/index.tsx | 4 +- .../components/MyPages/Surprises/Client.tsx | 6 +- .../components/MyPages/Surprises/Slide.tsx | 2 +- apps/scandic-web/constants/rewards.ts | 16 -- .../hooks/rewards/useFilteredRewards.ts | 79 ------ .../routers/contentstack/reward/output.ts | 142 ++++------ .../routers/contentstack/reward/query.ts | 111 ++++---- .../routers/contentstack/reward/utils.ts | 181 ++++++------- .../types/components/blocks/surprises.ts | 2 +- .../components/myPages/myPage/accountPage.ts | 9 +- .../types/components/myPages/rewards.ts | 47 ++-- .../types/components/overviewTable.ts | 8 +- .../types/trpc/routers/contentstack/reward.ts | 48 ++++ apps/scandic-web/utils/loyaltyTable.ts | 10 +- apps/scandic-web/utils/rewards.ts | 52 +++- 23 files changed, 345 insertions(+), 891 deletions(-) delete mode 100644 apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/filterRewardsModal.module.css delete mode 100644 apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/index.tsx delete mode 100644 apps/scandic-web/hooks/rewards/useFilteredRewards.ts create mode 100644 apps/scandic-web/types/trpc/routers/contentstack/reward.ts diff --git a/apps/scandic-web/.env.test b/apps/scandic-web/.env.test index 3b46b880d..701ac3ae2 100644 --- a/apps/scandic-web/.env.test +++ b/apps/scandic-web/.env.test @@ -53,4 +53,4 @@ SHOW_SITE_WIDE_ALERT="false" NEXT_PUBLIC_SENTRY_ENVIRONMENT="test" NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE="0" -SITEMAP_SYNC_SECRET="test +SITEMAP_SYNC_SECRET="test" diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx index 3cb3e2c6e..8128c4cf0 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx @@ -19,10 +19,7 @@ import Redeem from "../Redeem" import styles from "./current.module.css" import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage" -import { - type Reward, - type RewardWithRedeem, -} from "@/types/components/myPages/rewards" +import type { Reward } from "@/types/components/myPages/rewards" export default function ClientCurrentRewards({ rewards: initialData, @@ -34,7 +31,7 @@ export default function ClientCurrentRewards({ const [currentPage, setCurrentPage] = useState(1) const { data } = trpc.contentstack.rewards.current.useQuery<{ - rewards: (Reward | RewardWithRedeem)[] + rewards: Reward[] }>( { lang, @@ -70,7 +67,7 @@ export default function ClientCurrentRewards({
{paginatedRewards.map((reward, idx) => { - const earliestExpirationDate = getEarliestExpirationDate(reward.data) + const earliestExpirationDate = getEarliestExpirationDate(reward) return (
diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/filterRewardsModal.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/filterRewardsModal.module.css deleted file mode 100644 index 3e0bc54ab..000000000 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/filterRewardsModal.module.css +++ /dev/null @@ -1,175 +0,0 @@ -.overlay { - background: rgba(0, 0, 0, 0.5); - height: var(--visual-viewport-height); - position: fixed; - top: 0; - left: 0; - width: 100vw; - z-index: 100; -} - -.modal { - background-color: var(--Base-Surface-Primary-light-Normal); - box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08); - width: 100%; - position: absolute; - left: 0; - bottom: 0; - z-index: 101; - max-height: 90vh; - display: flex; - border-radius: var(--Corner-radius-Medium); -} - -.dialog { - display: flex; - flex-direction: column; - width: 100%; -} - -.modalHeader { - --button-height: 32px; - box-sizing: content-box; - display: flex; - align-items: center; - height: var(--button-height); - position: relative; - justify-content: center; - padding: var(--Spacing-x2) var(--Spacing-x3); - border-bottom: 1px solid var(--Border-Divider-Subtle); -} - -.modalContent { - display: flex; - flex-direction: column; - align-items: center; - gap: var(--Spacing-x4); - padding: var(--Spacing-x3); - overflow-y: auto; - flex: 1; -} - -.modalFooter { - display: flex; - flex-direction: row; - justify-content: space-between; - gap: var(--Spacing-x2); - padding: var(--Spacing-x2) var(--Spacing-x3); - background-color: var(--Base-Surface-Secondary-light-Normal); - border-bottom-left-radius: var(--Corner-radius-Medium); - border-bottom-right-radius: var(--Corner-radius-Medium); -} - -.modalClose { - background: none; - border: none; - cursor: pointer; - position: absolute; - right: var(--Spacing-x3); - width: 32px; - height: var(--button-height); - display: flex; - align-items: center; -} - -.filterSection { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - width: 100%; -} - -.checkboxGroup { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: var(--Spacing-x2); -} - -.checkboxGroup > * { - min-width: 200px; -} - -.filterButton { - position: relative; - display: inline-flex; - align-items: center; - gap: var(--Spacing-x1); - place-self: flex-start; -} - -.filterCount { - display: flex; - align-items: center; - justify-content: center; - color: var(--Base-Text-Inverted); - background-color: var(--Base-Text-Accent); - border-radius: var(--Corner-radius-Rounded); - width: 20px; - height: 20px; - font-size: var(--typography-Footnote-Regular-fontSize); -} - -.customFormCheckbox { - min-width: 200px; -} - -.customCheckbox { - display: flex; - color: var(--text-color); - cursor: pointer; -} - -.customCheckbox[data-selected] .checkbox { - border: none; - background: var(--UI-Input-Controls-Fill-Selected); -} - -.customCheckbox[data-disabled] .checkbox { - border: 1px solid var(--UI-Input-Controls-Border-Disabled); - background: var(--UI-Input-Controls-Surface-Disabled); -} - -.customCheckbox[data-focus-visible="true"] { - outline: 2px solid var(--UI-Input-Controls-Fill-Selected); - outline-offset: 2px; -} - -.checkboxContainer { - display: flex; - align-items: center; - gap: var(--Spacing-x-one-and-half); -} - -.checkbox { - width: 24px; - height: 24px; - min-width: 24px; - background: var(--UI-Input-Controls-Surface-Normal); - border: 1px solid var(--UI-Input-Controls-Border-Normal); - border-radius: 4px; - transition: all 200ms; - display: flex; - align-items: center; - justify-content: center; - forced-color-adjust: none; -} - -@media screen and (min-width: 768px) { - .overlay { - display: flex; - justify-content: center; - align-items: center; - } - - .modal { - left: auto; - bottom: auto; - width: min(933px, 80vw); - max-height: 80vh; - } - - .checkboxGroup { - gap: var(--Spacing-x5); - } -} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/index.tsx deleted file mode 100644 index 07658a107..000000000 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/FilterRewardsModal/index.tsx +++ /dev/null @@ -1,247 +0,0 @@ -"use client" - -import { motion } from "framer-motion" -import { useState } from "react" -import { - Checkbox, - Dialog, - DialogTrigger, - Modal, - ModalOverlay, -} from "react-aria-components" -import { useIntl } from "react-intl" - -import { - type MembershipLevelEnum, - TIER_TO_FRIEND_MAP, -} from "@/constants/membershipLevels" - -import { CloseLargeIcon, FilterIcon } from "@/components/Icons" -import CheckIcon from "@/components/Icons/Check" -import Button from "@/components/TempDesignSystem/Button" -import Body from "@/components/TempDesignSystem/Text/Body" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" - -import styles from "./filterRewardsModal.module.css" - -import type { - FilterRewardsModalProps, - RewardCategory, -} from "@/types/components/myPages/rewards" - -type ModalState = "visible" | "hidden" | "unmounted" - -export default function FilterRewardsModal({ - selectedCategories, - selectedLevels, - onCategoriesChange, - onLevelsChange, - availableTierLevels, - availableCategories, -}: FilterRewardsModalProps) { - const intl = useIntl() - - const [animation, setAnimation] = useState("unmounted") - const [tempCategories, setTempCategories] = - useState(selectedCategories) - const [tempLevels, setTempLevels] = - useState(selectedLevels) - - const categoryTranslations: Record = { - Restaurants: intl.formatMessage({ id: "Restaurants" }), - Bar: intl.formatMessage({ id: "Bar" }), - Voucher: intl.formatMessage({ id: "Voucher" }), - "Services and rooms": intl.formatMessage({ id: "Services and rooms" }), - "Spa and gym": intl.formatMessage({ id: "Spa and gym" }), - } - - function handleClearAll() { - setTempCategories([]) - setTempLevels([]) - } - - function handleApply(close: () => void) { - onCategoriesChange(tempCategories) - onLevelsChange(tempLevels) - close() - } - - function handleOpenChange(isOpen: boolean) { - setAnimation(isOpen ? "visible" : "hidden") - if (isOpen) { - setTempCategories(selectedCategories) - setTempLevels(selectedLevels) - } - } - - return ( - - - - { - if (state === "hidden") { - setAnimation("unmounted") - } - }} - variants={variants.fade} - initial="hidden" - animate={animation} - > - - - {({ close }) => ( - <> -
- - {intl.formatMessage({ id: "Filter and sort" })} - - -
- -
- {availableCategories.length > 0 && ( -
- - {intl.formatMessage({ id: "Category" })} - -
- {availableCategories.map((category) => ( - { - setTempCategories( - isSelected - ? [...tempCategories, category] - : tempCategories.filter((c) => c !== category) - ) - }} - className={styles.customCheckbox} - > - {({ isSelected }) => ( - - - {isSelected && } - - {categoryTranslations[category]} - - )} - - ))} -
-
- )} - - {availableTierLevels.length > 0 && ( -
- - {intl.formatMessage({ id: "Level benefit" })} - -
- {availableTierLevels.map((level) => ( - { - setTempLevels( - isSelected - ? [...tempLevels, level] - : tempLevels.filter((l) => l !== level) - ) - }} - className={styles.customCheckbox} - > - {({ isSelected }) => ( - - - {isSelected && } - - {TIER_TO_FRIEND_MAP[level]} - - )} - - ))} -
-
- )} -
- -
- - -
- - )} -
-
-
-
- ) -} - -const MotionOverlay = motion(ModalOverlay) -const MotionModal = motion(Modal) - -const variants = { - fade: { - hidden: { - opacity: 0, - transition: { duration: 0.4, ease: "easeInOut" }, - }, - visible: { - opacity: 1, - transition: { duration: 0.4, ease: "easeInOut" }, - }, - }, - - slideInOut: { - hidden: { - opacity: 0, - y: 32, - transition: { duration: 0.4, ease: "easeInOut" }, - }, - visible: { - opacity: 1, - y: 0, - transition: { duration: 0.4, ease: "easeInOut" }, - }, - }, -} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Campaign.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Campaign.tsx index 46fad81cc..88bf12c84 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Campaign.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Campaign.tsx @@ -10,24 +10,14 @@ import Title from "@/components/TempDesignSystem/Text/Title" import { toast } from "@/components/TempDesignSystem/Toasts" import { RewardIcon } from "../../RewardIcon" -import useRedeemFlow from "../useRedeemFlow" import styles from "../redeem.module.css" -export default function Campaign() { - const { reward } = useRedeemFlow() +import type { Campaign } from "@/types/components/myPages/rewards" + +export default function Campaign({ reward }: { reward: Campaign }) { const intl = useIntl() - if (!reward) { - return null - } - - if (reward.data.rewardType !== "Campaign") { - return null - } - - const operaRewardId = reward.data.operaRewardId - return ( <>
@@ -41,7 +31,7 @@ export default function Campaign() { {intl.formatMessage({ id: "Promo code" })} - {operaRewardId} + {reward.operaRewardId}
@@ -49,7 +39,7 @@ export default function Campaign() { @@ -108,7 +107,7 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) { onClick={() => { if ( redeemStep === "redeemed" && - !isRestaurantOnSiteTierReward(reward.data) + !isRestaurantOnSiteTierReward(reward) ) { setRedeemStep("confirm-close") } else { @@ -164,18 +163,16 @@ const variants = { }, } -function getRedeemFlow(reward: RewardWithRedeem, membershipNumber: string) { - switch (reward.data.rewardType) { +function getRedeemFlow(reward: Reward, membershipNumber: string) { + const { rewardType } = reward + switch (rewardType) { case "Campaign": - return + return case "Surprise": case "Tier": - return + return default: - console.warn( - "Unsupported reward type for redeem:", - reward.data.rewardType - ) + console.warn("Unsupported reward type for redeem:", rewardType) return null } } diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/useRedeemFlow.ts b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/useRedeemFlow.ts index 8944ed830..317031586 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/useRedeemFlow.ts +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/useRedeemFlow.ts @@ -7,10 +7,9 @@ import { trpc } from "@/lib/trpc/client" import { getFirstRedeemableCoupon } from "@/utils/rewards" import type { RedeemFlowContext } from "@/types/components/myPages/myPage/accountPage" -import type { RewardWithRedeem } from "@/types/components/myPages/rewards" +import type { Reward } from "@/types/components/myPages/rewards" export const RedeemContext = createContext({ - reward: null, redeemStep: "initial", setRedeemStep: () => undefined, defaultTimeRemaining: 0, @@ -20,7 +19,6 @@ export const RedeemContext = createContext({ export default function useRedeemFlow() { const { - reward, redeemStep, setRedeemStep, defaultTimeRemaining, @@ -29,14 +27,14 @@ export default function useRedeemFlow() { } = useContext(RedeemContext) const update = trpc.contentstack.rewards.redeem.useMutation<{ - rewards: RewardWithRedeem[] + rewards: Reward[] }>() - const onRedeem = useCallback(() => { - if (reward?.data.id) { - const coupon = getFirstRedeemableCoupon(reward.data) + const onRedeem = useCallback( + (reward: Reward) => { + const coupon = getFirstRedeemableCoupon(reward) update.mutate( - { rewardId: reward.data.id, couponCode: coupon.couponCode }, + { rewardId: reward.id, couponCode: coupon.couponCode }, { onSuccess() { setRedeemStep("redeemed") @@ -46,8 +44,9 @@ export default function useRedeemFlow() { }, } ) - } - }, [reward, update, setRedeemStep]) + }, + [update, setRedeemStep] + ) useEffect(() => { if (redeemStep === "initial") { @@ -56,7 +55,6 @@ export default function useRedeemFlow() { }, [redeemStep, setTimeRemaining, defaultTimeRemaining]) return { - reward, onRedeem, redeemStep, setRedeemStep, diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/ScriptedRewardText/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/ScriptedRewardText/index.tsx index 417a4b11e..abd0d13ca 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/ScriptedRewardText/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/ScriptedRewardText/index.tsx @@ -13,9 +13,9 @@ export default function ScriptedRewardText({ const intl = useIntl() function getLabel() { - switch (reward.data.rewardType) { + switch (reward.rewardType) { case "Tier": { - const { rewardTierLevel } = reward.data + const { rewardTierLevel } = reward return rewardTierLevel && isMembershipLevel(rewardTierLevel) ? TIER_TO_FRIEND_MAP[rewardTierLevel] : null diff --git a/apps/scandic-web/components/MyPages/Surprises/Client.tsx b/apps/scandic-web/components/MyPages/Surprises/Client.tsx index 057343aa9..430c2f8a0 100644 --- a/apps/scandic-web/components/MyPages/Surprises/Client.tsx +++ b/apps/scandic-web/components/MyPages/Surprises/Client.tsx @@ -28,7 +28,7 @@ import Slide from "./Slide" import styles from "./surprises.module.css" import type { SurprisesProps } from "@/types/components/blocks/surprises" -import type { Surprise } from "@/server/routers/contentstack/reward/output" +import type { Surprise } from "@/types/components/myPages/rewards" const MotionModal = motion(Modal) @@ -125,11 +125,11 @@ export default function SurprisesNotification({ async function viewRewards() { const updates = surprises .map((surprise) => { - const coupons = surprise.data.coupon + const coupons = surprise.coupon .map((coupon) => { if (coupon.couponCode) { return { - rewardId: surprise.data.id, + rewardId: surprise.id, couponCode: coupon.couponCode, } } diff --git a/apps/scandic-web/components/MyPages/Surprises/Slide.tsx b/apps/scandic-web/components/MyPages/Surprises/Slide.tsx index 853bb0b4f..67f6dcaad 100644 --- a/apps/scandic-web/components/MyPages/Surprises/Slide.tsx +++ b/apps/scandic-web/components/MyPages/Surprises/Slide.tsx @@ -7,7 +7,7 @@ import Card from "./Card" import type { SlideProps } from "@/types/components/blocks/surprises" export default function Slide({ surprise }: SlideProps) { - const earliestExpirationDate = getEarliestExpirationDate(surprise.data) + const earliestExpirationDate = getEarliestExpirationDate(surprise) return ( diff --git a/apps/scandic-web/constants/rewards.ts b/apps/scandic-web/constants/rewards.ts index 6148b5e36..1959d9f27 100644 --- a/apps/scandic-web/constants/rewards.ts +++ b/apps/scandic-web/constants/rewards.ts @@ -38,20 +38,4 @@ export const RESTAURANT_REWARD_IDS = [ REWARD_IDS.FreeBreakfast, ] as const -export const COUPON_REWARD_TYPES = [ - "Surprise", - "Campaign", - "Member-voucher", -] as const - -export const REWARD_TYPES = [...COUPON_REWARD_TYPES, "Tier"] as const - export const REWARDS_PER_PAGE = 6 - -export const REWARD_CATEGORIES = [ - "Restaurants", - "Bar", - "Voucher", - "Services and rooms", - "Spa and gym", -] as const diff --git a/apps/scandic-web/hooks/rewards/useFilteredRewards.ts b/apps/scandic-web/hooks/rewards/useFilteredRewards.ts deleted file mode 100644 index f0e896755..000000000 --- a/apps/scandic-web/hooks/rewards/useFilteredRewards.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { useMemo } from "react" - -import { isMembershipLevel } from "@/utils/membershipLevels" -import { isRewardCategory } from "@/utils/rewards" - -import type { RewardCategory } from "@/types/components/myPages/rewards" -import type { MembershipLevelEnum } from "@/constants/membershipLevels" -import type { - Reward, - RewardWithRedeem, -} from "@/server/routers/contentstack/reward/output" - -export function useFilteredRewards( - rewards: (Reward | RewardWithRedeem)[], - selectedCategories: RewardCategory[] = [], - selectedLevels: MembershipLevelEnum[] = [] -) { - const availableCategories = Array.from( - new Set( - rewards - .flatMap((reward) => reward.categories || []) - .filter((category) => isRewardCategory(category)) - ) - ).sort() - - const availableTierLevels = Array.from( - new Set( - rewards - .map((reward) => reward.rewardTierLevel) - .filter( - (level): level is MembershipLevelEnum => - typeof level === "string" && isMembershipLevel(level) - ) - ) - ) - - const hasFilterableOptions = - availableCategories.length > 0 || availableTierLevels.length > 0 - - const filteredRewards = useMemo(() => { - const hasSelectedCategoryFilter = selectedCategories.length > 0 - const hasSelectedLevelFilter = selectedLevels.length > 0 - - if (!hasSelectedCategoryFilter && !hasSelectedLevelFilter) { - return rewards - } - - const useOrLogic = hasSelectedCategoryFilter && hasSelectedLevelFilter - - return rewards.filter((reward) => { - const matchesCategory = - !hasSelectedCategoryFilter || - (reward.categories?.some( - (category) => - isRewardCategory(category) && selectedCategories.includes(category) - ) ?? - false) - - const matchesLevel = - !hasSelectedLevelFilter || - (reward.rewardTierLevel && - isMembershipLevel(reward.rewardTierLevel) && - selectedLevels.includes(reward.rewardTierLevel)) - - // Apply OR logic if both filters are active, otherwise AND - return useOrLogic - ? matchesCategory || matchesLevel - : matchesCategory && matchesLevel - }) - }, [rewards, selectedCategories, selectedLevels]) - - return { - filteredRewards, - total: filteredRewards.length, - availableTierLevels, - availableCategories, - hasFilterableOptions, - } -} diff --git a/apps/scandic-web/server/routers/contentstack/reward/output.ts b/apps/scandic-web/server/routers/contentstack/reward/output.ts index 0fd29b4bb..80c5c6e35 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/output.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/output.ts @@ -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 -type CMSReward = z.output[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 - 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 -type RedeemableCoupon = Coupon & { - state: Exclude -} 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 & { - 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[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) ) diff --git a/apps/scandic-web/server/routers/contentstack/reward/query.ts b/apps/scandic-web/server/routers/contentstack/reward/query.ts index 5ca15b76b..aa65ae2ba 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/query.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/query.ts @@ -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 = 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] : [])) diff --git a/apps/scandic-web/server/routers/contentstack/reward/utils.ts b/apps/scandic-web/server/routers/contentstack/reward/utils.ts index 7cb073db1..10fafd95a 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/utils.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/utils.ts @@ -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( + 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( - 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( - GetRewardsWithReedem, - { - locale: lang, - rewardIds, - }, - { - key: tags, - ttl: "max", - } - ) - } else { - cmsRewardsResponse = await request( - 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( + 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" -} diff --git a/apps/scandic-web/types/components/blocks/surprises.ts b/apps/scandic-web/types/components/blocks/surprises.ts index d4bdfdbbf..5bb9e2459 100644 --- a/apps/scandic-web/types/components/blocks/surprises.ts +++ b/apps/scandic-web/types/components/blocks/surprises.ts @@ -1,4 +1,4 @@ -import type { Surprise } from "@/server/routers/contentstack/reward/output" +import type { Surprise } from "../myPages/rewards" export interface SurprisesProps { surprises: Surprise[] diff --git a/apps/scandic-web/types/components/myPages/myPage/accountPage.ts b/apps/scandic-web/types/components/myPages/myPage/accountPage.ts index 8f835192b..dfe2575c1 100644 --- a/apps/scandic-web/types/components/myPages/myPage/accountPage.ts +++ b/apps/scandic-web/types/components/myPages/myPage/accountPage.ts @@ -3,7 +3,7 @@ import type { z } from "zod" import type { DynamicContent } from "@/types/trpc/routers/contentstack/blocks" import type { blocksSchema } from "@/server/routers/contentstack/accountPage/output" -import type { Reward, RewardWithRedeem } from "../rewards" +import type { Reward } from "../rewards" export interface AccountPageContentProps extends Pick {} @@ -21,18 +21,18 @@ export type ContentProps = { } export interface CurrentRewardsClientProps { - rewards: (Reward | RewardWithRedeem)[] + rewards: Reward[] showRedeem: boolean membershipNumber?: string | null } export interface RedeemProps { - reward: RewardWithRedeem + reward: Reward membershipNumber?: string | null } export interface ScriptedRewardTextProps { - reward: Reward | RewardWithRedeem + reward: Reward } export type RedeemModalState = "unmounted" | "hidden" | "visible" @@ -44,7 +44,6 @@ export type RedeemStep = | "confirm-close" export type RedeemFlowContext = { - reward: RewardWithRedeem | null redeemStep: RedeemStep setRedeemStep: Dispatch> defaultTimeRemaining: number diff --git a/apps/scandic-web/types/components/myPages/rewards.ts b/apps/scandic-web/types/components/myPages/rewards.ts index 891db2418..f27128de2 100644 --- a/apps/scandic-web/types/components/myPages/rewards.ts +++ b/apps/scandic-web/types/components/myPages/rewards.ts @@ -1,19 +1,17 @@ import { type RESTAURANT_REWARD_IDS, - type REWARD_CATEGORIES, type REWARD_IDS, - type REWARD_TYPES, } from "@/constants/rewards" import type { IconProps } from "@/types/components/icon" -import type { MembershipLevelEnum } from "@/constants/membershipLevels" import type { ApiReward, + BenefitReward, CMSReward, - CMSRewardWithRedeem, -} from "@/server/routers/contentstack/reward/output" + CouponReward, +} from "@/types/trpc/routers/contentstack/reward" -export { type Reward, type RewardWithRedeem } +export type { BaseReward, Campaign, Reward, Surprise, Tier } export interface RewardIconProps extends IconProps { rewardId: string @@ -24,22 +22,21 @@ export type RewardId = (typeof REWARD_IDS)[keyof typeof REWARD_IDS] export type RestaurantRewardId = (typeof RESTAURANT_REWARD_IDS)[number] -export type RewardType = (typeof REWARD_TYPES)[number] - -export type RewardCategory = (typeof REWARD_CATEGORIES)[number] - -export interface FilterRewardsModalProps { - selectedCategories: RewardCategory[] - selectedLevels: MembershipLevelEnum[] - onCategoriesChange: (categories: RewardCategory[]) => void - onLevelsChange: (levels: MembershipLevelEnum[]) => void - availableTierLevels: MembershipLevelEnum[] - availableCategories: RewardCategory[] -} - -interface Reward extends CMSReward { - data: ApiReward -} -interface RewardWithRedeem extends CMSRewardWithRedeem { - data: ApiReward -} +type BaseReward = ApiReward & CMSReward +type Campaign = CouponReward & + CMSReward & { + rewardType: "Campaign" + } +type Surprise = CouponReward & + CMSReward & { + rewardType: "Surprise" + } +type Tier = BenefitReward & + CMSReward & { + rewardType: "Tier" + } +type MemberVoucher = CouponReward & + CMSReward & { + rewardType: "Member-voucher" + } +type Reward = Campaign | Surprise | Tier | MemberVoucher diff --git a/apps/scandic-web/types/components/overviewTable.ts b/apps/scandic-web/types/components/overviewTable.ts index 40ebb6838..0a242021e 100644 --- a/apps/scandic-web/types/components/overviewTable.ts +++ b/apps/scandic-web/types/components/overviewTable.ts @@ -1,6 +1,6 @@ import type { MembershipLevel } from "@/constants/membershipLevels" import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output" -import type { Reward } from "./myPages/rewards" +import type { CMSReward } from "../trpc/routers/contentstack/reward" export type OverviewTableClientProps = { activeMembership: MembershipLevel | null @@ -11,7 +11,7 @@ export type LevelCardProps = { level: LevelWithRewards } -export type LevelWithRewards = LoyaltyLevel & { rewards: Reward[] } +export type LevelWithRewards = LoyaltyLevel & { rewards: CMSReward[] } export type ComparisonLevel = LevelWithRewards @@ -21,13 +21,13 @@ export type LevelSummaryProps = { } export type RewardCardProps = { - comparedValues: (Reward | undefined)[] + comparedValues: (CMSReward | undefined)[] title: string description: string } export type RewardValueProps = { - reward?: Reward + reward?: CMSReward } export type RewardListProps = { diff --git a/apps/scandic-web/types/trpc/routers/contentstack/reward.ts b/apps/scandic-web/types/trpc/routers/contentstack/reward.ts new file mode 100644 index 000000000..fdf4a272f --- /dev/null +++ b/apps/scandic-web/types/trpc/routers/contentstack/reward.ts @@ -0,0 +1,48 @@ +import type { z } from "zod" + +import type { + BenefitReward, + CouponData, + CouponReward, + REDEEM_LOCATIONS, + REWARD_TYPES, + rewardRefsSchema, + validateCmsRewardsSchema, +} from "@/server/routers/contentstack/reward/output" + +export type { + ApiReward, + BenefitReward, + CMSReward, + CMSRewardsResponse, + Coupon, + CouponReward, + GetRewardRefsSchema, + RedeemableCoupon, + RedeemLocation, + RewardType, + SurpriseReward, +} + +type CMSRewardsResponse = z.input +type CMSReward = z.output[number] + +type GetRewardRefsSchema = z.input + +type RedeemLocation = (typeof REDEEM_LOCATIONS)[number] + +type RewardType = (typeof REWARD_TYPES)[keyof typeof REWARD_TYPES] + +type Coupon = z.output +type RedeemableCoupon = Coupon & { + state: Exclude +} + +type BenefitReward = z.output + +type CouponReward = z.output +type SurpriseReward = CouponReward & { + rewardType: "Surprise" +} + +type ApiReward = BenefitReward | CouponReward diff --git a/apps/scandic-web/utils/loyaltyTable.ts b/apps/scandic-web/utils/loyaltyTable.ts index 61bf46a4d..bc6f91a44 100644 --- a/apps/scandic-web/utils/loyaltyTable.ts +++ b/apps/scandic-web/utils/loyaltyTable.ts @@ -1,5 +1,5 @@ -import type { Reward } from "@/types/components/myPages/rewards" import type { ComparisonLevel } from "@/types/components/overviewTable" +import type { CMSReward } from "@/types/trpc/routers/contentstack/reward" export function getGroupedRewards(levels: ComparisonLevel[]) { const allRewards = levels @@ -8,7 +8,7 @@ export function getGroupedRewards(levels: ComparisonLevel[]) { }) .flat() - const mappedRewards = allRewards.reduce>( + const mappedRewards = allRewards.reduce>( (acc, curr) => { const taxonomiTerm = curr.taxonomies.find((tax) => tax.term_uid)?.term_uid @@ -38,10 +38,8 @@ export function findAvailableRewards( return level.rewards.find((r) => allRewardIds.includes(r.reward_id)) } -export function getGroupedLabelAndDescription(rewards: Reward[]) { - const reward = rewards.find( - (reward) => !!(reward.grouped_label && reward.grouped_label) - ) +export function getGroupedLabelAndDescription(rewards: CMSReward[]) { + const reward = rewards.find((reward) => !!reward.grouped_label) return { label: reward?.grouped_label ?? "", description: reward?.grouped_description ?? "", diff --git a/apps/scandic-web/utils/rewards.ts b/apps/scandic-web/utils/rewards.ts index d5c062385..3bb170d09 100644 --- a/apps/scandic-web/utils/rewards.ts +++ b/apps/scandic-web/utils/rewards.ts @@ -1,31 +1,29 @@ -import { - RESTAURANT_REWARD_IDS, - REWARD_CATEGORIES, - REWARD_IDS, -} from "@/constants/rewards" +import { RESTAURANT_REWARD_IDS, REWARD_IDS } from "@/constants/rewards" import { dt } from "@/lib/dt" import type { Dayjs } from "dayjs" import type { RestaurantRewardId, - RewardCategory, RewardId, } from "@/types/components/myPages/rewards" import type { ApiReward, RedeemableCoupon, RedeemLocation, -} from "@/server/routers/contentstack/reward/output" + SurpriseReward, +} from "@/types/trpc/routers/contentstack/reward" export { getEarliestExpirationDate, getFirstRedeemableCoupon, + getRedeemableRewards, getReedemableCoupons, + getUnwrappedSurpriseRewards, isOnSiteTierReward, isRestaurantOnSiteTierReward, isRestaurantReward, - isRewardCategory, + isSurpriseReward, isTierType, isValidRewardId, redeemLocationIsOnSite, @@ -39,10 +37,6 @@ function isRestaurantReward(rewardId: string): rewardId is RestaurantRewardId { return RESTAURANT_REWARD_IDS.some((id) => id === rewardId) } -function isRewardCategory(value: string): value is RewardCategory { - return REWARD_CATEGORIES.some((category) => category === value) -} - function redeemLocationIsOnSite( location: RedeemLocation ): location is "On-site" { @@ -94,3 +88,37 @@ function getEarliestExpirationDate(reward: ApiReward) { return earliestDate.isBefore(expiresAtDate) ? earliestDate : expiresAtDate }, null) } + +function isSurpriseReward(reward: ApiReward): reward is SurpriseReward { + return reward.rewardType === "Surprise" +} + +function getUnwrappedSurpriseRewards(rewards: ApiReward[]) { + return rewards + .filter(isSurpriseReward) + .filter((reward) => getReedemableCoupons(reward).length) + .filter((reward) => { + const unwrappedCoupons = + reward.coupon.filter((coupon) => !coupon.unwrapped) || [] + + return unwrappedCoupons.length + }) +} + +function getRedeemableRewards(rewards: ApiReward[]) { + return rewards + .filter((reward) => { + if ("coupon" in reward && reward.coupon.length > 0) { + if (reward.coupon.every((coupon) => coupon.state === "redeemed")) { + return false + } + } + return true + }) + .filter((reward) => { + if (isSurpriseReward(reward)) { + return !reward.coupon.some(({ unwrapped }) => !unwrapped) + } + return true + }) +}