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:
@@ -10,7 +10,6 @@ import { protectedServerActionProcedure } from "@/server/trpc"
|
|||||||
|
|
||||||
import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
|
import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getFriendsMembership } from "@/utils/user"
|
|
||||||
import { phoneValidator } from "@/utils/zod/phoneValidator"
|
import { phoneValidator } from "@/utils/zod/phoneValidator"
|
||||||
|
|
||||||
import { Status } from "@/types/components/myPages/myProfile/edit"
|
import { Status } from "@/types/components/myPages/myProfile/edit"
|
||||||
@@ -142,9 +141,8 @@ export const editProfile = protectedServerActionProcedure
|
|||||||
status: Status.success,
|
status: Status.success,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const membership = getFriendsMembership(profile.memberships)
|
|
||||||
console.log(
|
console.log(
|
||||||
`[edit profile: ${membership?.membershipNumber}] body keys: ${JSON.stringify(Object.keys(body))}`
|
`[edit profile: ${profile.membershipNumber}] body keys: ${JSON.stringify(Object.keys(body))}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default async function SASxScandicLinkPage({
|
|||||||
|
|
||||||
if (!profile) return null
|
if (!profile) return null
|
||||||
|
|
||||||
const eurobonusMembership = getEurobonusMembership(profile.memberships)
|
const eurobonusMembership = getEurobonusMembership(profile.loyalty)
|
||||||
|
|
||||||
if (eurobonusMembership) {
|
if (eurobonusMembership) {
|
||||||
redirect(`/${params.lang}/sas-x-scandic/error?errorCode=alreadyLinked`)
|
redirect(`/${params.lang}/sas-x-scandic/error?errorCode=alreadyLinked`)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type { UserProps } from "@/types/components/myPages/user"
|
|||||||
|
|
||||||
export default async function ExpiringPoints({ user }: UserProps) {
|
export default async function ExpiringPoints({ user }: UserProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const membership = getFriendsMembership(user.memberships)
|
const membership = getFriendsMembership(user.loyalty)
|
||||||
|
|
||||||
if (!membership || !membership.pointsToExpire) {
|
if (!membership || !membership.pointsToExpire) {
|
||||||
// TODO: handle this case?
|
// TODO: handle this case?
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import type { UserProps } from "@/types/components/myPages/user"
|
|||||||
export default async function Points({ user }: UserProps) {
|
export default async function Points({ user }: UserProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const membership = getFriendsMembership(user.memberships)
|
const membership = getFriendsMembership(user.loyalty)
|
||||||
|
|
||||||
const nextLevel =
|
const nextLevel =
|
||||||
membership?.nextLevel && MembershipLevelEnum[membership.nextLevel]
|
membership?.nextLevel && MembershipLevelEnum[membership.nextLevel]
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import DiamondAddIcon from "@scandic-hotels/design-system/Icons/DiamondAddIcon"
|
|||||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import { TIER_TO_FRIEND_MAP } from "@/constants/membershipLevels"
|
import {
|
||||||
|
SAS_EUROBONUS_TIER_TO_NAME_MAP,
|
||||||
|
TIER_TO_FRIEND_MAP,
|
||||||
|
} from "@/constants/membershipLevels"
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
@@ -14,17 +17,13 @@ import SectionHeader from "@/components/Section/Header"
|
|||||||
import SectionLink from "@/components/Section/Link"
|
import SectionLink from "@/components/Section/Link"
|
||||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import {
|
import { getEurobonusMembership } from "@/utils/user"
|
||||||
getEurobonusMembership,
|
|
||||||
getFriendsMembership,
|
|
||||||
scandicMemberships,
|
|
||||||
} from "@/utils/user"
|
|
||||||
|
|
||||||
import { UnlinkSAS } from "./UnlinkSAS"
|
import { UnlinkSAS } from "./UnlinkSAS"
|
||||||
|
|
||||||
import styles from "./linkedAccounts.module.css"
|
import styles from "./linkedAccounts.module.css"
|
||||||
|
|
||||||
import type { Membership } from "@/types/user"
|
import type { UserLoyalty } from "@/types/user"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
title?: string
|
title?: string
|
||||||
@@ -78,20 +77,20 @@ async function MatchedAccountInfo() {
|
|||||||
|
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const eurobonusMembership = getEurobonusMembership(user.memberships)
|
const eurobonusMembership = getEurobonusMembership(user.loyalty)
|
||||||
const friendsMembership = user.membership
|
const friendsMembership = user.membership
|
||||||
if (!eurobonusMembership || !friendsMembership) {
|
if (!eurobonusMembership || !friendsMembership) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const sasLevelName = eurobonusMembership.membershipLevel || "-"
|
const sasLevelName = SAS_EUROBONUS_TIER_TO_NAME_MAP[eurobonusMembership.tier]
|
||||||
const sasMembershipNumber = eurobonusMembership.membershipNumber
|
const sasMembershipNumber = eurobonusMembership.membershipNumber
|
||||||
const sasTierExpirationDate = eurobonusMembership.tierExpirationDate
|
const sasTierExpirationDate = eurobonusMembership.tierExpires
|
||||||
|
|
||||||
const scandicLevelName = TIER_TO_FRIEND_MAP[friendsMembership.membershipLevel]
|
const scandicLevelName = TIER_TO_FRIEND_MAP[friendsMembership.membershipLevel]
|
||||||
const scandicExpirationDate = friendsMembership.tierExpirationDate
|
const scandicExpirationDate = friendsMembership.tierExpirationDate
|
||||||
|
|
||||||
const matchState = calculateMatchState(user.memberships)
|
const matchState = calculateMatchState(user.loyalty)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.matchedAccountSection}>
|
<section className={styles.matchedAccountSection}>
|
||||||
@@ -278,30 +277,12 @@ function Label({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MatchState = "boostedBySAS" | "boostedByScandic" | "noBoost"
|
type MatchState = "boostedBySAS" | "boostedByScandic" | "noBoost"
|
||||||
function calculateMatchState(memberships: Membership[]): MatchState {
|
function calculateMatchState(loyalty: UserLoyalty): MatchState {
|
||||||
const eurobonusMembership = getEurobonusMembership(memberships)
|
if (!loyalty.tierBoostedBy) return "noBoost"
|
||||||
const friendsMembership = getFriendsMembership(memberships)
|
if (loyalty.tierBoostedBy === "SAS_EB") return "boostedBySAS"
|
||||||
const nativeMembership = memberships.find(
|
|
||||||
(x) => x.membershipType === scandicMemberships.scandic_native_tiers
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!eurobonusMembership || !friendsMembership || !nativeMembership) {
|
// const eurobonusMembership = getEurobonusMembership(loyalty)
|
||||||
return "noBoost"
|
// if (eurobonusMembership.boostedByScandic) return "boostedByScandic"
|
||||||
}
|
|
||||||
|
|
||||||
const nativeLevel = nativeMembership.membershipLevel
|
|
||||||
const friendsLevel = friendsMembership.membershipLevel
|
|
||||||
|
|
||||||
if (nativeLevel !== friendsLevel) {
|
|
||||||
return "boostedBySAS"
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO check if SAS have been boosted by Scandic when API is available
|
|
||||||
const isBoostedByScandic = false
|
|
||||||
|
|
||||||
if (isBoostedByScandic) {
|
|
||||||
return "boostedByScandic"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "noBoost"
|
return "noBoost"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import SkeletonShimmer from "@/components/SkeletonShimmer"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import useClickOutside from "@/hooks/useClickOutside"
|
import useClickOutside from "@/hooks/useClickOutside"
|
||||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||||
import { type FriendsMembership, getInitials } from "@/utils/user"
|
import { getInitials } from "@/utils/user"
|
||||||
|
|
||||||
import Avatar from "../Avatar"
|
import Avatar from "../Avatar"
|
||||||
import MainMenuButton from "../MainMenuButton"
|
import MainMenuButton from "../MainMenuButton"
|
||||||
@@ -20,7 +20,7 @@ import MyPagesMenuContent, { useMyPagesNavigation } from "../MyPagesMenuContent"
|
|||||||
import styles from "./myPagesMenu.module.css"
|
import styles from "./myPagesMenu.module.css"
|
||||||
|
|
||||||
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
import { DropdownTypeEnum } from "@/types/components/dropdown/dropdown"
|
||||||
import type { User } from "@/types/user"
|
import type { FriendsMembership,User } from "@/types/user"
|
||||||
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
|
import type { LoyaltyLevel } from "@/server/routers/contentstack/loyaltyLevel/output"
|
||||||
|
|
||||||
export type MyPagesMenuProps = {
|
export type MyPagesMenuProps = {
|
||||||
|
|||||||
@@ -127,8 +127,19 @@ const user: SafeUser = {
|
|||||||
firstName: "",
|
firstName: "",
|
||||||
language: undefined,
|
language: undefined,
|
||||||
lastName: "",
|
lastName: "",
|
||||||
|
membershipNumber: "",
|
||||||
membership: undefined,
|
membership: undefined,
|
||||||
|
loyalty: {
|
||||||
memberships: [],
|
memberships: [],
|
||||||
|
pointExpirations: [],
|
||||||
|
points: {
|
||||||
|
earned: 0,
|
||||||
|
spent: 0,
|
||||||
|
spendable: 0,
|
||||||
|
},
|
||||||
|
tier: "L1",
|
||||||
|
tierExpires: "",
|
||||||
|
},
|
||||||
name: "",
|
name: "",
|
||||||
phoneNumber: undefined,
|
phoneNumber: undefined,
|
||||||
profileId: "",
|
profileId: "",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export default async function SidebarMyPages() {
|
|||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const profile = await getProfileSafely()
|
const profile = await getProfileSafely()
|
||||||
const eurobonusMembership = profile
|
const eurobonusMembership = profile
|
||||||
? getEurobonusMembership(profile.memberships)
|
? getEurobonusMembership(profile.loyalty)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { EurobonusTier } from "@/types/user"
|
||||||
|
|
||||||
export enum membershipLevels {
|
export enum membershipLevels {
|
||||||
L1 = 1,
|
L1 = 1,
|
||||||
L2 = 2,
|
L2 = 2,
|
||||||
@@ -32,3 +34,11 @@ export const TIER_TO_FRIEND_MAP: Record<MembershipLevelEnum, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type MembershipLevel = keyof typeof MembershipLevelEnum
|
export type MembershipLevel = keyof typeof MembershipLevelEnum
|
||||||
|
|
||||||
|
export const SAS_EUROBONUS_TIER_TO_NAME_MAP: Record<EurobonusTier, string> = {
|
||||||
|
EBB: "Basic",
|
||||||
|
EBS: "Silver",
|
||||||
|
EBG: "Gold",
|
||||||
|
EBD: "Diamond",
|
||||||
|
EBP: "Pandion",
|
||||||
|
}
|
||||||
|
|||||||
@@ -207,6 +207,18 @@ export namespace endpoints {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export namespace v2 {
|
||||||
|
const version = "v2"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* profile (Swagger)
|
||||||
|
* https://tstapi.scandichotels.com/profile/swagger/v2/index.html
|
||||||
|
*/
|
||||||
|
export namespace Profile {
|
||||||
|
export const profile = `${base.path.profile}/${version}/${base.enitity.Profile}`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Endpoint = string
|
export type Endpoint = string
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { getVerifiedUser } from "@/server/routers/user/query"
|
|||||||
import { router, safeProtectedServiceProcedure } from "@/server/trpc"
|
import { router, safeProtectedServiceProcedure } from "@/server/trpc"
|
||||||
|
|
||||||
import { isValidSession } from "@/utils/session"
|
import { isValidSession } from "@/utils/session"
|
||||||
import { getFriendsMembership } from "@/utils/user"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addPackageInput,
|
addPackageInput,
|
||||||
@@ -85,8 +84,7 @@ async function getMembershipNumber(
|
|||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const membership = getFriendsMembership(verifiedUser.data.memberships)
|
return verifiedUser.data.membershipNumber
|
||||||
return membership?.membershipNumber
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const bookingMutationRouter = router({
|
export const bookingMutationRouter = router({
|
||||||
|
|||||||
@@ -7,20 +7,20 @@ import { getIntl } from "@/i18n"
|
|||||||
import { safeTry } from "@/utils/safeTry"
|
import { safeTry } from "@/utils/safeTry"
|
||||||
import { getEurobonusMembership } from "@/utils/user"
|
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 { Lang } from "@/constants/languages"
|
||||||
import type { MyPagesLink } from "./MyPagesLink"
|
import type { MyPagesLink } from "./MyPagesLink"
|
||||||
|
|
||||||
export const getPrimaryLinks = cache(
|
export const getPrimaryLinks = cache(
|
||||||
async ({
|
async ({
|
||||||
lang,
|
lang,
|
||||||
memberships,
|
userLoyalty,
|
||||||
}: {
|
}: {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
memberships: Membership[]
|
userLoyalty: UserLoyalty
|
||||||
}): Promise<MyPagesLink[]> => {
|
}): Promise<MyPagesLink[]> => {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const showSASLink = isScandicXSASActive(memberships)
|
const showSASLink = isScandicXSASActive(userLoyalty)
|
||||||
const [showTeamMemberLink] = await safeTry(showTeamMemberCard())
|
const [showTeamMemberLink] = await safeTry(showTeamMemberCard())
|
||||||
|
|
||||||
const menuItems: MyPagesLink[] = [
|
const menuItems: MyPagesLink[] = [
|
||||||
@@ -66,8 +66,8 @@ export const getPrimaryLinks = cache(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const isScandicXSASActive = (memberships: Membership[]) => {
|
const isScandicXSASActive = (loyalty: UserLoyalty) => {
|
||||||
const eurobonusMembership = getEurobonusMembership(memberships)
|
const eurobonusMembership = getEurobonusMembership(loyalty)
|
||||||
const isLinked = Boolean(eurobonusMembership)
|
const isLinked = Boolean(eurobonusMembership)
|
||||||
|
|
||||||
return env.SAS_ENABLED && isLinked
|
return env.SAS_ENABLED && isLinked
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const myPagesNavigation = safeProtectedProcedure
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [primaryLinks, secondaryLinks] = await Promise.all([
|
const [primaryLinks, secondaryLinks] = await Promise.all([
|
||||||
getPrimaryLinks({ lang, memberships: user.data.memberships }),
|
getPrimaryLinks({ lang, userLoyalty: user.data.loyalty }),
|
||||||
getSecondaryLinks({ lang }),
|
getSecondaryLinks({ lang }),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -2,53 +2,85 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { countriesMap } from "@/constants/countries"
|
import { countriesMap } from "@/constants/countries"
|
||||||
|
|
||||||
import { getFriendsMembership, scandicMemberships } from "@/utils/user"
|
import { getFriendsMembership } from "@/utils/user"
|
||||||
|
|
||||||
import { imageSchema } from "../hotels/schemas/image"
|
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({
|
const commonMembershipSchema = z.object({
|
||||||
currentPoints: z.number().optional(),
|
membershipNumber: z.string(),
|
||||||
expirationDate: z.string().optional(),
|
tierExpires: z.string(),
|
||||||
membershipLevel: z.string().optional(),
|
memberSince: z.string().nullish(),
|
||||||
nextLevel: z.string().optional(),
|
|
||||||
nightsToTopTier: z.number().optional(),
|
|
||||||
pointsExpiryDate: z.string().optional(),
|
|
||||||
pointsRequiredToNextlevel: z.number().optional(),
|
|
||||||
pointsToExpire: z.number().optional(),
|
|
||||||
tierExpirationDate: z.string().optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const toLowerCaseString = z.string().transform((s) => s.toLowerCase())
|
// This prevents validation errors if the API returns an unhandled membership type
|
||||||
const membershipType = (membershipType: scandicMemberships) =>
|
const otherMembershipSchema = z
|
||||||
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
|
|
||||||
.object({
|
.object({
|
||||||
membershipType: membershipType(scandicMemberships.guestpr),
|
// This ensures that `type` won't widen into "string", losing the literal types, when used in a union
|
||||||
membershipNumber: z.string(),
|
type: z.string().refine((val): val is string & {} => true),
|
||||||
memberSince: z.string(),
|
|
||||||
})
|
})
|
||||||
.merge(commonMembershipSchema)
|
.merge(commonMembershipSchema)
|
||||||
|
|
||||||
const otherMembershipSchema = z
|
export const sasMembershipSchema = z
|
||||||
.object({
|
.object({
|
||||||
membershipType: toLowerCaseString,
|
type: z.literal("SAS_EB"),
|
||||||
membershipNumber: z.string().optional(),
|
tier: sasEurobonusTier,
|
||||||
memberSince: z.string().optional(),
|
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)
|
.merge(commonMembershipSchema)
|
||||||
|
|
||||||
export const membershipSchema = z.union([
|
export const membershipSchema = z.union([
|
||||||
friendsMembershipSchema,
|
friendsMembershipSchema,
|
||||||
|
sasMembershipSchema,
|
||||||
otherMembershipSchema,
|
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
|
export const getUserSchema = z
|
||||||
.object({
|
.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
attributes: 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
|
address: z
|
||||||
.object({
|
.object({
|
||||||
city: z.string().optional(),
|
city: z.string().optional(),
|
||||||
@@ -59,14 +91,7 @@ export const getUserSchema = z
|
|||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
.nullable(),
|
.nullable(),
|
||||||
dateOfBirth: z.string().optional().default("1900-01-01"),
|
loyalty: userLoyaltySchema,
|
||||||
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(),
|
|
||||||
}),
|
}),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
}),
|
}),
|
||||||
@@ -74,7 +99,7 @@ export const getUserSchema = z
|
|||||||
.transform((apiResponse) => {
|
.transform((apiResponse) => {
|
||||||
return {
|
return {
|
||||||
...apiResponse.data.attributes,
|
...apiResponse.data.attributes,
|
||||||
membership: getFriendsMembership(apiResponse.data.attributes.memberships),
|
membership: getFriendsMembership(apiResponse.data.attributes.loyalty),
|
||||||
name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`,
|
name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -224,16 +249,6 @@ export const creditCardsSchema = z.object({
|
|||||||
data: z.array(creditCardSchema),
|
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({
|
export const initiateSaveCardSchema = z.object({
|
||||||
data: z.object({
|
data: z.object({
|
||||||
attribute: z.object({
|
attribute: z.object({
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
creditCardsSchema,
|
creditCardsSchema,
|
||||||
getFriendTransactionsSchema,
|
getFriendTransactionsSchema,
|
||||||
getMembershipCardsSchema,
|
|
||||||
getStaysSchema,
|
getStaysSchema,
|
||||||
getUserSchema,
|
getUserSchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
@@ -37,7 +36,6 @@ import type {
|
|||||||
} from "@/types/components/tracking"
|
} from "@/types/components/tracking"
|
||||||
import { Transactions } from "@/types/enums/transactions"
|
import { Transactions } from "@/types/enums/transactions"
|
||||||
import type { User } from "@/types/user"
|
import type { User } from "@/types/user"
|
||||||
import type { MembershipLevel } from "@/constants/membershipLevels"
|
|
||||||
|
|
||||||
// OpenTelemetry metrics: User
|
// OpenTelemetry metrics: User
|
||||||
const meter = metrics.getMeter("trpc.user")
|
const meter = metrics.getMeter("trpc.user")
|
||||||
@@ -46,10 +44,6 @@ const getVerifiedUserSuccessCounter = meter.createCounter(
|
|||||||
"trpc.user.get-success"
|
"trpc.user.get-success"
|
||||||
)
|
)
|
||||||
const getVerifiedUserFailCounter = meter.createCounter("trpc.user.get-fail")
|
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
|
// OpenTelemetry metrics: Stays
|
||||||
const getPreviousStaysCounter = meter.createCounter("trpc.user.stays.previous")
|
const getPreviousStaysCounter = meter.createCounter("trpc.user.stays.previous")
|
||||||
@@ -95,7 +89,7 @@ export const getVerifiedUser = cache(
|
|||||||
}
|
}
|
||||||
getVerifiedUserCounter.add(1)
|
getVerifiedUserCounter.add(1)
|
||||||
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
|
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: {
|
headers: {
|
||||||
Authorization: `Bearer ${session.token.access_token}`,
|
Authorization: `Bearer ${session.token.access_token}`,
|
||||||
},
|
},
|
||||||
@@ -186,8 +180,9 @@ export function parsedUser(data: User, isMFA: boolean) {
|
|||||||
firstName: data.firstName,
|
firstName: data.firstName,
|
||||||
language: data.language,
|
language: data.language,
|
||||||
lastName: data.lastName,
|
lastName: data.lastName,
|
||||||
membership: getFriendsMembership(data.memberships),
|
membershipNumber: data.membershipNumber,
|
||||||
memberships: data.memberships,
|
membership: getFriendsMembership(data.loyalty),
|
||||||
|
loyalty: data.loyalty,
|
||||||
name: `${data.firstName} ${data.lastName}`,
|
name: `${data.firstName} ${data.lastName}`,
|
||||||
phoneNumber: data.phoneNumber,
|
phoneNumber: data.phoneNumber,
|
||||||
profileId: data.profileId,
|
profileId: data.profileId,
|
||||||
@@ -343,7 +338,7 @@ export const userQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const membershipLevel = getFriendsMembership(verifiedData.data.memberships)
|
const membershipLevel = getFriendsMembership(verifiedData.data.loyalty)
|
||||||
return membershipLevel
|
return membershipLevel
|
||||||
}),
|
}),
|
||||||
safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) {
|
safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||||
@@ -356,7 +351,7 @@ export const userQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const membershipLevel = getFriendsMembership(verifiedData.data.memberships)
|
const membershipLevel = getFriendsMembership(verifiedData.data.loyalty)
|
||||||
return membershipLevel
|
return membershipLevel
|
||||||
}),
|
}),
|
||||||
userTrackingInfo: safeProtectedProcedure.query(async function ({ ctx }) {
|
userTrackingInfo: safeProtectedProcedure.query(async function ({ ctx }) {
|
||||||
@@ -429,14 +424,14 @@ export const userQueryRouter = router({
|
|||||||
getPreviousStaysSuccessCounter.add(1)
|
getPreviousStaysSuccessCounter.add(1)
|
||||||
console.info("api.booking.stays.past success", JSON.stringify({}))
|
console.info("api.booking.stays.past success", JSON.stringify({}))
|
||||||
|
|
||||||
const membership = getFriendsMembership(verifiedUserData.data.memberships)
|
const membership = getFriendsMembership(verifiedUserData.data.loyalty)
|
||||||
|
|
||||||
const loggedInUserTrackingData: TrackingSDKUserData = {
|
const loggedInUserTrackingData: TrackingSDKUserData = {
|
||||||
loginStatus: "logged in",
|
loginStatus: "logged in",
|
||||||
loginType: ctx.session.token.loginType as LoginType,
|
loginType: ctx.session.token.loginType as LoginType,
|
||||||
memberId: verifiedUserData.data.profileId,
|
memberId: verifiedUserData.data.profileId,
|
||||||
membershipNumber: membership?.membershipNumber,
|
membershipNumber: membership?.membershipNumber,
|
||||||
memberLevel: membership?.membershipLevel as MembershipLevel,
|
memberLevel: membership?.membershipLevel,
|
||||||
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
|
noOfNightsStayed: verifiedPreviousStaysData.data.links?.totalCount ?? 0,
|
||||||
totalPointsAvailableToSpend: membership?.currentPoints,
|
totalPointsAvailableToSpend: membership?.currentPoints,
|
||||||
loginAction: "login success",
|
loginAction: "login success",
|
||||||
@@ -795,23 +790,6 @@ export const userQueryRouter = router({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const verifiedData = getMembershipCardsSchema.safeParse(
|
return getMembershipCards(userData.data.loyalty)
|
||||||
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)
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export type PointsColumnProps = {
|
export type PointsColumnProps = {
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
value?: number
|
value?: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ import type { z } from "zod"
|
|||||||
import type { RouterOutput } from "@/lib/trpc/client"
|
import type { RouterOutput } from "@/lib/trpc/client"
|
||||||
import type {
|
import type {
|
||||||
creditCardSchema,
|
creditCardSchema,
|
||||||
|
friendsMembershipSchema,
|
||||||
getUserSchema,
|
getUserSchema,
|
||||||
membershipSchema,
|
sasMembershipSchema,
|
||||||
|
userLoyaltySchema,
|
||||||
} from "@/server/routers/user/output"
|
} from "@/server/routers/user/output"
|
||||||
|
import type { getFriendsMembership } from "@/utils/user"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All extended field needs to be added by API team to response or
|
* All extended field needs to be added by API team to response or
|
||||||
@@ -17,6 +20,12 @@ export type SafeUser = RouterOutput["user"]["getSafely"]
|
|||||||
|
|
||||||
export type CreditCard = z.output<typeof creditCardSchema>
|
export type CreditCard = z.output<typeof creditCardSchema>
|
||||||
|
|
||||||
export type Membership = z.output<typeof membershipSchema>
|
export type UserLoyalty = z.output<typeof userLoyaltySchema>
|
||||||
|
|
||||||
export type Memberships = Membership[]
|
export type Membership = UserLoyalty["memberships"][number]
|
||||||
|
export type NativeFriendsMembership = z.output<typeof friendsMembershipSchema>
|
||||||
|
export type EurobonusMembership = z.output<typeof sasMembershipSchema>
|
||||||
|
|
||||||
|
export type FriendsMembership = ReturnType<typeof getFriendsMembership>
|
||||||
|
|
||||||
|
export type EurobonusTier = EurobonusMembership["tier"]
|
||||||
|
|||||||
@@ -3,50 +3,78 @@ import {
|
|||||||
MembershipLevelEnum,
|
MembershipLevelEnum,
|
||||||
} from "@/constants/membershipLevels"
|
} from "@/constants/membershipLevels"
|
||||||
|
|
||||||
import type { z } from "zod"
|
import type {
|
||||||
|
EurobonusMembership,
|
||||||
|
Membership,
|
||||||
|
NativeFriendsMembership,
|
||||||
|
User,
|
||||||
|
UserLoyalty,
|
||||||
|
} from "@/types/user"
|
||||||
|
|
||||||
import type { Membership, Memberships, User } from "@/types/user"
|
export enum scandicMembershipTypes {
|
||||||
import type { getMembershipCardsSchema } from "@/server/routers/user/output"
|
SCANDIC_NATIVE = "SCANDIC_NATIVE",
|
||||||
|
SAS_EB = "SAS_EB",
|
||||||
export enum scandicMemberships {
|
|
||||||
guestpr = "guestpr",
|
|
||||||
scandicfriends = "scandicfriend's",
|
|
||||||
sas_eb = "sas_eb",
|
|
||||||
scandic_native_tiers = "scandic_native_tiers",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFriendsMembership(memberships: Memberships) {
|
export function isScandicNativeMembership(
|
||||||
return memberships?.find(
|
membership: Membership
|
||||||
(membership) =>
|
): membership is NativeFriendsMembership {
|
||||||
membership.membershipType.toLowerCase() === scandicMemberships.guestpr
|
return membership.type === scandicMembershipTypes.SCANDIC_NATIVE
|
||||||
) as FriendsMembership | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FriendsMembership = Omit<
|
export function getFriendsMembership(userLoyalty: UserLoyalty) {
|
||||||
NonNullable<Membership>,
|
const { memberships, ...loyalty } = userLoyalty
|
||||||
"membershipLevel" | "nextLevel"
|
|
||||||
> & {
|
|
||||||
membershipLevel: MembershipLevel
|
|
||||||
nextLevel: MembershipLevel
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getEurobonusMembership(memberships: Memberships) {
|
const friendsMembership = memberships.find(isScandicNativeMembership)
|
||||||
return memberships?.find(
|
|
||||||
(membership) =>
|
if (!friendsMembership) return undefined
|
||||||
membership.membershipType.toLowerCase() === scandicMemberships.sas_eb
|
|
||||||
|
const pointExpiration = loyalty.pointExpirations
|
||||||
|
.sort(
|
||||||
|
(a, b) => new Date(a.expires).getTime() - new Date(b.expires).getTime()
|
||||||
)
|
)
|
||||||
|
.at(0)
|
||||||
|
|
||||||
|
// Map to the same format that was used with Profile V1 to avoid larger changes for now.
|
||||||
|
const result = {
|
||||||
|
membershipType: friendsMembership.type,
|
||||||
|
membershipNumber: friendsMembership.membershipNumber,
|
||||||
|
membershipLevel: loyalty.tier,
|
||||||
|
nextLevel: friendsMembership.nextTier,
|
||||||
|
currentPoints: loyalty.points.spendable,
|
||||||
|
expirationDate: loyalty.tierExpires,
|
||||||
|
nightsToTopTier: friendsMembership.nightsToTopTier,
|
||||||
|
pointsRequiredToNextlevel: friendsMembership.pointsToNextTier,
|
||||||
|
tierExpirationDate: loyalty.tierExpires,
|
||||||
|
pointsExpiryDate: pointExpiration?.expires,
|
||||||
|
pointsToExpire: pointExpiration?.points,
|
||||||
|
memberSince: friendsMembership.memberSince,
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMembershipCards(
|
function isEurobonusMembership(
|
||||||
memberships: z.infer<typeof getMembershipCardsSchema>
|
membership: Membership
|
||||||
) {
|
): membership is EurobonusMembership {
|
||||||
return memberships.filter(function (membership) {
|
return membership.type === scandicMembershipTypes.SAS_EB
|
||||||
return (
|
}
|
||||||
membership.membershipType.toLowerCase() !== scandicMemberships.guestpr &&
|
|
||||||
membership.membershipType.toLowerCase() !==
|
export function getEurobonusMembership(loyalty: UserLoyalty) {
|
||||||
scandicMemberships.scandicfriends
|
return loyalty.memberships?.find(isEurobonusMembership)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMembershipCards(userLoyalty: UserLoyalty) {
|
||||||
|
return userLoyalty.memberships
|
||||||
|
.filter(
|
||||||
|
(membership) => membership.type !== scandicMembershipTypes.SCANDIC_NATIVE
|
||||||
)
|
)
|
||||||
})
|
.map((membership) => ({
|
||||||
|
currentPoints: 0, // We only have points for Friends so we can't set this for now
|
||||||
|
expirationDate: membership.tierExpires,
|
||||||
|
membershipNumber: membership.membershipNumber,
|
||||||
|
membershipType: membership.type,
|
||||||
|
memberSince: membership.memberSince,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isHighestMembership(
|
export function isHighestMembership(
|
||||||
|
|||||||
Reference in New Issue
Block a user