Merge branch 'develop' into feature/tracking

This commit is contained in:
Linus Flood
2024-10-28 15:23:18 +01:00
102 changed files with 2272 additions and 338 deletions

View File

@@ -1,3 +1,5 @@
import { cache } from "react"
import { Lang } from "@/constants/languages"
import { GetContactConfig } from "@/lib/graphql/Query/ContactConfig.graphql"
import {
@@ -92,7 +94,7 @@ import type {
GetSiteConfigRefData,
} from "@/types/trpc/routers/contentstack/siteConfig"
async function getContactConfig(lang: Lang) {
const getContactConfig = cache(async (lang: Lang) => {
getContactConfigCounter.add(1, { lang })
console.info(
"contentstack.contactConfig start",
@@ -153,7 +155,7 @@ async function getContactConfig(lang: Lang) {
JSON.stringify({ query: { lang } })
)
return validatedContactConfigConfig.data.all_contact_config.items[0]
}
})
export const baseQueryRouter = router({
contact: contentstackBaseProcedure.query(async ({ ctx }) => {

View File

@@ -17,3 +17,7 @@ export const rewardsCurrentInput = z.object({
cursor: z.number().optional().default(0),
lang: z.nativeEnum(Lang).optional(),
})
export const rewardsUpdateInput = z.object({
id: z.string(),
})

View File

@@ -2,24 +2,64 @@ import { z } from "zod"
import { MembershipLevelEnum } from "@/constants/membershipLevels"
export const validateApiRewardSchema = z.object({
data: 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(),
})
.optional()
),
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.literal("Surprise"),
endsAt: z.string().datetime({ offset: true }).optional(),
coupons: z.array(Coupon).optional(),
})
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(),
}),
SurpriseReward,
])
),
})
.transform((data) => data.data)
enum TierKey {
tier1 = MembershipLevelEnum.L1,
tier2 = MembershipLevelEnum.L2,
@@ -37,19 +77,17 @@ export const validateApiTierRewardsSchema = z.record(
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(),
})
.optional()
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(),
})
)
)
@@ -77,6 +115,10 @@ export const validateCmsRewardsSchema = z
})
.transform((data) => data.data.all_reward.items)
export type ApiReward = z.output<typeof validateApiRewardSchema>[0]
export type SurpriseReward = z.output<typeof SurpriseReward>
export type CmsRewardsResponse = z.input<typeof validateCmsRewardsSchema>
export type Reward = z.output<typeof validateCmsRewardsSchema>[0]

View File

@@ -19,15 +19,19 @@ import {
rewardsAllInput,
rewardsByLevelInput,
rewardsCurrentInput,
rewardsUpdateInput,
} from "./input"
import {
CmsRewardsResponse,
Reward,
SurpriseReward,
validateApiRewardSchema,
validateApiTierRewardsSchema,
validateCmsRewardsSchema,
} from "./output"
import { Surprise } from "@/types/components/blocks/surprises"
const meter = metrics.getMeter("trpc.reward")
// OpenTelemetry metrics: Reward
@@ -242,10 +246,10 @@ export const rewardQueryRouter = router({
return null
}
const rewardIds = validatedApiRewards.data.data
const rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter(Boolean)
.sort() as string[]
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const slicedData = rewardIds.slice(cursor, limit + cursor)
@@ -258,9 +262,21 @@ export const rewardQueryRouter = router({
const nextCursor =
limit + cursor < rewardIds.length ? limit + cursor : undefined
const surprisesIds = validatedApiRewards.data
.filter(
({ type, rewardType }) =>
type === "coupon" && rewardType === "Surprise"
)
.map(({ rewardId }) => rewardId)
const rewards = cmsRewards.filter(
(reward) => !surprisesIds.includes(reward.reward_id)
)
getCurrentRewardSuccessCounter.add(1)
return {
rewards: cmsRewards,
rewards,
nextCursor,
}
}),
@@ -374,4 +390,112 @@ export const rewardQueryRouter = router({
getAllRewardSuccessCounter.add(1)
return levelsWithRewards
}),
surprises: contentStackBaseWithProtectedProcedure.query(async ({ ctx }) => {
getCurrentRewardCounter.add(1)
const apiResponse = await api.get(api.endpoints.v1.rewards, {
cache: undefined, // override defaultOptions
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
next: { revalidate: 60 * 60 },
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getCurrentRewardFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
})
console.error(
"api.reward error ",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
return null
}
const data = await apiResponse.json()
const validatedApiRewards = validateApiRewardSchema.safeParse(data)
if (!validatedApiRewards.success) {
getCurrentRewardFailCounter.add(1, {
locale: ctx.lang,
error_type: "validation_error",
error: JSON.stringify(validatedApiRewards.error),
})
console.error(validatedApiRewards.error)
console.error(
"contentstack.surprises validation error",
JSON.stringify({
query: { locale: ctx.lang },
error: validatedApiRewards.error,
})
)
return null
}
const rewardIds = validatedApiRewards.data
.map((reward) => reward?.rewardId)
.filter((rewardId): rewardId is string => !!rewardId)
.sort()
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
if (!cmsRewards) {
return null
}
getCurrentRewardSuccessCounter.add(1)
const surprises =
validatedApiRewards.data
.filter(
(reward): reward is SurpriseReward =>
reward?.type === "coupon" && reward?.rewardType === "Surprise"
)
.map((surprise) => {
const reward = cmsRewards.find(
({ reward_id }) => surprise.rewardId === reward_id
)
if (!reward) {
return null
}
return {
...reward,
id: surprise.id,
endsAt: surprise.endsAt,
}
})
.filter((surprise): surprise is Surprise => !!surprise) ?? []
return surprises
}),
update: contentStackBaseWithProtectedProcedure
.input(rewardsUpdateInput)
.mutation(async ({ input, ctx }) => {
const response = await Promise.resolve({ ok: true })
// const response = await api.post(api.endpoints.v1.rewards, {
// body: {
// ids: [input.id],
// },
// })
if (!response.ok) {
return false
}
return true
}),
})

