-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add script to reconstruct points from transactions (#4809)
* Add script to reconstruct points from transactions * Update scripts for fetching and issuing points * Update OP * Fix points getters and setters * Remove defaulting to prisma * Implement PR feedback * Update points scripts * Refresh points from history: * Fix points * Cleanup scripts * Cleanup * Fix send points unit test * Add in unit test for fetching points stats history * Extra assertion * Cleanup
- Loading branch information
Showing
12 changed files
with
643 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { log } from "@charmverse/core/log"; | ||
import { prisma } from "@charmverse/core/prisma-client"; | ||
import { currentSeason, getLastWeek } from "@packages/scoutgame/dates"; | ||
import {sendPoints} from '@packages/scoutgame/points/sendPoints' | ||
import {refreshPointStatsFromHistory} from '@packages/scoutgame/points/refreshPointStatsFromHistory' | ||
|
||
const fids: number[] = []; | ||
|
||
const description = `Friends of Scout Game`; | ||
|
||
async function issuePoints({points}: {points: number}) { | ||
for (const fid of fids) { | ||
log.info(`Issuing points to fid: ${fid}`); | ||
|
||
const scout = await prisma.scout.findFirstOrThrow({ | ||
where: { | ||
farcasterId: fid | ||
}, | ||
include: { | ||
pointsReceived: { | ||
where: { | ||
event: { | ||
description: { | ||
contains: description, | ||
mode: 'insensitive' | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
|
||
if (scout.pointsReceived.length > 0) { | ||
log.info(`Points already issued to fid: ${fid}`); | ||
continue; | ||
} | ||
|
||
await prisma.$transaction(async (tx) => { | ||
|
||
await sendPoints({ | ||
builderId: scout.id, | ||
points, | ||
claimed: true, | ||
description: `Friends of Scout Game`, | ||
hideFromNotifications: true, | ||
season: currentSeason, | ||
week: getLastWeek(), | ||
tx | ||
}); | ||
|
||
await refreshPointStatsFromHistory({ userIdOrUsername: scout.id, tx }); | ||
}, {timeout: 15000}); | ||
} | ||
} |
31 changes: 31 additions & 0 deletions
31
apps/scoutgamecron/src/scripts/refreshPointsFromTransactionHistory.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { log } from "@charmverse/core/log"; | ||
import { prisma } from "@charmverse/core/prisma-client"; | ||
import { refreshPointStatsFromHistory } from "@packages/scoutgame/points/refreshPointStatsFromHistory"; | ||
import { prettyPrint } from "@packages/utils/strings"; | ||
|
||
|
||
|
||
async function refreshPointsFromTransactionHistory() { | ||
const scouts = await prisma.scout.findMany({ | ||
select: { id: true, username: true }, | ||
orderBy: { | ||
id: 'asc' | ||
}, | ||
where: { | ||
createdAt: { | ||
lte: new Date('2024-10-14') | ||
} | ||
} | ||
}); | ||
|
||
for (let i = 0; i < scouts.length; i++) { | ||
const scout = scouts[i]; | ||
try { | ||
log.info(`Fixing points for ${scout.username} ${i + 1} / ${scouts.length}`); | ||
const stats = await refreshPointStatsFromHistory({ userIdOrUsername: scout.id }); | ||
log.info(`Successfully fixed points for ${scout.username}. New balance: ${stats.balance}`); | ||
} catch (error) { | ||
log.error(`Failed to fix points for ${scout.username}: ${prettyPrint(error)}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
138 changes: 138 additions & 0 deletions
138
packages/scoutgame/src/points/__tests__/getPointStatsFromHistory.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { InvalidInputError } from '@charmverse/core/errors'; | ||
import type { PointsReceipt, Prisma, Scout } from '@charmverse/core/prisma-client'; | ||
import { prisma } from '@charmverse/core/prisma-client'; | ||
import { jest } from '@jest/globals'; | ||
import { v4 as uuid } from 'uuid'; | ||
|
||
import { mockScout } from '../../testing/database'; | ||
import type { PointStats } from '../getPointStatsFromHistory'; | ||
import { getPointStatsFromHistory } from '../getPointStatsFromHistory'; | ||
|
||
describe('getPointStatsFromHistory', () => { | ||
let user: Scout; | ||
|
||
beforeAll(async () => { | ||
user = await mockScout(); | ||
}); | ||
|
||
it('should return point stats when valid UUID is provided', async () => { | ||
const stats = await getPointStatsFromHistory({ userIdOrUsername: user.id }); | ||
expect(stats).toMatchObject({ | ||
userId: user.id, | ||
pointsSpent: expect.any(Number), | ||
pointsReceivedAsScout: expect.any(Number), | ||
pointsReceivedAsBuilder: expect.any(Number), | ||
bonusPointsReceived: expect.any(Number), | ||
claimedPoints: expect.any(Number), | ||
unclaimedPoints: expect.any(Number), | ||
balance: expect.any(Number) | ||
}); | ||
}); | ||
|
||
it('should return point stats when valid username is provided', async () => { | ||
const stats = await getPointStatsFromHistory({ userIdOrUsername: user.username }); | ||
expect(stats).toMatchObject({ | ||
userId: user.id, | ||
pointsSpent: expect.any(Number), | ||
pointsReceivedAsScout: expect.any(Number), | ||
pointsReceivedAsBuilder: expect.any(Number), | ||
bonusPointsReceived: expect.any(Number), | ||
claimedPoints: expect.any(Number), | ||
unclaimedPoints: expect.any(Number), | ||
balance: expect.any(Number) | ||
}); | ||
}); | ||
|
||
it('should return detailed point stats, with a balance calculated based on points claimed minus claimed points (unclaimed points not in balance)', async () => { | ||
const pointsSpentRecords = [{ value: 100 }, { value: 50 }]; | ||
|
||
const pointsSpent = 100 + 50; | ||
|
||
const pointsReceivedAsBuilderRecords = [ | ||
{ value: 80, claimedAt: new Date() }, | ||
{ value: 90, claimedAt: new Date() } | ||
]; | ||
|
||
const pointsReceivedAsScoutRecords = [{ value: 120 }, { value: 240, claimedAt: new Date() }]; | ||
|
||
const bonusPointsReceivedRecords = [{ value: 40 }]; | ||
|
||
const allPointsReceivedRecords = [ | ||
...pointsReceivedAsBuilderRecords, | ||
...pointsReceivedAsScoutRecords, | ||
...bonusPointsReceivedRecords | ||
]; | ||
|
||
const claimedPoints = allPointsReceivedRecords.reduce((acc, record) => { | ||
if ((record as Pick<PointsReceipt, 'claimedAt' | 'value'>).claimedAt) { | ||
return acc + record.value; | ||
} | ||
return acc; | ||
}, 0); | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsSpentRecords as PointsReceipt[]); | ||
|
||
jest | ||
.spyOn(prisma.pointsReceipt, 'findMany') | ||
.mockResolvedValueOnce(pointsReceivedAsBuilderRecords as PointsReceipt[]); | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsReceivedAsScoutRecords as PointsReceipt[]); | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(bonusPointsReceivedRecords as PointsReceipt[]); | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(allPointsReceivedRecords as PointsReceipt[]); | ||
|
||
const pointStats = await getPointStatsFromHistory({ userIdOrUsername: user.id }); | ||
|
||
// Sanity check that the points add up | ||
expect(pointStats.claimedPoints + pointStats.unclaimedPoints).toEqual( | ||
pointStats.pointsReceivedAsBuilder + pointStats.pointsReceivedAsScout + pointStats.bonusPointsReceived | ||
); | ||
|
||
expect(pointStats).toEqual<PointStats>({ | ||
balance: claimedPoints - pointsSpent, | ||
bonusPointsReceived: 40, | ||
claimedPoints, | ||
pointsReceivedAsBuilder: 170, | ||
pointsReceivedAsScout: 360, | ||
pointsSpent: 150, | ||
unclaimedPoints: 160, | ||
userId: user.id | ||
}); | ||
}); | ||
|
||
it('should throw InvalidInputError when userIdOrUsername is empty', async () => { | ||
await expect(getPointStatsFromHistory({ userIdOrUsername: '' })).rejects.toThrow(InvalidInputError); | ||
}); | ||
|
||
it('should throw an error when userIdOrUsername is invalid UUID and does not exist as a username', async () => { | ||
const nonExistentUserId = uuid(); | ||
await expect(getPointStatsFromHistory({ userIdOrUsername: nonExistentUserId })).rejects.toThrow(); | ||
}); | ||
|
||
it('should throw an assertion error if point records for individual categories do not match the full list of point records', async () => { | ||
const pointsSpentRecords = [{ value: 100 }, { value: 50 }]; | ||
const pointsReceivedAsBuilderRecords = [{ value: 80 }, { value: 90 }]; | ||
const pointsReceivedAsScoutRecords = [{ value: 120 }]; | ||
const bonusPointsReceivedRecords = [{ value: 40 }]; | ||
const allPointsReceivedRecords = [ | ||
...pointsReceivedAsBuilderRecords, | ||
// Scout points are missing, so we expected an error | ||
// ...pointsReceivedAsScoutRecords, | ||
...bonusPointsReceivedRecords | ||
]; | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsSpentRecords as PointsReceipt[]); // Mismatch points | ||
// | ||
jest | ||
.spyOn(prisma.pointsReceipt, 'findMany') | ||
.mockResolvedValueOnce(pointsReceivedAsBuilderRecords as PointsReceipt[]); // Mismatch points | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(pointsReceivedAsScoutRecords as PointsReceipt[]); // Mismatch points | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(bonusPointsReceivedRecords as PointsReceipt[]); // Mismatch points | ||
|
||
jest.spyOn(prisma.pointsReceipt, 'findMany').mockResolvedValueOnce(allPointsReceivedRecords as PointsReceipt[]); // Mismatch points | ||
await expect(getPointStatsFromHistory({ userIdOrUsername: user.id })).rejects.toThrow(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.