From 0ae4c5db178b76609c1a7d44782914fdd48c0066 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Mon, 10 Mar 2025 13:35:07 +0100 Subject: [PATCH 1/4] chore(LOY-175): remove references to old reward endpoints --- apps/scandic-web/.env.local.example | 1 - apps/scandic-web/.env.test | 3 +- .../Rewards/CurrentRewards/index.tsx | 2 +- apps/scandic-web/env/server.ts | 8 -- apps/scandic-web/lib/api/endpoints.ts | 4 - .../routers/contentstack/reward/output.ts | 84 ------------------- .../routers/contentstack/reward/query.ts | 33 +++----- .../routers/contentstack/reward/utils.ts | 71 +--------------- 8 files changed, 13 insertions(+), 193 deletions(-) diff --git a/apps/scandic-web/.env.local.example b/apps/scandic-web/.env.local.example index fd28543c1..651408b1e 100644 --- a/apps/scandic-web/.env.local.example +++ b/apps/scandic-web/.env.local.example @@ -60,6 +60,5 @@ ENABLE_BOOKING_WIDGET_HOTELRESERVATION_PATH="false" SHOW_SITE_WIDE_ALERT="false" SHOW_SIGNUP_FLOW="true" -USE_NEW_REWARDS_ENDPOINT="true" USE_NEW_REWARD_MODEL="true" diff --git a/apps/scandic-web/.env.test b/apps/scandic-web/.env.test index 3a0bf6912..3b46b880d 100644 --- a/apps/scandic-web/.env.test +++ b/apps/scandic-web/.env.test @@ -43,7 +43,6 @@ GOOGLE_STATIC_MAP_ID="test" GOOGLE_DYNAMIC_MAP_ID="test" NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE="true" SALESFORCE_PREFERENCE_BASE_URL="test" -USE_NEW_REWARDS_ENDPOINT="true" USE_NEW_REWARD_MODEL="true" TZ=UTC @@ -54,4 +53,4 @@ SHOW_SITE_WIDE_ALERT="false" NEXT_PUBLIC_SENTRY_ENVIRONMENT="test" NEXT_PUBLIC_SENTRY_CLIENT_SAMPLERATE="0" -SITEMAP_SYNC_SECRET="test \ No newline at end of file +SITEMAP_SYNC_SECRET="test diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx index 11094f382..aa681effa 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/index.tsx @@ -31,7 +31,7 @@ export default async function CurrentRewardsBlock({ diff --git a/apps/scandic-web/env/server.ts b/apps/scandic-web/env/server.ts index a08847ad2..b9bbc298b 100644 --- a/apps/scandic-web/env/server.ts +++ b/apps/scandic-web/env/server.ts @@ -141,13 +141,6 @@ export const env = createEnv({ // transform to boolean .transform((s) => s === "true") .default("false"), - USE_NEW_REWARDS_ENDPOINT: z - .string() - // only allow "true" or "false" - .refine((s) => s === "true" || s === "false") - // transform to boolean - .transform((s) => s === "true") - .default("false"), USE_NEW_REWARD_MODEL: z .string() // only allow "true" or "false" @@ -268,7 +261,6 @@ export const env = createEnv({ GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID, GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID, HIDE_FOR_NEXT_RELEASE: process.env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE, - USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT, USE_NEW_REWARD_MODEL: process.env.USE_NEW_REWARD_MODEL, ENABLE_BOOKING_FLOW: process.env.ENABLE_BOOKING_FLOW, ENABLE_BOOKING_WIDGET: process.env.ENABLE_BOOKING_WIDGET, diff --git a/apps/scandic-web/lib/api/endpoints.ts b/apps/scandic-web/lib/api/endpoints.ts index d7270bcc7..e4b01080c 100644 --- a/apps/scandic-web/lib/api/endpoints.ts +++ b/apps/scandic-web/lib/api/endpoints.ts @@ -171,10 +171,6 @@ export namespace endpoints { export const unlink = `${base.path.profile}/${version}/${base.enitity.Profile}/Unlink` export const matchTier = `${base.path.profile}/${version}/${base.enitity.Profile}/MatchTier` - // TODO: Remove once new endpoints are out in production. - export const reward = `${base.path.profile}/${version}/${base.enitity.Profile}/reward` - export const tierRewards = `${base.path.profile}/${version}/${base.enitity.Profile}/tierRewards` - export function deleteProfile(profileId: string) { return `${profile}/${profileId}` } diff --git a/apps/scandic-web/server/routers/contentstack/reward/output.ts b/apps/scandic-web/server/routers/contentstack/reward/output.ts index dafa56b25..a9703fa80 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/output.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/output.ts @@ -12,66 +12,6 @@ import { systemSchema } from "../schemas/system" import type { RewardCategory } from "@/types/components/myPages/rewards" -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, @@ -84,26 +24,6 @@ enum TierKey { 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({ @@ -168,10 +88,6 @@ export const validateCmsRewardsWithRedeemSchema = z }) .transform((data) => data.data.all_reward.items) -export type ApiReward = z.output[number] - -export type SurpriseReward = z.output - export type CmsRewardsResponse = z.input export type CmsRewardsWithRedeemResponse = z.input< diff --git a/apps/scandic-web/server/routers/contentstack/reward/query.ts b/apps/scandic-web/server/routers/contentstack/reward/query.ts index 5566db92c..6406a058b 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/query.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/query.ts @@ -1,4 +1,3 @@ -import { env } from "@/env/server" import * as api from "@/lib/api" import { dt } from "@/lib/dt" import { notFound } from "@/server/errors/trpc" @@ -20,11 +19,9 @@ import { import { type Reward, type Surprise, - validateApiRewardSchema, validateCategorizedRewardsSchema, } from "./output" import { - getAllCachedApiRewards, getAllRewardCounter, getAllRewardFailCounter, getAllRewardSuccessCounter, @@ -52,9 +49,7 @@ export const rewardQueryRouter = router({ .query(async function ({ input, ctx }) { getAllRewardCounter.add(1) - const allApiRewards = env.USE_NEW_REWARDS_ENDPOINT - ? await getCachedAllTierRewards(ctx.serviceToken) - : await getAllCachedApiRewards(ctx.serviceToken) + const allApiRewards = await getCachedAllTierRewards(ctx.serviceToken) if (!allApiRewards) { return [] @@ -114,9 +109,9 @@ export const rewardQueryRouter = router({ getByLevelRewardCounter.add(1) const { level_id } = input - const allUpcomingApiRewards = env.USE_NEW_REWARDS_ENDPOINT - ? await getCachedAllTierRewards(ctx.serviceToken) - : await getAllCachedApiRewards(ctx.serviceToken) + const allUpcomingApiRewards = await getCachedAllTierRewards( + ctx.serviceToken + ) if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) { getByLevelRewardFailCounter.add(1) @@ -167,10 +162,7 @@ export const rewardQueryRouter = router({ .query(async function ({ ctx }) { getCurrentRewardCounter.add(1) - const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT - const endpoint = isNewEndpoint - ? api.endpoints.v1.Profile.Reward.reward - : api.endpoints.v1.Profile.reward + const endpoint = api.endpoints.v1.Profile.Reward.reward const apiResponse = await api.get(endpoint, { headers: { @@ -203,9 +195,8 @@ export const rewardQueryRouter = router({ const data = await apiResponse.json() - const validatedApiRewards = isNewEndpoint - ? validateCategorizedRewardsSchema.safeParse(data) - : validateApiRewardSchema.safeParse(data) + const validatedApiRewards = + validateCategorizedRewardsSchema.safeParse(data) if (!validatedApiRewards.success) { getCurrentRewardFailCounter.add(1, { @@ -301,10 +292,7 @@ export const rewardQueryRouter = router({ .query(async ({ ctx }) => { getCurrentRewardCounter.add(1) - const isNewEndpoint = env.USE_NEW_REWARDS_ENDPOINT - const endpoint = isNewEndpoint - ? api.endpoints.v1.Profile.Reward.reward - : api.endpoints.v1.Profile.reward + const endpoint = api.endpoints.v1.Profile.Reward.reward const apiResponse = await api.get(endpoint, { cache: undefined, @@ -337,9 +325,8 @@ export const rewardQueryRouter = router({ } const data = await apiResponse.json() - const validatedApiRewards = isNewEndpoint - ? validateCategorizedRewardsSchema.safeParse(data) - : validateApiRewardSchema.safeParse(data) + const validatedApiRewards = + validateCategorizedRewardsSchema.safeParse(data) if (!validatedApiRewards.success) { getCurrentRewardFailCounter.add(1, { diff --git a/apps/scandic-web/server/routers/contentstack/reward/utils.ts b/apps/scandic-web/server/routers/contentstack/reward/utils.ts index 81f501cb9..af7d8d767 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/utils.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/utils.ts @@ -17,14 +17,12 @@ import { } from "@/utils/generateTag" import { - type ApiReward, type CategorizedApiReward, type CmsRewardsResponse, type CmsRewardsWithRedeemResponse, type GetRewardWithRedeemRefsSchema, rewardWithRedeemRefsSchema, validateApiAllTiersSchema, - validateApiTierRewardsSchema, validateCmsRewardsSchema, validateCmsRewardsWithRedeemSchema, } from "./output" @@ -93,71 +91,6 @@ export function getUniqueRewardIds(rewardIds: string[]) { return Array.from(uniqueRewardIds) } -/** - * Uses the legacy profile/v1/Profile/tierRewards endpoint. - * TODO: Delete when the new endpoint is out in production. - */ -export async function getAllCachedApiRewards(token: string) { - const cacheClient = await getCacheClient() - - return await cacheClient.cacheOrGet( - "getAllApiRewards", - async () => { - const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, { - headers: { - Authorization: `Bearer ${token}`, - }, - }) - - if (!apiResponse.ok) { - const text = await apiResponse.text() - getAllRewardFailCounter.add(1, { - error_type: "http_error", - error: JSON.stringify({ - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }), - }) - console.error( - "api.rewards.tierRewards error ", - JSON.stringify({ - error: { - status: apiResponse.status, - statusText: apiResponse.statusText, - text, - }, - }) - ) - - throw apiResponse - } - - const data = await apiResponse.json() - const validatedApiTierRewards = - validateApiTierRewardsSchema.safeParse(data) - - if (!validatedApiTierRewards.success) { - getAllRewardFailCounter.add(1, { - error_type: "validation_error", - error: JSON.stringify(validatedApiTierRewards.error), - }) - console.error(validatedApiTierRewards.error) - console.error( - "api.rewards validation error", - JSON.stringify({ - error: validatedApiTierRewards.error, - }) - ) - throw validatedApiTierRewards.error - } - - return validatedApiTierRewards.data - }, - "1h" - ) -} - /** * Cached for 1 hour. */ @@ -364,9 +297,7 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) { return validatedCmsRewards.data } -export function getNonRedeemedRewardIds( - rewards: Array -) { +export function getNonRedeemedRewardIds(rewards: Array) { return rewards .filter((reward) => { if ("coupon" in reward && reward.coupon.length > 0) { From b86347b4f4b0c33e5c6f717ba5f8d10820728519 Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Tue, 11 Mar 2025 16:09:15 +0100 Subject: [PATCH 2/4] refactor(LOY-175): rewrite reward types according to new api endpoints --- .../Rewards/CurrentRewards/Client.tsx | 22 ++- .../Rewards/Redeem/Flows/Campaign.tsx | 10 +- .../Rewards/Redeem/Flows/Tier.tsx | 8 +- .../DynamicContent/Rewards/Redeem/index.tsx | 13 +- .../Rewards/Redeem/useRedeemFlow.ts | 9 +- .../Rewards/ScriptedRewardText/index.tsx | 16 +-- .../components/MyPages/Surprises/Client.tsx | 4 +- .../routers/contentstack/reward/output.ts | 134 ++++++++---------- .../routers/contentstack/reward/query.ts | 120 +++++----------- .../routers/contentstack/reward/utils.ts | 28 ++-- .../components/myPages/myPage/accountPage.ts | 8 +- .../types/components/myPages/rewards.ts | 25 +++- .../types/components/overviewTable.ts | 6 +- apps/scandic-web/utils/loyaltyTable.ts | 3 +- apps/scandic-web/utils/rewards.ts | 36 +++-- 15 files changed, 196 insertions(+), 246 deletions(-) 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 3f0ddc359..bdb2a640f 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,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({
{paginatedRewards.map((reward, idx) => { - const earliestExpirationDate = - "coupons" in reward - ? getEarliestExpirationDate(reward.coupons) - : null + const earliestExpirationDate = getEarliestExpirationDate( + reward.data.coupon + ) return (
- {showRedeem && ( - - )} + {showRedeem && } <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" })) diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Tier.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Tier.tsx index 69831bd57..00ca98e05 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Tier.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/Flows/Tier.tsx @@ -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} - {reward.redeemLocation !== "Non-redeemable" ? ( + {reward.data.redeemLocation !== "Non-redeemable" ? ( <> {redeemStep === "initial" && ( {reward.description} @@ -63,7 +63,7 @@ export default function Tier({ )} {redeemStep === "redeemed" && - isRestaurantOnSiteTierReward(reward) && + isRestaurantOnSiteTierReward(reward.data) && membershipNumber && ( )} @@ -76,7 +76,7 @@ export default function Tier({ )}
- {reward.redeemLocation !== "Non-redeemable" ? ( + {reward.data.redeemLocation !== "Non-redeemable" ? ( <> {redeemStep === "initial" && (