Move read & update into transaction
This commit is contained in:
parent
0aa00b75d5
commit
2e6801ff72
|
@ -58,4 +58,3 @@ export type notification_reason_types =
|
||||||
| 'you_referred_user'
|
| 'you_referred_user'
|
||||||
| 'user_joined_to_bet_on_your_market'
|
| 'user_joined_to_bet_on_your_market'
|
||||||
| 'unique_bettors_on_your_contract'
|
| 'unique_bettors_on_your_contract'
|
||||||
| 'daily_bonus'
|
|
||||||
|
|
|
@ -16,13 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
||||||
amount: number
|
amount: number
|
||||||
token: 'M$' // | 'USD' | MarketOutcome
|
token: 'M$' // | 'USD' | MarketOutcome
|
||||||
|
|
||||||
category:
|
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
||||||
| 'CHARITY'
|
|
||||||
| 'MANALINK'
|
|
||||||
| 'TIP'
|
|
||||||
| 'REFERRAL'
|
|
||||||
| 'UNIQUE_BETTOR_BONUS'
|
|
||||||
| 'DAILY_BONUS' // | 'BET'
|
|
||||||
// Any extra data
|
// Any extra data
|
||||||
data?: { [key: string]: any }
|
data?: { [key: string]: any }
|
||||||
|
|
||||||
|
@ -61,7 +56,7 @@ type Referral = {
|
||||||
type Bonus = {
|
type Bonus = {
|
||||||
fromType: 'BANK'
|
fromType: 'BANK'
|
||||||
toType: 'USER'
|
toType: 'USER'
|
||||||
category: 'DAILY_BONUS' | 'UNIQUE_BETTOR_BONUS'
|
category: 'UNIQUE_BETTOR_BONUS'
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DonationTxn = Txn & Donation
|
export type DonationTxn = Txn & Donation
|
||||||
|
|
|
@ -57,7 +57,7 @@ export type PrivateUser = {
|
||||||
initialIpAddress?: string
|
initialIpAddress?: string
|
||||||
apiKey?: string
|
apiKey?: string
|
||||||
notificationPreferences?: notification_subscribe_types
|
notificationPreferences?: notification_subscribe_types
|
||||||
lastTimeReceivedBonuses?: number
|
lastTimeCheckedBonuses?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
export type notification_subscribe_types = 'all' | 'less' | 'none'
|
||||||
|
|
|
@ -13,31 +13,38 @@ import { Contract } from '../../common/lib/contract'
|
||||||
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
import { UNIQUE_BETTOR_BONUS_AMOUNT } from '../../common/numeric-constants'
|
||||||
|
|
||||||
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
|
const BONUS_START_DATE = new Date('2022-07-01T00:00:00.000Z').getTime()
|
||||||
|
const QUERY_LIMIT_SECONDS = 60
|
||||||
|
|
||||||
export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
log('Inside endpoint handler.')
|
const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
|
||||||
const { user, fromUser, lastReceivedBonusTime } =
|
async (trans) => {
|
||||||
await firestore.runTransaction(async (trans) => {
|
const userSnap = await trans.get(
|
||||||
const userSnap = await firestore.doc(`private-users/${auth.uid}`).get()
|
firestore.doc(`private-users/${auth.uid}`)
|
||||||
|
)
|
||||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||||
const user = userSnap.data() as PrivateUser
|
const user = userSnap.data() as PrivateUser
|
||||||
// TODO: switch to prod id
|
const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
|
||||||
const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
|
if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
|
||||||
//const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID, // prod manifold account
|
throw new APIError(
|
||||||
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
400,
|
||||||
if (!userSnap.exists) throw new APIError(400, 'From user not found.')
|
`Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
|
||||||
const fromUser = fromSnap.data() as User
|
)
|
||||||
const lastReceivedBonusTime = user.lastTimeReceivedBonuses ?? 0
|
await trans.update(userSnap.ref, {
|
||||||
if (Date.now() - lastReceivedBonusTime < 5 * 1000)
|
lastTimeCheckedBonuses: Date.now(),
|
||||||
throw new APIError(400, 'Limited to one query per user per 5 seconds.')
|
|
||||||
|
|
||||||
await trans.update(firestore.doc(`private-users/${user.id}`), {
|
|
||||||
lastTimeReceivedBonuses: Date.now(),
|
|
||||||
})
|
})
|
||||||
return { user, fromUser, lastReceivedBonusTime }
|
return {
|
||||||
})
|
user,
|
||||||
|
lastTimeCheckedBonuses,
|
||||||
// get all users contracts made since implementation time
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// TODO: switch to prod id
|
||||||
|
// const fromUserId = '94YYTk1AFWfbWMpfYcvnnwI1veP2' // dev manifold account
|
||||||
|
const fromUserId = HOUSE_LIQUIDITY_PROVIDER_ID // prod manifold account
|
||||||
|
const fromSnap = await firestore.doc(`users/${fromUserId}`).get()
|
||||||
|
if (!fromSnap.exists) throw new APIError(400, 'From user not found.')
|
||||||
|
const fromUser = fromSnap.data() as User
|
||||||
|
// Get all users contracts made since implementation time
|
||||||
const userContractsSnap = await firestore
|
const userContractsSnap = await firestore
|
||||||
.collection(`contracts`)
|
.collection(`contracts`)
|
||||||
.where('creatorId', '==', user.id)
|
.where('creatorId', '==', user.id)
|
||||||
|
@ -50,8 +57,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
for (const contract of userContracts) {
|
for (const contract of userContracts) {
|
||||||
const result = await firestore.runTransaction(async (trans) => {
|
const result = await firestore.runTransaction(async (trans) => {
|
||||||
const contractId = contract.id
|
const contractId = contract.id
|
||||||
// log(`Checking contract ${contractId}`)
|
// Get all bets made on user's contracts
|
||||||
// get all bets made on user's contracts
|
|
||||||
const bets = (
|
const bets = (
|
||||||
await firestore
|
await firestore
|
||||||
.collection(`contracts/${contractId}/bets`)
|
.collection(`contracts/${contractId}/bets`)
|
||||||
|
@ -63,21 +69,21 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
}
|
}
|
||||||
const contractBetsSnap = await trans.getAll(...bets)
|
const contractBetsSnap = await trans.getAll(...bets)
|
||||||
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
|
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
|
||||||
// log(`Got ${contractBets.length} bets`)
|
|
||||||
|
|
||||||
const uniqueBettorIdsBeforeLastResetTime = uniq(
|
const uniqueBettorIdsBeforeLastResetTime = uniq(
|
||||||
contractBets
|
contractBets
|
||||||
.filter((bet) => bet.createdTime < lastReceivedBonusTime)
|
.filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
|
||||||
.map((bet) => bet.userId)
|
.map((bet) => bet.userId)
|
||||||
)
|
)
|
||||||
|
|
||||||
// filter users for ONLY those that have made bets since the last daily bonus received time
|
// Filter users for ONLY those that have made bets since the last daily bonus received time
|
||||||
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
|
const uniqueBettorIdsWithBetsAfterLastResetTime = uniq(
|
||||||
contractBets
|
contractBets
|
||||||
.filter((bet) => bet.createdTime > lastReceivedBonusTime)
|
.filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
|
||||||
.map((bet) => bet.userId)
|
.map((bet) => bet.userId)
|
||||||
)
|
)
|
||||||
// filter for users only present in the above list
|
|
||||||
|
// Filter for users only present in the above list
|
||||||
const newUniqueBettorIds =
|
const newUniqueBettorIds =
|
||||||
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
|
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
|
||||||
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
|
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
|
||||||
|
@ -87,10 +93,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
|
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
|
||||||
)
|
)
|
||||||
if (newUniqueBettorIds.length === 0) {
|
if (newUniqueBettorIds.length === 0) {
|
||||||
// log(`No new unique bettors for contract ${contractId}`)
|
|
||||||
return nullReturn
|
return nullReturn
|
||||||
}
|
}
|
||||||
// create txn for each unique user
|
// Create combined txn for all unique bettors
|
||||||
const bonusTxnDetails = {
|
const bonusTxnDetails = {
|
||||||
contractId: contractId,
|
contractId: contractId,
|
||||||
uniqueBettors: newUniqueBettorIds.length,
|
uniqueBettors: newUniqueBettorIds.length,
|
||||||
|
@ -122,7 +127,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
||||||
result.txn.amount + '',
|
result.txn.amount + '',
|
||||||
contract,
|
contract,
|
||||||
undefined,
|
undefined,
|
||||||
// No need to set the user id, they're the contract creator id
|
// No need to set the user id, we'll use the contract creator id
|
||||||
undefined,
|
undefined,
|
||||||
contract.slug,
|
contract.slug,
|
||||||
contract.question
|
contract.question
|
||||||
|
|
|
@ -198,7 +198,7 @@ export default function Sidebar(props: { className?: string }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!privateUser) return
|
if (!privateUser) return
|
||||||
|
|
||||||
if (Date.now() - (privateUser.lastTimeReceivedBonuses ?? 0) > 1000)
|
if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 30 * 1000)
|
||||||
requestBonuses({}).catch((error) => {
|
requestBonuses({}).catch((error) => {
|
||||||
console.log("couldn't get bonuses:", error.message)
|
console.log("couldn't get bonuses:", error.message)
|
||||||
})
|
})
|
||||||
|
|
|
@ -42,6 +42,9 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
const bonusNotifications = notificationsGroupedByDay.filter(
|
const bonusNotifications = notificationsGroupedByDay.filter(
|
||||||
(notification) => notification.sourceType === 'bonus'
|
(notification) => notification.sourceType === 'bonus'
|
||||||
)
|
)
|
||||||
|
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
|
||||||
|
(notification) => notification.sourceType !== 'bonus'
|
||||||
|
)
|
||||||
if (bonusNotifications.length > 0) {
|
if (bonusNotifications.length > 0) {
|
||||||
notificationGroups = notificationGroups.concat({
|
notificationGroups = notificationGroups.concat({
|
||||||
notifications: bonusNotifications,
|
notifications: bonusNotifications,
|
||||||
|
@ -53,9 +56,7 @@ export function groupNotifications(notifications: Notification[]) {
|
||||||
}
|
}
|
||||||
// Group notifications by contract, filtering out bonuses:
|
// Group notifications by contract, filtering out bonuses:
|
||||||
const groupedNotificationsByContractId = groupBy(
|
const groupedNotificationsByContractId = groupBy(
|
||||||
notificationsGroupedByDay.filter(
|
normalNotificationsGroupedByDay,
|
||||||
(notification) => notification.sourceType !== 'bonus'
|
|
||||||
),
|
|
||||||
(notification) => {
|
(notification) => {
|
||||||
return notification.sourceContractId
|
return notification.sourceContractId
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
||||||
import { groupBy } from 'lodash'
|
import { groupBy } from 'lodash'
|
||||||
|
|
||||||
export const NOTIFICATIONS_PER_PAGE = 30
|
export const NOTIFICATIONS_PER_PAGE = 30
|
||||||
export const HIGHLIGHT_DURATION = 20000
|
export const HIGHLIGHT_DURATION = 30 * 1000
|
||||||
|
|
||||||
export default function Notifications() {
|
export default function Notifications() {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
@ -100,46 +100,52 @@ export default function Notifications() {
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
<nav
|
{groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
|
||||||
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
|
<nav
|
||||||
aria-label="Pagination"
|
className="flex items-center justify-between border-t border-gray-200 bg-white px-4 py-3 sm:px-6"
|
||||||
>
|
aria-label="Pagination"
|
||||||
<div className="hidden sm:block">
|
>
|
||||||
<p className="text-sm text-gray-700">
|
<div className="hidden sm:block">
|
||||||
Showing{' '}
|
<p className="text-sm text-gray-700">
|
||||||
<span className="font-medium">
|
Showing{' '}
|
||||||
{page === 1
|
<span className="font-medium">
|
||||||
? page
|
{page === 1
|
||||||
: (page - 1) * NOTIFICATIONS_PER_PAGE}
|
? page
|
||||||
</span>{' '}
|
: (page - 1) * NOTIFICATIONS_PER_PAGE}
|
||||||
to{' '}
|
</span>{' '}
|
||||||
<span className="font-medium">
|
to{' '}
|
||||||
{page * NOTIFICATIONS_PER_PAGE}
|
<span className="font-medium">
|
||||||
</span>{' '}
|
{page * NOTIFICATIONS_PER_PAGE}
|
||||||
of{' '}
|
</span>{' '}
|
||||||
<span className="font-medium">
|
of{' '}
|
||||||
{groupedNotifications.length}
|
<span className="font-medium">
|
||||||
</span>{' '}
|
{groupedNotifications.length}
|
||||||
results
|
</span>{' '}
|
||||||
</p>
|
results
|
||||||
</div>
|
</p>
|
||||||
<div className="flex flex-1 justify-between sm:justify-end">
|
</div>
|
||||||
<a
|
<div className="flex flex-1 justify-between sm:justify-end">
|
||||||
href="#"
|
<a
|
||||||
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
href="#"
|
||||||
onClick={() => page > 1 && setPage(page - 1)}
|
className="relative inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
>
|
onClick={() => page > 1 && setPage(page - 1)}
|
||||||
Previous
|
>
|
||||||
</a>
|
Previous
|
||||||
<a
|
</a>
|
||||||
href="#"
|
<a
|
||||||
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
href="#"
|
||||||
onClick={() => setPage(page + 1)}
|
className="relative ml-3 inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50"
|
||||||
>
|
onClick={() =>
|
||||||
Next
|
page <
|
||||||
</a>
|
groupedNotifications?.length /
|
||||||
</div>
|
NOTIFICATIONS_PER_PAGE && setPage(page + 1)
|
||||||
</nav>
|
}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<LoadingIndicator />
|
<LoadingIndicator />
|
||||||
|
@ -268,7 +274,7 @@ function IncomeNotificationGroupItem(props: {
|
||||||
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{'Income Summary: '}
|
{'Daily Income Summary: '}
|
||||||
<span className={'text-primary'}>{formatMoney(totalIncome)}</span>
|
<span className={'text-primary'}>{formatMoney(totalIncome)}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user