refactor(LOY-175): rewrite reward types according to new api endpoints

This commit is contained in:
Christian Andolf
2025-03-11 16:09:15 +01:00
parent 0ae4c5db17
commit b86347b4f4
15 changed files with 196 additions and 246 deletions

View File

@@ -19,10 +19,10 @@ import Redeem from "../Redeem"
import styles from "./current.module.css"
import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage"
import type {
Reward,
RewardWithRedeem,
} from "@/server/routers/contentstack/reward/output"
import {
type Reward,
type RewardWithRedeem,
} from "@/types/components/myPages/rewards"
export default function ClientCurrentRewards({
rewards: initialData,
@@ -70,21 +70,15 @@ export default function ClientCurrentRewards({
<div ref={containerRef} className={styles.container}>
<Grids.Stackable>
{paginatedRewards.map((reward, idx) => {
const earliestExpirationDate =
"coupons" in reward
? getEarliestExpirationDate(reward.coupons)
: null
const earliestExpirationDate = getEarliestExpirationDate(
reward.data.coupon
)
return (
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
<div className={styles.content}>
<RewardIcon rewardId={reward.reward_id} />
{showRedeem && (
<ScriptedRewardText
rewardType={reward.rewardType}
rewardTierLevel={reward.rewardTierLevel}
/>
)}
{showRedeem && <ScriptedRewardText reward={reward} />}
<Title
as="h4"
level="h3"

View File

@@ -22,6 +22,12 @@ export default function Campaign() {
return null
}
if (reward.data.rewardType !== "Campaign") {
return null
}
const operaRewardId = reward.data.operaRewardId
return (
<>
<div className={styles.modalContent}>
@@ -35,7 +41,7 @@ export default function Campaign() {
{intl.formatMessage({ id: "Promo code" })}
</Caption>
<Caption textAlign="center" color="uiTextHighContrast">
{reward.operaRewardId}
{operaRewardId}
</Caption>
</div>
</div>
@@ -43,7 +49,7 @@ export default function Campaign() {
<Button
onClick={() => {
try {
navigator.clipboard.writeText(reward.operaRewardId)
navigator.clipboard.writeText(operaRewardId)
toast.success(intl.formatMessage({ id: "Copied to clipboard" }))
} catch {
toast.error(intl.formatMessage({ id: "Failed to copy" }))

View File

@@ -35,7 +35,7 @@ export default function Tier({
<div className={styles.modalContent}>
{redeemStep === "redeemed" && (
<div className={styles.badge}>
{isRestaurantOnSiteTierReward(reward) ? (
{isRestaurantOnSiteTierReward(reward.data) ? (
<ActiveRedeemedBadge />
) : (
<TimedRedeemedBadge />
@@ -47,7 +47,7 @@ export default function Tier({
{reward.label}
</Title>
{reward.redeemLocation !== "Non-redeemable" ? (
{reward.data.redeemLocation !== "Non-redeemable" ? (
<>
{redeemStep === "initial" && (
<Body textAlign="center">{reward.description}</Body>
@@ -63,7 +63,7 @@ export default function Tier({
)}
{redeemStep === "redeemed" &&
isRestaurantOnSiteTierReward(reward) &&
isRestaurantOnSiteTierReward(reward.data) &&
membershipNumber && (
<MembershipNumberBadge membershipNumber={membershipNumber} />
)}
@@ -76,7 +76,7 @@ export default function Tier({
)}
</div>
{reward.redeemLocation !== "Non-redeemable" ? (
{reward.data.redeemLocation !== "Non-redeemable" ? (
<>
{redeemStep === "initial" && (
<footer className={styles.modalFooter}>

View File

@@ -29,7 +29,7 @@ import type {
RedeemProps,
RedeemStep,
} from "@/types/components/myPages/myPage/accountPage"
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
import type { RewardWithRedeem } from "@/types/components/myPages/rewards"
const MotionOverlay = motion(ModalOverlay)
const MotionModal = motion(Modal)
@@ -70,7 +70,7 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) {
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
>
<Button intent="primary" fullWidth>
{reward.redeemLocation === "Non-redeemable"
{reward.data.redeemLocation === "Non-redeemable"
? intl.formatMessage({ id: "How to use" })
: intl.formatMessage({ id: "Open" })}
</Button>
@@ -108,7 +108,7 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) {
onClick={() => {
if (
redeemStep === "redeemed" &&
!isRestaurantOnSiteTierReward(reward)
!isRestaurantOnSiteTierReward(reward.data)
) {
setRedeemStep("confirm-close")
} else {
@@ -165,14 +165,17 @@ const variants = {
}
function getRedeemFlow(reward: RewardWithRedeem, membershipNumber: string) {
switch (reward.rewardType) {
switch (reward.data.rewardType) {
case "Campaign":
return <Campaign />
case "Surprise":
case "Tier":
return <Tier membershipNumber={membershipNumber} />
default:
console.warn("Unsupported reward type for redeem:", reward.rewardType)
console.warn(
"Unsupported reward type for redeem:",
reward.data.rewardType
)
return null
}
}

View File

@@ -4,8 +4,10 @@ import { createContext, useCallback, useContext, useEffect } from "react"
import { trpc } from "@/lib/trpc/client"
import { getFirstRedeemableCoupon } from "@/utils/rewards"
import type { RedeemFlowContext } from "@/types/components/myPages/myPage/accountPage"
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
import type { RewardWithRedeem } from "@/types/components/myPages/rewards"
export const RedeemContext = createContext<RedeemFlowContext>({
reward: null,
@@ -31,9 +33,10 @@ export default function useRedeemFlow() {
}>()
const onRedeem = useCallback(() => {
if (reward?.id) {
if (reward?.data.id) {
const coupon = getFirstRedeemableCoupon(reward.data)
update.mutate(
{ rewardId: reward.id, couponCode: reward.couponCode },
{ rewardId: reward.data.id, couponCode: coupon.couponCode },
{
onSuccess() {
setRedeemStep("redeemed")

View File

@@ -4,24 +4,22 @@ import { TIER_TO_FRIEND_MAP } from "@/constants/membershipLevels"
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
import { isMembershipLevel } from "@/utils/membershipLevels"
import { getRewardType } from "@/utils/rewards"
import type { ScriptedRewardTextProps } from "@/types/components/myPages/myPage/accountPage"
export default function ScriptedRewardText({
rewardType,
rewardTierLevel,
reward,
}: ScriptedRewardTextProps) {
const intl = useIntl()
function getLabel(rewardType?: string, rewardTierLevel?: string) {
const type = getRewardType(rewardType)
switch (type) {
case "Tier":
function getLabel() {
switch (reward.data.rewardType) {
case "Tier": {
const { rewardTierLevel } = reward.data
return rewardTierLevel && isMembershipLevel(rewardTierLevel)
? TIER_TO_FRIEND_MAP[rewardTierLevel]
: null
}
case "Campaign":
return intl.formatMessage({ id: "Campaign" })
case "Surprise":
@@ -33,7 +31,7 @@ export default function ScriptedRewardText({
}
}
const label = getLabel(rewardType, rewardTierLevel)
const label = getLabel()
if (!label) return null

View File

@@ -125,11 +125,11 @@ export default function SurprisesNotification({
async function viewRewards() {
const updates = surprises
.map((surprise) => {
const coupons = surprise.coupons
const coupons = surprise.data.coupon
.map((coupon) => {
if (coupon.couponCode) {
return {
rewardId: surprise.id,
rewardId: surprise.data.id,
couponCode: coupon.couponCode,
}
}

View File

@@ -10,7 +10,24 @@ import {
} from "../schemas/pageLinks"
import { systemSchema } from "../schemas/system"
import type { RewardCategory } from "@/types/components/myPages/rewards"
export {
type ApiReward,
type CMSReward,
type CMSRewardsResponse,
type CMSRewardsWithRedeemResponse,
type CMSRewardWithRedeem,
type Coupon,
type GetRewardWithRedeemRefsSchema,
type RedeemableCoupon,
type RedeemLocation,
rewardWithRedeemRefsSchema,
type Surprise,
type SurpriseReward,
validateApiAllTiersSchema,
validateCategorizedRewardsSchema,
validateCmsRewardsSchema,
validateCmsRewardsWithRedeemSchema,
}
enum TierKey {
tier1 = MembershipLevelEnum.L1,
@@ -24,7 +41,10 @@ enum TierKey {
type Key = keyof typeof TierKey
export const validateCmsRewardsSchema = z
/*
* 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({
@@ -48,7 +68,10 @@ export const validateCmsRewardsSchema = z
})
.transform((data) => data.data.all_reward.items)
export const validateCmsRewardsWithRedeemSchema = z
type CMSRewardsResponse = z.input<typeof validateCmsRewardsSchema>
type CMSReward = z.output<typeof validateCmsRewardsSchema>[number]
const validateCmsRewardsWithRedeemSchema = z
.object({
data: z.object({
all_reward: z.object({
@@ -88,13 +111,14 @@ export const validateCmsRewardsWithRedeemSchema = z
})
.transform((data) => data.data.all_reward.items)
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
export type CmsRewardsWithRedeemResponse = z.input<
type CMSRewardsWithRedeemResponse = z.input<
typeof validateCmsRewardsWithRedeemSchema
>
type CMSRewardWithRedeem = z.output<
typeof validateCmsRewardsWithRedeemSchema
>[number]
export const rewardWithRedeemRefsSchema = z.object({
const rewardWithRedeemRefsSchema = z.object({
data: z.object({
all_reward: z.object({
items: z.array(
@@ -115,70 +139,36 @@ export const rewardWithRedeemRefsSchema = z.object({
}),
})
export interface GetRewardWithRedeemRefsSchema
extends z.input<typeof rewardWithRedeemRefsSchema> {}
type GetRewardWithRedeemRefsSchema = z.input<typeof rewardWithRedeemRefsSchema>
export type CMSReward = z.output<typeof validateCmsRewardsSchema>[0]
const REDEEM_LOCATIONS = ["Non-redeemable", "On-site", "Online"] as const
type RedeemLocation = (typeof REDEEM_LOCATIONS)[number]
export type CMSRewardWithRedeem = z.output<
typeof validateCmsRewardsWithRedeemSchema
>[0]
export type Reward = CMSReward & {
id: string | undefined
rewardType: string | undefined
redeemLocation: string | undefined
rewardTierLevel: string | undefined
operaRewardId: string
categories: RewardCategory[]
couponCode: string | undefined
coupons: Coupon[]
}
export type RewardWithRedeem = CMSRewardWithRedeem & {
id: string | undefined
rewardType: string | undefined
redeemLocation: string | undefined
rewardTierLevel: string | undefined
operaRewardId: string
categories: RewardCategory[]
couponCode: string | undefined
coupons: Coupon[]
}
export interface Coupon {
couponCode?: string
expiresAt?: string
unwrapped: boolean
state: "claimed" | "redeemed" | "viewed"
}
export interface Surprise extends Omit<Reward, "operaRewardId" | "couponCode"> {
coupons: Coupon[]
}
// New endpoint related types and schemas.
const BaseReward = z.object({
title: z.string().optional(),
id: z.string().optional(),
rewardId: z.string().optional(),
redeemLocation: z.string().optional(),
status: z.string().optional(),
id: z.string(),
rewardId: z.string(),
redeemLocation: z.enum(REDEEM_LOCATIONS),
status: z.enum(["active", "expired"]),
})
const BenefitReward = BaseReward.merge(
z.object({
rewardType: z.string().optional(), // TODO: Should be "Tier" but can't because of backwards compatibility
rewardType: z.enum(["Tier"]),
rewardTierLevel: z.string().optional(),
})
)
const CouponData = z.object({
couponCode: z.string().optional(),
couponCode: z.string(),
unwrapped: z.boolean().default(false),
state: z.enum(["claimed", "redeemed", "viewed"]),
expiresAt: z.string().datetime({ offset: true }).optional(),
})
type Coupon = z.output<typeof CouponData>
type RedeemableCoupon = Coupon & {
state: Exclude<Coupon["state"], "redeemed">
}
const CouponReward = BaseReward.merge(
z.object({
@@ -192,38 +182,26 @@ const CouponReward = BaseReward.merge(
})
)
/**
* 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
type SurpriseReward = z.output<typeof CouponReward> & {
rewardType: "Surprise"
}
interface Surprise extends CMSReward {
data: SurpriseReward
}
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.
})),
])
.transform((data) => [...data.benefits, ...data.coupons])
export type CategorizedApiReward = z.output<
typeof validateCategorizedRewardsSchema
>[number]
type ApiReward = z.output<typeof validateCategorizedRewardsSchema>[number]
export const validateApiAllTiersSchema = z.record(
const validateApiAllTiersSchema = z.record(
z.nativeEnum(TierKey).transform((data) => {
return TierKey[data as unknown as Key]
}),
z.array(BenefitReward)
)
export type RedeemLocation = "Non-redeemable" | "On-site" | "Online"

View File

@@ -1,5 +1,4 @@
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { notFound } from "@/server/errors/trpc"
import {
contentStackBaseWithProtectedProcedure,
@@ -9,6 +8,8 @@ import {
} from "@/server/trpc"
import { langInput } from "@/server/utils"
import { getReedemableCoupons } from "@/utils/rewards"
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import {
rewardsAllInput,
@@ -16,11 +17,7 @@ import {
rewardsRedeemInput,
rewardsUpdateInput,
} from "./input"
import {
type Reward,
type Surprise,
validateCategorizedRewardsSchema,
} from "./output"
import { type Surprise, validateCategorizedRewardsSchema } from "./output"
import {
getAllRewardCounter,
getAllRewardFailCounter,
@@ -33,7 +30,6 @@ import {
getCurrentRewardCounter,
getCurrentRewardFailCounter,
getCurrentRewardSuccessCounter,
getNonRedeemedRewardIds,
getRedeemCounter,
getRedeemFailCounter,
getRedeemSuccessCounter,
@@ -41,8 +37,16 @@ import {
getUnwrapSurpriseCounter,
getUnwrapSurpriseFailCounter,
getUnwrapSurpriseSuccessCounter,
isSurpriseReward,
} from "./utils"
import type {
Reward,
RewardWithRedeem,
} from "@/types/components/myPages/rewards"
const ONE_HOUR = 60 * 60
export const rewardQueryRouter = router({
all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
@@ -215,71 +219,37 @@ export const rewardQueryRouter = router({
return null
}
const rewardIds = getNonRedeemedRewardIds(validatedApiRewards.data)
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
const rewards: Array<Reward | RewardWithRedeem> = cmsRewards
.filter(
(reward) =>
reward.type === "coupon" &&
reward.rewardType === "Surprise" &&
"coupon" in reward &&
reward.coupon.some(({ unwrapped }) => !unwrapped)
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards
.filter(
(cmsReward) => !wrappedSurprisesIds.includes(cmsReward.reward_id)
(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 redeemableCoupons =
(apiReward &&
"coupon" in apiReward &&
apiReward.coupon.filter(
(coupon) => coupon.state !== "redeemed" && coupon.unwrapped
)) ||
[]
const firstRedeemableCouponToExpire = redeemableCoupons.reduce(
(earliest, coupon) => {
if (dt(coupon.expiresAt).isBefore(dt(earliest.expiresAt))) {
return coupon
}
return earliest
},
redeemableCoupons[0]
)?.couponCode
)!
return {
...cmsReward,
id: apiReward?.id,
rewardType: apiReward?.rewardType,
redeemLocation: apiReward?.redeemLocation,
rewardTierLevel:
apiReward && "rewardTierLevel" in apiReward
? apiReward.rewardTierLevel
: undefined,
operaRewardId:
apiReward && "operaRewardId" in apiReward
? apiReward.operaRewardId
: "",
categories:
apiReward && "categories" in apiReward
? apiReward.categories || []
: [],
couponCode: firstRedeemableCouponToExpire,
coupons:
apiReward && "coupon" in apiReward ? apiReward.coupon || [] : [],
data: apiReward,
}
})
@@ -346,12 +316,11 @@ export const rewardQueryRouter = router({
}
const rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter((reward) => getReedemableCoupons(reward).length)
.map((reward) => reward.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
@@ -359,42 +328,25 @@ export const rewardQueryRouter = router({
getCurrentRewardSuccessCounter.add(1)
const surprises: Surprise[] = validatedApiRewards.data
// TODO: Add predicates once legacy endpoints are removed
.filter(isSurpriseReward)
.filter((reward) => {
if (reward?.rewardType !== "Surprise") {
return false
}
if (!("coupon" in reward)) {
return false
}
const unwrappedCoupons =
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
if (unwrappedCoupons.length === 0) {
return false
}
return true
return unwrappedCoupons.length
})
.map((surprise) => {
const reward = cmsRewards.find(
const cmsReward = cmsRewards.find(
({ reward_id }) => surprise.rewardId === reward_id
)
if (!reward) {
if (!cmsReward) {
return null
}
return {
...reward,
id: surprise.id,
rewardType: surprise.rewardType,
rewardTierLevel: undefined,
redeemLocation: surprise.redeemLocation,
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
categories:
"categories" in surprise ? surprise.categories || [] : [],
...cmsReward,
data: surprise,
}
})
.flatMap((surprises) => (surprises ? [surprises] : []))

View File

@@ -17,11 +17,12 @@ import {
} from "@/utils/generateTag"
import {
type CategorizedApiReward,
type CmsRewardsResponse,
type CmsRewardsWithRedeemResponse,
type ApiReward,
type CMSRewardsResponse,
type CMSRewardsWithRedeemResponse,
type GetRewardWithRedeemRefsSchema,
rewardWithRedeemRefsSchema,
type SurpriseReward,
validateApiAllTiersSchema,
validateCmsRewardsSchema,
validateCmsRewardsWithRedeemSchema,
@@ -29,6 +30,8 @@ import {
import type { Lang } from "@/constants/languages"
export { isSurpriseReward }
const meter = metrics.getMeter("trpc.reward")
export const getAllRewardCounter = meter.createCounter(
"trpc.contentstack.reward.all"
@@ -230,7 +233,7 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
})
)
cmsRewardsResponse = await request<CmsRewardsWithRedeemResponse>(
cmsRewardsResponse = await request<CMSRewardsWithRedeemResponse>(
GetRewardsWithReedem,
{
locale: lang,
@@ -242,7 +245,7 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
}
)
} else {
cmsRewardsResponse = await request<CmsRewardsResponse>(
cmsRewardsResponse = await request<CMSRewardsResponse>(
GetRewards,
{
locale: lang,
@@ -297,17 +300,6 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
return validatedCmsRewards.data
}
export function getNonRedeemedRewardIds(rewards: Array<CategorizedApiReward>) {
return rewards
.filter((reward) => {
if ("coupon" in reward && reward.coupon.length > 0) {
if (reward.coupon.every((coupon) => coupon.state === "redeemed")) {
return false
}
}
return true
})
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
function isSurpriseReward(reward: ApiReward): reward is SurpriseReward {
return reward.rewardType === "Surprise"
}

View File

@@ -3,10 +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 "@/server/routers/contentstack/reward/output"
import type { Reward, RewardWithRedeem } from "../rewards"
export interface AccountPageContentProps
extends Pick<DynamicContent, "dynamic_content"> {}
@@ -35,8 +32,7 @@ export interface RedeemProps {
}
export interface ScriptedRewardTextProps {
rewardType?: string
rewardTierLevel?: string
reward: Reward | RewardWithRedeem
}
export type RedeemModalState = "unmounted" | "hidden" | "visible"

View File

@@ -1,11 +1,19 @@
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 {
RESTAURANT_REWARD_IDS,
REWARD_CATEGORIES,
REWARD_IDS,
REWARD_TYPES,
} from "@/constants/rewards"
ApiReward,
CMSReward,
CMSRewardWithRedeem,
} from "@/server/routers/contentstack/reward/output"
export { type Reward, type RewardWithRedeem }
export interface RewardIconProps extends IconProps {
rewardId: string
@@ -28,3 +36,10 @@ export interface FilterRewardsModalProps {
availableTierLevels: MembershipLevelEnum[]
availableCategories: RewardCategory[]
}
interface Reward extends CMSReward {
data: ApiReward
}
interface RewardWithRedeem extends CMSRewardWithRedeem {
data: ApiReward
}

View File

@@ -1,6 +1,6 @@
import { MembershipLevel } from "@/constants/membershipLevels"
import { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
import { Reward } from "@/server/routers/contentstack/reward/output"
import type { MembershipLevel } from "@/constants/membershipLevels"
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
import type { Reward } from "./myPages/rewards"
export type OverviewTableClientProps = {
activeMembership: MembershipLevel | null

View File

@@ -1,5 +1,4 @@
import { Reward } from "@/server/routers/contentstack/reward/output"
import type { Reward } from "@/types/components/myPages/rewards"
import type { ComparisonLevel } from "@/types/components/overviewTable"
export function getGroupedRewards(levels: ComparisonLevel[]) {

View File

@@ -2,7 +2,6 @@ import {
RESTAURANT_REWARD_IDS,
REWARD_CATEGORIES,
REWARD_IDS,
REWARD_TYPES,
} from "@/constants/rewards"
import { dt } from "@/lib/dt"
@@ -12,16 +11,18 @@ import type {
RestaurantRewardId,
RewardCategory,
RewardId,
RewardType,
} from "@/types/components/myPages/rewards"
import type {
ApiReward,
Coupon,
RewardWithRedeem,
RedeemableCoupon,
RedeemLocation,
} from "@/server/routers/contentstack/reward/output"
export {
getEarliestExpirationDate,
getRewardType,
getFirstRedeemableCoupon,
getReedemableCoupons,
isOnSiteTierReward,
isRestaurantOnSiteTierReward,
isRestaurantReward,
@@ -44,28 +45,41 @@ function isRewardCategory(value: string): value is RewardCategory {
}
function redeemLocationIsOnSite(
location: RewardWithRedeem["redeemLocation"]
location: RedeemLocation
): location is "On-site" {
return location === "On-site"
}
function isTierType(type: RewardWithRedeem["rewardType"]): type is "Tier" {
function isTierType(type: string): type is "Tier" {
return type === "Tier"
}
function isOnSiteTierReward(reward: RewardWithRedeem): boolean {
function isOnSiteTierReward(reward: ApiReward): boolean {
return (
redeemLocationIsOnSite(reward.redeemLocation) &&
isTierType(reward.rewardType)
)
}
function isRestaurantOnSiteTierReward(reward: RewardWithRedeem): boolean {
return isOnSiteTierReward(reward) && isRestaurantReward(reward.reward_id)
function isRestaurantOnSiteTierReward(reward: ApiReward): boolean {
return isOnSiteTierReward(reward) && isRestaurantReward(reward.rewardId)
}
function getRewardType(type?: string): RewardType | null {
return REWARD_TYPES.find((t) => t === type) ?? null
function getReedemableCoupons(reward: ApiReward): RedeemableCoupon[] {
if ("coupon" in reward) {
return reward.coupon.filter(
(coupon): coupon is RedeemableCoupon => coupon.state !== "redeemed"
)
}
return []
}
function getFirstRedeemableCoupon(reward: ApiReward): RedeemableCoupon {
const sortedCoupons = getReedemableCoupons(reward).sort((a, b) => {
// null values used instead of undefined, otherwise it will return current time
return dt(a.expiresAt ?? null).valueOf() - dt(b.expiresAt ?? null).valueOf()
})
return sortedCoupons[0]
}
function getEarliestExpirationDate(coupons: Coupon[]) {