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,
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<Contract>
) => {

View File

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

View File

@ -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,

View File

@ -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<PortfolioMetrics>(
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<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 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,
},
},

View File

@ -181,7 +181,9 @@ export function UserPage(props: { user: User }) {
}
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>
</Col>
</Row>