refactor(LOY-175): rewrite reward types according to new api endpoints
This commit is contained in:
@@ -19,10 +19,10 @@ import Redeem from "../Redeem"
|
|||||||
import styles from "./current.module.css"
|
import styles from "./current.module.css"
|
||||||
|
|
||||||
import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { CurrentRewardsClientProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
import type {
|
import {
|
||||||
Reward,
|
type Reward,
|
||||||
RewardWithRedeem,
|
type RewardWithRedeem,
|
||||||
} from "@/server/routers/contentstack/reward/output"
|
} from "@/types/components/myPages/rewards"
|
||||||
|
|
||||||
export default function ClientCurrentRewards({
|
export default function ClientCurrentRewards({
|
||||||
rewards: initialData,
|
rewards: initialData,
|
||||||
@@ -70,21 +70,15 @@ export default function ClientCurrentRewards({
|
|||||||
<div ref={containerRef} className={styles.container}>
|
<div ref={containerRef} className={styles.container}>
|
||||||
<Grids.Stackable>
|
<Grids.Stackable>
|
||||||
{paginatedRewards.map((reward, idx) => {
|
{paginatedRewards.map((reward, idx) => {
|
||||||
const earliestExpirationDate =
|
const earliestExpirationDate = getEarliestExpirationDate(
|
||||||
"coupons" in reward
|
reward.data.coupon
|
||||||
? getEarliestExpirationDate(reward.coupons)
|
)
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<RewardIcon rewardId={reward.reward_id} />
|
<RewardIcon rewardId={reward.reward_id} />
|
||||||
{showRedeem && (
|
{showRedeem && <ScriptedRewardText reward={reward} />}
|
||||||
<ScriptedRewardText
|
|
||||||
rewardType={reward.rewardType}
|
|
||||||
rewardTierLevel={reward.rewardTierLevel}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Title
|
<Title
|
||||||
as="h4"
|
as="h4"
|
||||||
level="h3"
|
level="h3"
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ export default function Campaign() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (reward.data.rewardType !== "Campaign") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const operaRewardId = reward.data.operaRewardId
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
@@ -35,7 +41,7 @@ export default function Campaign() {
|
|||||||
{intl.formatMessage({ id: "Promo code" })}
|
{intl.formatMessage({ id: "Promo code" })}
|
||||||
</Caption>
|
</Caption>
|
||||||
<Caption textAlign="center" color="uiTextHighContrast">
|
<Caption textAlign="center" color="uiTextHighContrast">
|
||||||
{reward.operaRewardId}
|
{operaRewardId}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -43,7 +49,7 @@ export default function Campaign() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
try {
|
try {
|
||||||
navigator.clipboard.writeText(reward.operaRewardId)
|
navigator.clipboard.writeText(operaRewardId)
|
||||||
toast.success(intl.formatMessage({ id: "Copied to clipboard" }))
|
toast.success(intl.formatMessage({ id: "Copied to clipboard" }))
|
||||||
} catch {
|
} catch {
|
||||||
toast.error(intl.formatMessage({ id: "Failed to copy" }))
|
toast.error(intl.formatMessage({ id: "Failed to copy" }))
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function Tier({
|
|||||||
<div className={styles.modalContent}>
|
<div className={styles.modalContent}>
|
||||||
{redeemStep === "redeemed" && (
|
{redeemStep === "redeemed" && (
|
||||||
<div className={styles.badge}>
|
<div className={styles.badge}>
|
||||||
{isRestaurantOnSiteTierReward(reward) ? (
|
{isRestaurantOnSiteTierReward(reward.data) ? (
|
||||||
<ActiveRedeemedBadge />
|
<ActiveRedeemedBadge />
|
||||||
) : (
|
) : (
|
||||||
<TimedRedeemedBadge />
|
<TimedRedeemedBadge />
|
||||||
@@ -47,7 +47,7 @@ export default function Tier({
|
|||||||
{reward.label}
|
{reward.label}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
{reward.redeemLocation !== "Non-redeemable" ? (
|
{reward.data.redeemLocation !== "Non-redeemable" ? (
|
||||||
<>
|
<>
|
||||||
{redeemStep === "initial" && (
|
{redeemStep === "initial" && (
|
||||||
<Body textAlign="center">{reward.description}</Body>
|
<Body textAlign="center">{reward.description}</Body>
|
||||||
@@ -63,7 +63,7 @@ export default function Tier({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{redeemStep === "redeemed" &&
|
{redeemStep === "redeemed" &&
|
||||||
isRestaurantOnSiteTierReward(reward) &&
|
isRestaurantOnSiteTierReward(reward.data) &&
|
||||||
membershipNumber && (
|
membershipNumber && (
|
||||||
<MembershipNumberBadge membershipNumber={membershipNumber} />
|
<MembershipNumberBadge membershipNumber={membershipNumber} />
|
||||||
)}
|
)}
|
||||||
@@ -76,7 +76,7 @@ export default function Tier({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{reward.redeemLocation !== "Non-redeemable" ? (
|
{reward.data.redeemLocation !== "Non-redeemable" ? (
|
||||||
<>
|
<>
|
||||||
{redeemStep === "initial" && (
|
{redeemStep === "initial" && (
|
||||||
<footer className={styles.modalFooter}>
|
<footer className={styles.modalFooter}>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import type {
|
|||||||
RedeemProps,
|
RedeemProps,
|
||||||
RedeemStep,
|
RedeemStep,
|
||||||
} from "@/types/components/myPages/myPage/accountPage"
|
} 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 MotionOverlay = motion(ModalOverlay)
|
||||||
const MotionModal = motion(Modal)
|
const MotionModal = motion(Modal)
|
||||||
@@ -70,7 +70,7 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) {
|
|||||||
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
|
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
|
||||||
>
|
>
|
||||||
<Button intent="primary" fullWidth>
|
<Button intent="primary" fullWidth>
|
||||||
{reward.redeemLocation === "Non-redeemable"
|
{reward.data.redeemLocation === "Non-redeemable"
|
||||||
? intl.formatMessage({ id: "How to use" })
|
? intl.formatMessage({ id: "How to use" })
|
||||||
: intl.formatMessage({ id: "Open" })}
|
: intl.formatMessage({ id: "Open" })}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -108,7 +108,7 @@ export default function Redeem({ reward, membershipNumber }: RedeemProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (
|
if (
|
||||||
redeemStep === "redeemed" &&
|
redeemStep === "redeemed" &&
|
||||||
!isRestaurantOnSiteTierReward(reward)
|
!isRestaurantOnSiteTierReward(reward.data)
|
||||||
) {
|
) {
|
||||||
setRedeemStep("confirm-close")
|
setRedeemStep("confirm-close")
|
||||||
} else {
|
} else {
|
||||||
@@ -165,14 +165,17 @@ const variants = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getRedeemFlow(reward: RewardWithRedeem, membershipNumber: string) {
|
function getRedeemFlow(reward: RewardWithRedeem, membershipNumber: string) {
|
||||||
switch (reward.rewardType) {
|
switch (reward.data.rewardType) {
|
||||||
case "Campaign":
|
case "Campaign":
|
||||||
return <Campaign />
|
return <Campaign />
|
||||||
case "Surprise":
|
case "Surprise":
|
||||||
case "Tier":
|
case "Tier":
|
||||||
return <Tier membershipNumber={membershipNumber} />
|
return <Tier membershipNumber={membershipNumber} />
|
||||||
default:
|
default:
|
||||||
console.warn("Unsupported reward type for redeem:", reward.rewardType)
|
console.warn(
|
||||||
|
"Unsupported reward type for redeem:",
|
||||||
|
reward.data.rewardType
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import { createContext, useCallback, useContext, useEffect } from "react"
|
|||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import { getFirstRedeemableCoupon } from "@/utils/rewards"
|
||||||
|
|
||||||
import type { RedeemFlowContext } from "@/types/components/myPages/myPage/accountPage"
|
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>({
|
export const RedeemContext = createContext<RedeemFlowContext>({
|
||||||
reward: null,
|
reward: null,
|
||||||
@@ -31,9 +33,10 @@ export default function useRedeemFlow() {
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const onRedeem = useCallback(() => {
|
const onRedeem = useCallback(() => {
|
||||||
if (reward?.id) {
|
if (reward?.data.id) {
|
||||||
|
const coupon = getFirstRedeemableCoupon(reward.data)
|
||||||
update.mutate(
|
update.mutate(
|
||||||
{ rewardId: reward.id, couponCode: reward.couponCode },
|
{ rewardId: reward.data.id, couponCode: coupon.couponCode },
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
setRedeemStep("redeemed")
|
setRedeemStep("redeemed")
|
||||||
|
|||||||
@@ -4,24 +4,22 @@ import { TIER_TO_FRIEND_MAP } from "@/constants/membershipLevels"
|
|||||||
|
|
||||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||||
import { isMembershipLevel } from "@/utils/membershipLevels"
|
import { isMembershipLevel } from "@/utils/membershipLevels"
|
||||||
import { getRewardType } from "@/utils/rewards"
|
|
||||||
|
|
||||||
import type { ScriptedRewardTextProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { ScriptedRewardTextProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
|
||||||
export default function ScriptedRewardText({
|
export default function ScriptedRewardText({
|
||||||
rewardType,
|
reward,
|
||||||
rewardTierLevel,
|
|
||||||
}: ScriptedRewardTextProps) {
|
}: ScriptedRewardTextProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
function getLabel(rewardType?: string, rewardTierLevel?: string) {
|
function getLabel() {
|
||||||
const type = getRewardType(rewardType)
|
switch (reward.data.rewardType) {
|
||||||
|
case "Tier": {
|
||||||
switch (type) {
|
const { rewardTierLevel } = reward.data
|
||||||
case "Tier":
|
|
||||||
return rewardTierLevel && isMembershipLevel(rewardTierLevel)
|
return rewardTierLevel && isMembershipLevel(rewardTierLevel)
|
||||||
? TIER_TO_FRIEND_MAP[rewardTierLevel]
|
? TIER_TO_FRIEND_MAP[rewardTierLevel]
|
||||||
: null
|
: null
|
||||||
|
}
|
||||||
case "Campaign":
|
case "Campaign":
|
||||||
return intl.formatMessage({ id: "Campaign" })
|
return intl.formatMessage({ id: "Campaign" })
|
||||||
case "Surprise":
|
case "Surprise":
|
||||||
@@ -33,7 +31,7 @@ export default function ScriptedRewardText({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const label = getLabel(rewardType, rewardTierLevel)
|
const label = getLabel()
|
||||||
|
|
||||||
if (!label) return null
|
if (!label) return null
|
||||||
|
|
||||||
|
|||||||
@@ -125,11 +125,11 @@ export default function SurprisesNotification({
|
|||||||
async function viewRewards() {
|
async function viewRewards() {
|
||||||
const updates = surprises
|
const updates = surprises
|
||||||
.map((surprise) => {
|
.map((surprise) => {
|
||||||
const coupons = surprise.coupons
|
const coupons = surprise.data.coupon
|
||||||
.map((coupon) => {
|
.map((coupon) => {
|
||||||
if (coupon.couponCode) {
|
if (coupon.couponCode) {
|
||||||
return {
|
return {
|
||||||
rewardId: surprise.id,
|
rewardId: surprise.data.id,
|
||||||
couponCode: coupon.couponCode,
|
couponCode: coupon.couponCode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,24 @@ import {
|
|||||||
} from "../schemas/pageLinks"
|
} from "../schemas/pageLinks"
|
||||||
import { systemSchema } from "../schemas/system"
|
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 {
|
enum TierKey {
|
||||||
tier1 = MembershipLevelEnum.L1,
|
tier1 = MembershipLevelEnum.L1,
|
||||||
@@ -24,7 +41,10 @@ enum TierKey {
|
|||||||
|
|
||||||
type Key = keyof typeof 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({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
all_reward: z.object({
|
all_reward: z.object({
|
||||||
@@ -48,7 +68,10 @@ export const validateCmsRewardsSchema = z
|
|||||||
})
|
})
|
||||||
.transform((data) => data.data.all_reward.items)
|
.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({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
all_reward: z.object({
|
all_reward: z.object({
|
||||||
@@ -88,13 +111,14 @@ export const validateCmsRewardsWithRedeemSchema = z
|
|||||||
})
|
})
|
||||||
.transform((data) => data.data.all_reward.items)
|
.transform((data) => data.data.all_reward.items)
|
||||||
|
|
||||||
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
|
type CMSRewardsWithRedeemResponse = z.input<
|
||||||
|
|
||||||
export type CmsRewardsWithRedeemResponse = z.input<
|
|
||||||
typeof validateCmsRewardsWithRedeemSchema
|
typeof validateCmsRewardsWithRedeemSchema
|
||||||
>
|
>
|
||||||
|
type CMSRewardWithRedeem = z.output<
|
||||||
|
typeof validateCmsRewardsWithRedeemSchema
|
||||||
|
>[number]
|
||||||
|
|
||||||
export const rewardWithRedeemRefsSchema = z.object({
|
const rewardWithRedeemRefsSchema = z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
all_reward: z.object({
|
all_reward: z.object({
|
||||||
items: z.array(
|
items: z.array(
|
||||||
@@ -115,70 +139,36 @@ export const rewardWithRedeemRefsSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
export interface GetRewardWithRedeemRefsSchema
|
type GetRewardWithRedeemRefsSchema = z.input<typeof rewardWithRedeemRefsSchema>
|
||||||
extends 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({
|
const BaseReward = z.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
id: z.string().optional(),
|
id: z.string(),
|
||||||
rewardId: z.string().optional(),
|
rewardId: z.string(),
|
||||||
redeemLocation: z.string().optional(),
|
redeemLocation: z.enum(REDEEM_LOCATIONS),
|
||||||
status: z.string().optional(),
|
status: z.enum(["active", "expired"]),
|
||||||
})
|
})
|
||||||
|
|
||||||
const BenefitReward = BaseReward.merge(
|
const BenefitReward = BaseReward.merge(
|
||||||
z.object({
|
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(),
|
rewardTierLevel: z.string().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
const CouponData = z.object({
|
const CouponData = z.object({
|
||||||
couponCode: z.string().optional(),
|
couponCode: z.string(),
|
||||||
unwrapped: z.boolean().default(false),
|
unwrapped: z.boolean().default(false),
|
||||||
state: z.enum(["claimed", "redeemed", "viewed"]),
|
state: z.enum(["claimed", "redeemed", "viewed"]),
|
||||||
expiresAt: z.string().datetime({ offset: true }).optional(),
|
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(
|
const CouponReward = BaseReward.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -192,38 +182,26 @@ const CouponReward = BaseReward.merge(
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
type SurpriseReward = z.output<typeof CouponReward> & {
|
||||||
* Schema for the new /profile/v1/Reward endpoint.
|
rewardType: "Surprise"
|
||||||
*
|
}
|
||||||
* TODO: Once we fully migrate to the new endpoint:
|
|
||||||
* 1. Remove the data transform and use the categorized structure directly.
|
interface Surprise extends CMSReward {
|
||||||
* 2. Simplify surprise filtering in the query.
|
data: SurpriseReward
|
||||||
*/
|
}
|
||||||
export const validateCategorizedRewardsSchema = z
|
|
||||||
|
const validateCategorizedRewardsSchema = z
|
||||||
.object({
|
.object({
|
||||||
benefits: z.array(BenefitReward),
|
benefits: z.array(BenefitReward),
|
||||||
coupons: z.array(CouponReward),
|
coupons: z.array(CouponReward),
|
||||||
})
|
})
|
||||||
.transform((data) => [
|
.transform((data) => [...data.benefits, ...data.coupons])
|
||||||
...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.
|
|
||||||
})),
|
|
||||||
])
|
|
||||||
|
|
||||||
export type CategorizedApiReward = z.output<
|
type ApiReward = z.output<typeof validateCategorizedRewardsSchema>[number]
|
||||||
typeof validateCategorizedRewardsSchema
|
|
||||||
>[number]
|
|
||||||
|
|
||||||
export const validateApiAllTiersSchema = z.record(
|
const validateApiAllTiersSchema = z.record(
|
||||||
z.nativeEnum(TierKey).transform((data) => {
|
z.nativeEnum(TierKey).transform((data) => {
|
||||||
return TierKey[data as unknown as Key]
|
return TierKey[data as unknown as Key]
|
||||||
}),
|
}),
|
||||||
z.array(BenefitReward)
|
z.array(BenefitReward)
|
||||||
)
|
)
|
||||||
|
|
||||||
export type RedeemLocation = "Non-redeemable" | "On-site" | "Online"
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { dt } from "@/lib/dt"
|
|
||||||
import { notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
import {
|
import {
|
||||||
contentStackBaseWithProtectedProcedure,
|
contentStackBaseWithProtectedProcedure,
|
||||||
@@ -9,6 +8,8 @@ import {
|
|||||||
} from "@/server/trpc"
|
} from "@/server/trpc"
|
||||||
import { langInput } from "@/server/utils"
|
import { langInput } from "@/server/utils"
|
||||||
|
|
||||||
|
import { getReedemableCoupons } from "@/utils/rewards"
|
||||||
|
|
||||||
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
|
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
|
||||||
import {
|
import {
|
||||||
rewardsAllInput,
|
rewardsAllInput,
|
||||||
@@ -16,11 +17,7 @@ import {
|
|||||||
rewardsRedeemInput,
|
rewardsRedeemInput,
|
||||||
rewardsUpdateInput,
|
rewardsUpdateInput,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
import {
|
import { type Surprise, validateCategorizedRewardsSchema } from "./output"
|
||||||
type Reward,
|
|
||||||
type Surprise,
|
|
||||||
validateCategorizedRewardsSchema,
|
|
||||||
} from "./output"
|
|
||||||
import {
|
import {
|
||||||
getAllRewardCounter,
|
getAllRewardCounter,
|
||||||
getAllRewardFailCounter,
|
getAllRewardFailCounter,
|
||||||
@@ -33,7 +30,6 @@ import {
|
|||||||
getCurrentRewardCounter,
|
getCurrentRewardCounter,
|
||||||
getCurrentRewardFailCounter,
|
getCurrentRewardFailCounter,
|
||||||
getCurrentRewardSuccessCounter,
|
getCurrentRewardSuccessCounter,
|
||||||
getNonRedeemedRewardIds,
|
|
||||||
getRedeemCounter,
|
getRedeemCounter,
|
||||||
getRedeemFailCounter,
|
getRedeemFailCounter,
|
||||||
getRedeemSuccessCounter,
|
getRedeemSuccessCounter,
|
||||||
@@ -41,8 +37,16 @@ import {
|
|||||||
getUnwrapSurpriseCounter,
|
getUnwrapSurpriseCounter,
|
||||||
getUnwrapSurpriseFailCounter,
|
getUnwrapSurpriseFailCounter,
|
||||||
getUnwrapSurpriseSuccessCounter,
|
getUnwrapSurpriseSuccessCounter,
|
||||||
|
isSurpriseReward,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Reward,
|
||||||
|
RewardWithRedeem,
|
||||||
|
} from "@/types/components/myPages/rewards"
|
||||||
|
|
||||||
|
const ONE_HOUR = 60 * 60
|
||||||
|
|
||||||
export const rewardQueryRouter = router({
|
export const rewardQueryRouter = router({
|
||||||
all: contentStackBaseWithServiceProcedure
|
all: contentStackBaseWithServiceProcedure
|
||||||
.input(rewardsAllInput)
|
.input(rewardsAllInput)
|
||||||
@@ -215,71 +219,37 @@ export const rewardQueryRouter = router({
|
|||||||
return null
|
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)
|
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||||
|
|
||||||
if (!cmsRewards) {
|
if (!cmsRewards) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const wrappedSurprisesIds = validatedApiRewards.data
|
const rewards: Array<Reward | RewardWithRedeem> = cmsRewards
|
||||||
.filter(
|
.filter(
|
||||||
(reward) =>
|
(cmsReward) =>
|
||||||
reward.type === "coupon" &&
|
// filters out any rewards tied to wrapped surprises
|
||||||
reward.rewardType === "Surprise" &&
|
!validatedApiRewards.data
|
||||||
"coupon" in reward &&
|
.filter(isSurpriseReward)
|
||||||
reward.coupon.some(({ unwrapped }) => !unwrapped)
|
.filter((reward) =>
|
||||||
)
|
reward.coupon.some(({ unwrapped }) => !unwrapped)
|
||||||
.map(({ rewardId }) => rewardId)
|
)
|
||||||
|
.map(({ rewardId }) => rewardId)
|
||||||
const rewards = cmsRewards
|
.includes(cmsReward.reward_id)
|
||||||
.filter(
|
|
||||||
(cmsReward) => !wrappedSurprisesIds.includes(cmsReward.reward_id)
|
|
||||||
)
|
)
|
||||||
.map((cmsReward) => {
|
.map((cmsReward) => {
|
||||||
|
// Non-null assertion is used here because we know our reward exist
|
||||||
const apiReward = validatedApiRewards.data.find(
|
const apiReward = validatedApiRewards.data.find(
|
||||||
({ rewardId }) => rewardId === cmsReward.reward_id
|
({ 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 {
|
return {
|
||||||
...cmsReward,
|
...cmsReward,
|
||||||
id: apiReward?.id,
|
data: apiReward,
|
||||||
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 || [] : [],
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -346,12 +316,11 @@ export const rewardQueryRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const rewardIds = validatedApiRewards.data
|
const rewardIds = validatedApiRewards.data
|
||||||
.map((reward) => reward?.rewardId)
|
.filter((reward) => getReedemableCoupons(reward).length)
|
||||||
|
.map((reward) => reward.rewardId)
|
||||||
.filter((rewardId): rewardId is string => !!rewardId)
|
.filter((rewardId): rewardId is string => !!rewardId)
|
||||||
.sort()
|
.sort()
|
||||||
|
|
||||||
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||||
|
|
||||||
if (!cmsRewards) {
|
if (!cmsRewards) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -359,42 +328,25 @@ export const rewardQueryRouter = router({
|
|||||||
getCurrentRewardSuccessCounter.add(1)
|
getCurrentRewardSuccessCounter.add(1)
|
||||||
|
|
||||||
const surprises: Surprise[] = validatedApiRewards.data
|
const surprises: Surprise[] = validatedApiRewards.data
|
||||||
// TODO: Add predicates once legacy endpoints are removed
|
.filter(isSurpriseReward)
|
||||||
.filter((reward) => {
|
.filter((reward) => {
|
||||||
if (reward?.rewardType !== "Surprise") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!("coupon" in reward)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const unwrappedCoupons =
|
const unwrappedCoupons =
|
||||||
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
|
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
|
||||||
if (unwrappedCoupons.length === 0) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return unwrappedCoupons.length
|
||||||
})
|
})
|
||||||
.map((surprise) => {
|
.map((surprise) => {
|
||||||
const reward = cmsRewards.find(
|
const cmsReward = cmsRewards.find(
|
||||||
({ reward_id }) => surprise.rewardId === reward_id
|
({ reward_id }) => surprise.rewardId === reward_id
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!reward) {
|
if (!cmsReward) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...reward,
|
...cmsReward,
|
||||||
id: surprise.id,
|
data: surprise,
|
||||||
rewardType: surprise.rewardType,
|
|
||||||
rewardTierLevel: undefined,
|
|
||||||
redeemLocation: surprise.redeemLocation,
|
|
||||||
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
|
|
||||||
categories:
|
|
||||||
"categories" in surprise ? surprise.categories || [] : [],
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatMap((surprises) => (surprises ? [surprises] : []))
|
.flatMap((surprises) => (surprises ? [surprises] : []))
|
||||||
|
|||||||
@@ -17,11 +17,12 @@ import {
|
|||||||
} from "@/utils/generateTag"
|
} from "@/utils/generateTag"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type CategorizedApiReward,
|
type ApiReward,
|
||||||
type CmsRewardsResponse,
|
type CMSRewardsResponse,
|
||||||
type CmsRewardsWithRedeemResponse,
|
type CMSRewardsWithRedeemResponse,
|
||||||
type GetRewardWithRedeemRefsSchema,
|
type GetRewardWithRedeemRefsSchema,
|
||||||
rewardWithRedeemRefsSchema,
|
rewardWithRedeemRefsSchema,
|
||||||
|
type SurpriseReward,
|
||||||
validateApiAllTiersSchema,
|
validateApiAllTiersSchema,
|
||||||
validateCmsRewardsSchema,
|
validateCmsRewardsSchema,
|
||||||
validateCmsRewardsWithRedeemSchema,
|
validateCmsRewardsWithRedeemSchema,
|
||||||
@@ -29,6 +30,8 @@ import {
|
|||||||
|
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
export { isSurpriseReward }
|
||||||
|
|
||||||
const meter = metrics.getMeter("trpc.reward")
|
const meter = metrics.getMeter("trpc.reward")
|
||||||
export const getAllRewardCounter = meter.createCounter(
|
export const getAllRewardCounter = meter.createCounter(
|
||||||
"trpc.contentstack.reward.all"
|
"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,
|
GetRewardsWithReedem,
|
||||||
{
|
{
|
||||||
locale: lang,
|
locale: lang,
|
||||||
@@ -242,7 +245,7 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
cmsRewardsResponse = await request<CmsRewardsResponse>(
|
cmsRewardsResponse = await request<CMSRewardsResponse>(
|
||||||
GetRewards,
|
GetRewards,
|
||||||
{
|
{
|
||||||
locale: lang,
|
locale: lang,
|
||||||
@@ -297,17 +300,6 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
|||||||
return validatedCmsRewards.data
|
return validatedCmsRewards.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNonRedeemedRewardIds(rewards: Array<CategorizedApiReward>) {
|
function isSurpriseReward(reward: ApiReward): reward is SurpriseReward {
|
||||||
return rewards
|
return reward.rewardType === "Surprise"
|
||||||
.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()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ import type { z } from "zod"
|
|||||||
|
|
||||||
import type { DynamicContent } from "@/types/trpc/routers/contentstack/blocks"
|
import type { DynamicContent } from "@/types/trpc/routers/contentstack/blocks"
|
||||||
import type { blocksSchema } from "@/server/routers/contentstack/accountPage/output"
|
import type { blocksSchema } from "@/server/routers/contentstack/accountPage/output"
|
||||||
import type {
|
import type { Reward, RewardWithRedeem } from "../rewards"
|
||||||
Reward,
|
|
||||||
RewardWithRedeem,
|
|
||||||
} from "@/server/routers/contentstack/reward/output"
|
|
||||||
|
|
||||||
export interface AccountPageContentProps
|
export interface AccountPageContentProps
|
||||||
extends Pick<DynamicContent, "dynamic_content"> {}
|
extends Pick<DynamicContent, "dynamic_content"> {}
|
||||||
@@ -35,8 +32,7 @@ export interface RedeemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptedRewardTextProps {
|
export interface ScriptedRewardTextProps {
|
||||||
rewardType?: string
|
reward: Reward | RewardWithRedeem
|
||||||
rewardTierLevel?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RedeemModalState = "unmounted" | "hidden" | "visible"
|
export type RedeemModalState = "unmounted" | "hidden" | "visible"
|
||||||
|
|||||||
@@ -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 { IconProps } from "@/types/components/icon"
|
||||||
import type { MembershipLevelEnum } from "@/constants/membershipLevels"
|
import type { MembershipLevelEnum } from "@/constants/membershipLevels"
|
||||||
import type {
|
import type {
|
||||||
RESTAURANT_REWARD_IDS,
|
ApiReward,
|
||||||
REWARD_CATEGORIES,
|
CMSReward,
|
||||||
REWARD_IDS,
|
CMSRewardWithRedeem,
|
||||||
REWARD_TYPES,
|
} from "@/server/routers/contentstack/reward/output"
|
||||||
} from "@/constants/rewards"
|
|
||||||
|
export { type Reward, type RewardWithRedeem }
|
||||||
|
|
||||||
export interface RewardIconProps extends IconProps {
|
export interface RewardIconProps extends IconProps {
|
||||||
rewardId: string
|
rewardId: string
|
||||||
@@ -28,3 +36,10 @@ export interface FilterRewardsModalProps {
|
|||||||
availableTierLevels: MembershipLevelEnum[]
|
availableTierLevels: MembershipLevelEnum[]
|
||||||
availableCategories: RewardCategory[]
|
availableCategories: RewardCategory[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Reward extends CMSReward {
|
||||||
|
data: ApiReward
|
||||||
|
}
|
||||||
|
interface RewardWithRedeem extends CMSRewardWithRedeem {
|
||||||
|
data: ApiReward
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { MembershipLevel } from "@/constants/membershipLevels"
|
import type { MembershipLevel } from "@/constants/membershipLevels"
|
||||||
import { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
|
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
|
||||||
import { Reward } from "@/server/routers/contentstack/reward/output"
|
import type { Reward } from "./myPages/rewards"
|
||||||
|
|
||||||
export type OverviewTableClientProps = {
|
export type OverviewTableClientProps = {
|
||||||
activeMembership: MembershipLevel | null
|
activeMembership: MembershipLevel | null
|
||||||
|
|||||||
@@ -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"
|
import type { ComparisonLevel } from "@/types/components/overviewTable"
|
||||||
|
|
||||||
export function getGroupedRewards(levels: ComparisonLevel[]) {
|
export function getGroupedRewards(levels: ComparisonLevel[]) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
RESTAURANT_REWARD_IDS,
|
RESTAURANT_REWARD_IDS,
|
||||||
REWARD_CATEGORIES,
|
REWARD_CATEGORIES,
|
||||||
REWARD_IDS,
|
REWARD_IDS,
|
||||||
REWARD_TYPES,
|
|
||||||
} from "@/constants/rewards"
|
} from "@/constants/rewards"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
@@ -12,16 +11,18 @@ import type {
|
|||||||
RestaurantRewardId,
|
RestaurantRewardId,
|
||||||
RewardCategory,
|
RewardCategory,
|
||||||
RewardId,
|
RewardId,
|
||||||
RewardType,
|
|
||||||
} from "@/types/components/myPages/rewards"
|
} from "@/types/components/myPages/rewards"
|
||||||
import type {
|
import type {
|
||||||
|
ApiReward,
|
||||||
Coupon,
|
Coupon,
|
||||||
RewardWithRedeem,
|
RedeemableCoupon,
|
||||||
|
RedeemLocation,
|
||||||
} from "@/server/routers/contentstack/reward/output"
|
} from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getEarliestExpirationDate,
|
getEarliestExpirationDate,
|
||||||
getRewardType,
|
getFirstRedeemableCoupon,
|
||||||
|
getReedemableCoupons,
|
||||||
isOnSiteTierReward,
|
isOnSiteTierReward,
|
||||||
isRestaurantOnSiteTierReward,
|
isRestaurantOnSiteTierReward,
|
||||||
isRestaurantReward,
|
isRestaurantReward,
|
||||||
@@ -44,28 +45,41 @@ function isRewardCategory(value: string): value is RewardCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function redeemLocationIsOnSite(
|
function redeemLocationIsOnSite(
|
||||||
location: RewardWithRedeem["redeemLocation"]
|
location: RedeemLocation
|
||||||
): location is "On-site" {
|
): location is "On-site" {
|
||||||
return location === "On-site"
|
return location === "On-site"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTierType(type: RewardWithRedeem["rewardType"]): type is "Tier" {
|
function isTierType(type: string): type is "Tier" {
|
||||||
return type === "Tier"
|
return type === "Tier"
|
||||||
}
|
}
|
||||||
|
|
||||||
function isOnSiteTierReward(reward: RewardWithRedeem): boolean {
|
function isOnSiteTierReward(reward: ApiReward): boolean {
|
||||||
return (
|
return (
|
||||||
redeemLocationIsOnSite(reward.redeemLocation) &&
|
redeemLocationIsOnSite(reward.redeemLocation) &&
|
||||||
isTierType(reward.rewardType)
|
isTierType(reward.rewardType)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRestaurantOnSiteTierReward(reward: RewardWithRedeem): boolean {
|
function isRestaurantOnSiteTierReward(reward: ApiReward): boolean {
|
||||||
return isOnSiteTierReward(reward) && isRestaurantReward(reward.reward_id)
|
return isOnSiteTierReward(reward) && isRestaurantReward(reward.rewardId)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRewardType(type?: string): RewardType | null {
|
function getReedemableCoupons(reward: ApiReward): RedeemableCoupon[] {
|
||||||
return REWARD_TYPES.find((t) => t === type) ?? null
|
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[]) {
|
function getEarliestExpirationDate(coupons: Coupon[]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user