View File

@@ -92,7 +92,7 @@ export function transform(data: Data) {
system: data.system,
title: data.title,
url: data.web.original_url
? removeMultipleSlashes(data.web.original_url)
? data.web.original_url
: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
web: data.web,
}

View File

@@ -89,5 +89,6 @@ export const roomSchema = z
roomSize: data.attributes.roomSize,
sortOrder: data.attributes.sortOrder,
type: data.type,
roomFacilities: data.attributes.roomFacilities,
}
})

View File

@@ -1,4 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { cache } from "react"
import * as api from "@/lib/api"
import {
@@ -80,86 +81,87 @@ const getCreditCardsFailCounter = meter.createCounter(
"trpc.user.creditCards-fail"
)
export async function getVerifiedUser({ session }: { session: Session }) {
const now = Date.now()
if (session.token.expires_at && session.token.expires_at < now) {
return { error: true, cause: "token_expired" } as const
}
getVerifiedUserCounter.add(1)
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, {
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
})
if (!apiResponse.ok) {
const text = await apiResponse.text()
getVerifiedUserFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
}),
export const getVerifiedUser = cache(
async ({ session }: { session: Session }) => {
const now = Date.now()
if (session.token.expires_at && session.token.expires_at < now) {
return { error: true, cause: "token_expired" } as const
}
getVerifiedUserCounter.add(1)
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, {
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
})
console.error(
"api.user.profile getVerifiedUser error",
JSON.stringify({
error: {
if (!apiResponse.ok) {
const text = await apiResponse.text()
getVerifiedUserFailCounter.add(1, {
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
}),
})
)
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
} else if (apiResponse.status === 404) {
return { error: true, cause: "notfound" } as const
console.error(
"api.user.profile getVerifiedUser error",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
})
)
if (apiResponse.status === 401) {
return { error: true, cause: "unauthorized" } as const
} else if (apiResponse.status === 403) {
return { error: true, cause: "forbidden" } as const
} else if (apiResponse.status === 404) {
return { error: true, cause: "notfound" } as const
}
return {
error: true,
cause: "unknown",
status: apiResponse.status,
} as const
}
return {
error: true,
cause: "unknown",
status: apiResponse.status,
} as const
}
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
getVerifiedUserFailCounter.add(1, {
error_type: "data_error",
})
console.error(
"api.user.profile getVerifiedUser data error",
JSON.stringify({
apiResponse: apiJson,
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
getVerifiedUserFailCounter.add(1, {
error_type: "data_error",
})
)
return null
}
console.error(
"api.user.profile getVerifiedUser data error",
JSON.stringify({
apiResponse: apiJson,
})
)
return null
}
const verifiedData = getUserSchema.safeParse(apiJson)
if (!verifiedData.success) {
getVerifiedUserFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
console.error(
"api.user.profile validation error",
JSON.stringify({
errors: verifiedData.error,
const verifiedData = getUserSchema.safeParse(apiJson)
if (!verifiedData.success) {
getVerifiedUserFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedData.error),
})
)
return null
console.error(
"api.user.profile validation error",
JSON.stringify({
errors: verifiedData.error,
})
)
return null
}
getVerifiedUserSuccessCounter.add(1)
console.info("api.user.profile getVerifiedUser success", JSON.stringify({}))
return verifiedData
}
getVerifiedUserSuccessCounter.add(1)
console.info("api.user.profile getVerifiedUser success", JSON.stringify({}))
return verifiedData
}
)
function parsedUser(data: User, isMFA: boolean) {
const country = countries.find((c) => c.code === data.address.countryCode)