diff --git a/src/resolvers/qAccResolver.test.ts b/src/resolvers/qAccResolver.test.ts index 27e08ba28..2a24b3d26 100644 --- a/src/resolvers/qAccResolver.test.ts +++ b/src/resolvers/qAccResolver.test.ts @@ -24,6 +24,7 @@ import { import { projectUserDonationCap, projectUserTotalDonationAmounts, + qAccStat, userCaps, } from '../../test/graphqlQueries'; import { ProjectRoundRecord } from '../entities/projectRoundRecord'; @@ -33,6 +34,7 @@ import { GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE, } from '../constants/gitcoin'; import { PrivadoAdapter } from '../adapters/privado/privadoAdapter'; +import { AppDataSource } from '../orm'; describe( 'projectUserTotalDonationAmount() test cases', @@ -46,6 +48,8 @@ describe( describe('userCaps() test cases', userCapsTestCases); +describe('qAccStat() test cases', qAccStatTestCases); + function projectUserTotalDonationAmountTestCases() { it('should return total donation amount of a user for a project', async () => { it('should return total donation amount of a user for a project', async () => { @@ -491,3 +495,96 @@ function userCapsTestCases() { } }); } + +function qAccStatTestCases() { + let project; + let user; + let qfRound1: QfRound; + beforeEach(async () => { + project = await saveProjectDirectlyToDb(createProjectData()); + user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + qfRound1 = await QfRound.create({ + roundNumber: 1, + isActive: true, + name: new Date().toString() + ' - 1', + allocatedFund: 100, + minimumPassportScore: 12, + slug: new Date().getTime().toString() + ' - 1', + beginDate: new Date('2001-01-14'), + endDate: new Date('2001-01-16'), + roundUSDCapPerProject: 10000, + roundUSDCapPerUserPerProject: 2500, + roundUSDCapPerUserPerProjectWithGitcoinScoreOnly: 1000, + tokenPrice: 0.5, + }).save(); + sinon.useFakeTimers({ + now: new Date('2001-01-15').getTime(), + }); + }); + afterEach(async () => { + // Clean up the database after each test + await ProjectRoundRecord.delete({}); + await Donation.delete({ projectId: project.id }); + await QfRound.delete(qfRound1.id); + + sinon.restore(); + }); + it('should return correct qacc stats', async () => { + const qfDonationAmount = Math.round(Math.random() * 1_000_000_00) / 100; + const nonQfDonationAmount = Math.round(Math.random() * 1_000_000_00) / 100; + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: nonQfDonationAmount, + status: DONATION_STATUS.VERIFIED, + }, + user.id, + project.id, + ); + await saveDonationDirectlyToDb( + { + ...createDonationData(), + amount: qfDonationAmount, + status: DONATION_STATUS.VERIFIED, + qfRoundId: qfRound1.id, + }, + user.id, + project.id, + ); + const result: AxiosResponse< + ExecutionResult<{ + qAccStat: { + totalCollected: number; + qfTotalCollected: number; + contributorsCount: number; + }; + }> + > = await axios.post(graphqlUrl, { + query: qAccStat, + }); + + assert.isOk(result.data); + + const dataSource = AppDataSource.getDataSource(); + const totalDonations = await dataSource.query(` + SELECT COALESCE(SUM(amount), 0) as totalCollected from donation where status = '${DONATION_STATUS.VERIFIED}' + `); + const qfTotalDonations = await dataSource.query(` + SELECT COALESCE(SUM(amount), 0) as qfTotalCollected from donation where status = '${DONATION_STATUS.VERIFIED}' AND "qfRoundId" IS NOT NULL + `); + // count unique contributors + const contributorsCount = await Donation.createQueryBuilder('donation') + .select('COUNT(DISTINCT "userId")', 'contributorsCount') + .where('donation.status = :status', { + status: DONATION_STATUS.VERIFIED, + }) + .getRawOne(); + + assert.deepEqual(result.data.data?.qAccStat, { + totalCollected: totalDonations[0].totalcollected, + qfTotalCollected: qfTotalDonations[0].qftotalcollected, + contributorsCount: +contributorsCount?.contributorsCount, + }); + }); +} diff --git a/src/resolvers/qAccResolver.ts b/src/resolvers/qAccResolver.ts index 6ffabf1d4..5f1bd075c 100644 --- a/src/resolvers/qAccResolver.ts +++ b/src/resolvers/qAccResolver.ts @@ -26,6 +26,18 @@ class ProjectUserRecordAmounts { qfTotalDonationAmount: number; } +@ObjectType() +class QaccStat { + @Field(_type => Float) + totalCollected: number; + + @Field(_type => Float) + qfTotalCollected: number; + + @Field(_type => Int) + contributorsCount: number; +} + @ObjectType() class UnusedCapResponse { @Field(_type => Float) @@ -118,4 +130,14 @@ export class QAccResolver { return response; } + + @Query(_returns => QaccStat) + async qAccStat() { + const state = await qAccService.getQAccStat(); + return { + totalCollected: state.totalCollected, + qfTotalCollected: state.qfTotalCollected, + contributorsCount: state.totalContributors, + }; + } } diff --git a/src/services/qAccService.ts b/src/services/qAccService.ts index 0253711c6..97c3d3155 100644 --- a/src/services/qAccService.ts +++ b/src/services/qAccService.ts @@ -14,6 +14,7 @@ import { GITCOIN_PASSPORT_MIN_VALID_ANALYSIS_SCORE, GITCOIN_PASSPORT_MIN_VALID_SCORER_SCORE, } from '../constants/gitcoin'; +import { Donation, DONATION_STATUS } from '../entities/donation'; const getEaProjectRoundRecord = async ({ projectId, @@ -270,8 +271,49 @@ const validDonationAmountBasedOnKYCAndScore = async ({ return true; }; +const getQAccStat = async (): Promise<{ + totalCollected: number; + qfTotalCollected: number; + totalContributors: number; +}> => { + const [qfTotalCollected, totalCollected, totalContributors] = + await Promise.all([ + Donation.createQueryBuilder('donation') + .select('COALESCE(sum(donation.amount), 0)', 'total_qf_collected') + .where('donation.status = :status', { + status: DONATION_STATUS.VERIFIED, + }) + .andWhere('donation."qfRoundId" IS NOT NULL') + .cache('qf_total_collected_donation', 1000) + .getRawOne(), + + Donation.createQueryBuilder('donation') + .select('COALESCE(sum(donation.amount), 0)', 'total_collected') + .where('donation.status = :status', { + status: DONATION_STATUS.VERIFIED, + }) + .cache('total_collected_donation', 1000) + .getRawOne(), + + Donation.createQueryBuilder('donation') + .select('count(distinct donation."userId")', 'total_contributors') + .where('donation.status = :status', { + status: DONATION_STATUS.VERIFIED, + }) + .cache('total_contributors', 1000) + .getRawOne(), + ]); + + return { + totalCollected: totalCollected.total_collected, + qfTotalCollected: qfTotalCollected.total_qf_collected, + totalContributors: totalContributors.total_contributors, + }; +}; + export default { getQAccDonationCap, validDonationAmountBasedOnKYCAndScore, getUserRemainedCapBasedOnGitcoinScore, + getQAccStat, }; diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index e5092380b..615de4b75 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2186,3 +2186,13 @@ export const userCaps = ` } } `; + +export const qAccStat = ` + query { + qAccStat { + totalCollected + qfTotalCollected + contributorsCount + } + } +`;