From 274819c41239f9bdd4e883eaab5aad8efbc00af8 Mon Sep 17 00:00:00 2001 From: Ramin Date: Fri, 12 Apr 2024 16:43:49 +0530 Subject: [PATCH 001/406] add activeQfRoundId to sortingBy InstantBoosting --- src/resolvers/projectResolver.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index ad2f6726f..6af8c1a95 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -727,7 +727,8 @@ export class ProjectResolver { if ( sortingBy === SortingField.ActiveQfRoundRaisedFunds || - sortingBy === SortingField.EstimatedMatching + sortingBy === SortingField.EstimatedMatching || + sortingBy === SortingField.InstantBoosting ) { activeQfRoundId = (await findActiveQfRound())?.id; } From 992e32c67c87672039cf74d90925a6e6273762e4 Mon Sep 17 00:00:00 2001 From: Ramin Date: Fri, 12 Apr 2024 16:44:48 +0530 Subject: [PATCH 002/406] add orderBy totalDonations and totalReactions --- src/repositories/projectRepository.ts | 37 ++++++++++++++++++--------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 1b340e0d9..edabc7044 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -157,6 +157,15 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { } // query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); + if (activeQfRoundId) { + query.leftJoin( + 'project.projectEstimatedMatchingView', + 'projectEstimatedMatchingView', + 'projectEstimatedMatchingView.qfRoundId = :qfRoundId', + { qfRoundId: activeQfRoundId }, + ); + } + switch (sortingBy) { case SortingField.MostFunded: query.orderBy('project.totalDonations', OrderDirection.DESC); @@ -186,6 +195,12 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { ); break; case SortingField.InstantBoosting: + if (activeQfRoundId) { + query.addSelect([ + 'projectEstimatedMatchingView.sumValueUsd', + 'projectEstimatedMatchingView.qfRoundId', + ]); + } query .orderBy(`project.verified`, OrderDirection.DESC) .addOrderBy( @@ -193,16 +208,20 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { OrderDirection.DESC, 'NULLS LAST', ); + if (activeQfRoundId) { + query.addOrderBy( + 'projectEstimatedMatchingView.sumValueUsd', + OrderDirection.DESC, + 'NULLS LAST', + ); + } else { + query.addOrderBy('project.totalDonations', OrderDirection.DESC); + } + query.addOrderBy('project.totalReactions', OrderDirection.DESC); break; case SortingField.ActiveQfRoundRaisedFunds: if (activeQfRoundId) { query - .leftJoin( - 'project.projectEstimatedMatchingView', - 'projectEstimatedMatchingView', - 'projectEstimatedMatchingView.qfRoundId = :qfRoundId', - { qfRoundId: activeQfRoundId }, - ) .addSelect([ 'projectEstimatedMatchingView.sumValueUsd', 'projectEstimatedMatchingView.qfRoundId', @@ -218,12 +237,6 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { case SortingField.EstimatedMatching: if (activeQfRoundId) { query - .leftJoin( - 'project.projectEstimatedMatchingView', - 'projectEstimatedMatchingView', - 'projectEstimatedMatchingView.qfRoundId = :qfRoundId', - { qfRoundId: activeQfRoundId }, - ) .addSelect([ 'projectEstimatedMatchingView.sqrtRootSum', 'projectEstimatedMatchingView.qfRoundId', From 48019ec70ada5e8e0ea1e564be94e4e51885793a Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Mon, 15 Apr 2024 05:06:29 +0100 Subject: [PATCH 003/406] feat: add getRecurringDonationStats resolver --- .../recurringDonationResolver.test.ts | 223 ++++++++++++++++++ src/resolvers/recurringDonationResolver.ts | 84 ++++++- .../validators/graphqlQueryValidators.ts | 11 + test/graphqlQueries.ts | 17 ++ test/testUtils.ts | 1 + 5 files changed, 335 insertions(+), 1 deletion(-) diff --git a/src/resolvers/recurringDonationResolver.test.ts b/src/resolvers/recurringDonationResolver.test.ts index 8c1e3a28a..3c0d1da44 100644 --- a/src/resolvers/recurringDonationResolver.test.ts +++ b/src/resolvers/recurringDonationResolver.test.ts @@ -17,6 +17,7 @@ import { fetchRecurringDonationsByUserIdQuery, updateRecurringDonationQuery, updateRecurringDonationStatusMutation, + fetchRecurringDonationStatsQuery, } from '../../test/graphqlQueries'; import { errorMessages } from '../utils/errorMessages'; import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; @@ -45,6 +46,11 @@ describe( updateRecurringDonationStatusTestCases, ); +describe( + 'getRecurringDonationStatsTestCases test cases', + getRecurringDonationStatsTestCases, +); + function createRecurringDonationTestCases() { it('should create recurringDonation successfully', async () => { const projectOwner = await saveUserDirectlyToDb( @@ -2114,3 +2120,220 @@ function updateRecurringDonationStatusTestCases() { ); }); } + +function getRecurringDonationStatsTestCases() { + it('should return the correct stats for the given date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: new Date().toISOString().slice(0, 8) + '01', + endDate: new Date().toISOString().slice(0, 8) + '30', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 7); + assert.equal(stats.totalStreamedUsdValue, 500); + }); + + it('should return the correct stats for the given date range and currency', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + currency: 'DAI', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + currency: 'USDT', + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: new Date().toISOString().slice(0, 8) + '01', + endDate: new Date().toISOString().slice(0, 8) + '30', + currency: 'USDT', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 4); + assert.equal(stats.totalStreamedUsdValue, 600); + }); + + it('should return the correct stats for the given date range and status', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + status: RECURRING_DONATION_STATUS.PENDING, + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: new Date().toISOString().slice(0, 8) + '01', + endDate: new Date().toISOString().slice(0, 8) + '30', + status: RECURRING_DONATION_STATUS.ACTIVE, + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 8); + assert.equal(stats.totalStreamedUsdValue, 1500); + }); + + it('should return the correct stats for the given date range and status and currency', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + status: RECURRING_DONATION_STATUS.PENDING, + currency: 'USDT', + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: new Date().toISOString().slice(0, 8) + '01', + endDate: new Date().toISOString().slice(0, 8) + '30', + status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 1); + assert.equal(stats.totalStreamedUsdValue, 800); + }); + + it('should return the correct stats for the given date range of the current month', async () => { + const currentDate = new Date().getTime(); + + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(currentDate - 1000 * 60 * 60 * 24 * 35), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(currentDate - 1000 * 60 * 60 * 24 * 10), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: new Date().toISOString().slice(0, 8) + '01', + endDate: new Date().toISOString().slice(0, 8) + '30', + }, + }); + + const stats = result.data.data.getRecurringDonationStats; + assert.equal(stats.activeRecurringDonationsCount, 9); + assert.equal(stats.totalStreamedUsdValue, 2100); + }); + + it('should return an error for the given an empty date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: '', + endDate: '', + }, + }); + + assert.isNotNull(result.data.errors); + }); + + it('should return an error for the given an invalid date range', async () => { + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 400, + status: RECURRING_DONATION_STATUS.ACTIVE, + createdAt: new Date(), + }, + }); + await saveRecurringDonationDirectlyToDb({ + donationData: { + donorId: donor.id, + totalUsdStreamed: 100, + createdAt: new Date(), + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: 'invalid date', + endDate: 'invalid date', + }, + }); + + assert.isNotNull(result.data.errors); + }); +} diff --git a/src/resolvers/recurringDonationResolver.ts b/src/resolvers/recurringDonationResolver.ts index 5245b64c6..b2bc98a8e 100644 --- a/src/resolvers/recurringDonationResolver.ts +++ b/src/resolvers/recurringDonationResolver.ts @@ -39,13 +39,13 @@ import { import { detectAddressChainType } from '../utils/networks'; import { logger } from '../utils/logger'; import { + getRecurringDonationStatsArgsValidator, updateDonationQueryValidator, validateWithJoiSchema, } from '../utils/validators/graphqlQueryValidators'; import { sleep } from '../utils/utils'; import SentryLogger from '../sentryLogger'; import { updateRecurringDonationStatusWithNetwork } from '../services/recurringDonationService'; - @InputType() class RecurringDonationSortBy { @Field(_type => RecurringDonationSortField) @@ -153,6 +153,28 @@ class UserRecurringDonations { totalCount: number; } +@Service() +@ArgsType() +class GetRecurringDonationStatsArgs { + @Field(_type => String) + beginDate: string; + + @Field(_type => String) + endDate: string; + + @Field(_type => String, { nullable: true }) + currency?: string; +} + +@ObjectType() +class RecurringDonationStats { + @Field(_type => Int) + activeRecurringDonationsCount: number; + + @Field(_type => Int) + totalStreamedUsdValue: number; +} + @Resolver(_of => AnchorContractAddress) export class RecurringDonationResolver { @Mutation(_returns => RecurringDonation, { nullable: true }) @@ -536,4 +558,64 @@ export class RecurringDonationResolver { throw e; } } + + @Query(_returns => RecurringDonationStats) + async getRecurringDonationStats( + @Args() { beginDate, endDate, currency }: GetRecurringDonationStatsArgs, + ): Promise { + try { + // Validate input arguments using Joi schema + validateWithJoiSchema( + { beginDate, endDate }, + getRecurringDonationStatsArgsValidator, + ); + + // Query to calculate total streamed USD value + const totalStreamedUsdValueQuery = RecurringDonation.createQueryBuilder( + 'recurring_donation', + ) + .select( + 'SUM(recurring_donation.totalUsdStreamed)', + 'totalStreamedUsdValue', + ) + .where( + `recurring_donation.createdAt >= :beginDate AND recurring_donation.createdAt <= :endDate`, + { beginDate, endDate }, + ); + + // Query to calculate active recurring donations count + const activeRecurringDonationsCountQuery = + RecurringDonation.createQueryBuilder('recurring_donation').where( + `recurring_donation.createdAt >= :beginDate AND recurring_donation.createdAt <= :endDate AND recurring_donation.status = 'active'`, + { beginDate, endDate }, + ); + + // Add currency filter if provided + if (currency) { + totalStreamedUsdValueQuery.andWhere( + `recurring_donation.currency = :currency`, + { currency }, + ); + activeRecurringDonationsCountQuery.andWhere( + `recurring_donation.currency = :currency`, + { currency }, + ); + } + + const [activeRecurringDonationsCount, totalStreamedUsdValue] = + await Promise.all([ + activeRecurringDonationsCountQuery.getCount(), + totalStreamedUsdValueQuery.getRawOne(), + ]); + + return { + activeRecurringDonationsCount: activeRecurringDonationsCount || 0, + totalStreamedUsdValue: totalStreamedUsdValue.totalStreamedUsdValue || 0, + }; + } catch (e) { + SentryLogger.captureException(e); + logger.error('getRecurringDonationStats() error ', e); + throw e; + } + } } diff --git a/src/utils/validators/graphqlQueryValidators.ts b/src/utils/validators/graphqlQueryValidators.ts index b283dad70..ee6958210 100644 --- a/src/utils/validators/graphqlQueryValidators.ts +++ b/src/utils/validators/graphqlQueryValidators.ts @@ -152,6 +152,17 @@ export const updateDonationQueryValidator = Joi.object({ status: Joi.string().valid(DONATION_STATUS.VERIFIED, DONATION_STATUS.FAILED), }); +export const getRecurringDonationStatsArgsValidator = Joi.object({ + beginDate: Joi.string().pattern(resourcePerDateRegex).messages({ + 'string.base': errorMessages.INVALID_FROM_DATE, + 'string.pattern.base': errorMessages.INVALID_DATE_FORMAT, + }), + endDate: Joi.string().pattern(resourcePerDateRegex).messages({ + 'string.base': errorMessages.INVALID_FROM_DATE, + 'string.pattern.base': errorMessages.INVALID_DATE_FORMAT, + }), +}); + export const createProjectVerificationRequestValidator = Joi.object({ slug: Joi.string().required(), }); diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 83c992592..03a5c5b58 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -2336,3 +2336,20 @@ export const updateRecurringDonationQuery = ` } } `; + +export const fetchRecurringDonationStatsQuery = ` + query ( + $beginDate: String! + $endDate: String! + $currency: String + ) { + getRecurringDonationStats( + beginDate: $beginDate + endDate: $endDate + currency: $currency + ) { + totalStreamedUsdValue, + activeRecurringDonationsCount, + } + } +`; diff --git a/test/testUtils.ts b/test/testUtils.ts index 58ba6b75e..17fed9aa8 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1958,6 +1958,7 @@ export const saveRecurringDonationDirectlyToDb = async (params?: { donorId, projectId, anchorContractAddressId, + createdAt: params?.donationData?.createdAt || moment(), }).save(); }; From af8d37e8b595217a5c0b128957665f3b5cda69c9 Mon Sep 17 00:00:00 2001 From: Ramin Date: Mon, 15 Apr 2024 15:00:27 +0330 Subject: [PATCH 004/406] fix filtering by QF --- src/repositories/projectRepository.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index edabc7044..ec7f1aec4 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -157,7 +157,10 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { } // query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); - if (activeQfRoundId) { + const isFilterByQF = + !!filters?.find(f => f === FilterField.ActiveQfRound) && activeQfRoundId; + + if (isFilterByQF) { query.leftJoin( 'project.projectEstimatedMatchingView', 'projectEstimatedMatchingView', @@ -194,8 +197,8 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { 'NULLS LAST', ); break; - case SortingField.InstantBoosting: - if (activeQfRoundId) { + case SortingField.InstantBoosting: // This is our default sorting + if (isFilterByQF) { query.addSelect([ 'projectEstimatedMatchingView.sumValueUsd', 'projectEstimatedMatchingView.qfRoundId', @@ -208,7 +211,7 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { OrderDirection.DESC, 'NULLS LAST', ); - if (activeQfRoundId) { + if (isFilterByQF) { query.addOrderBy( 'projectEstimatedMatchingView.sumValueUsd', OrderDirection.DESC, From 641fc9fbec0648951e0847edea5ab87c85e66682 Mon Sep 17 00:00:00 2001 From: Ramin Date: Mon, 15 Apr 2024 16:14:07 +0330 Subject: [PATCH 005/406] remove qfRounds joins for non qf round filters --- src/repositories/projectRepository.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index ec7f1aec4..f5eb813e3 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -119,7 +119,11 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { `project.statusId = ${ProjStatus.active} AND project.reviewStatus = :reviewStatus`, { reviewStatus: ReviewStatus.Listed }, ); - if (qfRoundId || activeQfRoundId) { + + const isFilterByQF = + !!filters?.find(f => f === FilterField.ActiveQfRound) && activeQfRoundId; + + if (qfRoundId || isFilterByQF) { query.innerJoinAndSelect( 'project.qfRounds', 'qf_rounds', @@ -157,9 +161,6 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { } // query = ProjectResolver.addUserReaction(query, connectedWalletUserId, user); - const isFilterByQF = - !!filters?.find(f => f === FilterField.ActiveQfRound) && activeQfRoundId; - if (isFilterByQF) { query.leftJoin( 'project.projectEstimatedMatchingView', From 3e1f67c63387afc56528ea19b8d52d5f8994fb9d Mon Sep 17 00:00:00 2001 From: Ramin Date: Mon, 15 Apr 2024 16:21:14 +0330 Subject: [PATCH 006/406] add some temp logs --- src/resolvers/projectResolver.allProject.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 5d1ae0678..d50f6837e 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -504,6 +504,15 @@ function allProjectsTestCases() { }, }); + // eslint-disable-next-line no-console + console.log('---------------------'); + // eslint-disable-next-line no-console + console.log(result.data.data.allProjects.projects); + // eslint-disable-next-line no-console + console.log(result.data); + // eslint-disable-next-line no-console + console.log('---------------------'); + let projects = result.data.data.allProjects.projects; assert.equal(projects[0].id, project3.id); From 39c4f77696ae35e18830bc237c4c578694d236cf Mon Sep 17 00:00:00 2001 From: Ramin Date: Mon, 15 Apr 2024 17:29:20 +0330 Subject: [PATCH 007/406] remove temp logs --- src/resolvers/projectResolver.allProject.test.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index d50f6837e..5d1ae0678 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -504,15 +504,6 @@ function allProjectsTestCases() { }, }); - // eslint-disable-next-line no-console - console.log('---------------------'); - // eslint-disable-next-line no-console - console.log(result.data.data.allProjects.projects); - // eslint-disable-next-line no-console - console.log(result.data); - // eslint-disable-next-line no-console - console.log('---------------------'); - let projects = result.data.data.allProjects.projects; assert.equal(projects[0].id, project3.id); From 61d028063d87d571203a09540343501317b6f02d Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Fri, 19 Apr 2024 02:32:35 +0100 Subject: [PATCH 008/406] fix: changes test cases of recuring donations stats - create recored with createdAt field in past so test result won't be related to other endpoints test cases --- .../recurringDonationResolver.test.ts | 108 ++++++++++-------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/src/resolvers/recurringDonationResolver.test.ts b/src/resolvers/recurringDonationResolver.test.ts index 1e46fc805..6786b04cd 100644 --- a/src/resolvers/recurringDonationResolver.test.ts +++ b/src/resolvers/recurringDonationResolver.test.ts @@ -1674,7 +1674,6 @@ function updateRecurringDonationByIdTestCases() { ); }); } - function recurringDonationsByProjectIdTestCases() { it('should sort by the createdAt DESC', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); @@ -2824,41 +2823,50 @@ function updateRecurringDonationStatusTestCases() { } function getRecurringDonationStatsTestCases() { - it('should return the correct stats for the given date range', async () => { + const lastYear = new Date().getFullYear() - 1; + const beginDate = `${lastYear}-01-01`; + const endDate = `${lastYear}-03-01`; + + it(`should return the correct stats for a given date range (${beginDate} to ${endDate})`, async () => { const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, totalUsdStreamed: 400, + createdAt: new Date(`${lastYear}-01-02`), }, }); + await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, totalUsdStreamed: 100, + createdAt: new Date(`${lastYear}-01-24`), }, }); + // we are querying from January 1st of last year to the 1st of March of last year const result = await axios.post(graphqlUrl, { query: fetchRecurringDonationStatsQuery, variables: { - beginDate: new Date().toISOString().slice(0, 8) + '01', - endDate: new Date().toISOString().slice(0, 8) + '30', + beginDate, + endDate, }, }); const stats = result.data.data.getRecurringDonationStats; - assert.equal(stats.activeRecurringDonationsCount, 7); + assert.equal(stats.activeRecurringDonationsCount, 0); assert.equal(stats.totalStreamedUsdValue, 500); }); - it('should return the correct stats for the given date range and currency', async () => { + it(`should return the correct stats for a given date range (${beginDate} -> ${endDate}) and currency`, async () => { const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, totalUsdStreamed: 400, currency: 'DAI', + createdAt: new Date(`${lastYear}-01-01`), }, }); await saveRecurringDonationDirectlyToDb({ @@ -2866,90 +2874,81 @@ function getRecurringDonationStatsTestCases() { donorId: donor.id, totalUsdStreamed: 100, currency: 'USDT', + createdAt: new Date(`${lastYear}-02-01`), }, }); const result = await axios.post(graphqlUrl, { query: fetchRecurringDonationStatsQuery, variables: { - beginDate: new Date().toISOString().slice(0, 8) + '01', - endDate: new Date().toISOString().slice(0, 8) + '30', + beginDate, + endDate, currency: 'USDT', }, }); const stats = result.data.data.getRecurringDonationStats; - assert.equal(stats.activeRecurringDonationsCount, 4); + assert.equal(stats.activeRecurringDonationsCount, 0); assert.equal(stats.totalStreamedUsdValue, 600); }); - it('should return the correct stats for the given date range and status', async () => { - const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + it(`should return the correct stats for a given date range (${beginDate} -> ${endDate}) with correct active count`, async () => { + const donor1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donor2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveRecurringDonationDirectlyToDb({ donationData: { - donorId: donor.id, + donorId: donor1.id, totalUsdStreamed: 400, status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + createdAt: new Date(`${lastYear}-01-01`), }, }); await saveRecurringDonationDirectlyToDb({ donationData: { - donorId: donor.id, + donorId: donor2.id, totalUsdStreamed: 100, - status: RECURRING_DONATION_STATUS.PENDING, - }, - }); - - const result = await axios.post(graphqlUrl, { - query: fetchRecurringDonationStatsQuery, - variables: { - beginDate: new Date().toISOString().slice(0, 8) + '01', - endDate: new Date().toISOString().slice(0, 8) + '30', status: RECURRING_DONATION_STATUS.ACTIVE, + currency: 'DAI', + createdAt: new Date(`${lastYear}-02-01`), }, }); - - const stats = result.data.data.getRecurringDonationStats; - assert.equal(stats.activeRecurringDonationsCount, 8); - assert.equal(stats.totalStreamedUsdValue, 1500); - }); - - it('should return the correct stats for the given date range and status and currency', async () => { - const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); await saveRecurringDonationDirectlyToDb({ donationData: { - donorId: donor.id, - totalUsdStreamed: 400, - status: RECURRING_DONATION_STATUS.ACTIVE, + donorId: donor1.id, + totalUsdStreamed: 200, currency: 'DAI', + createdAt: new Date(`${lastYear}-02-01`), }, }); await saveRecurringDonationDirectlyToDb({ donationData: { - donorId: donor.id, + donorId: donor2.id, totalUsdStreamed: 100, - status: RECURRING_DONATION_STATUS.PENDING, + status: RECURRING_DONATION_STATUS.ACTIVE, currency: 'USDT', + createdAt: new Date(`${lastYear}-02-01`), }, }); const result = await axios.post(graphqlUrl, { query: fetchRecurringDonationStatsQuery, variables: { - beginDate: new Date().toISOString().slice(0, 8) + '01', - endDate: new Date().toISOString().slice(0, 8) + '30', + beginDate, + endDate, status: RECURRING_DONATION_STATUS.ACTIVE, currency: 'DAI', }, }); const stats = result.data.data.getRecurringDonationStats; - assert.equal(stats.activeRecurringDonationsCount, 1); - assert.equal(stats.totalStreamedUsdValue, 800); + assert.equal(stats.activeRecurringDonationsCount, 2); + assert.equal(stats.totalStreamedUsdValue, 1100); }); - it('should return the correct stats for the given date range of the current month', async () => { - const currentDate = new Date().getTime(); + it('should return the correct stats for the given date range where beginDate', async () => { + const lastYear15thOfJanuary = new Date(`${lastYear}-01-15T09:00:00`); const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); await saveRecurringDonationDirectlyToDb({ @@ -2957,28 +2956,43 @@ function getRecurringDonationStatsTestCases() { donorId: donor.id, totalUsdStreamed: 400, status: RECURRING_DONATION_STATUS.ACTIVE, - createdAt: new Date(currentDate - 1000 * 60 * 60 * 24 * 35), + createdAt: lastYear15thOfJanuary, }, }); await saveRecurringDonationDirectlyToDb({ donationData: { donorId: donor.id, totalUsdStreamed: 100, - createdAt: new Date(currentDate - 1000 * 60 * 60 * 24 * 10), + createdAt: lastYear15thOfJanuary, + }, + }); + + const result = await axios.post(graphqlUrl, { + query: fetchRecurringDonationStatsQuery, + variables: { + beginDate: `${lastYear}-01-15T09:00:00`, + endDate: `${lastYear}-01-15T09:00:00`, }, }); + const stats = result.data.data.getRecurringDonationStats; + + assert.equal(stats.activeRecurringDonationsCount, 1); + assert.equal(stats.totalStreamedUsdValue, 500); + }); + + it(`should return empty stats for the given date range where beginDate is same as endDate`, async () => { const result = await axios.post(graphqlUrl, { query: fetchRecurringDonationStatsQuery, variables: { - beginDate: new Date().toISOString().slice(0, 8) + '01', - endDate: new Date().toISOString().slice(0, 8) + '30', + beginDate: `${lastYear}-04-01`, + endDate: `${lastYear}-05-01`, }, }); const stats = result.data.data.getRecurringDonationStats; - assert.equal(stats.activeRecurringDonationsCount, 9); - assert.equal(stats.totalStreamedUsdValue, 2100); + assert.equal(stats.activeRecurringDonationsCount, 0); + assert.equal(stats.totalStreamedUsdValue, 0); }); it('should return an error for the given an empty date range', async () => { From 95997e004f1af9c405d6f5b540a6cd495aff496f Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Sun, 21 Apr 2024 16:53:49 +0330 Subject: [PATCH 009/406] Fix projectActualserviceView --- migration/1713700147145-project_actual_matchin_view_15.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/migration/1713700147145-project_actual_matchin_view_15.ts b/migration/1713700147145-project_actual_matchin_view_15.ts index 248f24bb6..255d35859 100644 --- a/migration/1713700147145-project_actual_matchin_view_15.ts +++ b/migration/1713700147145-project_actual_matchin_view_15.ts @@ -21,10 +21,11 @@ export class ProjectActualMatchinView151713700147145 STRING_AGG(DISTINCT CONCAT(pa."networkId", '-', pa."address"), ', ') AS "networkAddresses" FROM public.project p - CROSS JOIN public.qf_round qr + INNER JOIN project_qf_rounds_qf_round pqrq ON pqrq."projectId" = p.id INNER JOIN public."user" u on p."adminUserId" = u.id + INNER JOIN public.qf_round qr on qr.id = pqrq."qfRoundId" LEFT JOIN project_address pa ON pa."projectId" = p.id AND pa."networkId" = ANY(qr."eligibleNetworks") AND pa."isRecipient" = true - group by + group by p.id, u.email, qr.id From e821acb7b0dbdbb5056d652584e79ca5ed29b127 Mon Sep 17 00:00:00 2001 From: Ramin Date: Sun, 21 Apr 2024 16:59:24 +0330 Subject: [PATCH 010/406] fix stream balance depleted issue (#1496) Co-authored-by: mohammadranjbarz --- .../notifications/NotificationCenterAdapter.ts | 2 +- src/services/recurringDonationService.ts | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index a7cbe71b9..6d4d67df2 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -64,7 +64,7 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { isEnded: boolean; networkName: string; }): Promise { - logger.debug('userSuperTokensCritical', { params }); + logger.debug('userSuperTokensCritical has been called', { params }); const { eventName, tokenSymbol, project, user, isEnded, networkName } = params; const { email, walletAddress } = user; diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 3a160d4c7..537fc55e8 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -5,13 +5,18 @@ import { getNotificationAdapter, getSuperFluidAdapter, } from '../adapters/adaptersFactory'; -import { DONATION_STATUS, Donation } from '../entities/donation'; +import { Donation, DONATION_STATUS } from '../entities/donation'; import { RECURRING_DONATION_STATUS, RecurringDonation, } from '../entities/recurringDonation'; import { Token } from '../entities/token'; -import { getProvider, NETWORK_IDS, superTokensToToken } from '../provider'; +import { + getNetworkNameById, + getProvider, + NETWORK_IDS, + superTokensToToken, +} from '../provider'; import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; import { findProjectById } from '../repositories/projectRepository'; import { @@ -31,6 +36,7 @@ import { calculateGivbackFactor } from './givbackService'; import { updateUserTotalDonated, updateUserTotalReceived } from './userService'; import config from '../config'; import { User } from '../entities/user'; +import { NOTIFICATIONS_EVENT_NAMES } from '../analytics/analytics'; // Initially it will only be monthly data export const priceDisplay = 'month'; @@ -78,6 +84,14 @@ export const createRelatedDonationsToStream = async ( recurringDonation.finished = true; recurringDonation.status = RECURRING_DONATION_STATUS.ENDED; await recurringDonation.save(); + await getNotificationAdapter().userSuperTokensCritical({ + user: recurringDonation.donor, + eventName: NOTIFICATIONS_EVENT_NAMES.SUPER_TOKENS_BALANCE_DEPLETED, + tokenSymbol: recurringDonation.currency, + isEnded: recurringDonation.finished, + project: recurringDonation.project, + networkName: getNetworkNameById(recurringDonation.networkId), + }); } const project = await findProjectById(recurringDonation.projectId); From 2d9f398881af682acc0b6b84c786bb221e23d4b9 Mon Sep 17 00:00:00 2001 From: Ramin Date: Tue, 23 Apr 2024 02:37:22 +0330 Subject: [PATCH 011/406] rebuild --- src/services/cronJobs/checkProjectVerificationStatus.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/cronJobs/checkProjectVerificationStatus.ts b/src/services/cronJobs/checkProjectVerificationStatus.ts index 9351f8a5c..d4cde104a 100644 --- a/src/services/cronJobs/checkProjectVerificationStatus.ts +++ b/src/services/cronJobs/checkProjectVerificationStatus.ts @@ -6,7 +6,6 @@ import config from '../../config'; import { logger } from '../../utils/logger'; import { projectsWithoutUpdateAfterTimeFrame } from '../../repositories/projectRepository'; import { i18n, translationErrorMessagesKeys } from '../../utils/errorMessages'; - import { makeFormDraft } from '../../repositories/projectVerificationRepository'; import { sleep } from '../../utils/utils'; import { getNotificationAdapter } from '../../adapters/adaptersFactory'; From d16fd55b945ee5fa3305f604cfe5d8f85e6a3bb6 Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Mon, 22 Apr 2024 21:12:01 -0500 Subject: [PATCH 012/406] refresh and fetch user address separately (#1499) --- src/resolvers/userResolver.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index bbec66ecd..4a56ae2a6 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -92,9 +92,14 @@ export class UserResolver { if (!foundUser) return; try { - const passportScore = await getGitcoinAdapter().submitPassport({ + // Refresh user score + await getGitcoinAdapter().submitPassport({ address, }); + + const passportScore = + await getGitcoinAdapter().getWalletAddressScore(address); + const passportStamps = await getGitcoinAdapter().getPassportStamps(address); From d6a08dbd375c83928ef99fca5e67c1abfa5e5dcb Mon Sep 17 00:00:00 2001 From: Amin Latifi Date: Tue, 23 Apr 2024 08:48:48 +0000 Subject: [PATCH 013/406] Added pg_trgm extension migration (#1502) --- migration/1713859866338-enable_pg_trgm_extension.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 migration/1713859866338-enable_pg_trgm_extension.ts diff --git a/migration/1713859866338-enable_pg_trgm_extension.ts b/migration/1713859866338-enable_pg_trgm_extension.ts new file mode 100644 index 000000000..64c650580 --- /dev/null +++ b/migration/1713859866338-enable_pg_trgm_extension.ts @@ -0,0 +1,11 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class EnablePgTrgmExtension1713859866338 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query('DROP EXTENSION IF EXISTS pg_trgm'); + } +} From 01c6e21bdbddf0f8fd24cbe8d99f0ee7e66724ee Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Tue, 23 Apr 2024 13:35:02 +0100 Subject: [PATCH 014/406] fix: change recurring donations stats query to single query --- src/resolvers/recurringDonationResolver.ts | 40 +++++----------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/src/resolvers/recurringDonationResolver.ts b/src/resolvers/recurringDonationResolver.ts index 159b7187b..06bf9c3c4 100644 --- a/src/resolvers/recurringDonationResolver.ts +++ b/src/resolvers/recurringDonationResolver.ts @@ -659,53 +659,31 @@ export class RecurringDonationResolver { @Args() { beginDate, endDate, currency }: GetRecurringDonationStatsArgs, ): Promise { try { - // Validate input arguments using Joi schema validateWithJoiSchema( { beginDate, endDate }, getRecurringDonationStatsArgsValidator, ); - // Query to calculate total streamed USD value - const totalStreamedUsdValueQuery = RecurringDonation.createQueryBuilder( - 'recurring_donation', - ) - .select( + const query = RecurringDonation.createQueryBuilder('recurring_donation') + .select([ + 'COUNT(CASE WHEN recurring_donation.status = :active THEN 1 END)', 'SUM(recurring_donation.totalUsdStreamed)', - 'totalStreamedUsdValue', - ) + ]) + .setParameter('active', 'active') .where( `recurring_donation.createdAt >= :beginDate AND recurring_donation.createdAt <= :endDate`, { beginDate, endDate }, ); - // Query to calculate active recurring donations count - const activeRecurringDonationsCountQuery = - RecurringDonation.createQueryBuilder('recurring_donation').where( - `recurring_donation.createdAt >= :beginDate AND recurring_donation.createdAt <= :endDate AND recurring_donation.status = 'active'`, - { beginDate, endDate }, - ); - - // Add currency filter if provided if (currency) { - totalStreamedUsdValueQuery.andWhere( - `recurring_donation.currency = :currency`, - { currency }, - ); - activeRecurringDonationsCountQuery.andWhere( - `recurring_donation.currency = :currency`, - { currency }, - ); + query.andWhere(`recurring_donation.currency = :currency`, { currency }); } - const [activeRecurringDonationsCount, totalStreamedUsdValue] = - await Promise.all([ - activeRecurringDonationsCountQuery.getCount(), - totalStreamedUsdValueQuery.getRawOne(), - ]); + const [result] = await query.getRawMany(); return { - activeRecurringDonationsCount: activeRecurringDonationsCount || 0, - totalStreamedUsdValue: totalStreamedUsdValue.totalStreamedUsdValue || 0, + activeRecurringDonationsCount: parseInt(result.count), + totalStreamedUsdValue: parseFloat(result.sum) || 0, }; } catch (e) { SentryLogger.captureException(e); From c5c9a435fd4042f819d3c227fcc2af56c63dd8c7 Mon Sep 17 00:00:00 2001 From: Ramin Date: Wed, 24 Apr 2024 20:27:39 +0330 Subject: [PATCH 015/406] fix recurring donation count --- .../recurringDonationRepository.test.ts | 8 ++++---- src/repositories/recurringDonationRepository.ts | 13 +++++++++---- src/resolvers/donationResolver.ts | 4 ++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/repositories/recurringDonationRepository.test.ts b/src/repositories/recurringDonationRepository.test.ts index e8b49bc0d..7669bb9df 100644 --- a/src/repositories/recurringDonationRepository.test.ts +++ b/src/repositories/recurringDonationRepository.test.ts @@ -12,7 +12,7 @@ import { import { NETWORK_IDS } from '../provider'; import { addNewAnchorAddress } from './anchorContractAddressRepository'; import { - countOfActiveRecurringDonationsByProjectId, + nonZeroRecurringDonationsByProjectId, createNewRecurringDonation, findRecurringDonationById, findRecurringDonationByProjectIdAndUserIdAndCurrency, @@ -263,7 +263,7 @@ function countOfActiveRecurringDonationsByProjectIdTestCases() { }); recurringDonation.status = RECURRING_DONATION_STATUS.ACTIVE; await recurringDonation.save(); - const count = await countOfActiveRecurringDonationsByProjectId(project.id); + const count = await nonZeroRecurringDonationsByProjectId(project.id); assert.equal(count, 1); }); it('should return count correctly, when there is more than 1 active recurring donation', async () => { @@ -316,7 +316,7 @@ function countOfActiveRecurringDonationsByProjectIdTestCases() { recurringDonation2.status = RECURRING_DONATION_STATUS.ACTIVE; await recurringDonation2.save(); - const count = await countOfActiveRecurringDonationsByProjectId(project.id); + const count = await nonZeroRecurringDonationsByProjectId(project.id); assert.equal(count, 2); }); it('should return count correctly, when there is active and non active donations', async () => { @@ -398,7 +398,7 @@ function countOfActiveRecurringDonationsByProjectIdTestCases() { recurringDonation4.status = RECURRING_DONATION_STATUS.FAILED; await recurringDonation4.save(); - const count = await countOfActiveRecurringDonationsByProjectId(project.id); + const count = await nonZeroRecurringDonationsByProjectId(project.id); assert.equal(count, 1); }); } diff --git a/src/repositories/recurringDonationRepository.ts b/src/repositories/recurringDonationRepository.ts index 0635dba4b..2cc8a4fdd 100644 --- a/src/repositories/recurringDonationRepository.ts +++ b/src/repositories/recurringDonationRepository.ts @@ -129,14 +129,19 @@ export const findRecurringDonationById = async ( .getOne(); }; -export const countOfActiveRecurringDonationsByProjectId = async ( +export const nonZeroRecurringDonationsByProjectId = async ( projectId: number, ): Promise => { return await RecurringDonation.createQueryBuilder('recurringDonation') .where(`recurringDonation.projectId = :projectId`, { projectId }) - .andWhere(`recurringDonation.status = :status`, { - status: RECURRING_DONATION_STATUS.ACTIVE, - }) + .andWhere( + `(recurringDonation.status = :activeStatus OR + (recurringDonation.status = :endedStatus AND recurringDonation.totalAmountStreamed > 0))`, + { + activeStatus: RECURRING_DONATION_STATUS.ACTIVE, + endedStatus: RECURRING_DONATION_STATUS.ENDED, + }, + ) .getCount(); }; diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index fe23bf45e..57c665513 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -68,7 +68,7 @@ import { DRAFT_DONATION_STATUS, DraftDonation, } from '../entities/draftDonation'; -import { countOfActiveRecurringDonationsByProjectId } from '../repositories/recurringDonationRepository'; +import { nonZeroRecurringDonationsByProjectId } from '../repositories/recurringDonationRepository'; const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true'; @@ -590,7 +590,7 @@ export class DonationResolver { } const recurringDonationsCount = - await countOfActiveRecurringDonationsByProjectId(projectId); + await nonZeroRecurringDonationsByProjectId(projectId); const [donations, donationsCount] = await query .take(take) From 3e38a280192d444704f7c89440c753d45b5fa58a Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Wed, 24 Apr 2024 21:57:52 +0100 Subject: [PATCH 016/406] WIP: projectIds textArea --- .../tabs/components/ProjectIdsTextArea.tsx | 28 +++++++++++++++++++ .../tabs/components/ProjectsInQfRound.tsx | 2 +- src/server/adminJs/tabs/qfRoundTab.ts | 3 ++ 3 files changed, 32 insertions(+), 1 deletion(-) create mode 100644 src/server/adminJs/tabs/components/ProjectIdsTextArea.tsx diff --git a/src/server/adminJs/tabs/components/ProjectIdsTextArea.tsx b/src/server/adminJs/tabs/components/ProjectIdsTextArea.tsx new file mode 100644 index 000000000..b33781ec3 --- /dev/null +++ b/src/server/adminJs/tabs/components/ProjectIdsTextArea.tsx @@ -0,0 +1,28 @@ +import React, { useEffect, useState } from 'react'; +import { withTheme } from 'styled-components'; +import { TextArea } from '@adminjs/design-system'; +import { getRelatedProjectsOfQfRound } from '../../../../repositories/qfRoundRepository'; + +const ProjectIdsTextArea = props => { + const { onChange, record } = props; + const [projectIds, setProjectIds] = useState(''); + + useEffect(() => { + const asyncFunc = async () => { + const qfRoundId = record?.params?.qfRoundId || record?.params?.id; + const qfRoundProjects = await getRelatedProjectsOfQfRound(qfRoundId); + console.log('qfRoundProjects', qfRoundProjects); + + const ids = qfRoundProjects.map(project => project.slug).join(','); + console.log('ids', ids); + + setProjectIds(ids); + }; + + asyncFunc(); + }, []); + + return