import { createCounter } from "@scandic-hotels/common/telemetry" import { router } from "../.." import * as api from "../../api" import { Transactions } from "../../enums/transactions" import { languageProtectedProcedure, protectedProcedure, safeProtectedProcedure, } from "../../procedures" import { getFriendsMembership, getMembershipCards, } from "../../routers/user/helpers" import { getVerifiedUser } from "../../routers/user/utils" import { toApiLang } from "../../utils" import { isValidSession } from "../../utils/session" import { friendTransactionsInput, getSavedPaymentCardsInput, staysInput, userTrackingInput, } from "./input" import { getFriendTransactionsSchema } from "./output" import { getCreditCards, getPreviousStays, getUpcomingStays, parsedUser, updateStaysBookingUrl, } from "./utils" import type { LoginType } from "../../types/loginType" import type { TrackingUserData } from "../types" 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 data = await getVerifiedUser({ session: ctx.session }) if (!data) { return null } if ("error" in data && data.error) { return data } return parsedUser(data.data, ctx.isMFA) }), getSafely: safeProtectedProcedure.query(async function getUser({ ctx }) { if (!isValidSession(ctx.session)) { return null } const data = await getVerifiedUser({ session: ctx.session }) if (!data || "error" in data) { return null } return parsedUser(data.data, true) }), getWithExtendedPartnerData: safeProtectedProcedure.query( async function getUser({ ctx }) { if (!isValidSession(ctx.session)) { return null } const data = await getVerifiedUser({ session: ctx.session, includeExtendedPartnerData: true, }) if (!data || "error" in data) { return null } return parsedUser(data.data, true) } ), name: safeProtectedProcedure.query(async function ({ ctx }) { if (!isValidSession(ctx.session)) { return null } const verifiedData = await getVerifiedUser({ session: ctx.session }) if (!verifiedData || "error" in verifiedData) { return null } return { firstName: verifiedData.data.firstName, lastName: verifiedData.data.lastName, } }), membershipLevel: protectedProcedure.query(async function ({ ctx }) { const verifiedData = await getVerifiedUser({ session: ctx.session }) if ( !verifiedData || "error" in verifiedData || !verifiedData.data.loyalty ) { return null } const membershipLevel = getFriendsMembership(verifiedData.data.loyalty) return membershipLevel }), safeMembershipLevel: safeProtectedProcedure.query(async function ({ ctx }) { if (!isValidSession(ctx.session)) { return null } const verifiedData = await getVerifiedUser({ session: ctx.session }) if ( !verifiedData || "error" in verifiedData || !verifiedData.data.loyalty ) { return null } const membershipLevel = getFriendsMembership(verifiedData.data.loyalty) return membershipLevel }), userTrackingInfo: safeProtectedProcedure .input(userTrackingInput) .query(async function ({ ctx, input }) { const { lang } = input const language = lang || ctx.lang const userTrackingInfoCounter = createCounter("user", "userTrackingInfo") const metricsUserTrackingInfo = userTrackingInfoCounter.init() metricsUserTrackingInfo.start() const notLoggedInUserTrackingData: TrackingUserData = { loginStatus: "Non-logged in", } if (!isValidSession(ctx.session)) { metricsUserTrackingInfo.success({ reason: "invalid session", data: notLoggedInUserTrackingData, }) return notLoggedInUserTrackingData } try { const verifiedUserData = await getVerifiedUser({ session: ctx.session }) if ( !verifiedUserData || "error" in verifiedUserData || !verifiedUserData.data.loyalty ) { metricsUserTrackingInfo.success({ reason: "invalid user data", data: notLoggedInUserTrackingData, }) return notLoggedInUserTrackingData } const previousStaysData = await getPreviousStays( ctx.session.token.access_token, 1, language ) if (!previousStaysData) { metricsUserTrackingInfo.success({ reason: "no previous stays data", data: notLoggedInUserTrackingData, }) return notLoggedInUserTrackingData } const membership = getFriendsMembership(verifiedUserData.data.loyalty) const loggedInUserTrackingData: TrackingUserData = { loginStatus: "logged in", loginType: ctx.session.token.loginType as LoginType, memberId: verifiedUserData.data.profileId, membershipNumber: membership?.membershipNumber, memberLevel: membership?.membershipLevel, noOfNightsStayed: previousStaysData.links?.totalCount ?? 0, totalPointsAvailableToSpend: membership?.currentPoints, loginAction: "login success", } metricsUserTrackingInfo.success({ reason: "valid logged in", data: loggedInUserTrackingData, }) return loggedInUserTrackingData } catch (error) { metricsUserTrackingInfo.fail(error) return notLoggedInUserTrackingData } }), 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 } = 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 ) return { data: updatedData, nextCursor, } } return null }), }), transaction: router({ friendTransactions: languageProtectedProcedure .input(friendTransactionsInput) .query(async ({ ctx, input }) => { const { limit, page, lang } = input const friendTransactionsCounter = createCounter( "trpc.user.transactions", "friendTransactions" ) const metricsFriendTransactions = friendTransactionsCounter.init({ limit, page, lang, }) metricsFriendTransactions.start() const language = lang ?? ctx.lang 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, ctx.lang ) const pageData = updatedData .filter((t) => t.type !== Transactions.rewardType.expired) .sort((a, b) => { // 'BALFWD' are transactions from Opera migration that happended in May 2021 if (a.attributes.confirmationNumber === "BALFWD") return 1 if (b.attributes.confirmationNumber === "BALFWD") return -1 const dateA = new Date( a.attributes.checkinDate ? a.attributes.checkinDate : a.attributes.transactionDate ) const dateB = new Date( b.attributes.checkinDate ? b.attributes.checkinDate : b.attributes.transactionDate ) return dateA > dateB ? -1 : 1 }) const slicedData = pageData.slice(limit * (page - 1), limit * page) const result = { data: { transactions: slicedData.map(({ type, attributes }) => { return { type, awardPoints: attributes.awardPoints, checkinDate: attributes.checkinDate, checkoutDate: attributes.checkoutDate, city: attributes.hotelInformation?.city, confirmationNumber: attributes.confirmationNumber, hotelName: attributes.hotelInformation?.name, nights: attributes.nights, pointsCalculated: attributes.pointsCalculated, hotelId: attributes.hotelOperaId, transactionDate: attributes.transactionDate, bookingUrl: attributes.bookingUrl, } }), }, meta: { totalPages: Math.ceil(pageData.length / limit), }, } metricsFriendTransactions.success() return result }), }), 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 = await getVerifiedUser({ session: ctx.session }) if (!userData || "error" in userData || !userData.data.loyalty) { return null } return getMembershipCards(userData.data.loyalty) }), })