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 { notFound } 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 notFound() } 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) }), 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 } = 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 }), 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, 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, error] = await safeTry( getVerifiedUser({ token: ctx.session.token }) ) if (!userData?.loyalty || error) { return null } return getMembershipCards(userData.loyalty) }), })