From c56a0b8ce9dcba9d51219a978950c4bcbb82f858 Mon Sep 17 00:00:00 2001 From: Anton Gunnarsson Date: Tue, 8 Apr 2025 06:26:00 +0000 Subject: [PATCH] Merged in feat/sw-1975-get-profile-v2 (pull request #1651) Use get Profile V2 endpoint Approved-by: Linus Flood --- apps/scandic-web/actions/editProfile.ts | 4 +- .../(protected)/sas-x-scandic/link/page.tsx | 2 +- .../Overview/Stats/ExpiringPoints/index.tsx | 2 +- .../Overview/Stats/Points/index.tsx | 2 +- .../SAS/LinkedAccounts/index.tsx | 49 +++------ .../Header/MainMenu/MyPagesMenu/index.tsx | 4 +- .../MyStay/accessBooking.test.ts | 13 ++- .../components/MyPages/Sidebar/index.tsx | 2 +- .../scandic-web/constants/membershipLevels.ts | 10 ++ apps/scandic-web/lib/api/endpoints.ts | 12 +++ .../server/routers/booking/mutation.ts | 4 +- .../navigation/mypages/getPrimaryLinks.ts | 12 +-- .../routers/navigation/mypages/index.ts | 2 +- .../scandic-web/server/routers/user/output.ts | 101 ++++++++++-------- apps/scandic-web/server/routers/user/query.ts | 40 ++----- .../types/components/myPages/points.ts | 2 +- apps/scandic-web/types/user.ts | 15 ++- apps/scandic-web/utils/user.ts | 100 ++++++++++------- 18 files changed, 208 insertions(+), 168 deletions(-) diff --git a/apps/scandic-web/actions/editProfile.ts b/apps/scandic-web/actions/editProfile.ts index a44f1a4ea..b429ba161 100644 --- a/apps/scandic-web/actions/editProfile.ts +++ b/apps/scandic-web/actions/editProfile.ts @@ -10,7 +10,6 @@ import { protectedServerActionProcedure } from "@/server/trpc" import { editProfileSchema } from "@/components/Forms/Edit/Profile/schema" import { getIntl } from "@/i18n" -import { getFriendsMembership } from "@/utils/user" import { phoneValidator } from "@/utils/zod/phoneValidator" import { Status } from "@/types/components/myPages/myProfile/edit" @@ -142,9 +141,8 @@ export const editProfile = protectedServerActionProcedure status: Status.success, } } else { - const membership = getFriendsMembership(profile.memberships) console.log( - `[edit profile: ${membership?.membershipNumber}] body keys: ${JSON.stringify(Object.keys(body))}` + `[edit profile: ${profile.membershipNumber}] body keys: ${JSON.stringify(Object.keys(body))}` ) } diff --git a/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/link/page.tsx b/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/link/page.tsx index 490b72972..e037cae45 100644 --- a/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/link/page.tsx +++ b/apps/scandic-web/app/[lang]/(partner)/(sas)/(protected)/sas-x-scandic/link/page.tsx @@ -17,7 +17,7 @@ export default async function SASxScandicLinkPage({ if (!profile) return null - const eurobonusMembership = getEurobonusMembership(profile.memberships) + const eurobonusMembership = getEurobonusMembership(profile.loyalty) if (eurobonusMembership) { redirect(`/${params.lang}/sas-x-scandic/error?errorCode=alreadyLinked`) diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx index 46b5dbeea..e4fc03099 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/ExpiringPoints/index.tsx @@ -10,7 +10,7 @@ import type { UserProps } from "@/types/components/myPages/user" export default async function ExpiringPoints({ user }: UserProps) { const intl = await getIntl() - const membership = getFriendsMembership(user.memberships) + const membership = getFriendsMembership(user.loyalty) if (!membership || !membership.pointsToExpire) { // TODO: handle this case? diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx index 545442c8f..29d991485 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Overview/Stats/Points/index.tsx @@ -12,7 +12,7 @@ import type { UserProps } from "@/types/components/myPages/user" export default async function Points({ user }: UserProps) { const intl = await getIntl() - const membership = getFriendsMembership(user.memberships) + const membership = getFriendsMembership(user.loyalty) const nextLevel = membership?.nextLevel && MembershipLevelEnum[membership.nextLevel] diff --git a/apps/scandic-web/components/Blocks/DynamicContent/SAS/LinkedAccounts/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/SAS/LinkedAccounts/index.tsx index 2912e34dd..c93ce02c4 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/SAS/LinkedAccounts/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/SAS/LinkedAccounts/index.tsx @@ -5,7 +5,10 @@ import DiamondAddIcon from "@scandic-hotels/design-system/Icons/DiamondAddIcon" import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon" 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 { getProfile } from "@/lib/trpc/memoizedRequests" @@ -14,17 +17,13 @@ import SectionHeader from "@/components/Section/Header" import SectionLink from "@/components/Section/Link" import SkeletonShimmer from "@/components/SkeletonShimmer" import { getIntl } from "@/i18n" -import { - getEurobonusMembership, - getFriendsMembership, - scandicMemberships, -} from "@/utils/user" +import { getEurobonusMembership } from "@/utils/user" import { UnlinkSAS } from "./UnlinkSAS" import styles from "./linkedAccounts.module.css" -import type { Membership } from "@/types/user" +import type { UserLoyalty } from "@/types/user" type Props = { title?: string @@ -78,20 +77,20 @@ async function MatchedAccountInfo() { const intl = await getIntl() - const eurobonusMembership = getEurobonusMembership(user.memberships) + const eurobonusMembership = getEurobonusMembership(user.loyalty) const friendsMembership = user.membership if (!eurobonusMembership || !friendsMembership) { return null } - const sasLevelName = eurobonusMembership.membershipLevel || "-" + const sasLevelName = SAS_EUROBONUS_TIER_TO_NAME_MAP[eurobonusMembership.tier] const sasMembershipNumber = eurobonusMembership.membershipNumber - const sasTierExpirationDate = eurobonusMembership.tierExpirationDate + const sasTierExpirationDate = eurobonusMembership.tierExpires const scandicLevelName = TIER_TO_FRIEND_MAP[friendsMembership.membershipLevel] const scandicExpirationDate = friendsMembership.tierExpirationDate - const matchState = calculateMatchState(user.memberships) + const matchState = calculateMatchState(user.loyalty) return (
@@ -278,30 +277,12 @@ function Label({ children }: { children: ReactNode }) { } type MatchState = "boostedBySAS" | "boostedByScandic" | "noBoost" -function calculateMatchState(memberships: Membership[]): MatchState { - const eurobonusMembership = getEurobonusMembership(memberships) - const friendsMembership = getFriendsMembership(memberships) - const nativeMembership = memberships.find( - (x) => x.membershipType === scandicMemberships.scandic_native_tiers - ) +function calculateMatchState(loyalty: UserLoyalty): MatchState { + if (!loyalty.tierBoostedBy) return "noBoost" + if (loyalty.tierBoostedBy === "SAS_EB") return "boostedBySAS" - if (!eurobonusMembership || !friendsMembership || !nativeMembership) { - return "noBoost" - } - - 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" - } + // const eurobonusMembership = getEurobonusMembership(loyalty) + // if (eurobonusMembership.boostedByScandic) return "boostedByScandic" return "noBoost" } diff --git a/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx b/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx index f6f2d8704..1f55f37ea 100644 --- a/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx +++ b/apps/scandic-web/components/Header/MainMenu/MyPagesMenu/index.tsx @@ -11,7 +11,7 @@ import SkeletonShimmer from "@/components/SkeletonShimmer" import Body from "@/components/TempDesignSystem/Text/Body" import useClickOutside from "@/hooks/useClickOutside" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import { type FriendsMembership, getInitials } from "@/utils/user" +import { getInitials } from "@/utils/user" import Avatar from "../Avatar" import MainMenuButton from "../MainMenuButton" @@ -20,7 +20,7 @@ import MyPagesMenuContent, { useMyPagesNavigation } from "../MyPagesMenuContent" import styles from "./myPagesMenu.module.css" 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" export type MyPagesMenuProps = { diff --git a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts index c8147ce08..199be2988 100644 --- a/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts +++ b/apps/scandic-web/components/HotelReservation/MyStay/accessBooking.test.ts @@ -127,8 +127,19 @@ const user: SafeUser = { firstName: "", language: undefined, lastName: "", + membershipNumber: "", membership: undefined, - memberships: [], + loyalty: { + memberships: [], + pointExpirations: [], + points: { + earned: 0, + spent: 0, + spendable: 0, + }, + tier: "L1", + tierExpires: "", + }, name: "", phoneNumber: undefined, profileId: "", diff --git a/apps/scandic-web/components/MyPages/Sidebar/index.tsx b/apps/scandic-web/components/MyPages/Sidebar/index.tsx index 1ee387be7..8bda3e4ee 100644 --- a/apps/scandic-web/components/MyPages/Sidebar/index.tsx +++ b/apps/scandic-web/components/MyPages/Sidebar/index.tsx @@ -16,7 +16,7 @@ export default async function SidebarMyPages() { const intl = await getIntl() const profile = await getProfileSafely() const eurobonusMembership = profile - ? getEurobonusMembership(profile.memberships) + ? getEurobonusMembership(profile.loyalty) : null return ( diff --git a/apps/scandic-web/constants/membershipLevels.ts b/apps/scandic-web/constants/membershipLevels.ts index 2bbcc772b..8883c7bf3 100644 --- a/apps/scandic-web/constants/membershipLevels.ts +++ b/apps/scandic-web/constants/membershipLevels.ts @@ -1,3 +1,5 @@ +import type { EurobonusTier } from "@/types/user" + export enum membershipLevels { L1 = 1, L2 = 2, @@ -32,3 +34,11 @@ export const TIER_TO_FRIEND_MAP: Record = { } export type MembershipLevel = keyof typeof MembershipLevelEnum + +export const SAS_EUROBONUS_TIER_TO_NAME_MAP: Record = { + EBB: "Basic", + EBS: "Silver", + EBG: "Gold", + EBD: "Diamond", + EBP: "Pandion", +} diff --git a/apps/scandic-web/lib/api/endpoints.ts b/apps/scandic-web/lib/api/endpoints.ts index a85ac3578..d505fa9ec 100644 --- a/apps/scandic-web/lib/api/endpoints.ts +++ b/apps/scandic-web/lib/api/endpoints.ts @@ -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 diff --git a/apps/scandic-web/server/routers/booking/mutation.ts b/apps/scandic-web/server/routers/booking/mutation.ts index 150b9be68..2a6225f43 100644 --- a/apps/scandic-web/server/routers/booking/mutation.ts +++ b/apps/scandic-web/server/routers/booking/mutation.ts @@ -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({ diff --git a/apps/scandic-web/server/routers/navigation/mypages/getPrimaryLinks.ts b/apps/scandic-web/server/routers/navigation/mypages/getPrimaryLinks.ts index ed073649a..444fde44a 100644 --- a/apps/scandic-web/server/routers/navigation/mypages/getPrimaryLinks.ts +++ b/apps/scandic-web/server/routers/navigation/mypages/getPrimaryLinks.ts @@ -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 => { 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 diff --git a/apps/scandic-web/server/routers/navigation/mypages/index.ts b/apps/scandic-web/server/routers/navigation/mypages/index.ts index 709960837..7b11787b7 100644 --- a/apps/scandic-web/server/routers/navigation/mypages/index.ts +++ b/apps/scandic-web/server/routers/navigation/mypages/index.ts @@ -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 }), ]) diff --git a/apps/scandic-web/server/routers/user/output.ts b/apps/scandic-web/server/routers/user/output.ts index 838736bb9..73385702a 100644 --- a/apps/scandic-web/server/routers/user/output.ts +++ b/apps/scandic-web/server/routers/user/output.ts @@ -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({ diff --git a/apps/scandic-web/server/routers/user/query.ts b/apps/scandic-web/server/routers/user/query.ts index f848cc4da..8a4137ac0 100644 --- a/apps/scandic-web/server/routers/user/query.ts +++ b/apps/scandic-web/server/routers/user/query.ts @@ -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) }), }) diff --git a/apps/scandic-web/types/components/myPages/points.ts b/apps/scandic-web/types/components/myPages/points.ts index 7f301e26e..33721f483 100644 --- a/apps/scandic-web/types/components/myPages/points.ts +++ b/apps/scandic-web/types/components/myPages/points.ts @@ -1,5 +1,5 @@ export type PointsColumnProps = { title: string subtitle?: string - value?: number + value?: number | null } diff --git a/apps/scandic-web/types/user.ts b/apps/scandic-web/types/user.ts index 575bae645..052db1a5b 100644 --- a/apps/scandic-web/types/user.ts +++ b/apps/scandic-web/types/user.ts @@ -3,9 +3,12 @@ import type { z } from "zod" import type { RouterOutput } from "@/lib/trpc/client" import type { creditCardSchema, + friendsMembershipSchema, getUserSchema, - membershipSchema, + sasMembershipSchema, + userLoyaltySchema, } from "@/server/routers/user/output" +import type { getFriendsMembership } from "@/utils/user" /** * 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 -export type Membership = z.output +export type UserLoyalty = z.output -export type Memberships = Membership[] +export type Membership = UserLoyalty["memberships"][number] +export type NativeFriendsMembership = z.output +export type EurobonusMembership = z.output + +export type FriendsMembership = ReturnType + +export type EurobonusTier = EurobonusMembership["tier"] diff --git a/apps/scandic-web/utils/user.ts b/apps/scandic-web/utils/user.ts index 35cdb21b2..55e986537 100644 --- a/apps/scandic-web/utils/user.ts +++ b/apps/scandic-web/utils/user.ts @@ -3,50 +3,78 @@ import { MembershipLevelEnum, } 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" -import type { getMembershipCardsSchema } from "@/server/routers/user/output" - -export enum scandicMemberships { - guestpr = "guestpr", - scandicfriends = "scandicfriend's", - sas_eb = "sas_eb", - scandic_native_tiers = "scandic_native_tiers", +export enum scandicMembershipTypes { + SCANDIC_NATIVE = "SCANDIC_NATIVE", + SAS_EB = "SAS_EB", } -export function getFriendsMembership(memberships: Memberships) { - return memberships?.find( - (membership) => - membership.membershipType.toLowerCase() === scandicMemberships.guestpr - ) as FriendsMembership | undefined +export function isScandicNativeMembership( + membership: Membership +): membership is NativeFriendsMembership { + return membership.type === scandicMembershipTypes.SCANDIC_NATIVE } -export type FriendsMembership = Omit< - NonNullable, - "membershipLevel" | "nextLevel" -> & { - membershipLevel: MembershipLevel - nextLevel: MembershipLevel -} +export function getFriendsMembership(userLoyalty: UserLoyalty) { + const { memberships, ...loyalty } = userLoyalty -export function getEurobonusMembership(memberships: Memberships) { - return memberships?.find( - (membership) => - membership.membershipType.toLowerCase() === scandicMemberships.sas_eb - ) -} + const friendsMembership = memberships.find(isScandicNativeMembership) -export function getMembershipCards( - memberships: z.infer -) { - return memberships.filter(function (membership) { - return ( - membership.membershipType.toLowerCase() !== scandicMemberships.guestpr && - membership.membershipType.toLowerCase() !== - scandicMemberships.scandicfriends + if (!friendsMembership) return undefined + + 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 +} + +function isEurobonusMembership( + membership: Membership +): membership is EurobonusMembership { + return membership.type === scandicMembershipTypes.SAS_EB +} + +export function getEurobonusMembership(loyalty: UserLoyalty) { + 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(