Files
web/packages/trpc/lib/routers/contentstack/reward/query.ts
Matilda Landström 22dd2f60fe Merged in feat/LOY-431-profile-v2 (pull request #3202)
Feat/LOY-431: Switch to V2 of Profile endpoint

* feat(LOY-431): switch to v2 of profile endpoint

* feat(LOY-431): use CreditCard

* feat(LOY-431): remove hotelinformation from friendTransaction schema

* chore(LOY-431): add hotel data request to transactions

* fix(LOY-431): use v1 of friendTransactions


Approved-by: Linus Flood
Approved-by: Erik Tiekstra
Approved-by: Anton Gunnarsson
2025-11-28 13:58:06 +00:00

392 lines
11 KiB
TypeScript

import { createCounter } from "@scandic-hotels/common/telemetry"
import { router } from "../../.."
import * as api from "../../../api"
import { notFound } from "../../../errors"
import {
contentStackBaseWithProtectedProcedure,
contentStackBaseWithServiceProcedure,
protectedProcedure,
} from "../../../procedures"
import { langInput } from "../../../utils"
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
import { getRedeemableRewards, getUnwrappedSurpriseRewards } from "./helpers"
import {
rewardsAllInput,
rewardsByLevelInput,
rewardsRedeemInput,
rewardsUpdateInput,
} from "./input"
import { validateCategorizedRewardsSchema } from "./output"
import {
getCachedAllTierRewards,
getCmsRewards,
getUniqueRewardIds,
} from "./utils"
import type { CMSReward } from "../../../types/reward"
import type { BaseReward, Surprise } from "../../../types/rewards"
import type { LevelWithRewards } from "../loyaltyLevel/output"
export const rewardQueryRouter = router({
all: contentStackBaseWithServiceProcedure
.input(rewardsAllInput)
.query(async function ({ input, ctx }) {
const getContentstackRewardAllCounter = createCounter(
"trpc.contentstack",
"reward.all"
)
const metricsGetContentstackRewardAll =
getContentstackRewardAllCounter.init()
metricsGetContentstackRewardAll.start()
const allApiRewards = await getCachedAllTierRewards(ctx.serviceToken)
if (!allApiRewards) {
return []
}
const rewardIds = Object.values(allApiRewards)
.flatMap((level) => level.map((reward) => reward?.rewardId))
.filter((id): id is string => Boolean(id))
const contentStackRewards = await getCmsRewards(
ctx.lang,
getUniqueRewardIds(rewardIds)
)
if (!contentStackRewards) {
return []
}
const loyaltyLevelsConfig = await getAllLoyaltyLevels(ctx.lang)
const levelsWithRewards = Object.entries(allApiRewards).map(
([level, rewards]) => {
const combinedRewards = rewards
.filter((reward) =>
input.unique ? reward.rewardTierLevel === level : true
)
.map((reward) => {
const contentStackReward = contentStackRewards.find((r) => {
return r.reward_id === reward.rewardId
})
if (contentStackReward) {
return contentStackReward
} else {
metricsGetContentstackRewardAll.dataError(
`Failed to find reward in CMS for reward ${reward.rewardId} `,
{
rewardId: reward.rewardId,
}
)
}
})
.filter((reward): reward is CMSReward => Boolean(reward))
const levelConfig = loyaltyLevelsConfig.find(
(l) => l.level_id === level
)
if (!levelConfig) {
metricsGetContentstackRewardAll.dataError(
`Failed to matched loyalty level between API and CMS for level ${level}`
)
throw notFound()
}
const result: LevelWithRewards = {
...levelConfig,
rewards: combinedRewards,
}
return result
}
)
metricsGetContentstackRewardAll.success()
return levelsWithRewards
}),
byLevel: contentStackBaseWithServiceProcedure
.input(rewardsByLevelInput)
.query(async function ({ input, ctx }) {
const { level_id } = input
const getRewardByLevelCounter = createCounter(
"trpc.contentstack",
"reward.byLevel"
)
const metricsGetRewardByLevel = getRewardByLevelCounter.init({
level_id,
})
metricsGetRewardByLevel.start()
const allUpcomingApiRewards = await getCachedAllTierRewards(
ctx.serviceToken
)
if (!allUpcomingApiRewards || !allUpcomingApiRewards[level_id]) {
metricsGetRewardByLevel.noDataError()
return null
}
let apiRewards = allUpcomingApiRewards[level_id]!
if (input.unique) {
apiRewards = allUpcomingApiRewards[level_id]!.filter(
(reward) => reward?.rewardTierLevel === level_id
)
}
const rewardIds = apiRewards
.map((reward) => reward?.rewardId)
.filter((id): id is string => Boolean(id))
const [contentStackRewards, loyaltyLevelsConfig] = await Promise.all([
getCmsRewards(ctx.lang, rewardIds),
getLoyaltyLevel(ctx.lang, input.level_id),
])
if (!contentStackRewards) {
return null
}
const levelsWithRewards = apiRewards
.map((reward) => {
const contentStackReward = contentStackRewards.find((r) => {
return r.reward_id === reward.rewardId
})
if (contentStackReward) {
return contentStackReward
} else {
metricsGetRewardByLevel.dataError(
`Failed to find reward in Contentstack with rewardId: ${reward.rewardId}`,
{
rewardId: reward.rewardId,
}
)
}
})
.filter((reward): reward is CMSReward => Boolean(reward))
metricsGetRewardByLevel.success()
return { level: loyaltyLevelsConfig, rewards: levelsWithRewards }
}),
current: contentStackBaseWithProtectedProcedure
.input(langInput.optional()) // lang is required for client, but not for server
.query(async function ({ ctx }) {
const getCurrentRewardCounter = createCounter(
"trpc.contentstack",
"reward.current"
)
const metricsGetCurrentReward = getCurrentRewardCounter.init()
metricsGetCurrentReward.start()
const apiResponse = await api.get(
api.endpoints.v2.Profile.Reward.reward,
{
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
}
)
if (!apiResponse.ok) {
await metricsGetCurrentReward.httpError(apiResponse)
return null
}
const data = await apiResponse.json()
const validatedApiRewards =
validateCategorizedRewardsSchema.safeParse(data)
if (!validatedApiRewards.success) {
metricsGetCurrentReward.validationError(validatedApiRewards.error)
return null
}
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: 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 {
...apiReward,
...cmsReward,
}
})
metricsGetCurrentReward.success()
return { rewards }
}),
surprises: contentStackBaseWithProtectedProcedure
.input(langInput.optional()) // lang is required for client, but not for server
.query(async ({ ctx }) => {
const getSurprisesCounter = createCounter(
"trpc.contentstack",
"surprises"
)
const metricsGetSurprises = getSurprisesCounter.init()
metricsGetSurprises.start()
const endpoint = api.endpoints.v2.Profile.Reward.reward
const apiResponse = await api.get(endpoint, {
cache: undefined,
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
})
if (!apiResponse.ok) {
await metricsGetSurprises.httpError(apiResponse)
return null
}
const data = await apiResponse.json()
const validatedApiRewards =
validateCategorizedRewardsSchema.safeParse(data)
if (!validatedApiRewards.success) {
metricsGetSurprises.validationError(validatedApiRewards.error)
return null
}
const unwrappedSurpriseRewards = getUnwrappedSurpriseRewards(
validatedApiRewards.data.coupons
)
const rewardIds = unwrappedSurpriseRewards
.map(({ rewardId }) => rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
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
)!
return {
...apiReward,
...cmsReward,
}
})
metricsGetSurprises.success()
return surprises
}),
unwrap: protectedProcedure
.input(rewardsUpdateInput)
.mutation(async ({ input, ctx }) => {
const results = await Promise.allSettled(
// Execute each unwrap individually
input.map(({ rewardId, couponCode }) => {
async function handleUnwrap() {
const getUnwrapSurpriseCounter = createCounter(
"trpc.contentstack",
"reward.unwrap"
)
const metricsGetUnwrapSurprise = getUnwrapSurpriseCounter.init({
rewardId,
couponCode,
})
metricsGetUnwrapSurprise.start()
const apiResponse = await api.post(
api.endpoints.v2.Profile.Reward.unwrap,
{
body: {
rewardId,
couponCode,
},
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
}
)
if (!apiResponse.ok) {
metricsGetUnwrapSurprise.httpError(apiResponse)
return false
}
metricsGetUnwrapSurprise.success()
return true
}
return handleUnwrap()
})
)
if (
results.some(
(result) => result.status === "rejected" || result.value === false
)
) {
return null
}
return true
}),
redeem: protectedProcedure
.input(rewardsRedeemInput)
.mutation(async ({ input, ctx }) => {
const { rewardId, couponCode } = input
const getRedeemCounter = createCounter(
"trpc.contentstack",
"reward.redeem"
)
const metricGetRedeem = getRedeemCounter.init({ rewardId, couponCode })
metricGetRedeem.start()
const apiResponse = await api.post(
api.endpoints.v2.Profile.Reward.redeem,
{
body: {
rewardId,
couponCode,
},
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
}
)
if (!apiResponse.ok) {
metricGetRedeem.httpError(apiResponse)
return null
}
metricGetRedeem.success()
return true
}),
})