import { countries } from "@/constants/countries" import { myBookingPath } from "@/constants/myBooking" import { myStay } from "@/constants/routes/myStay" import { env } from "@/env/server" import * as api from "@/lib/api" import { dt } from "@/lib/dt" import { encrypt } from "@/server/routers/utils/encryption" import { createCounter } from "@/server/telemetry" import { cache } from "@/utils/cache" import * as maskValue from "@/utils/maskValue" import { isValidSession } from "@/utils/session" import { getCurrentWebUrl } from "@/utils/url" import { getFriendsMembership } from "@/utils/user" import { creditCardsSchema, type FriendTransaction, getStaysSchema, getUserSchema, type Stay, } from "./output" import type { Session } from "next-auth" import type { User } from "@/types/user" import type { Lang } from "@/constants/languages" export const getVerifiedUser = cache( async ({ session, includeExtendedPartnerData, }: { session: Session includeExtendedPartnerData?: boolean }) => { const getVerifiedUserCounter = createCounter("user", "getVerifiedUser") const metricsGetVerifiedUser = getVerifiedUserCounter.init() metricsGetVerifiedUser.start() const now = Date.now() if (session.token.expires_at && session.token.expires_at < now) { metricsGetVerifiedUser.dataError(`Token expired`) return { error: true, cause: "token_expired" } as const } const apiResponse = await api.get( api.endpoints.v2.Profile.profile, { headers: { Authorization: `Bearer ${session.token.access_token}`, }, }, includeExtendedPartnerData ? { includes: "extendedPartnerInformation" } : {} ) if (!apiResponse.ok) { await metricsGetVerifiedUser.httpError(apiResponse) 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 } const apiJson = await apiResponse.json() if (!apiJson.data?.attributes) { metricsGetVerifiedUser.dataError( `Missing data attributes in API response`, { data: apiJson, } ) return null } const verifiedData = getUserSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetVerifiedUser.validationError(verifiedData.error) return null } metricsGetVerifiedUser.success() return verifiedData } ) export async function getMembershipNumber( session: Session | null ): Promise { if (!isValidSession(session)) return undefined const verifiedUser = await getVerifiedUser({ session }) if (!verifiedUser || "error" in verifiedUser) { return undefined } return verifiedUser.data.membershipNumber } export async function getPreviousStays( accessToken: string, limit: number = 10, cursor?: string ) { const getPreviousStaysCounter = createCounter("user", "getPreviousStays") const metricsGetPreviousStays = getPreviousStaysCounter.init({ limit, cursor, }) metricsGetPreviousStays.start() const apiResponse = await api.get( api.endpoints.v1.Booking.Stays.past, { headers: { Authorization: `Bearer ${accessToken}`, }, }, { limit, cursor, } ) if (!apiResponse.ok) { await metricsGetPreviousStays.httpError(apiResponse) return null } const apiJson = await apiResponse.json() const verifiedData = getStaysSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetPreviousStays.validationError(verifiedData.error) return null } metricsGetPreviousStays.success() return verifiedData.data } export async function getUpcomingStays( accessToken: string, limit: number = 10, cursor?: string ) { const getUpcomingStaysCounter = createCounter("user", "getUpcomingStays") const metricsGetUpcomingStays = getUpcomingStaysCounter.init({ limit, cursor, }) metricsGetUpcomingStays.start() const apiResponse = await api.get( api.endpoints.v1.Booking.Stays.future, { headers: { Authorization: `Bearer ${accessToken}`, }, }, { limit, cursor, } ) if (!apiResponse.ok) { await metricsGetUpcomingStays.httpError(apiResponse) return null } const apiJson = await apiResponse.json() const verifiedData = getStaysSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetUpcomingStays.validationError(verifiedData.error) return null } metricsGetUpcomingStays.success() return verifiedData.data } export function parsedUser(data: User, isMFA: boolean) { const country = countries.find((c) => c.code === data.address?.countryCode) const user = { address: { city: data.address?.city, country: country?.name ?? "", countryCode: data.address?.countryCode, streetAddress: data.address?.streetAddress, zipCode: data.address?.zipCode, }, dateOfBirth: data.dateOfBirth, email: data.email, firstName: data.firstName, language: data.language, lastName: data.lastName, membershipNumber: data.membershipNumber, membership: getFriendsMembership(data.loyalty), loyalty: data.loyalty, name: `${data.firstName} ${data.lastName}`, phoneNumber: data.phoneNumber, profileId: data.profileId, } if (!isMFA) { if (user.address.city) { user.address.city = maskValue.text(user.address.city) } if (user.address.streetAddress) { user.address.streetAddress = maskValue.text(user.address.streetAddress) } user.address.zipCode = data.address?.zipCode ? maskValue.text(data.address.zipCode) : "" user.dateOfBirth = maskValue.all(user.dateOfBirth) user.email = maskValue.email(user.email) user.phoneNumber = user.phoneNumber ? maskValue.phone(user.phoneNumber) : "" } return user } export const getCreditCards = cache( async ({ session, onlyNonExpired, }: { session: Session onlyNonExpired?: boolean }) => { const getCreditCardsCounter = createCounter("user", "getCreditCards") const metricsGetCreditCards = getCreditCardsCounter.init({ onlyNonExpired, }) metricsGetCreditCards.start() const apiResponse = await api.get(api.endpoints.v1.Profile.creditCards, { headers: { Authorization: `Bearer ${session.token.access_token}`, }, }) if (!apiResponse.ok) { await metricsGetCreditCards.httpError(apiResponse) return null } const apiJson = await apiResponse.json() const verifiedData = creditCardsSchema.safeParse(apiJson) if (!verifiedData.success) { metricsGetCreditCards.validationError(verifiedData.error) return null } const result = verifiedData.data.data.filter((card) => { if (onlyNonExpired) { try { const expirationDate = dt(card.expirationDate).startOf("day") const currentDate = dt().startOf("day") return expirationDate > currentDate } catch (_) { return false } } return true }) metricsGetCreditCards.success() return result } ) export async function updateStaysBookingUrl( data: Stay[], session: Session, lang: Lang ): Promise export async function updateStaysBookingUrl( data: FriendTransaction[], session: Session, lang: Lang ): Promise export async function updateStaysBookingUrl( data: Stay[] | FriendTransaction[], session: Session, lang: Lang ) { const user = await getVerifiedUser({ session, }) if (user && !("error" in user)) { return data.map((d) => { const originalString = d.attributes.confirmationNumber.toString() + "," + user.data.lastName const encryptedBookingValue = encrypt(originalString) // Get base URL with fallback for ephemeral environments (like deploy previews). const baseUrl = env.PUBLIC_URL || "https://www.scandichotels.com" // Construct Booking URL. const bookingUrl = env.HIDE_FOR_NEXT_RELEASE ? new URL( getCurrentWebUrl({ path: myBookingPath[lang], lang, baseUrl, }) ) : new URL(myStay[lang], baseUrl) // Add search parameters. if (encryptedBookingValue) { bookingUrl.searchParams.set("RefId", encryptedBookingValue) } else { bookingUrl.searchParams.set("lastName", user.data.lastName) bookingUrl.searchParams.set( "bookingId", d.attributes.confirmationNumber.toString() ) } return { ...d, attributes: { ...d.attributes, bookingUrl: bookingUrl.toString(), }, } }) } return data }