import { BALFWD } from "@scandic-hotels/common/constants/transactionType" import { createCounter } from "@scandic-hotels/common/telemetry" import { safeTry } from "@scandic-hotels/common/utils/safeTry" import { router } from "../../.." import * as api from "../../../api" import { Transactions } from "../../../enums/transactions" import { notFoundError } from "../../../errors" import { languageProtectedProcedure, protectedProcedure, safeProtectedProcedure, } from "../../../procedures" import { getFriendsMembership, getMembershipCards, } from "../../../routers/user/helpers" import { toApiLang } from "../../../utils" import { isValidSession } from "../../../utils/session" import { friendTransactionsInput, getSavedPaymentCardsInput, staysInput, } from "../input" import { getFriendTransactionsSchema } from "../output" import { getCreditCards } from "../services/getCreditCards" import { getPreviousStays } from "../services/getPreviousStays" import { getUpcomingStays } from "../services/getUpcomingStays" import { getVerifiedUser } from "../utils/getVerifiedUser" import { parsedUser } from "../utils/parsedUser" import { updateStaysBookingUrl } from "../utils/updateStaysBookingUrl" import { userTrackingInfo } from "./userTrackingInfo" export const userQueryRouter = router({ get: protectedProcedure .use(async function (opts) { return opts.next({ ctx: { ...opts.ctx, isMFA: !!opts.ctx.session.token.mfa_scope && !!opts.ctx.session.token.mfa_expires_at && opts.ctx.session.token.mfa_expires_at > Date.now(), }, }) }) .query(async function getUser({ ctx }) { const user = await ctx.getScandicUser() if (!user) { throw notFoundError() } return parsedUser(user, !ctx.isMFA) }), getSafely: safeProtectedProcedure.query(async function getUser({ ctx }) { if (!isValidSession(ctx.session)) { return null } const user = await ctx.getScandicUser() if (!user) { return null } return parsedUser(user, false) }), getBasic: protectedProcedure.query(async function getBasicUser({ ctx }) { const user = await ctx.getScandicBasicUser() if (!user) { throw notFoundError() } return user }), getWithExtendedPartnerData: safeProtectedProcedure.query( async function getUser({ ctx }) { if (!isValidSession(ctx.session)) { return null } const user = await ctx.getScandicUser({ withExtendedPartnerData: true }) if (!user) { return null } return parsedUser(user, false) } ), name: safeProtectedProcedure.query(async function ({ ctx }) { if (!isValidSession(ctx.session)) { return null } const user = await ctx.getScandicBasicUser() if (!user) { return null } return { firstName: user.firstName, lastName: user.lastName, } }), membershipLevel: protectedProcedure.query(async function ({ ctx }) { const user = await ctx.getScandicUser() if (!user?.loyalty) { return null } const membershipLevel = getFriendsMembership(user.loyalty) return membershipLevel }), safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) { if (!isValidSession(ctx.session)) { return null } const user = await ctx.getScandicUser() if (!user?.loyalty) { return null } const membershipLevel = getFriendsMembership(user.loyalty) return membershipLevel }), userTrackingInfo, stays: router({ previous: languageProtectedProcedure .input(staysInput) .query(async ({ ctx, input }) => { const { limit, cursor, lang } = input const language = lang || ctx.lang const data = await getPreviousStays( ctx.session.token.access_token, limit, language, cursor ) if (data) { const nextCursor = data.links && data.links.offset < data.links.totalCount ? data.links.offset : undefined const updatedData = await updateStaysBookingUrl( data.data, ctx.session, language ) return { data: updatedData, nextCursor, } } return null }), upcoming: languageProtectedProcedure .input(staysInput) .query(async ({ ctx, input }) => { const { limit, cursor, lang, includeFirstStay } = input const language = lang || ctx.lang const data = await getUpcomingStays( ctx.session.token.access_token, limit, language, cursor ) if (data) { const nextCursor = data.links && data.links.offset < data.links.totalCount ? data.links.offset : undefined const updatedData = await updateStaysBookingUrl( data.data, ctx.session, language ) // When includeFirstStay is true (used by SidePeek), return all stays if (includeFirstStay) { return { data: updatedData, nextCursor, } } if (updatedData.length <= 1) { // If there are 1 or fewer stays, return null since NextStay handles this return null } // If there are multiple stays, filter out the first one since NextStay shows it const filteredData = updatedData.slice(1) return { data: filteredData, nextCursor, } } return null }), next: languageProtectedProcedure.query(async ({ ctx }) => { const data = await getUpcomingStays( ctx.session.token.access_token, 1, // Only get the closest stay ctx.lang ) if (data && data.data.length > 0) { const updatedData = await updateStaysBookingUrl( data.data, ctx.session, ctx.lang ) // Return only the first (closest) stay return updatedData[0] } return null }), }), transaction: router({ friendTransactions: languageProtectedProcedure .input(friendTransactionsInput) .query(async ({ ctx, input }) => { const { limit, cursor, lang } = input const language = lang ?? ctx.lang const page = cursor ? Number(cursor) : 1 const friendTransactionsCounter = createCounter( "trpc.user.transactions.friendTransactions2" ) const metricsFriendTransactions = friendTransactionsCounter.init({ limit, cursor, language, }) metricsFriendTransactions.start() const apiResponse = await api.get( api.endpoints.v1.Profile.Transaction.friendTransactions, { headers: { Authorization: `Bearer ${ctx.session.token.access_token}`, }, }, { language: toApiLang(language), } ) if (!apiResponse.ok) { await metricsFriendTransactions.httpError(apiResponse) return null } const apiJson = await apiResponse.json() const verifiedData = getFriendTransactionsSchema.safeParse(apiJson) if (!verifiedData.success) { metricsFriendTransactions.validationError(verifiedData.error) return null } const updatedData = await updateStaysBookingUrl( verifiedData.data.data, ctx.session, language ) const allTransactions = updatedData .filter( (t) => t.type !== Transactions.rewardType.expired && t.attributes.awardPoints !== 0 ) .sort((a, b) => { if (a.attributes.confirmationNumber === BALFWD) return 1 if (b.attributes.confirmationNumber === BALFWD) return -1 const dateA = new Date(a.attributes.transactionDate) const dateB = new Date(b.attributes.transactionDate) return dateA > dateB ? -1 : 1 }) const startIndex = limit * (page - 1) const endIndex = limit * page const paginatedTransactions = allTransactions.slice( startIndex, endIndex ) const hasMore = endIndex < allTransactions.length metricsFriendTransactions.success() return { data: paginatedTransactions, nextCursor: hasMore ? Number(page + 1) : undefined, } }), }), creditCards: protectedProcedure.query(async function ({ ctx }) { return await getCreditCards({ session: ctx.session }) }), safePaymentCards: safeProtectedProcedure .input(getSavedPaymentCardsInput) .query(async function ({ ctx, input }) { if (!isValidSession(ctx.session)) { return null } const savedCards = await getCreditCards({ session: ctx.session, onlyNonExpired: true, }) if (!savedCards) { return null } return savedCards.filter((card) => input.supportedCards.includes(card.type) ) }), membershipCards: protectedProcedure.query(async function ({ ctx }) { const [userData, error] = await safeTry( getVerifiedUser({ token: ctx.session.token }) ) if (!userData?.loyalty || error) { return null } return getMembershipCards(userData.loyalty) }), })