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'
|
||||
| 'user_joined_to_bet_on_your_market'
|
||||
| 'unique_bettors_on_your_contract'
|
||||
| 'daily_bonus'
|
||||
|
|
|
@ -16,13 +16,8 @@ export type Txn<T extends AnyTxnType = AnyTxnType> = {
|
|||
amount: number
|
||||
token: 'M$' // | 'USD' | MarketOutcome
|
||||
|
||||
category:
|
||||
| 'CHARITY'
|
||||
| 'MANALINK'
|
||||
| 'TIP'
|
||||
| 'REFERRAL'
|
||||
| 'UNIQUE_BETTOR_BONUS'
|
||||
| 'DAILY_BONUS' // | 'BET'
|
||||
category: 'CHARITY' | 'MANALINK' | 'TIP' | 'REFERRAL' | 'UNIQUE_BETTOR_BONUS'
|
||||
|
||||
// Any extra data
|
||||
data?: { [key: string]: any }
|
||||
|
||||
|
@ -61,7 +56,7 @@ type Referral = {
|
|||
type Bonus = {
|
||||
fromType: 'BANK'
|
||||
toType: 'USER'
|
||||
category: 'DAILY_BONUS' | 'UNIQUE_BETTOR_BONUS'
|
||||
category: 'UNIQUE_BETTOR_BONUS'
|
||||
}
|
||||
|
||||
export type DonationTxn = Txn & Donation
|
||||
|
|
|
@ -57,7 +57,7 @@ export type PrivateUser = {
|
|||
initialIpAddress?: string
|
||||
apiKey?: string
|
||||
notificationPreferences?: notification_subscribe_types
|
||||
lastTimeReceivedBonuses?: number
|
||||
lastTimeCheckedBonuses?: number
|
||||
}
|
||||
|
||||
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'
|
||||
|
||||
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) => {
|
||||
log('Inside endpoint handler.')
|
||||
const { user, fromUser, lastReceivedBonusTime } =
|
||||
await firestore.runTransaction(async (trans) => {
|
||||
const userSnap = await firestore.doc(`private-users/${auth.uid}`).get()
|
||||
const { user, lastTimeCheckedBonuses } = await firestore.runTransaction(
|
||||
async (trans) => {
|
||||
const userSnap = await trans.get(
|
||||
firestore.doc(`private-users/${auth.uid}`)
|
||||
)
|
||||
if (!userSnap.exists) throw new APIError(400, 'User not found.')
|
||||
const user = userSnap.data() as PrivateUser
|
||||
// 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 (!userSnap.exists) throw new APIError(400, 'From user not found.')
|
||||
const fromUser = fromSnap.data() as User
|
||||
const lastReceivedBonusTime = user.lastTimeReceivedBonuses ?? 0
|
||||
if (Date.now() - lastReceivedBonusTime < 5 * 1000)
|
||||
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(),
|
||||
const lastTimeCheckedBonuses = user.lastTimeCheckedBonuses ?? 0
|
||||
if (Date.now() - lastTimeCheckedBonuses < QUERY_LIMIT_SECONDS * 1000)
|
||||
throw new APIError(
|
||||
400,
|
||||
`Limited to one query per user per ${QUERY_LIMIT_SECONDS} seconds.`
|
||||
)
|
||||
await trans.update(userSnap.ref, {
|
||||
lastTimeCheckedBonuses: Date.now(),
|
||||
})
|
||||
return { user, fromUser, lastReceivedBonusTime }
|
||||
})
|
||||
|
||||
// get all users contracts made since implementation time
|
||||
return {
|
||||
user,
|
||||
lastTimeCheckedBonuses,
|
||||
}
|
||||
}
|
||||
)
|
||||
// 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
|
||||
.collection(`contracts`)
|
||||
.where('creatorId', '==', user.id)
|
||||
|
@ -50,8 +57,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
|||
for (const contract of userContracts) {
|
||||
const result = await firestore.runTransaction(async (trans) => {
|
||||
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 = (
|
||||
await firestore
|
||||
.collection(`contracts/${contractId}/bets`)
|
||||
|
@ -63,21 +69,21 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
|||
}
|
||||
const contractBetsSnap = await trans.getAll(...bets)
|
||||
const contractBets = contractBetsSnap.map((doc) => doc.data() as Bet)
|
||||
// log(`Got ${contractBets.length} bets`)
|
||||
|
||||
const uniqueBettorIdsBeforeLastResetTime = uniq(
|
||||
contractBets
|
||||
.filter((bet) => bet.createdTime < lastReceivedBonusTime)
|
||||
.filter((bet) => bet.createdTime < lastTimeCheckedBonuses)
|
||||
.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(
|
||||
contractBets
|
||||
.filter((bet) => bet.createdTime > lastReceivedBonusTime)
|
||||
.filter((bet) => bet.createdTime > lastTimeCheckedBonuses)
|
||||
.map((bet) => bet.userId)
|
||||
)
|
||||
// filter for users only present in the above list
|
||||
|
||||
// Filter for users only present in the above list
|
||||
const newUniqueBettorIds =
|
||||
uniqueBettorIdsWithBetsAfterLastResetTime.filter(
|
||||
(userId) => !uniqueBettorIdsBeforeLastResetTime.includes(userId)
|
||||
|
@ -87,10 +93,9 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
|||
`Got ${newUniqueBettorIds.length} new unique bettors since last bonus`
|
||||
)
|
||||
if (newUniqueBettorIds.length === 0) {
|
||||
// log(`No new unique bettors for contract ${contractId}`)
|
||||
return nullReturn
|
||||
}
|
||||
// create txn for each unique user
|
||||
// Create combined txn for all unique bettors
|
||||
const bonusTxnDetails = {
|
||||
contractId: contractId,
|
||||
uniqueBettors: newUniqueBettorIds.length,
|
||||
|
@ -122,7 +127,7 @@ export const getdailybonuses = newEndpoint({}, async (req, auth) => {
|
|||
result.txn.amount + '',
|
||||
contract,
|
||||
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,
|
||||
contract.slug,
|
||||
contract.question
|
||||
|
|
|
@ -198,7 +198,7 @@ export default function Sidebar(props: { className?: string }) {
|
|||
useEffect(() => {
|
||||
if (!privateUser) return
|
||||
|
||||
if (Date.now() - (privateUser.lastTimeReceivedBonuses ?? 0) > 1000)
|
||||
if (Date.now() - (privateUser.lastTimeCheckedBonuses ?? 0) > 30 * 1000)
|
||||
requestBonuses({}).catch((error) => {
|
||||
console.log("couldn't get bonuses:", error.message)
|
||||
})
|
||||
|
|
|
@ -42,6 +42,9 @@ export function groupNotifications(notifications: Notification[]) {
|
|||
const bonusNotifications = notificationsGroupedByDay.filter(
|
||||
(notification) => notification.sourceType === 'bonus'
|
||||
)
|
||||
const normalNotificationsGroupedByDay = notificationsGroupedByDay.filter(
|
||||
(notification) => notification.sourceType !== 'bonus'
|
||||
)
|
||||
if (bonusNotifications.length > 0) {
|
||||
notificationGroups = notificationGroups.concat({
|
||||
notifications: bonusNotifications,
|
||||
|
@ -53,9 +56,7 @@ export function groupNotifications(notifications: Notification[]) {
|
|||
}
|
||||
// Group notifications by contract, filtering out bonuses:
|
||||
const groupedNotificationsByContractId = groupBy(
|
||||
notificationsGroupedByDay.filter(
|
||||
(notification) => notification.sourceType !== 'bonus'
|
||||
),
|
||||
normalNotificationsGroupedByDay,
|
||||
(notification) => {
|
||||
return notification.sourceContractId
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ import { UNIQUE_BETTOR_BONUS_AMOUNT } from 'common/numeric-constants'
|
|||
import { groupBy } from 'lodash'
|
||||
|
||||
export const NOTIFICATIONS_PER_PAGE = 30
|
||||
export const HIGHLIGHT_DURATION = 20000
|
||||
export const HIGHLIGHT_DURATION = 30 * 1000
|
||||
|
||||
export default function Notifications() {
|
||||
const user = useUser()
|
||||
|
@ -100,46 +100,52 @@ export default function Notifications() {
|
|||
/>
|
||||
)
|
||||
)}
|
||||
<nav
|
||||
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">
|
||||
Showing{' '}
|
||||
<span className="font-medium">
|
||||
{page === 1
|
||||
? page
|
||||
: (page - 1) * NOTIFICATIONS_PER_PAGE}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="font-medium">
|
||||
{page * NOTIFICATIONS_PER_PAGE}
|
||||
</span>{' '}
|
||||
of{' '}
|
||||
<span className="font-medium">
|
||||
{groupedNotifications.length}
|
||||
</span>{' '}
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-1 justify-between sm:justify-end">
|
||||
<a
|
||||
href="#"
|
||||
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>
|
||||
<a
|
||||
href="#"
|
||||
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={() => setPage(page + 1)}
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
{groupedNotifications.length > NOTIFICATIONS_PER_PAGE && (
|
||||
<nav
|
||||
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">
|
||||
Showing{' '}
|
||||
<span className="font-medium">
|
||||
{page === 1
|
||||
? page
|
||||
: (page - 1) * NOTIFICATIONS_PER_PAGE}
|
||||
</span>{' '}
|
||||
to{' '}
|
||||
<span className="font-medium">
|
||||
{page * NOTIFICATIONS_PER_PAGE}
|
||||
</span>{' '}
|
||||
of{' '}
|
||||
<span className="font-medium">
|
||||
{groupedNotifications.length}
|
||||
</span>{' '}
|
||||
results
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-1 justify-between sm:justify-end">
|
||||
<a
|
||||
href="#"
|
||||
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>
|
||||
<a
|
||||
href="#"
|
||||
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={() =>
|
||||
page <
|
||||
groupedNotifications?.length /
|
||||
NOTIFICATIONS_PER_PAGE && setPage(page + 1)
|
||||
}
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingIndicator />
|
||||
|
@ -268,7 +274,7 @@ function IncomeNotificationGroupItem(props: {
|
|||
className={'line-clamp-1 cursor-pointer pl-1 sm:pl-0'}
|
||||
>
|
||||
<span>
|
||||
{'Income Summary: '}
|
||||
{'Daily Income Summary: '}
|
||||
<span className={'text-primary'}>{formatMoney(totalIncome)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user