Merged in feat/sw-2863-move-contentstack-router-to-trpc-package (pull request #2389)
feat(SW-2863): Move contentstack router to trpc package * Add exports to packages and lint rule to prevent relative imports * Add env to trpc package * Add eslint to trpc package * Apply lint rules * Use direct imports from trpc package * Add lint-staged config to trpc * Move lang enum to common * Restructure trpc package folder structure * WIP first step * update internal imports in trpc * Fix most errors in scandic-web Just 100 left... * Move Props type out of trpc * Fix CategorizedFilters types * Move more schemas in hotel router * Fix deps * fix getNonContentstackUrls * Fix import error * Fix entry error handling * Fix generateMetadata metrics * Fix alertType enum * Fix duplicated types * lint:fix * Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package * Fix broken imports * Merge branch 'master' into feat/sw-2863-move-contentstack-router-to-trpc-package Approved-by: Linus Flood
This commit is contained in:
391
packages/trpc/lib/routers/contentstack/reward/query.ts
Normal file
391
packages/trpc/lib/routers/contentstack/reward/query.ts
Normal file
@@ -0,0 +1,391 @@
|
||||
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.v1.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.v1.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.v1.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.v1.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
|
||||
}),
|
||||
})
|
||||
Reference in New Issue
Block a user