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
392 lines
11 KiB
TypeScript
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
|
|
}),
|
|
})
|