Compute and store nextLoanCached for all users

This commit is contained in:
James Grugett 2022-08-21 22:53:01 -05:00
parent 599a68d93d
commit 565b6d3a87
6 changed files with 91 additions and 49 deletions

View File

@ -7,6 +7,7 @@ import {
FreeResponseContract, FreeResponseContract,
MultipleChoiceContract, MultipleChoiceContract,
} from './contract' } from './contract'
import { PortfolioMetrics, User } from './user'
import { filterDefined } from './util/array' import { filterDefined } from './util/array'
const LOAN_WEEKLY_RATE = 0.05 const LOAN_WEEKLY_RATE = 0.05
@ -16,7 +17,50 @@ const calculateNewLoan = (investedValue: number, loanTotal: number) => {
return netValue * LOAN_WEEKLY_RATE 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[], bets: Bet[],
contractsById: Dictionary<Contract> contractsById: Dictionary<Contract>
) => { ) => {

View File

@ -32,6 +32,7 @@ export type User = {
allTime: number allTime: number
} }
nextLoanCached: number
followerCountCached: number followerCountCached: number
followedCategories?: string[] followedCategories?: string[]

View File

@ -75,6 +75,7 @@ export const createuser = newEndpoint(opts, async (req, auth) => {
createdTime: Date.now(), createdTime: Date.now(),
profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, profitCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 }, creatorVolumeCached: { daily: 0, weekly: 0, monthly: 0, allTime: 0 },
nextLoanCached: 0,
followerCountCached: 0, followerCountCached: 0,
followedCategories: DEFAULT_CATEGORIES, followedCategories: DEFAULT_CATEGORIES,
shouldShowWelcome: true, shouldShowWelcome: true,

View File

@ -1,12 +1,11 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' 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 { getValues, log, payUser, writeAsync } from './utils'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
import { PortfolioMetrics, User } from '../../common/user' import { PortfolioMetrics, User } from '../../common/user'
import { filterDefined } from '../../common/util/array' import { getLoanUpdates } from '../../common/loans'
import { getUserLoanUpdates } from '../../common/loans'
import { createLoanIncomeNotification } from './create-notification' import { createLoanIncomeNotification } from './create-notification'
const firestore = admin.firestore() const firestore = admin.firestore()
@ -31,31 +30,34 @@ async function updateLoansCore() {
log( log(
`Loaded ${users.length} users, ${contracts.length} contracts, and ${bets.length} bets.` `Loaded ${users.length} users, ${contracts.length} contracts, and ${bets.length} bets.`
) )
const userPortfolios = await Promise.all(
users.map(async (user) => {
const portfolio = await getValues<PortfolioMetrics>(
firestore
.collection(`users/${user.id}/portfolioHistory`)
.orderBy('timestamp', 'desc')
.limit(1)
)
return portfolio[0]
})
)
log(`Loaded ${userPortfolios.length} portfolios`)
const portfolioByUser = keyBy(userPortfolios, (portfolio) => portfolio.userId)
const eligibleUsers = filterDefined( const contractsById = Object.fromEntries(
await Promise.all( contracts.map((contract) => [contract.id, contract])
users.map((user) =>
isUserEligibleForLoan(user).then((isEligible) =>
isEligible ? user : undefined
) )
)
)
)
log(`${eligibleUsers.length} users are eligible for loans.`)
const contractsById = keyBy(contracts, (contract) => contract.id)
const betsByUser = groupBy(bets, (bet) => bet.userId) const betsByUser = groupBy(bets, (bet) => bet.userId)
const { betUpdates, userPayouts } = getLoanUpdates(
const userLoanUpdates = eligibleUsers users,
.map( contractsById,
(user) => portfolioByUser,
getUserLoanUpdates(betsByUser[user.id] ?? [], contractsById).betUpdates betsByUser
) )
.flat()
log(`${userLoanUpdates.length} bet updates.`) log(`${betUpdates.length} bet updates.`)
const betUpdates = userLoanUpdates.map((update) => ({ const betDocUpdates = betUpdates.map((update) => ({
doc: firestore doc: firestore
.collection('contracts') .collection('contracts')
.doc(update.contractId) .doc(update.contractId)
@ -66,17 +68,7 @@ async function updateLoansCore() {
}, },
})) }))
await writeAsync(firestore, betUpdates) await writeAsync(firestore, betDocUpdates)
const userPayouts = eligibleUsers.map((user) => {
const updates = userLoanUpdates.filter(
(update) => update.userId === user.id
)
return {
user,
payout: sumBy(updates, (update) => update.newLoan),
}
})
log(`${userPayouts.length} user payouts`) log(`${userPayouts.length} user payouts`)
@ -94,16 +86,3 @@ async function updateLoansCore() {
log('Notifications sent!') log('Notifications sent!')
} }
const isUserEligibleForLoan = async (user: User) => {
const [portfolio] = await getValues<PortfolioMetrics>(
firestore
.collection(`users/${user.id}/portfolioHistory`)
.orderBy('timestamp', 'desc')
.limit(1)
)
if (!portfolio) return true
const { balance, investmentValue } = portfolio
return balance + investmentValue > 0
}

View File

@ -1,6 +1,6 @@
import * as functions from 'firebase-functions' import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin' 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 { getValues, log, logMemory, writeAsync } from './utils'
import { Bet } from '../../common/bet' import { Bet } from '../../common/bet'
import { Contract } from '../../common/contract' import { Contract } from '../../common/contract'
@ -8,6 +8,7 @@ import { PortfolioMetrics, User } from '../../common/user'
import { calculatePayout } from '../../common/calculate' import { calculatePayout } from '../../common/calculate'
import { DAY_MS } from '../../common/util/time' import { DAY_MS } from '../../common/util/time'
import { last } from 'lodash' import { last } from 'lodash'
import { getLoanUpdates } from '../../common/loans'
const firestore = admin.firestore() const firestore = admin.firestore()
@ -71,6 +72,18 @@ export const updateMetricsCore = async () => {
const contractsByUser = groupBy(contracts, (contract) => contract.creatorId) const contractsByUser = groupBy(contracts, (contract) => contract.creatorId)
const betsByUser = groupBy(bets, (bet) => bet.userId) const betsByUser = groupBy(bets, (bet) => bet.userId)
const portfolioHistoryByUser = groupBy(allPortfolioHistories, (p) => p.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 userUpdates = users.map((user) => {
const currentBets = betsByUser[user.id] ?? [] const currentBets = betsByUser[user.id] ?? []
const portfolioHistory = portfolioHistoryByUser[user.id] ?? [] const portfolioHistory = portfolioHistoryByUser[user.id] ?? []
@ -93,6 +106,7 @@ export const updateMetricsCore = async () => {
newPortfolio, newPortfolio,
didProfitChange didProfitChange
) )
const nextLoanCached = nextLoanByUser[user.id]?.payout ?? 0
return { return {
fieldUpdates: { fieldUpdates: {
@ -102,6 +116,7 @@ export const updateMetricsCore = async () => {
...(didProfitChange && { ...(didProfitChange && {
profitCached: newProfit, profitCached: newProfit,
}), }),
nextLoanCached,
}, },
}, },

View File

@ -181,7 +181,9 @@ export function UserPage(props: { user: User }) {
} }
onClick={() => setShowLoansModal(true)} onClick={() => setShowLoansModal(true)}
> >
<span className="text-green-600">🏦 {formatMoney(153)}</span> <span className="text-green-600">
🏦 {formatMoney(user.nextLoanCached ?? 0)}
</span>
<span>next loan</span> <span>next loan</span>
</Col> </Col>
</Row> </Row>