Compute and store nextLoanCached for all users
This commit is contained in:
parent
599a68d93d
commit
565b6d3a87
|
@ -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>
|
||||||
) => {
|
) => {
|
||||||
|
|
|
@ -32,6 +32,7 @@ export type User = {
|
||||||
allTime: number
|
allTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextLoanCached: number
|
||||||
followerCountCached: number
|
followerCountCached: number
|
||||||
|
|
||||||
followedCategories?: string[]
|
followedCategories?: string[]
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user