From 3ae8570fd0976b377b2b152b3e657755cd09f103 Mon Sep 17 00:00:00 2001 From: Carlos Quintero Date: Tue, 18 Oct 2022 20:10:14 -0500 Subject: [PATCH 1/5] add donorCount, donationsAmount and projectsPerDate queries --- src/resolvers/donationResolver.ts | 81 +++++++++++++++++++++++++++++++ src/resolvers/projectResolver.ts | 24 +++++++++ 2 files changed, 105 insertions(+) diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index d5c182edd..ce865c5eb 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -175,6 +175,87 @@ export class DonationResolver { } } + @Query(returns => [Donation], { nullable: true }) + async donationsAmount( + // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss + @Arg('fromDate', { nullable: true }) fromDate?: string, + @Arg('toDate', { nullable: true }) toDate?: string, + ) { + try { + validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); + const query = this.donationRepository + .createQueryBuilder('donation') + .select(`SUM(donation."valueUsd")`, 'sum'); + + if (fromDate) { + query.andWhere(`donation."createdAt" >= '${fromDate}'`); + } + if (toDate) { + query.andWhere(`donation."createdAt" <= '${toDate}'`); + } + const [sum] = await query.getRawOne(); + + return sum; + } catch (e) { + logger.error('donations query error', e); + throw e; + } + } + + @Query(returns => [Donation], { nullable: true }) + async donorsCount( + // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss + @Arg('fromDate', { nullable: true }) fromDate?: string, + @Arg('toDate', { nullable: true }) toDate?: string, + ) { + try { + validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); + const query = this.donationRepository + .createQueryBuilder('donation') + .select( + `COUNT(DISTINCT(donation."userId")) + SUM(CASE WHEN donation."userId" IS NULL THEN 1 ELSE 0 END)`, + 'count', + ); + + if (fromDate) { + query.andWhere(`donation."createdAt" >= '${fromDate}'`); + } + if (toDate) { + query.andWhere(`donation."createdAt" <= '${toDate}'`); + } + const [donors] = await query.getRawOne(); + } catch (e) { + logger.error('donations query error', e); + throw e; + } + } + + @Query(returns => [Donation], { nullable: true }) + async donationsPerCategory( + // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss + @Arg('fromDate', { nullable: true }) fromDate?: string, + @Arg('toDate', { nullable: true }) toDate?: string, + ) { + try { + validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); + const query = this.donationRepository + .createQueryBuilder('donation') + .leftJoinAndSelect('donation.project', 'project') + .leftJoinAndSelect('project.categories', 'categories'); + + if (fromDate) { + query.andWhere(`donation."createdAt" >= '${fromDate}'`); + } + if (toDate) { + query.andWhere(`donation."createdAt" <= '${toDate}'`); + } + return await query.getMany(); + } catch (e) { + logger.error('donations query error', e); + throw e; + } + } + // TODO I think we can delete this resolver @Query(returns => [Donation], { nullable: true }) async donationsFromWallets( diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index eb18a6a78..956e54c0c 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1056,6 +1056,30 @@ export class ProjectResolver { throw Error('Upload file failed'); } + @Query(returns => [Donation], { nullable: true }) + async projectsPerDate( + // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss + @Arg('fromDate', { nullable: true }) fromDate?: string, + @Arg('toDate', { nullable: true }) toDate?: string, + ) { + try { + const query = this.projectRepository.createQueryBuilder('project'); + + if (fromDate) { + query.andWhere(`project."createdAt" >= '${fromDate}'`); + } + if (toDate) { + query.andWhere(`project."createdAt" <= '${toDate}'`); + } + const projectCount = await query.getCount(); + + return projectCount; + } catch (e) { + logger.error('donations query error', e); + throw e; + } + } + @Mutation(returns => Project) async createProject( @Arg('project') projectInput: CreateProjectInput, From 8170fc8f6942e153e88d10f301f20fe3bebb0f03 Mon Sep 17 00:00:00 2001 From: Carlos Quintero Date: Wed, 19 Oct 2022 01:38:56 -0500 Subject: [PATCH 2/5] add initial tests and donationsPerCategory query --- src/resolvers/donationResolver.test.ts | 14 ++++++++ src/resolvers/donationResolver.ts | 49 +++++++++++++++++++++----- src/resolvers/projectResolver.test.ts | 23 ++++++++++++ src/resolvers/projectResolver.ts | 6 +++- test/graphqlQueries.ts | 36 +++++++++++++++++++ 5 files changed, 119 insertions(+), 9 deletions(-) diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index aecf23d97..d0365f763 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -25,6 +25,7 @@ import { donationsFromWallets, createDonationMutation, updateDonationStatusMutation, + fetchTotalDonationsUsdAmount, } from '../../test/graphqlQueries'; import { NETWORK_IDS } from '../provider'; import { User } from '../entities/user'; @@ -44,12 +45,25 @@ describe('createDonation() test cases', createDonationTestCases); describe('updateDonationStatus() test cases', updateDonationStatusTestCases); describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); +// describe('donationsUsdAmount() test cases', donationsUsdAmountTestCases); // describe('tokens() test cases', tokensTestCases); // TODO I think we can delete addUserVerification query // describe('addUserVerification() test cases', addUserVerificationTestCases); +// function donationsUsdAmountTestCases() { +// it('should return total usd amount for donations made in a time range', async () => { +// const donationsResponse = await axios.post(graphqlUrl, { +// query: fetchTotalDonationsUsdAmount, +// variables: { +// fromDate: '20221203', +// toDate: '20220215', +// }, +// }); +// }); +// } + function donationsTestCases() { it('should throw error if send invalid fromDate format', async () => { const donationsResponse = await axios.post(graphqlUrl, { diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index ce865c5eb..764b739b2 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -58,6 +58,7 @@ import { import { findDonationById } from '../repositories/donationRepository'; import { sleep } from '../utils/utils'; import { findProjectRecipientAddressByNetworkId } from '../repositories/projectAddressRepository'; +import { MainCategory } from '../entities/mainCategory'; const analytics = getAnalytics(); @@ -175,12 +176,43 @@ export class DonationResolver { } } - @Query(returns => [Donation], { nullable: true }) - async donationsAmount( + @Query(returns => Number, { nullable: true }) + async donationsPerCategoryPerDate( + @Arg('fromDate', { nullable: true }) fromDate?: string, + @Arg('toDate', { nullable: true }) toDate?: string, + ): Promise { + try { + validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); + const query = MainCategory.createQueryBuilder('mainCategory') + .select( + 'mainCategory.id, mainCategory.title, mainCategory.slug, sum(d.valueUsd) as totalUsd', + ) + .innerJoin('mainCategory.categories', 'categories') + .innerJoin('categories.projects', 'projects') + .innerJoin('projects.donations', 'donations') + .orderBy('mainCategory.id, mainCategory.title, totalUsd'); + + if (fromDate) { + query.andWhere(`donations."createdAt" >= '${fromDate}'`); + } + if (toDate) { + query.andWhere(`donations."createdAt" <= '${toDate}'`); + } + + const result = await query.getRawMany(); + return result; + } catch (e) { + logger.error('donations query error', e); + throw e; + } + } + + @Query(returns => Number, { nullable: true }) + async donationsUsdAmount( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, - ) { + ): Promise { try { validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); const query = this.donationRepository @@ -193,21 +225,21 @@ export class DonationResolver { if (toDate) { query.andWhere(`donation."createdAt" <= '${toDate}'`); } - const [sum] = await query.getRawOne(); + const donationsUsdAmount = (await query.getRawOne())[0]; - return sum; + return donationsUsdAmount; } catch (e) { logger.error('donations query error', e); throw e; } } - @Query(returns => [Donation], { nullable: true }) + @Query(returns => Number, { nullable: true }) async donorsCount( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, - ) { + ): Promise { try { validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); const query = this.donationRepository @@ -223,7 +255,8 @@ export class DonationResolver { if (toDate) { query.andWhere(`donation."createdAt" <= '${toDate}'`); } - const [donors] = await query.getRawOne(); + const donors = (await query.getRawOne())[0]; + return donors; } catch (e) { logger.error('donations query error', e); throw e; diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 0c4de4c5b..e83eb901e 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -22,6 +22,7 @@ import { fetchAllProjectsQuery, fetchLikedProjectsQuery, fetchMultiFilterAllProjectsQuery, + fetchNewProjectsPerDate, fetchProjectsBySlugQuery, fetchProjectUpdatesQuery, fetchSimilarProjectsBySlugQuery, @@ -76,6 +77,7 @@ import { getConnection } from 'typeorm'; import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; import { ProjectAddress } from '../entities/projectAddress'; +import moment from 'moment'; describe('createProject test cases --->', createProjectTestCases); describe('updateProject test cases --->', updateProjectTestCases); @@ -132,6 +134,27 @@ describe( // describe('activateProject test cases --->', activateProjectTestCases); +// describe('projectsPerDate() test cases --->', projectsPerDateTestCases); + +// function projectsPerDateTestCases() { +// it('should projects created in a time range', async () => { +// const project = await saveProjectDirectlyToDb({ +// ...createProjectData(), +// creationDate: moment().add(10, 'days').toDate(), +// }); +// const projectsResponse = await axios.post(graphqlUrl, { +// query: fetchNewProjectsPerDate, +// variables: { +// fromDate: moment().add(9, 'days').toDate().toISOString().split('T')[0], +// toDate: moment().add(11, 'days').toDate().toISOString().split('T')[0], +// }, +// }); + +// assert.isOk(projectsResponse); +// // assert.isTrue(projectsResponse, 1); +// }); +// } + function getProjectsAcceptTokensTestCases() { it('should return all tokens for giveth projects', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 956e54c0c..755acfb98 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -83,6 +83,10 @@ import { sortTokensByOrderAndAlphabets } from '../utils/tokenUtils'; import { getNotificationAdapter } from '../adapters/adaptersFactory'; import { NETWORK_IDS } from '../provider'; import { getVerificationFormByProjectId } from '../repositories/projectVerificationRepository'; +import { + getDonationsQueryValidator, + validateWithJoiSchema, +} from '../utils/validators/graphqlQueryValidators'; const analytics = getAnalytics(); @@ -1061,7 +1065,7 @@ export class ProjectResolver { // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, - ) { + ): Promise { try { const query = this.projectRepository.createQueryBuilder('project'); diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 19d5c7187..93d1ca26f 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -261,6 +261,42 @@ export const donationsToWallets = ` } `; +export const fetchNewProjectsPerDate = ` + query ( + $fromDate: String + $toDate: String + ) { + projectsPerDate( + fromDate: $fromDate + toDate: $toDate + ) + } +`; + +export const fetchTotalDonationsUsdAmount = ` + query ( + $fromDate: String + $toDate: String + ) { + donorsCount( + fromDate: $fromDate + toDate: $toDate + ) + } +`; + +export const fetchTotalDonors = ` + query ( + $fromDate: String + $toDate: String + ) { + donationsUsdAmount( + fromDate: $fromDate + toDate: $toDate + ) + } +`; + export const fetchAllDonationsQuery = ` query ( $fromDate: String From 36fa916a2cf88663e355fa7f482f08ea6a9d76ac Mon Sep 17 00:00:00 2001 From: Carlos Quintero Date: Mon, 24 Oct 2022 02:53:34 -0500 Subject: [PATCH 3/5] add tests for reports queries --- config/test.env | 2 +- docker-compose-local-postgres-redis.yml | 4 +- package-lock.json | 108 ++++++++++++++++++ src/resolvers/donationResolver.test.ts | 145 +++++++++++++++++++++--- src/resolvers/donationResolver.ts | 94 +++++++-------- src/resolvers/projectResolver.test.ts | 40 +++---- src/resolvers/projectResolver.ts | 6 +- test/graphqlQueries.ts | 19 +++- test/testUtils.ts | 13 ++- 9 files changed, 323 insertions(+), 108 deletions(-) diff --git a/config/test.env b/config/test.env index 0230bd3fa..72a84e861 100644 --- a/config/test.env +++ b/config/test.env @@ -7,7 +7,7 @@ TYPEORM_DATABASE_NAME=givethio TYPEORM_DATABASE_USER=postgres TYPEORM_DATABASE_PASSWORD=postgres TYPEORM_DATABASE_HOST=localhost -TYPEORM_DATABASE_PORT=5432 +TYPEORM_DATABASE_PORT=5433 TYPEORM_LOGGING=all DROP_DATABASE=true diff --git a/docker-compose-local-postgres-redis.yml b/docker-compose-local-postgres-redis.yml index 566b337d1..4342572a0 100644 --- a/docker-compose-local-postgres-redis.yml +++ b/docker-compose-local-postgres-redis.yml @@ -11,7 +11,7 @@ services: - POSTGRES_PASSWORD=postgres - PGDATA=/var/lib/postgresql/data/pgdata ports: - - "5442:5432" + - "5443:5432" volumes: - db-data:/var/lib/postgresql/data @@ -28,7 +28,7 @@ services: - POSTGRES_PASSWORD=postgres - PGDATA=/var/lib/postgresql/data/pgdata ports: - - "5432:5432" + - "5433:5432" volumes: - db-data-test:/var/lib/postgresql/data diff --git a/package-lock.json b/package-lock.json index ceca9cd98..011bcfe08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18664,6 +18664,14 @@ "node": ">= 10" } }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents/node_modules/npm-bundled": { + "version": "1.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { "version": "2.0.3", "inBundle": true, @@ -18872,6 +18880,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/npm/node_modules/bin-links/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/binary-extensions": { "version": "2.2.0", "inBundle": true, @@ -19860,6 +19876,20 @@ "node": "*" } }, + "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/npm/node_modules/nopt": { "version": "5.0.0", "inBundle": true, @@ -19907,6 +19937,14 @@ "npm-normalize-package-bin": "^1.0.1" } }, + "node_modules/npm/node_modules/npm-bundled/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/npm-install-checks": { "version": "5.0.0", "inBundle": true, @@ -19953,6 +19991,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/npm/node_modules/npm-packlist/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/npm-pick-manifest": { "version": "7.0.1", "inBundle": true, @@ -19967,6 +20013,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/npm/node_modules/npm-pick-manifest/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/npm-profile": { "version": "6.0.3", "inBundle": true, @@ -20201,6 +20255,14 @@ "node": ">=10" } }, + "node_modules/npm/node_modules/read-package-json/node_modules/npm-normalize-package-bin": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/npm/node_modules/readable-stream": { "version": "3.6.0", "inBundle": true, @@ -43518,6 +43580,15 @@ "requires": { "npm-bundled": "^1.1.1", "npm-normalize-package-bin": "^1.0.1" + }, + "dependencies": { + "npm-bundled": { + "version": "1.1.2", + "bundled": true, + "requires": { + "npm-normalize-package-bin": "^1.0.1" + } + } } }, "@npmcli/map-workspaces": { @@ -43657,6 +43728,12 @@ "read-cmd-shim": "^3.0.0", "rimraf": "^3.0.0", "write-file-atomic": "^4.0.0" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } } }, "binary-extensions": { @@ -44331,6 +44408,13 @@ "requires": { "brace-expansion": "^1.1.7" } + }, + "nopt": { + "version": "5.0.0", + "bundled": true, + "requires": { + "abbrev": "1" + } } } }, @@ -44363,6 +44447,12 @@ "bundled": true, "requires": { "npm-normalize-package-bin": "^1.0.1" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } } }, "npm-install-checks": { @@ -44393,6 +44483,12 @@ "ignore-walk": "^5.0.1", "npm-bundled": "^1.1.2", "npm-normalize-package-bin": "^1.0.1" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } } }, "npm-pick-manifest": { @@ -44403,6 +44499,12 @@ "npm-normalize-package-bin": "^1.0.1", "npm-package-arg": "^9.0.0", "semver": "^7.3.5" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } } }, "npm-profile": { @@ -44552,6 +44654,12 @@ "json-parse-even-better-errors": "^2.3.1", "normalize-package-data": "^4.0.0", "npm-normalize-package-bin": "^1.0.1" + }, + "dependencies": { + "npm-normalize-package-bin": { + "version": "2.0.0", + "bundled": true + } } }, "read-package-json-fast": { diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index d0365f763..8748395b1 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -26,6 +26,8 @@ import { createDonationMutation, updateDonationStatusMutation, fetchTotalDonationsUsdAmount, + fetchTotalDonors, + fetchTotalDonationsPerCategoryPerDate, } from '../../test/graphqlQueries'; import { NETWORK_IDS } from '../provider'; import { User } from '../entities/user'; @@ -45,24 +47,131 @@ describe('createDonation() test cases', createDonationTestCases); describe('updateDonationStatus() test cases', updateDonationStatusTestCases); describe('donationsToWallets() test cases', donationsToWalletsTestCases); describe('donationsFromWallets() test cases', donationsFromWalletsTestCases); -// describe('donationsUsdAmount() test cases', donationsUsdAmountTestCases); - -// describe('tokens() test cases', tokensTestCases); - -// TODO I think we can delete addUserVerification query -// describe('addUserVerification() test cases', addUserVerificationTestCases); - -// function donationsUsdAmountTestCases() { -// it('should return total usd amount for donations made in a time range', async () => { -// const donationsResponse = await axios.post(graphqlUrl, { -// query: fetchTotalDonationsUsdAmount, -// variables: { -// fromDate: '20221203', -// toDate: '20220215', -// }, -// }); -// }); -// } +describe('totalDonationsUsdAmount() test cases', donationsUsdAmountTestCases); +describe('totalDonorsCountPerDate() test cases', donorsCountPerDateTestCases); +describe( + 'totalDonationsPerCategoryPerDate() test cases', + totalDonationsPerCategoryPerDateTestCases, +); + +// // describe('tokens() test cases', tokensTestCases); + +// // TODO I think we can delete addUserVerification query +// // describe('addUserVerification() test cases', addUserVerificationTestCases); + +function totalDonationsPerCategoryPerDateTestCases() { + it('should return donations count per category per time range', async () => { + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + }); + const foodDonationsTotalUsd = await Donation.createQueryBuilder('donation') + .select('COALESCE(SUM(donation."valueUsd")) AS sum') + .where(`donation.status = 'verified'`) + .getRawMany(); + + assert.isOk(donationsResponse); + + const foodDonationsResponseTotal = + donationsResponse.data.data.totalDonationsPerCategory.find( + d => d.title === 'food', + ); + assert.equal( + foodDonationsResponseTotal.totalUsd, + foodDonationsTotalUsd[0].sum, + ); + }); +} + +function donorsCountPerDateTestCases() { + it('should return donors unique total count in a time range', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as its the same user + const donation = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + }), + user.id, + project.id, + ); + const donation2 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + }), + user.id, + project.id, + ); + + // anonymous donations count as separate + const anonymousDonation1 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + undefined, + project.id, + ); + const anonymousDonation2 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(50, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + undefined, + project.id, + ); + + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonors, + variables: { + fromDate: moment().add(49, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(51, 'days').toDate().toISOString().split('T')[0], + }, + }); + assert.isOk(donationsResponse); + // 1 unique donor and 2 anonymous + assert.equal(donationsResponse.data.data.totalDonorsCountPerDate, 3); + }); +} + +function donationsUsdAmountTestCases() { + it('should return total usd amount for donations made in a time range', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + const donation = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(100, 'days').toDate(), + valueUsd: 20, + }), + user.id, + project.id, + ); + + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsUsdAmount, + variables: { + fromDate: moment().add(99, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(101, 'days').toDate().toISOString().split('T')[0], + }, + }); + + assert.isOk(donationsResponse); + assert.equal( + donationsResponse.data.data.donationsTotalUsdPerDate, + donation.valueUsd, + ); + }); +} function donationsTestCases() { it('should throw error if send invalid fromDate format', async () => { diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 764b739b2..084879750 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -141,6 +141,21 @@ class UserDonations { totalCount: number; } +@ObjectType() +class MainCategoryDonations { + @Field(type => Int) + id: number; + + @Field(type => String) + title: string; + + @Field(type => String) + slug: string; + + @Field(type => Number) + totalUsd: number; +} + @Resolver(of => User) export class DonationResolver { constructor( @@ -176,28 +191,21 @@ export class DonationResolver { } } - @Query(returns => Number, { nullable: true }) - async donationsPerCategoryPerDate( - @Arg('fromDate', { nullable: true }) fromDate?: string, - @Arg('toDate', { nullable: true }) toDate?: string, - ): Promise { + @Query(returns => [MainCategoryDonations], { nullable: true }) + async totalDonationsPerCategory(): Promise { try { - validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); const query = MainCategory.createQueryBuilder('mainCategory') .select( - 'mainCategory.id, mainCategory.title, mainCategory.slug, sum(d.valueUsd) as totalUsd', + 'mainCategory.id, mainCategory.title, mainCategory.slug, COALESCE(sum(donations.valueUsd), 0) as "totalUsd"', ) - .innerJoin('mainCategory.categories', 'categories') - .innerJoin('categories.projects', 'projects') - .innerJoin('projects.donations', 'donations') - .orderBy('mainCategory.id, mainCategory.title, totalUsd'); - - if (fromDate) { - query.andWhere(`donations."createdAt" >= '${fromDate}'`); - } - if (toDate) { - query.andWhere(`donations."createdAt" <= '${toDate}'`); - } + .leftJoin('mainCategory.categories', 'categories') + .leftJoin('categories.projects', 'projects') + .leftJoin( + 'projects.donations', + 'donations', + `donations.status = 'verified'`, + ) + .groupBy('mainCategory.id, mainCategory.title'); const result = await query.getRawMany(); return result; @@ -208,16 +216,17 @@ export class DonationResolver { } @Query(returns => Number, { nullable: true }) - async donationsUsdAmount( + async donationsTotalUsdPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, ): Promise { try { - validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); + // validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); const query = this.donationRepository .createQueryBuilder('donation') - .select(`SUM(donation."valueUsd")`, 'sum'); + .select(`COALESCE(SUM(donation."valueUsd"), 0)`, 'sum') + .where(`donation.status = 'verified'`); if (fromDate) { query.andWhere(`donation."createdAt" >= '${fromDate}'`); @@ -225,9 +234,9 @@ export class DonationResolver { if (toDate) { query.andWhere(`donation."createdAt" <= '${toDate}'`); } - const donationsUsdAmount = (await query.getRawOne())[0]; + const donationsUsdAmount = await query.getRawOne(); - return donationsUsdAmount; + return donationsUsdAmount.sum; } catch (e) { logger.error('donations query error', e); throw e; @@ -235,46 +244,20 @@ export class DonationResolver { } @Query(returns => Number, { nullable: true }) - async donorsCount( + async totalDonorsCountPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, ): Promise { try { - validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); + // validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); const query = this.donationRepository .createQueryBuilder('donation') .select( - `COUNT(DISTINCT(donation."userId")) + SUM(CASE WHEN donation."userId" IS NULL THEN 1 ELSE 0 END)`, + `CAST((COUNT(DISTINCT(donation."userId")) + SUM(CASE WHEN donation."userId" IS NULL THEN 1 ELSE 0 END)) AS int)`, 'count', - ); - - if (fromDate) { - query.andWhere(`donation."createdAt" >= '${fromDate}'`); - } - if (toDate) { - query.andWhere(`donation."createdAt" <= '${toDate}'`); - } - const donors = (await query.getRawOne())[0]; - return donors; - } catch (e) { - logger.error('donations query error', e); - throw e; - } - } - - @Query(returns => [Donation], { nullable: true }) - async donationsPerCategory( - // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss - @Arg('fromDate', { nullable: true }) fromDate?: string, - @Arg('toDate', { nullable: true }) toDate?: string, - ) { - try { - validateWithJoiSchema({ fromDate, toDate }, getDonationsQueryValidator); - const query = this.donationRepository - .createQueryBuilder('donation') - .leftJoinAndSelect('donation.project', 'project') - .leftJoinAndSelect('project.categories', 'categories'); + ) + .where(`donation.status = 'verified'`); if (fromDate) { query.andWhere(`donation."createdAt" >= '${fromDate}'`); @@ -282,7 +265,8 @@ export class DonationResolver { if (toDate) { query.andWhere(`donation."createdAt" <= '${toDate}'`); } - return await query.getMany(); + const donors = await query.getRawOne(); + return donors.count; } catch (e) { logger.error('donations query error', e); throw e; diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index e83eb901e..702e14f74 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -134,26 +134,26 @@ describe( // describe('activateProject test cases --->', activateProjectTestCases); -// describe('projectsPerDate() test cases --->', projectsPerDateTestCases); - -// function projectsPerDateTestCases() { -// it('should projects created in a time range', async () => { -// const project = await saveProjectDirectlyToDb({ -// ...createProjectData(), -// creationDate: moment().add(10, 'days').toDate(), -// }); -// const projectsResponse = await axios.post(graphqlUrl, { -// query: fetchNewProjectsPerDate, -// variables: { -// fromDate: moment().add(9, 'days').toDate().toISOString().split('T')[0], -// toDate: moment().add(11, 'days').toDate().toISOString().split('T')[0], -// }, -// }); - -// assert.isOk(projectsResponse); -// // assert.isTrue(projectsResponse, 1); -// }); -// } +describe('projectsPerDate() test cases --->', projectsPerDateTestCases); + +function projectsPerDateTestCases() { + it('should projects created in a time range', async () => { + const project = await saveProjectDirectlyToDb({ + ...createProjectData(), + creationDate: moment().add(10, 'days').toDate(), + }); + const projectsResponse = await axios.post(graphqlUrl, { + query: fetchNewProjectsPerDate, + variables: { + fromDate: moment().add(9, 'days').toDate().toISOString().split('T')[0], + toDate: moment().add(11, 'days').toDate().toISOString().split('T')[0], + }, + }); + + assert.isOk(projectsResponse); + assert.equal(projectsResponse.data.data.projectsPerDate, 1); + }); +} function getProjectsAcceptTokensTestCases() { it('should return all tokens for giveth projects', async () => { diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 755acfb98..27c6c6733 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1060,7 +1060,7 @@ export class ProjectResolver { throw Error('Upload file failed'); } - @Query(returns => [Donation], { nullable: true }) + @Query(returns => Number, { nullable: true }) async projectsPerDate( // fromDate and toDate should be in this format YYYYMMDD HH:mm:ss @Arg('fromDate', { nullable: true }) fromDate?: string, @@ -1070,10 +1070,10 @@ export class ProjectResolver { const query = this.projectRepository.createQueryBuilder('project'); if (fromDate) { - query.andWhere(`project."createdAt" >= '${fromDate}'`); + query.andWhere(`project."creationDate" >= '${fromDate}'`); } if (toDate) { - query.andWhere(`project."createdAt" <= '${toDate}'`); + query.andWhere(`project."creationDate" <= '${toDate}'`); } const projectCount = await query.getCount(); diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 93d1ca26f..cb1f9ca84 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -273,24 +273,35 @@ export const fetchNewProjectsPerDate = ` } `; -export const fetchTotalDonationsUsdAmount = ` +export const fetchTotalDonationsPerCategoryPerDate = ` + query { + totalDonationsPerCategory { + id + title + slug + totalUsd + } + } +`; + +export const fetchTotalDonors = ` query ( $fromDate: String $toDate: String ) { - donorsCount( + totalDonorsCountPerDate( fromDate: $fromDate toDate: $toDate ) } `; -export const fetchTotalDonors = ` +export const fetchTotalDonationsUsdAmount = ` query ( $fromDate: String $toDate: String ) { - donationsUsdAmount( + donationsTotalUsdPerDate( fromDate: $fromDate toDate: $toDate ) diff --git a/test/testUtils.ts b/test/testUtils.ts index 2dae86975..9b1f9e8a1 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -257,6 +257,9 @@ export const createProjectData = (): CreateProjectData => { }; export const createDonationData = (params?: { status?: string; + createdAt?: Date; + valueUsd?: number; + anonymous?: boolean; }): CreateDonationData => { return { transactionId: generateRandomTxHash(), @@ -265,10 +268,10 @@ export const createDonationData = (params?: { fromWalletAddress: SEED_DATA.FIRST_USER.walletAddress, currency: 'ETH', status: params?.status || DONATION_STATUS.PENDING, - anonymous: false, + anonymous: params?.anonymous || false, amount: 15, - valueUsd: 15, - createdAt: moment(), + valueUsd: params?.valueUsd || 15, + createdAt: params?.createdAt || moment().toDate(), segmentNotified: true, }; }; @@ -1527,8 +1530,8 @@ export interface MainCategoryData { export const saveDonationDirectlyToDb = async ( donationData: CreateDonationData, - userId: number, - projectId: number, + userId?: number, + projectId?: number, ) => { const user = (await User.findOne({ id: userId, From 28a1b54b2a707aa83be7520443aa02bfcf265c2d Mon Sep 17 00:00:00 2001 From: Carlos Quintero Date: Mon, 24 Oct 2022 02:55:00 -0500 Subject: [PATCH 4/5] revert test env --- config/test.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test.env b/config/test.env index 72a84e861..0230bd3fa 100644 --- a/config/test.env +++ b/config/test.env @@ -7,7 +7,7 @@ TYPEORM_DATABASE_NAME=givethio TYPEORM_DATABASE_USER=postgres TYPEORM_DATABASE_PASSWORD=postgres TYPEORM_DATABASE_HOST=localhost -TYPEORM_DATABASE_PORT=5433 +TYPEORM_DATABASE_PORT=5432 TYPEORM_LOGGING=all DROP_DATABASE=true From 8e20221a038fb3e95486b3486fbbd33a997fd5aa Mon Sep 17 00:00:00 2001 From: Carlos Quintero Date: Mon, 24 Oct 2022 02:55:47 -0500 Subject: [PATCH 5/5] revert local docker for postgres --- docker-compose-local-postgres-redis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose-local-postgres-redis.yml b/docker-compose-local-postgres-redis.yml index 4342572a0..566b337d1 100644 --- a/docker-compose-local-postgres-redis.yml +++ b/docker-compose-local-postgres-redis.yml @@ -11,7 +11,7 @@ services: - POSTGRES_PASSWORD=postgres - PGDATA=/var/lib/postgresql/data/pgdata ports: - - "5443:5432" + - "5442:5432" volumes: - db-data:/var/lib/postgresql/data @@ -28,7 +28,7 @@ services: - POSTGRES_PASSWORD=postgres - PGDATA=/var/lib/postgresql/data/pgdata ports: - - "5433:5432" + - "5432:5432" volumes: - db-data-test:/var/lib/postgresql/data