From 565b6d3a87e95b739ba9c6018f0333afb161b55a Mon Sep 17 00:00:00 2001 From: James Grugett Date: Sun, 21 Aug 2022 22:53:01 -0500 Subject: [PATCH] Compute and store nextLoanCached for all users --- common/loans.ts | 46 ++++++++++++++++++++- common/user.ts | 1 + functions/src/create-user.ts | 1 + functions/src/update-loans.ts | 71 ++++++++++++--------------------- functions/src/update-metrics.ts | 17 +++++++- web/components/user-page.tsx | 4 +- 6 files changed, 91 insertions(+), 49 deletions(-) diff --git a/common/loans.ts b/common/loans.ts index 049b3476..01a8a790 100644 --- a/common/loans.ts +++ b/common/loans.ts @@ -7,6 +7,7 @@ import { FreeResponseContract, MultipleChoiceContract, } from './contract' +import { PortfolioMetrics, User } from './user' import { filterDefined } from './util/array' const LOAN_WEEKLY_RATE = 0.05 @@ -16,7 +17,50 @@ const calculateNewLoan = (investedValue: number, loanTotal: number) => { return netValue * LOAN_WEEKLY_RATE } -export const getUserLoanUpdates = ( +export const getLoanUpdates = ( + users: User[], + contractsById: { [contractId: string]: Contract }, + portfolioByUser: { [userId: string]: PortfolioMetrics | undefined }, + betsByUser: { [userId: string]: Bet[] } +) => { + const eligibleUsers = filterDefined( + users.map((user) => + isUserEligibleForLoan(portfolioByUser[user.id]) ? user : undefined + ) + ) + + const betUpdates = eligibleUsers + .map((user) => { + const updates = calculateLoanBetUpdates( + betsByUser[user.id] ?? [], + contractsById + ).betUpdates + return updates.map((update) => ({ ...update, user })) + }) + .flat() + + const updatesByUser = groupBy(betUpdates, (update) => update.userId) + const userPayouts = Object.values(updatesByUser).map((updates) => { + return { + user: updates[0].user, + payout: sumBy(updates, (update) => update.newLoan), + } + }) + + return { + betUpdates, + userPayouts, + } +} + +const isUserEligibleForLoan = (portfolio: PortfolioMetrics | undefined) => { + if (!portfolio) return true + + const { balance, investmentValue } = portfolio + return balance + investmentValue > 0 +} + +const calculateLoanBetUpdates = ( bets: Bet[], contractsById: Dictionary ) => { diff --git a/common/user.ts b/common/user.ts index 2910c54e..4b21fe2d 100644 --- a/common/user.ts +++ b/common/user.ts @@ -32,6 +32,7 @@ export type User = { allTime: number } + nextLoanCached: number followerCountCached: number followedCategories?: string[] diff --git a/functions/src/create-user.ts b/functions/src/create-user.ts index 7156855e..f42fb5c3 100644 --- a/functions/src/create-user.ts +++ b/functions/src/create-user.ts @@ -75,6 +75,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => { createdTime: Date.now(), profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, + nextLoanCached: 0, followerCountCached: 0, followedCategories: DEFAULT_CATEGORIES, shouldShowWelcome: true, diff --git a/functions/src/update-loans.ts b/functions/src/update-loans.ts index d6eb9f77..dbde5a2e 100644 --- a/functions/src/update-loans.ts +++ b/functions/src/update-loans.ts @@ -1,12 +1,11 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { groupBy, keyBy, sumBy } from 'lodash' +import { groupBy, keyBy } from 'lodash' import { getValues, log, payUser, writeAsync } from './utils' import { Bet } from '../../common/bet' import { Contract } from '../../common/contract' import { PortfolioMetrics, User } from '../../common/user' -import { filterDefined } from '../../common/util/array' -import { getUserLoanUpdates } from '../../common/loans' +import { getLoanUpdates } from '../../common/loans' import { createLoanIncomeNotification } from './create-notification' const firestore = admin.firestore() @@ -31,31 +30,34 @@ async function updateLoansCore() { log( `Loaded ${users.length} users, ${contracts.length} contracts, and ${bets.length} bets.` ) - - const eligibleUsers = filterDefined( - await Promise.all( - users.map((user) => - isUserEligibleForLoan(user).then((isEligible) => - isEligible ? user : undefined - ) + const userPortfolios = await Promise.all( + users.map(async (user) => { + const portfolio = await getValues( + firestore + .collection(`users/${user.id}/portfolioHistory`) + .orderBy('timestamp', 'desc') + .limit(1) ) - ) + return portfolio[0] + }) ) - log(`${eligibleUsers.length} users are eligible for loans.`) + log(`Loaded ${userPortfolios.length} portfolios`) + const portfolioByUser = keyBy(userPortfolios, (portfolio) => portfolio.userId) - const contractsById = keyBy(contracts, (contract) => contract.id) + const contractsById = Object.fromEntries( + contracts.map((contract) => [contract.id, contract]) + ) const betsByUser = groupBy(bets, (bet) => bet.userId) + const { betUpdates, userPayouts } = getLoanUpdates( + users, + contractsById, + portfolioByUser, + betsByUser + ) - const userLoanUpdates = eligibleUsers - .map( - (user) => - getUserLoanUpdates(betsByUser[user.id] ?? [], contractsById).betUpdates - ) - .flat() + log(`${betUpdates.length} bet updates.`) - log(`${userLoanUpdates.length} bet updates.`) - - const betUpdates = userLoanUpdates.map((update) => ({ + const betDocUpdates = betUpdates.map((update) => ({ doc: firestore .collection('contracts') .doc(update.contractId) @@ -66,17 +68,7 @@ async function updateLoansCore() { }, })) - await writeAsync(firestore, betUpdates) - - const userPayouts = eligibleUsers.map((user) => { - const updates = userLoanUpdates.filter( - (update) => update.userId === user.id - ) - return { - user, - payout: sumBy(updates, (update) => update.newLoan), - } - }) + await writeAsync(firestore, betDocUpdates) log(`${userPayouts.length} user payouts`) @@ -94,16 +86,3 @@ async function updateLoansCore() { log('Notifications sent!') } - -const isUserEligibleForLoan = async (user: User) => { - const [portfolio] = await getValues( - firestore - .collection(`users/${user.id}/portfolioHistory`) - .orderBy('timestamp', 'desc') - .limit(1) - ) - if (!portfolio) return true - - const { balance, investmentValue } = portfolio - return balance + investmentValue > 0 -} diff --git a/functions/src/update-metrics.ts b/functions/src/update-metrics.ts index cc9f8ebe..960f0b3a 100644 --- a/functions/src/update-metrics.ts +++ b/functions/src/update-metrics.ts @@ -1,6 +1,6 @@ import * as functions from 'firebase-functions' import * as admin from 'firebase-admin' -import { groupBy, isEmpty, sum, sumBy } from 'lodash' +import { groupBy, isEmpty, keyBy, mapValues, maxBy, sum, sumBy } from 'lodash' import { getValues, log, logMemory, writeAsync } from './utils' import { Bet } from '../../common/bet' import { Contract } from '../../common/contract' @@ -8,6 +8,7 @@ import { PortfolioMetrics, User } from '../../common/user' import { calculatePayout } from '../../common/calculate' import { DAY_MS } from '../../common/util/time' import { last } from 'lodash' +import { getLoanUpdates } from '../../common/loans' const firestore = admin.firestore() @@ -71,6 +72,18 @@ export const updateMetricsCore = async () => { const contractsByUser = groupBy(contracts, (contract) => contract.creatorId) const betsByUser = groupBy(bets, (bet) => bet.userId) const portfolioHistoryByUser = groupBy(allPortfolioHistories, (p) => p.userId) + + const portfolioByUser = mapValues(portfolioHistoryByUser, (history) => + maxBy(history, (portfolio) => portfolio.timestamp) + ) + const { userPayouts } = getLoanUpdates( + users, + contractsById, + portfolioByUser, + betsByUser + ) + const nextLoanByUser = keyBy(userPayouts, (payout) => payout.user.id) + const userUpdates = users.map((user) => { const currentBets = betsByUser[user.id] ?? [] const portfolioHistory = portfolioHistoryByUser[user.id] ?? [] @@ -93,6 +106,7 @@ export const updateMetricsCore = async () => { newPortfolio, didProfitChange ) + const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0 return { fieldUpdates: { @@ -102,6 +116,7 @@ export const updateMetricsCore = async () => { ...(didProfitChange && { profitCached: newProfit, }), + nextLoanCached, }, }, diff --git a/web/components/user-page.tsx b/web/components/user-page.tsx index 4db09b45..a9a1e346 100644 --- a/web/components/user-page.tsx +++ b/web/components/user-page.tsx @@ -181,7 +181,9 @@ export function UserPage(props: { user: User }) { } onClick={() => setShowLoansModal(true)} > - 🏦 {formatMoney(153)} + + 🏦 {formatMoney(user.nextLoanCached ?? 0)} + next loan