import { z } from "zod" import { MembershipLevelEnum } from "@/constants/membershipLevels" const Coupon = z.object({ code: z.string().optional(), status: z.string().optional(), createdAt: z.string().datetime({ offset: true }).optional(), customer: z.object({ id: z.string().optional(), }), name: z.string().optional(), claimedAt: z.string().datetime({ offset: true }).optional(), redeemedAt: z .date({ coerce: true }) .optional() .transform((value) => { if (value?.getFullYear() === 1) { return null } return value }), type: z.string().optional(), value: z.number().optional(), pool: z.string().optional(), cfUnwrapped: z.boolean().default(false), }) const SurpriseReward = z.object({ title: z.string().optional(), id: z.string().optional(), type: z.literal("coupon"), status: z.string().optional(), rewardId: z.string().optional(), redeemLocation: z.string().optional(), autoApplyReward: z.boolean().default(false), rewardType: z.string().optional(), endsAt: z.string().datetime({ offset: true }).optional(), coupons: z.array(Coupon).optional(), operaRewardId: z.string().default(""), }) export const validateApiRewardSchema = z .object({ data: z.array( z.discriminatedUnion("type", [ z.object({ title: z.string().optional(), id: z.string().optional(), type: z.literal("custom"), status: z.string().optional(), rewardId: z.string().optional(), redeemLocation: z.string().optional(), autoApplyReward: z.boolean().default(false), rewardType: z.string().optional(), rewardTierLevel: z.string().optional(), operaRewardId: z.string().default(""), }), SurpriseReward, ]) ), }) .transform((data) => data.data) 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 export const validateApiTierRewardsSchema = z.record( z.nativeEnum(TierKey).transform((data) => { return TierKey[data as unknown as Key] }), z.array( z.object({ title: z.string().optional(), id: z.string().optional(), type: z.string().optional(), status: z.string().optional(), rewardId: z.string().optional(), redeemLocation: z.string().optional(), autoApplyReward: z.boolean().default(false), rewardType: z.string().optional(), rewardTierLevel: z.string().optional(), operaRewardId: z.string().default(""), }) ) ) export 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) export const validateCmsRewardsWithRedeemSchema = 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(), redeem_description: z .string() .nullable() .transform((val) => val || ""), grouped_description: z.string().optional(), value: z.string().optional(), }) ), }), }), }) .transform((data) => data.data.all_reward.items) export type ApiReward = z.output[0] export type SurpriseReward = z.output export type CmsRewardsResponse = z.input export type CmsRewardsWithRedeemResponse = z.input< typeof validateCmsRewardsWithRedeemSchema > export type CMSReward = z.output[0] 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 couponCode: string | undefined } export type RewardWithRedeem = CMSRewardWithRedeem & { id: string | undefined rewardType: string | undefined redeemLocation: string | undefined rewardTierLevel: string | undefined operaRewardId: string couponCode: string | undefined } // New endpoint related types and schemas. const BenefitReward = z.object({ title: z.string().optional(), id: z.string().optional(), redeemLocation: z.string().optional(), rewardId: z.string().optional(), rewardType: z.string().optional(), // TODO: Should be "Tier" but can't because of backwards compatibility rewardTierLevel: z.string().optional(), status: z.string().optional(), }) const CouponData = z.object({ couponCode: z.string().optional(), unwrapped: z.boolean().default(false), state: z.enum(["claimed", "redeemed", "viewed"]), expiresAt: z.string().datetime({ offset: true }).optional(), }) const CouponReward = z.object({ title: z.string().optional(), id: z.string().optional(), rewardId: z.string().optional(), rewardType: z.enum(["Surprise", "Campaign", "Member-voucher"]), redeemLocation: z.string().optional(), operaRewardId: z.string().default(""), status: z.string().optional(), coupon: z.array(CouponData).optional(), }) /** * 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 .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. })), ]) export 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"