Merged in feat/sw-1975-get-profile-v2 (pull request #1651)

Use get Profile V2 endpoint

Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-04-08 06:26:00 +00:00
parent d282437a3d
commit c56a0b8ce9
18 changed files with 208 additions and 168 deletions

View File

@@ -6,7 +6,6 @@ import { getVerifiedUser } from "@/server/routers/user/query"
import { router, safeProtectedServiceProcedure } from "@/server/trpc"
import { isValidSession } from "@/utils/session"
import { getFriendsMembership } from "@/utils/user"
import {
addPackageInput,
@@ -85,8 +84,7 @@ async function getMembershipNumber(
return undefined
}
const membership = getFriendsMembership(verifiedUser.data.memberships)
return membership?.membershipNumber
return verifiedUser.data.membershipNumber
}
export const bookingMutationRouter = router({

View File

@@ -7,20 +7,20 @@ import { getIntl } from "@/i18n"
import { safeTry } from "@/utils/safeTry"
import { getEurobonusMembership } from "@/utils/user"
import type { Membership } from "@/types/user"
import type { UserLoyalty } from "@/types/user"
import type { Lang } from "@/constants/languages"
import type { MyPagesLink } from "./MyPagesLink"
export const getPrimaryLinks = cache(
async ({
lang,
memberships,
userLoyalty,
}: {
lang: Lang
memberships: Membership[]
userLoyalty: UserLoyalty
}): Promise<MyPagesLink[]> => {
const intl = await getIntl()
const showSASLink = isScandicXSASActive(memberships)
const showSASLink = isScandicXSASActive(userLoyalty)
const [showTeamMemberLink] = await safeTry(showTeamMemberCard())
const menuItems: MyPagesLink[] = [
@@ -66,8 +66,8 @@ export const getPrimaryLinks = cache(
}
)
const isScandicXSASActive = (memberships: Membership[]) => {
const eurobonusMembership = getEurobonusMembership(memberships)
const isScandicXSASActive = (loyalty: UserLoyalty) => {
const eurobonusMembership = getEurobonusMembership(loyalty)
const isLinked = Boolean(eurobonusMembership)
return env.SAS_ENABLED && isLinked

View File

@@ -45,7 +45,7 @@ export const myPagesNavigation = safeProtectedProcedure
}
const [primaryLinks, secondaryLinks] = await Promise.all([
getPrimaryLinks({ lang, memberships: user.data.memberships }),
getPrimaryLinks({ lang, userLoyalty: user.data.loyalty }),
getSecondaryLinks({ lang }),
])

View File

@@ -2,53 +2,85 @@ import { z } from "zod"
import { countriesMap } from "@/constants/countries"
import { getFriendsMembership, scandicMemberships } from "@/utils/user"
import { getFriendsMembership } from "@/utils/user"
import { imageSchema } from "../hotels/schemas/image"
const scandicFriendsTier = z.enum(["L1", "L2", "L3", "L4", "L5", "L6", "L7"])
const sasEurobonusTier = z.enum(["EBB", "EBS", "EBG", "EBD", "EBP"])
const commonMembershipSchema = z.object({
currentPoints: z.number().optional(),
expirationDate: z.string().optional(),
membershipLevel: z.string().optional(),
nextLevel: z.string().optional(),
nightsToTopTier: z.number().optional(),
pointsExpiryDate: z.string().optional(),
pointsRequiredToNextlevel: z.number().optional(),
pointsToExpire: z.number().optional(),
tierExpirationDate: z.string().optional(),
membershipNumber: z.string(),
tierExpires: z.string(),
memberSince: z.string().nullish(),
})
const toLowerCaseString = z.string().transform((s) => s.toLowerCase())
const membershipType = (membershipType: scandicMemberships) =>
toLowerCaseString
// The memberships enum is in lower case so this makes sure it will match regardless of casing in the API response.
.pipe(z.literal(membershipType))
const friendsMembershipSchema = z
// This prevents validation errors if the API returns an unhandled membership type
const otherMembershipSchema = z
.object({
membershipType: membershipType(scandicMemberships.guestpr),
membershipNumber: z.string(),
memberSince: z.string(),
// This ensures that `type` won't widen into "string", losing the literal types, when used in a union
type: z.string().refine((val): val is string & {} => true),
})
.merge(commonMembershipSchema)
const otherMembershipSchema = z
export const sasMembershipSchema = z
.object({
membershipType: toLowerCaseString,
membershipNumber: z.string().optional(),
memberSince: z.string().optional(),
type: z.literal("SAS_EB"),
tier: sasEurobonusTier,
nextTier: sasEurobonusTier.nullish(),
})
.merge(commonMembershipSchema)
export const friendsMembershipSchema = z
.object({
type: z.literal("SCANDIC_NATIVE"),
tier: scandicFriendsTier,
nextTier: scandicFriendsTier.nullish(),
pointsToNextTier: z.number().nullish(),
nightsToTopTier: z.number().nullish(),
})
.merge(commonMembershipSchema)
export const membershipSchema = z.union([
friendsMembershipSchema,
sasMembershipSchema,
otherMembershipSchema,
])
const pointExpirationSchema = z.object({
points: z.number().int(),
expires: z.string(),
})
export const userLoyaltySchema = z.object({
memberships: z.array(membershipSchema),
points: z.object({
spendable: z.number().int(),
earned: z.number().int(),
spent: z.number().int(),
}),
tier: scandicFriendsTier,
tierExpires: z.string(),
tierBoostedBy: z.string().nullish(),
pointExpirations: z.array(pointExpirationSchema),
})
export const getUserSchema = z
.object({
data: z.object({
attributes: z.object({
dateOfBirth: z.string().optional().default("1900-01-01"),
email: z.string().email(),
firstName: z.string(),
language: z
.string()
// Preserve Profile v1 formatting for now so it matches ApiLang enum
.transform((s) => s.charAt(0).toUpperCase() + s.slice(1))
.optional(),
lastName: z.string(),
phoneNumber: z.string().optional(),
profileId: z.string(),
membershipNumber: z.string(),
address: z
.object({
city: z.string().optional(),
@@ -59,14 +91,7 @@ export const getUserSchema = z
})
.optional()
.nullable(),
dateOfBirth: z.string().optional().default("1900-01-01"),
email: z.string().email(),
firstName: z.string(),
language: z.string().optional(),
lastName: z.string(),
memberships: z.array(membershipSchema),
phoneNumber: z.string().optional(),
profileId: z.string(),
loyalty: userLoyaltySchema,
}),
type: z.string(),
}),
@@ -74,7 +99,7 @@ export const getUserSchema = z
.transform((apiResponse) => {
return {
...apiResponse.data.attributes,
membership: getFriendsMembership(apiResponse.data.attributes.memberships),
membership: getFriendsMembership(apiResponse.data.attributes.loyalty),
name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`,
}
})
@@ -224,16 +249,6 @@ export const creditCardsSchema = z.object({
data: z.array(creditCardSchema),
})
export const getMembershipCardsSchema = z.array(
z.object({
currentPoints: z.number(),
expirationDate: z.string(),
membershipNumber: z.string(),
memberSince: z.string(),
membershipType: z.string(),
})
)
export const initiateSaveCardSchema = z.object({
data: z.object({
attribute: z.object({

View File

@@ -23,7 +23,6 @@ import {
import {
creditCardsSchema,
getFriendTransactionsSchema,
getMembershipCardsSchema,
getStaysSchema,
getUserSchema,
} from "./output"
@@ -37,7 +36,6 @@ import type {
} from "@/types/components/tracking"
import { Transactions } from "@/types/enums/transactions"
import type { User } from "@/types/user"
import type { MembershipLevel } from "@/constants/membershipLevels"
// OpenTelemetry metrics: User
const meter = metrics.getMeter("trpc.user")
@@ -46,10 +44,6 @@ const getVerifiedUserSuccessCounter = meter.createCounter(
"trpc.user.get-success"
)
const getVerifiedUserFailCounter = meter.createCounter("trpc.user.get-fail")
const getProfileSuccessCounter = meter.createCounter(
"trpc.user.profile-success"
)
const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail")
// OpenTelemetry metrics: Stays
const getPreviousStaysCounter = meter.createCounter("trpc.user.stays.previous")
@@ -95,7 +89,7 @@ export const getVerifiedUser = cache(
}
getVerifiedUserCounter.add(1)
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.Profile.profile, {
const apiResponse = await api.get(api.endpoints.v2.Profile.profile, {
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
@@ -186,8 +180,9 @@ export function parsedUser(data: User, isMFA: boolean) {
firstName: data.firstName,
language: data.language,
lastName: data.lastName,
membership: getFriendsMembership(data.memberships),
memberships: data.memberships,
membershipNumber: data.membershipNumber,
membership: getFriendsMembership(data.loyalty),
loyalty: data.loyalty,
name: `${data.firstName} ${data.lastName}`,
phoneNumber: data.phoneNumber,
profileId: data.profileId,
@@ -343,7 +338,7 @@ export const userQueryRouter = router({
return null
}
const membershipLevel = getFriendsMembership(verifiedData.data.memberships)
const membershipLevel = getFriendsMembership(verifiedData.data.loyalty)
return membershipLevel
}),
safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) {
@@ -356,7 +351,7 @@ export const userQueryRouter = router({
return null
}
const membershipLevel = getFriendsMembership(verifiedData.data.memberships)
const membershipLevel = getFriendsMembership(verifiedData.data.loyalty)
return membershipLevel
}),
userTrackingInfo: safeProtectedProcedure.query(async function ({ ctx }) {
@@ -429,14 +424,14 @@ export const userQueryRouter = router({
getPreviousStaysSuccessCounter.add(1)
console.info("api.booking.stays.past success", JSON.stringify({}))
const membership = getFriendsMembership(verifiedUserData.data.memberships)
const membership = getFriendsMembership(verifiedUserData.data.loyalty)
const loggedInUserTrackingData: TrackingSDKUserData = {
loginStatus: "logged in",
loginType: ctx.session.token.loginType as LoginType,
memberId: verifiedUserData.data.profileId,
membershipNumber: membership?.membershipNumber,
memberLevel: membership?.membershipLevel as MembershipLevel,
memberLevel: membership?.membershipLevel,
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
totalPointsAvailableToSpend: membership?.currentPoints,
loginAction: "login success",
@@ -795,23 +790,6 @@ export const userQueryRouter = router({
return null
}
const verifiedData = getMembershipCardsSchema.safeParse(
userData.data.memberships
)
if (!verifiedData.success) {
getProfileFailCounter.add(1, {
error_type: "validation_error",
error: JSON.stringify(verifiedData),
})
console.error(
"api.profile validation error",
JSON.stringify({ error: verifiedData })
)
return null
}
getProfileSuccessCounter.add(1)
return getMembershipCards(verifiedData.data)
return getMembershipCards(userData.data.loyalty)
}),
})