From 6519399a47ac8464700286557b387d7402c83ad5 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Mon, 2 Oct 2023 15:04:50 +0330 Subject: [PATCH] Implement doesDonatedToProjectInQfRound webservice related #1138 --- src/repositories/donationRepository.test.ts | 193 ++++++++++++++++++ src/repositories/donationRepository.ts | 31 +++ src/repositories/qfRoundRepository.ts | 1 + src/resolvers/donationResolver.test.ts | 206 +++++++++++++++++++- src/resolvers/donationResolver.ts | 18 +- test/graphqlQueries.ts | 14 ++ 6 files changed, 457 insertions(+), 6 deletions(-) diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index 89cfba913..ca9136615 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -20,6 +20,7 @@ import { findDonationsByTransactionId, findStableCoinDonationsWithoutPrice, getPendingDonationsIds, + isVerifiedDonationExistsInQfRound, sumDonationValueUsd, sumDonationValueUsdForQfRound, } from './donationRepository'; @@ -64,6 +65,10 @@ describe( describe('countUniqueDonors() test cases', countUniqueDonorsTestCases); describe('sumDonationValueUsd() test cases', sumDonationValueUsdTestCases); describe('estimatedMatching() test cases', estimatedMatchingTestCases); +describe( + 'isVerifiedDonationExistsInQfRound() test cases', + isVerifiedDonationExistsInQfRoundTestCases, +); function fillQfRoundDonationsUserScoresTestCases() { let qfRound: QfRound; @@ -1262,3 +1267,191 @@ function sumDonationValueUsdTestCases() { assert.equal(donationSum, valueUsd1 + valueUsd2 + valueUsd3); }); } + +function isVerifiedDonationExistsInQfRoundTestCases() { + it('should return true when there is a verified donation', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: qfRound.id, + userId: donor.id, + }); + assert.isTrue(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when there is a non-verified donation', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'pending', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: qfRound.id, + userId: donor.id, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when sending invalid userId', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: qfRound.id, + userId: 999999, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when sending invalid projectId', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: 9999900, + qfRoundId: qfRound.id, + userId: donor.id, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when sending invalid qfRoundId', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + }); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + status: 'verified', + qfRoundId: qfRound.id, + }, + donor.id, + project.id, + ); + const doesExist = await isVerifiedDonationExistsInQfRound({ + projectId: project.id, + qfRoundId: 9999999, + userId: donor.id, + }); + assert.isFalse(doesExist); + + qfRound.isActive = false; + await qfRound.save(); + }); +} diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 9bf2825fc..90329f1a0 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -7,6 +7,7 @@ import moment from 'moment'; import { ProjectEstimatedMatchingView } from '../entities/ProjectEstimatedMatchingView'; import { AppDataSource } from '../orm'; import { getProjectDonationsSqrtRootSum } from './qfRoundRepository'; +import { logger } from '../utils/logger'; export const fillQfRoundDonationsUserScores = async (): Promise => { await Donation.query(` @@ -379,3 +380,33 @@ export async function sumDonationValueUsd(projectId: number): Promise { return result[0]?.sumVerifiedDonations || 0; } + +export async function isVerifiedDonationExistsInQfRound(params: { + qfRoundId: number; + projectId: number; + userId: number; +}): Promise { + try { + const result = await Donation.query( + ` + SELECT EXISTS ( + SELECT 1 + FROM donation + WHERE + status = 'verified' AND + "qfRoundId" = $1 AND + "projectId" = $2 AND + "userId" = $3 + ) AS exists; + `, + [params.qfRoundId, params.projectId, params.userId], + ); + return result?.[0]?.exists || false; + } catch (err) { + logger.error( + 'Error executing the query in isVerifiedDonationExists() function', + err, + ); + return false; + } +} diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index b0b3d9c06..0f66f3c1f 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -1,5 +1,6 @@ import { QfRound } from '../entities/qfRound'; import { AppDataSource } from '../orm'; +import { logger } from '../utils/logger'; const qfRoundEstimatedMatchingParamsCacheDuration = Number( process.env.QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION || 60000, diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index ffb5711c6..67179b555 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -16,7 +16,6 @@ import { import axios from 'axios'; import { errorMessages } from '../utils/errorMessages'; import { Donation, DONATION_STATUS } from '../entities/donation'; -import sinon from 'sinon'; import { fetchDonationsByUserIdQuery, fetchDonationsByDonorQuery, @@ -31,6 +30,7 @@ import { fetchTotalDonationsPerCategoryPerDate, fetchRecentDonations, fetchTotalDonationsNumberPerDateRange, + doesDonatedToProjectInQfRoundQuery, } from '../../test/graphqlQueries'; import { NETWORK_IDS } from '../provider'; import { User } from '../entities/user'; @@ -69,6 +69,10 @@ describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); describe('totalDonationsUsdAmount() test cases', donationsUsdAmountTestCases); describe('totalDonorsCountPerDate() test cases', donorsCountPerDateTestCases); +describe( + 'doesDonatedToProjectInQfRound() test cases', + doesDonatedToProjectInQfRoundTestCases, +); describe( 'totalDonationsNumberPerDate() test cases', totalDonationsNumberPerDateTestCases, @@ -77,7 +81,7 @@ describe( 'totalDonationsPerCategoryPerDate() test cases', totalDonationsPerCategoryPerDateTestCases, ); -describe('resetDonations() test cases', recentDonationsTestCases); +describe('recentDonations() test cases', recentDonationsTestCases); // // describe('tokens() test cases', tokensTestCases); @@ -209,6 +213,204 @@ function donorsCountPerDateTestCases() { }); } +function doesDonatedToProjectInQfRoundTestCases() { + it('should return true when there is verified donation', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: user.id, + qfRoundId: qfRound.id, + }, + }); + assert.isTrue(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation is non-verified', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: user.id, + qfRoundId: qfRound.id, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation projectId is invalid', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: 99999, + userId: user.id, + qfRoundId: qfRound.id, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation qfRoundId is invalid', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: user.id, + qfRoundId: 99999, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); + it('should return false when donation userId is invalid', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const qfRound = await QfRound.create({ + isActive: true, + name: new Date().toString(), + allocatedFund: 100, + minimumPassportScore: 12, + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }).save(); + project.qfRounds = [qfRound]; + await project.save(); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.PENDING, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + qfRoundId: qfRound.id, + }), + user.id, + project.id, + ); + + const result = await axios.post(graphqlUrl, { + query: doesDonatedToProjectInQfRoundQuery, + variables: { + projectId: project.id, + userId: 99999, + qfRoundId: qfRound.id, + }, + }); + assert.isFalse(result.data.data.doesDonatedToProjectInQfRound); + + qfRound.isActive = false; + await qfRound.save(); + }); +} + function donationsUsdAmountTestCases() { it('should return total usd amount for donations made in a time range', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index f103e1b8f..6d95fc3cd 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -45,8 +45,6 @@ import { setUserAsReferrer, } from '../repositories/userRepository'; import { - countUniqueDonors, - countUniqueDonorsForRound, donationsNumberPerDateRange, donationsTotalAmountPerDateRange, donationsTotalAmountPerDateRangeByMonth, @@ -55,8 +53,7 @@ import { donorsCountPerDateByMonthAndYear, findDonationById, getRecentDonations, - sumDonationValueUsd, - sumDonationValueUsdForQfRound, + isVerifiedDonationExistsInQfRound, } from '../repositories/donationRepository'; import { sleep } from '../utils/utils'; import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; @@ -842,4 +839,17 @@ export class DonationResolver { throw e; } } + + @Query(() => Boolean) + async doesDonatedToProjectInQfRound( + @Arg('projectId', _ => Int) projectId: number, + @Arg('qfRoundId', _ => Int) qfRoundId: number, + @Arg('userId', _ => Int) userId: number, + ): Promise { + return isVerifiedDonationExistsInQfRound({ + projectId, + qfRoundId, + userId, + }); + } } diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index e041a1625..7836308ad 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2016,3 +2016,17 @@ export const getProjectUserInstantPowerQuery = ` } } `; + +export const doesDonatedToProjectInQfRoundQuery = ` + query ( + $projectId: Int!, + $qfRoundId: Int!, + $userId: Int! + ) { + doesDonatedToProjectInQfRound( + projectId: $projectId + qfRoundId: $qfRoundId + userId: $userId + ) + } +`;