From dd4a672e976dc4d680fdd5b05228956b02940772 Mon Sep 17 00:00:00 2001 From: mohammadranjbarz Date: Thu, 3 Oct 2024 16:50:19 +0300 Subject: [PATCH 1/9] Release Oct 2024 - decentralized verification (#1843) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: remove memo for project verification managing funds * fix: remove memo for project verification managing funds * add projectId and qfRoundId to qf data export * fix: getDraftDonationById bug (toWalletMemo can be null) * fix: add memo for stellar project address uniqueness * fix: add memo for manage address validation * fix: add duplicate address error message for stellar * fix: linter error * add index for project stellar address * eslint error * fix: case when owner donate to his own peoject (Stellar chain) * fix: add calculateGivbackFactor to Stellar cron job * onlyEndaement option added to donationResolvers to get only endaoment projects * chore: implementing coderabbitai suggestion to remove string literal * feat: register secondary donation * running migration to set project banners appropriately for endaoment … (#1778) * running migration to set project banners appropriately for endaoment projects * chore: correcting tab spaces for syntax * fix: linter errors * Modify add banner to endaoment projects migration (#1791) related to #1600 * Fix lint errors * Fix running tests * Fix projectResolver test cases * Fix donationResolver test cases * skip should renew the expiration date of the draft donation test case --------- Co-authored-by: Hrithik Sampson Co-authored-by: mohammadranjbarz * improve adminjs to import qfround matching and better filters * fix eslint * fix: remove adding secondary donation logic * fix minor form issues * order middleware in bootstrap file * test: add test cases to fetch only Endaoment projects * chore: change the second Project to first Project * chore: change the second Project to first Project * chore: change the second Project to first Project * chore: change the second user to new user since it is interfering with the pre-existing test cases * delete previous_round_rank when deleting a project (#1809) * Implement allocatedGivbacks function (#1808) * WIP Implement allocatedGivbacks function related to https://github.com/Giveth/giveth-dapps-v2/issues/4678 https://github.com/Giveth/giveth-dapps-v2/issues/4679 * allocatedGivbacks() endpoint implemented and works related to https://github.com/Giveth/giveth-dapps-v2/issues/4678 https://github.com/Giveth/giveth-dapps-v2/issues/4679 * Fix allocatedGivbacksQuery test cases * migration: project banners for endaoment projects need to have the correct banners * chore: underscore before unused variable in add_endaoment_project_banners * Use Gnosis giv token for getting price of GIV * Use superfluid mock adapter for test cases * Use superfluid adapter on test env again * Feat/separate givback verfied (#1770) * add isGivbackEligible field * add AddIsGivbackEligibleColumnToProject1637168932304 * add UpdateIsGivbackEligibleForVerifiedProjects1637168932305 migration * add migration to rename isProjectVerified to isProjectGivbackEligible * change isProjectVerified tp isProjectGivbackEligible * update octant donation * add approve project * treat project.verified and project.isGivbackEligible equally on sorting * remove reset verification status on verify * check isGivbackEligible on create ProjectVerificationForm * add ProjectInstantPowerViewV3 migration * use verifiedOrIsGivbackEligibleCondition * Use different materialized view for givback factor related to #1770 * Fix build error * Fix build error * Fix project query for isGivbackEligible and verified * Fix add base token migration * Fix eslint errors * Fix add base token migration * Fix add base token migration * Fix add base token migration * Fix donation test cases related to isGivbackEligible * Fix build error --------- Co-authored-by: Mohammad Ranjbar Z * Fix test cases related to isProjectVerified * add isImported And categories to project tab * fix isProjectGivbackEligible Migration in wrong folder * add chaintype and solana networks to tokenTab * update branch * add environment and energy image mapping * add categories to show and edit forms in adminjs for projects * fix eslint * add best match sort option * update addSearchQuery to prioritize the title * Add Stellar to QFRound * run linter * remove eager from project categories in entity * Add isGivbackEligible filter * Hotfix automatic model score sync (#1849) * add user mbdscore sync workers and cronjob * add active env var for syncing score * add tests to the user sync worker and cronjob * prevent duplicate tokens being added in adminJS * Ensure correct emails are sent for project status changes related to decentralized verification * fix test * fix test cases * fix test cases --------- Co-authored-by: Meriem-BM Co-authored-by: Carlos Co-authored-by: HrithikSampson Co-authored-by: HrithikSampson <56023811+HrithikSampson@users.noreply.github.com> Co-authored-by: Hrithik Sampson Co-authored-by: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Co-authored-by: Cherik Co-authored-by: Ramin --- .DS_Store | Bin 10244 -> 10244 bytes ...ctVerifiedToIsGivbackEligibleInDonation.ts | 19 + migration/1646295724658-createTokensTable.ts | 1 + ...696918830123-add_octant_donations_to_db.ts | 2 +- .../1716367359560-add_base_chain_tokens.ts | 4 + ...213-AddIsGivbackEligibleColumnToProject.ts | 23 + ...ateIsGivbackEligibleForVerifiedProjects.ts | 23 + ...1724223781248-ProjectInstantPowerViewV3.ts | 66 +++ .../1725260193333-projectGivbackRankView.ts | 66 +++ ...069430594-add_endaoment_project_banners.ts | 86 +++ package.json | 3 + .../superFluid/superFluidMockAdapter.ts | 15 +- src/entities/ProjectGivbackRankView.ts | 53 ++ src/entities/donation.ts | 2 +- src/entities/entities.ts | 3 + src/entities/project.ts | 6 + src/repositories/donationRepository.test.ts | 509 +++++++++++++++++- src/repositories/donationRepository.ts | 86 ++- .../projectGivbackViewRepository.test.ts | 209 +++++++ .../projectGivbackViewRepository.ts | 34 ++ src/repositories/projectRepository.test.ts | 1 - src/repositories/projectRepository.ts | 37 +- .../projectVerificationRepository.ts | 39 ++ src/repositories/qfRoundRepository.test.ts | 56 ++ src/repositories/qfRoundRepository.ts | 29 + src/resolvers/donationResolver.test.ts | 432 ++++++++++++++- src/resolvers/donationResolver.ts | 26 +- .../projectResolver.allProject.test.ts | 95 +++- src/resolvers/projectResolver.ts | 21 +- .../projectVerificationFormResolver.test.ts | 5 +- .../projectVerificationFormResolver.ts | 2 +- src/routers/apiGivRoutes.ts | 2 +- src/server/adminJs/adminJs-types.ts | 4 +- src/server/adminJs/adminJs.ts | 2 +- src/server/adminJs/adminJsPermissions.test.ts | 8 +- src/server/adminJs/adminJsPermissions.ts | 10 +- .../components/CustomIdFilterComponent.tsx | 28 + .../CustomProjectReferenceComponent.tsx | 14 + .../CustomProjectReferenceShowComponent.tsx | 20 + .../CustomQfRoundMultiUpdateComponent.tsx | 191 +++++++ .../CustomQfRoundReferenceComponent.tsx | 14 + .../CustomQfRoundReferenceShowComponent.tsx | 20 + .../tabs/components/ProjectCategories.tsx | 30 ++ src/server/adminJs/tabs/donationTab.test.ts | 4 +- src/server/adminJs/tabs/donationTab.ts | 22 +- .../adminJs/tabs/projectVerificationTab.ts | 42 +- src/server/adminJs/tabs/projectsTab.test.ts | 47 +- src/server/adminJs/tabs/projectsTab.ts | 221 ++++++-- src/server/adminJs/tabs/qfRoundHistoryTab.ts | 103 +++- src/server/adminJs/tabs/tokenTab.ts | 37 ++ src/server/bootstrap.ts | 13 +- src/services/Idriss/contractDonations.ts | 2 +- src/services/campaignService.ts | 53 +- .../checkProjectVerificationStatus.test.ts | 16 +- .../cronJobs/checkQRTransactionJob.ts | 2 +- .../cronJobs/importLostDonationsJob.ts | 2 +- .../cronJobs/syncUsersModelScore.test.ts | 85 +++ src/services/cronJobs/syncUsersModelScore.ts | 63 +++ src/services/cronJobs/updatePowerRoundJob.ts | 2 + src/services/givbackService.ts | 20 +- src/services/googleSheets.ts | 4 +- src/services/onramper/donationService.ts | 2 +- src/services/projectViewsService.ts | 2 + src/services/recurringDonationService.test.ts | 6 +- src/services/recurringDonationService.ts | 2 +- src/workers/userMBDScoreSyncWorker.ts | 17 + test/graphqlQueries.ts | 37 +- test/pre-test-scripts.ts | 2 + test/testUtils.ts | 4 +- 69 files changed, 2841 insertions(+), 265 deletions(-) create mode 100644 migration-old-backup/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts create mode 100644 migration/1724060343213-AddIsGivbackEligibleColumnToProject.ts create mode 100644 migration/1724060408379-UpdateIsGivbackEligibleForVerifiedProjects.ts create mode 100644 migration/1724223781248-ProjectInstantPowerViewV3.ts create mode 100644 migration/1725260193333-projectGivbackRankView.ts create mode 100644 migration/1726069430594-add_endaoment_project_banners.ts create mode 100644 src/entities/ProjectGivbackRankView.ts create mode 100644 src/repositories/projectGivbackViewRepository.test.ts create mode 100644 src/repositories/projectGivbackViewRepository.ts create mode 100644 src/server/adminJs/tabs/components/CustomIdFilterComponent.tsx create mode 100644 src/server/adminJs/tabs/components/CustomProjectReferenceComponent.tsx create mode 100644 src/server/adminJs/tabs/components/CustomProjectReferenceShowComponent.tsx create mode 100644 src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx create mode 100644 src/server/adminJs/tabs/components/CustomQfRoundReferenceComponent.tsx create mode 100644 src/server/adminJs/tabs/components/CustomQfRoundReferenceShowComponent.tsx create mode 100644 src/server/adminJs/tabs/components/ProjectCategories.tsx create mode 100644 src/services/cronJobs/syncUsersModelScore.test.ts create mode 100644 src/services/cronJobs/syncUsersModelScore.ts create mode 100644 src/workers/userMBDScoreSyncWorker.ts diff --git a/.DS_Store b/.DS_Store index 58d2ae234dbf26fd958abf4c7102880099651500..eff6557793dcc5cf2574f09fa92dd702139f0659 100644 GIT binary patch delta 35 rcmZn(XbG6$&nUSuU^hRbc#AW{1XcrHnS`IV%gj%%FGM^(G3eD delta 161 zcmZn(XbG6$&nUYwU^hRb>|`E+>Uvg&B!*IkOokkWl$>Zfkx#>WHo)g@B5J3?v)YI{>wBZWCbS-^{M?i)C}82s1MPglH;y diff --git a/migration-old-backup/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts b/migration-old-backup/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts new file mode 100644 index 000000000..94abf6ad5 --- /dev/null +++ b/migration-old-backup/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RenameIsProjectVerifiedToIsGivbackEligibleInDonation1637168932306 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE donation + RENAME COLUMN "isProjectVerified" TO "isProjectGivbackEligible"; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE donation + RENAME COLUMN "isProjectGivbackEligible" TO "isProjectVerified"; + `); + } +} diff --git a/migration/1646295724658-createTokensTable.ts b/migration/1646295724658-createTokensTable.ts index e107727f4..d832a1c7f 100644 --- a/migration/1646295724658-createTokensTable.ts +++ b/migration/1646295724658-createTokensTable.ts @@ -10,6 +10,7 @@ export class createTokensTable1646295724658 implements MigrationInterface { name text COLLATE pg_catalog."default" NOT NULL, symbol text COLLATE pg_catalog."default" NOT NULL, address text COLLATE pg_catalog."default" NOT NULL, + "isQR" BOOLEAN DEFAULT FALSE NOT NUL, "networkId" integer NOT NULL, decimals integer NOT NULL, "order" integer, diff --git a/migration/1696918830123-add_octant_donations_to_db.ts b/migration/1696918830123-add_octant_donations_to_db.ts index 6a14d6eb0..7d606c8a6 100644 --- a/migration/1696918830123-add_octant_donations_to_db.ts +++ b/migration/1696918830123-add_octant_donations_to_db.ts @@ -68,7 +68,7 @@ const transactions: Partial[] = [ transactionId: '0x30954cb441cb7b2184e6cd1afc6acbd1318f86a68b669f6bfb2786dd459e2d6c', currency: 'ETH', - isProjectVerified: true, + isProjectGivbackEligible: true, isTokenEligibleForGivback: true, amount: 5, valueUsd: 9_458.4, diff --git a/migration/1716367359560-add_base_chain_tokens.ts b/migration/1716367359560-add_base_chain_tokens.ts index 4e5f0eb00..7622bb86f 100644 --- a/migration/1716367359560-add_base_chain_tokens.ts +++ b/migration/1716367359560-add_base_chain_tokens.ts @@ -6,6 +6,10 @@ import { NETWORK_IDS } from '../src/provider'; export class AddBaseChainTokens1716367359560 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE token ADD COLUMN IF NOT EXISTS "isQR" BOOLEAN DEFAULT FALSE NOT NULL`, + ); + const environment = config.get('ENVIRONMENT') as string; const networkId = diff --git a/migration/1724060343213-AddIsGivbackEligibleColumnToProject.ts b/migration/1724060343213-AddIsGivbackEligibleColumnToProject.ts new file mode 100644 index 000000000..c0071c00f --- /dev/null +++ b/migration/1724060343213-AddIsGivbackEligibleColumnToProject.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm'; + +export class AddIsGivbackEligibleColumnToProject1637168932304 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // Add the new column + await queryRunner.addColumn( + 'project', + new TableColumn({ + name: 'isGivbackEligible', + type: 'boolean', + isNullable: false, + default: false, + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Drop the isGivbackEligible column + await queryRunner.dropColumn('project', 'isGivbackEligible'); + } +} diff --git a/migration/1724060408379-UpdateIsGivbackEligibleForVerifiedProjects.ts b/migration/1724060408379-UpdateIsGivbackEligibleForVerifiedProjects.ts new file mode 100644 index 000000000..284b84319 --- /dev/null +++ b/migration/1724060408379-UpdateIsGivbackEligibleForVerifiedProjects.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateIsGivbackEligibleForVerifiedProjects1637168932305 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // Update isGivbackEligible to true for verified projects + await queryRunner.query(` + UPDATE project + SET "isGivbackEligible" = true + WHERE "verified" = true; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + // Revert the update (optional) + await queryRunner.query(` + UPDATE project + SET "isGivbackEligible" = false + WHERE "verified" = true; + `); + } +} diff --git a/migration/1724223781248-ProjectInstantPowerViewV3.ts b/migration/1724223781248-ProjectInstantPowerViewV3.ts new file mode 100644 index 000000000..2b61012e7 --- /dev/null +++ b/migration/1724223781248-ProjectInstantPowerViewV3.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectInstantPowerViewV31724223781248 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP MATERIALIZED VIEW IF EXISTS public.project_instant_power_view; + CREATE MATERIALIZED VIEW IF NOT EXISTS public.project_instant_power_view AS + SELECT + innerview."projectId", + ROUND(CAST(innerview."totalPower" as NUMERIC), 2) as "totalPower", + rank() OVER ( + ORDER BY + innerview."totalPower" DESC + ) AS "powerRank" + FROM + ( + SELECT + project.id AS "projectId", + CASE + WHEN (project.verified = true OR project."isGivbackEligible" = true) AND project."statusId" = 5 THEN COALESCE(sum(pp."boostedPower"), 0 :: double precision) + ELSE 0 :: double precision + END AS "totalPower" + FROM + project + LEFT JOIN ( + SELECT + "powerBoosting"."projectId", + sum("instantPowerBalance".balance * "powerBoosting".percentage :: double precision / 100 :: double precision) AS "boostedPower", + now() AS "updateTime" + FROM + instant_power_balance "instantPowerBalance" + JOIN power_boosting "powerBoosting" ON "powerBoosting"."userId" = "instantPowerBalance"."userId" + GROUP BY + "powerBoosting"."projectId" + ) pp ON pp."projectId" = project.id + GROUP BY + project.id + ) innerview + ORDER BY + innerview."totalPower" DESC WITH DATA; + `); + + await queryRunner.query(` + CREATE UNIQUE INDEX idx_project_instant_power_view_unique ON public.project_instant_power_view ("projectId"); + `); + + await queryRunner.query(` + CREATE INDEX project_instant_power_view_project_id ON public.project_instant_power_view USING hash ("projectId") TABLESPACE pg_default; + `); + + await queryRunner.query(` + CREATE INDEX project_instant_power_view_total_power ON public.project_instant_power_view USING btree ("totalPower" DESC) TABLESPACE pg_default; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP MATERIALIZED VIEW IF EXISTS public.project_instant_power_view; + DROP INDEX IF EXISTS public.idx_project_instant_power_view_unique; + DROP INDEX IF EXISTS public.project_instant_power_view_project_id; + DROP INDEX IF EXISTS public.project_instant_power_view_total_power; + `); + } +} diff --git a/migration/1725260193333-projectGivbackRankView.ts b/migration/1725260193333-projectGivbackRankView.ts new file mode 100644 index 000000000..abd20eeea --- /dev/null +++ b/migration/1725260193333-projectGivbackRankView.ts @@ -0,0 +1,66 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ProjectGivbackRankViewV31725260193333 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + ` + DROP + MATERIALIZED VIEW IF EXISTS public.project_givback_rank_view; + CREATE MATERIALIZED VIEW IF NOT EXISTS public.project_givback_rank_view AS + SELECT + innerview."projectId", + ROUND(CAST(innerview."totalPower" as NUMERIC), 2) as "totalPower", + rank() OVER ( + ORDER BY + innerview."totalPower" DESC + ) AS "powerRank", + "powerRound".round + FROM + ( + SELECT + project.id AS "projectId", + CASE project."isGivbackEligible" and project."statusId" = 5 WHEN false THEN 0 :: double precision ELSE COALESCE( + sum(pp."boostedPower"), + 0 :: double precision + ) END AS "totalPower" + FROM + project project + LEFT JOIN ( + SELECT + "powerRound".round, + "powerBoostingSnapshot"."projectId", + "powerBoostingSnapshot"."userId", + avg( + "powerBalanceSnapshot".balance * "powerBoostingSnapshot".percentage :: double precision / 100 :: double precision + ) AS "boostedPower", + now() AS "updateTime" + FROM + power_round "powerRound" + JOIN power_snapshot "powerSnapshot" ON "powerSnapshot"."roundNumber" = "powerRound".round + JOIN power_balance_snapshot "powerBalanceSnapshot" ON "powerBalanceSnapshot"."powerSnapshotId" = "powerSnapshot".id + JOIN power_boosting_snapshot "powerBoostingSnapshot" ON "powerBoostingSnapshot"."powerSnapshotId" = "powerSnapshot".id + AND "powerBoostingSnapshot"."userId" = "powerBalanceSnapshot"."userId" + GROUP BY + "powerRound".round, + "powerBoostingSnapshot"."projectId", + "powerBoostingSnapshot"."userId" + ) pp ON pp."projectId" = project.id + GROUP BY + project.id + ) innerview, + power_round "powerRound" + ORDER BY + innerview."totalPower" DESC WITH DATA; + CREATE UNIQUE INDEX project_givback_rank_view_project_id_round_unique ON public.project_givback_rank_view ("projectId", "round"); + CREATE INDEX project_givback_rank_view_project_id ON public.project_givback_rank_view USING hash ("projectId") TABLESPACE pg_default; + CREATE INDEX project_givback_rank_view_total_power ON public.project_givback_rank_view USING btree ("totalPower" DESC) TABLESPACE pg_default; + `, + ); + } + + public async down(_queryRunner: QueryRunner): Promise { + // + } +} diff --git a/migration/1726069430594-add_endaoment_project_banners.ts b/migration/1726069430594-add_endaoment_project_banners.ts new file mode 100644 index 000000000..7118dc76a --- /dev/null +++ b/migration/1726069430594-add_endaoment_project_banners.ts @@ -0,0 +1,86 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { endaomentProjects } from './data/importedEndaomentProjects'; +import { endaomentProjectCategoryMapping } from './data/endaomentProjectCategoryMapping'; +import { NETWORK_IDS } from '../src/provider'; +export class AddEndaomentProjectBanners1726069430594 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const mainCategorySlugToBannerMapping = { + 'environment-and-energy': 'environment-energy', + 'health-and-wellness': 'health-wellness', + 'art-and-culture': 'art-culture', + nature: 'nature', + community: 'community', + finance: 'finance', + education: 'education', + equality: 'equality', + other: '1', + 'economic-and-infrastructure': 'economic-infrastructure', + ngo: 'non-profit', + technology: 'technology', + }; + const subCategoryToCategory = await queryRunner.query( + 'SELECT category.value, main_category.slug from category LEFT JOIN main_category on category."mainCategoryId" = main_category.id;', + ); + + const imageCategoryMapping = subCategoryToCategory.reduce(function ( + categoryImageKeyPair, + category: { value: string; slug: string }, + ) { + const bannerLink = mainCategorySlugToBannerMapping[category.slug] || '1'; + categoryImageKeyPair[category.value] = bannerLink; + return categoryImageKeyPair; + }, {}); + + for (const project of endaomentProjects) { + const mainnetAddress = project.mainnetAddress; + const projectAddresses = await queryRunner.query( + `SELECT * FROM project_address WHERE LOWER(address) = $1 AND "networkId" = $2 LIMIT 1`, + [mainnetAddress!.toLowerCase(), NETWORK_IDS.MAIN_NET], + ); + + const projectAddress = await projectAddresses?.[0]; + + if (!projectAddress) { + // eslint-disable-next-line no-console + console.log(`Could not find project address for ${mainnetAddress}`); + continue; + } + + // Insert the project-category relationship in a single query + const getCategoryNames = (nteeCode: string): string[] => { + const mapping = endaomentProjectCategoryMapping.find( + category => category.nteeCode === nteeCode, + ); + return mapping + ? [ + mapping.category1, + mapping.category2, + mapping.category3 || '', + mapping.category4 || '', + ].filter(Boolean) + : []; + }; + if (!project.nteeCode) { + // eslint-disable-next-line no-console + console.log(`Could not find nteeCode for ${mainnetAddress}`); + continue; + } + const categoryNames = getCategoryNames(String(project.nteeCode)); + const bannerImage = `/images/defaultProjectImages/${imageCategoryMapping[categoryNames[1]] || '1'}.png`; + await queryRunner.query(`UPDATE project SET image = $1 WHERE id = $2`, [ + bannerImage, + projectAddress.projectId, + ]); + // eslint-disable-next-line no-console + console.log( + `Updated project ${projectAddress.projectId} with image ${bannerImage}`, + ); + } + } + + public async down(_queryRunner: QueryRunner): Promise { + // No down migration + } +} diff --git a/package.json b/package.json index 075cfa477..17349eadc 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,8 @@ "test:qfRoundHistoryRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/qfRoundHistoryRepository.test.ts", "test:qfRoundService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/qfRoundService.test.ts", "test:project": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/entities/project.test.ts", + "test:projectsTab": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/server/adminJs/tabs/projectsTab.test.ts", + "test:syncUsersModelScore": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/syncUsersModelScore.test.ts", "test:notifyDonationsWithSegment": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/notifyDonationsWithSegment.test.ts", "test:checkProjectVerificationStatus": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/cronJobs/checkProjectVerificationStatus.test.ts", "test:statusReasonResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/statusReasonResolver.test.ts", @@ -166,6 +168,7 @@ "test:anchorContractAddressRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/anchorContractAddressRepository.test.ts", "test:recurringDonationRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/recurringDonationRepository.test.ts", "test:userPassportScoreRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/userPassportScoreRepository.test.ts", + "test:projectGivbackRepository": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/repositories/projectGivbackViewRepository.test.ts", "test:recurringDonationService": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/services/recurringDonationService.test.ts", "test:dbCronRepository": "NODE_ENV=test mocha -t 90000 ./test/pre-test-scripts.ts ./src/repositories/dbCronRepository.test.ts", "test:powerBoostingResolver": "NODE_ENV=test mocha ./test/pre-test-scripts.ts ./src/resolvers/powerBoostingResolver.test.ts", diff --git a/src/adapters/superFluid/superFluidMockAdapter.ts b/src/adapters/superFluid/superFluidMockAdapter.ts index bd99190f8..288f6ff52 100644 --- a/src/adapters/superFluid/superFluidMockAdapter.ts +++ b/src/adapters/superFluid/superFluidMockAdapter.ts @@ -2,6 +2,7 @@ import { FlowUpdatedEvent, SuperFluidAdapterInterface, } from './superFluidAdapterInterface'; +import { generateRandomString } from '../../utils/utils'; export class SuperFluidMockAdapter implements SuperFluidAdapterInterface { async streamPeriods() { @@ -88,12 +89,22 @@ export class SuperFluidMockAdapter implements SuperFluidAdapterInterface { return Promise.resolve(undefined); } - getFlowByTxHash(_params: { + getFlowByTxHash(params: { receiver: string; sender: string; flowRate: string; transactionHash: string; }): Promise { - return Promise.resolve(undefined); + const { receiver, sender, flowRate, transactionHash } = params; + return Promise.resolve({ + id: generateRandomString(20), + flowOperator: 'flowOperator', + flowRate, + transactionHash, + receiver, + sender, + token: '', + timestamp: String(new Date().getTime()), + }); } } diff --git a/src/entities/ProjectGivbackRankView.ts b/src/entities/ProjectGivbackRankView.ts new file mode 100644 index 000000000..5bff5b691 --- /dev/null +++ b/src/entities/ProjectGivbackRankView.ts @@ -0,0 +1,53 @@ +import { + OneToOne, + ViewColumn, + ViewEntity, + JoinColumn, + RelationId, + BaseEntity, + PrimaryColumn, + Column, + Index, +} from 'typeorm'; +import { Field, Float, Int, ObjectType } from 'type-graphql'; +import { Project } from '../entities/project'; +import { ColumnNumericTransformer } from '../utils/entities'; + +@ViewEntity('project_givback_rank_view', { synchronize: false }) +@Index('project_givback_rank_view_project_id_unique', ['projectId', 'round'], { + unique: true, +}) +// It's similar to ProjectPowerView, but with a small difference that it uses a different view +// That just includes project with isGivbackEligible = true +@ObjectType() +export class ProjectGivbackRankView extends BaseEntity { + @Field() + @ViewColumn() + @PrimaryColumn() + @RelationId( + (projectGivbackRankView: ProjectGivbackRankView) => + projectGivbackRankView.project, + ) + projectId: number; + + @ViewColumn() + @Field(_type => Float) + @Column('numeric', { + scale: 2, + transformer: new ColumnNumericTransformer(), + }) + totalPower: number; + + @Field(_type => Project) + @OneToOne(_type => Project, project => project.projectPower) + @JoinColumn({ referencedColumnName: 'id' }) + project: Project; + + @ViewColumn() + @Field(_type => Int) + powerRank: number; + + @ViewColumn() + @Field(_type => Int) + round: number; +} diff --git a/src/entities/donation.ts b/src/entities/donation.ts index b8fe75a25..ca04587e9 100644 --- a/src/entities/donation.ts +++ b/src/entities/donation.ts @@ -77,7 +77,7 @@ export class Donation extends BaseEntity { @Field() @Column('boolean', { default: false }) // https://github.com/Giveth/impact-graph/issues/407#issuecomment-1066892258 - isProjectVerified: boolean; + isProjectGivbackEligible: boolean; @Field() @Column('text', { default: DONATION_STATUS.PENDING }) diff --git a/src/entities/entities.ts b/src/entities/entities.ts index feeb0d0f6..0e5e204a5 100644 --- a/src/entities/entities.ts +++ b/src/entities/entities.ts @@ -51,6 +51,7 @@ import { ProjectActualMatchingView } from './ProjectActualMatchingView'; import { ProjectSocialMedia } from './projectSocialMedia'; import { DraftRecurringDonation } from './draftRecurringDonation'; import { UserQfRoundModelScore } from './userQfRoundModelScore'; +import { ProjectGivbackRankView } from './ProjectGivbackRankView'; export const getEntities = (): DataSourceOptions['entities'] => { return [ @@ -118,5 +119,7 @@ export const getEntities = (): DataSourceOptions['entities'] => { AnchorContractAddress, RecurringDonation, DraftRecurringDonation, + + ProjectGivbackRankView, ]; }; diff --git a/src/entities/project.ts b/src/entities/project.ts index c0b02bcf6..8a1a386c0 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -77,10 +77,12 @@ export enum SortingField { InstantBoosting = 'InstantBoosting', ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds', EstimatedMatching = 'EstimatedMatching', + BestMatch = 'BestMatch', } export enum FilterField { Verified = 'verified', + IsGivbackEligible = 'isGivbackEligible', AcceptGiv = 'givingBlocksId', AcceptFundOnGnosis = 'acceptFundOnGnosis', AcceptFundOnMainnet = 'acceptFundOnMainnet', @@ -408,6 +410,10 @@ export class Project extends BaseEntity { // @Column({ type: 'boolean', default: false }) // tunnableQf?: boolean; + @Field(_type => Boolean, { nullable: true }) + @Column({ type: 'boolean', default: false }) + isGivbackEligible: boolean; + @Field(_type => String) @Column({ type: 'enum', diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index 7d7ff7143..589cf4318 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -15,6 +15,11 @@ import { User, UserRole } from '../entities/user'; import { countUniqueDonorsAndSumDonationValueUsd, createDonation, + donationsNumberPerDateRange, + donationsTotalAmountPerDateRange, + donationsTotalAmountPerDateRangeByMonth, + donationsTotalNumberPerDateRangeByMonth, + donorsCountPerDateByMonthAndYear, fillQfRoundDonationsUserScores, findDonationById, findDonationsByProjectIdWhichUseDonationBox, @@ -29,6 +34,7 @@ import { QfRound } from '../entities/qfRound'; import { Project } from '../entities/project'; import { refreshProjectEstimatedMatchingView } from '../services/projectViewsService'; import { calculateEstimateMatchingForProjectById } from '../utils/qfUtils'; +import { ORGANIZATION_LABELS } from '../entities/organization'; import { setPowerRound } from './powerRoundRepository'; describe('createDonation test cases', createDonationTestCases); @@ -62,12 +68,481 @@ describe( 'isVerifiedDonationExistsInQfRound() test cases', isVerifiedDonationExistsInQfRoundTestCases, ); +describe( + 'donationsTotalAmountPerDateRange() test cases', + donationsTotalAmountPerDateRangeTestCases, +); describe('findDonationsToGiveth() test cases', findDonationsToGivethTestCases); +describe( + 'donationsTotalAmountPerDateRangeByMonth() test cases', + donationsTotalAmountPerDateRangeByMonthTestCases, +); +describe( + 'donationsTotalNumberPerDateRangeByMonth() test cases', + donationsTotalNumberPerDateRangeByMonthTestCase, +); +describe( + 'donationsNumberPerDateRange() test cases', + donationsNumberPerDateRangeTestCases, +); +describe( + 'donorsCountPerDateByMonthAndYear() test cases', + donorsCountPerDateByMonthAndYearTestCase, +); +describe('donorsCountPerDate() test cases', donorsCountPerDateTestCases); + describe( 'getSumOfGivbackEligibleDonationsForSpecificRound() test cases', getSumOfGivbackEligibleDonationsForSpecificRoundTestCases, ); +function donorsCountPerDateByMonthAndYearTestCase() { + it('should return per month number of donations for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationStart = moment().add(30, 'months'); + const donationStart1month = moment().add(31, 'month'); + const donationStart2month = moment().add(32, 'month'); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 30, + }), + SEED_DATA.FIRST_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 30, + }), + SEED_DATA.THIRD_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 20, + }), + SEED_DATA.THIRD_USER.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 30, + }), + SEED_DATA.FIRST_USER.id, + endaomentProject.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 40, + }), + user.id, + endaomentProject.id, + ); + const expectedReturnForAllProjects: number[] = [2, 2, 2]; + const expectedReturnEndaomentProjects: number[] = [1, 1, 1]; + + const fromDate = donationStart.toISOString(true); + const toDate = donationStart2month.toISOString(true); + const actualReturnEndaomentProjects = + await donorsCountPerDateByMonthAndYear(fromDate, toDate, undefined, true); + const actualReturnAllProjects = await donorsCountPerDateByMonthAndYear( + fromDate, + toDate, + ); + const endaomentProjectsReturns: number[] = actualReturnEndaomentProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + const allProjectsReturns: number[] = actualReturnAllProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + + assert.deepEqual(expectedReturnForAllProjects, allProjectsReturns); + assert.deepEqual(expectedReturnEndaomentProjects, endaomentProjectsReturns); + }); +} + +function donorsCountPerDateTestCases() { + it('should return total donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(221, 'days').toDate(), + valueUsd: 30, + }), + SEED_DATA.FIRST_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(221, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const fromDate = moment().add(220, 'days').format('YYYY/MM/DD'); + const toDate = moment().add(222, 'days').toDate().toDateString(); + const totalDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + ); + const endaomentDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + undefined, + undefined, + true, + ); + assert.equal(totalDonationInTimeFrame, 2); + assert.equal(endaomentDonationInTimeFrame, 1); + }); +} + +function donationsTotalNumberPerDateRangeByMonthTestCase() { + it('should return per month number of donations for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationStart = moment().add(20, 'months'); + const donationStart1month = moment().add(21, 'month'); + const donationStart2month = moment().add(22, 'month'); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 30, + }), + user.id, + endaomentProject.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 40, + }), + user.id, + endaomentProject.id, + ); + const expectedReturnForAllProjects: number[] = [2, 2, 2]; + const expectedReturnEndaomentProjects: number[] = [1, 1, 1]; + + const fromDate = donationStart.toISOString(true); + const toDate = donationStart2month.toISOString(true); + const actualReturnEndaomentProjects = + await donationsTotalNumberPerDateRangeByMonth( + fromDate, + toDate, + undefined, + undefined, + true, + ); + const actualReturnAllProjects = + await donationsTotalNumberPerDateRangeByMonth(fromDate, toDate); + const endaomentProjectsReturns: number[] = actualReturnEndaomentProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + const allProjectsReturns: number[] = actualReturnAllProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + + assert.deepEqual(expectedReturnForAllProjects, allProjectsReturns); + assert.deepEqual(expectedReturnEndaomentProjects, endaomentProjectsReturns); + }); +} + +function donationsNumberPerDateRangeTestCases() { + it('should return total donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(445, 'days').toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(445, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const fromDate = moment().add(444, 'days').format('YYYY/MM/DD'); + const toDate = moment().add(446, 'days').toDate().toDateString(); + const totalDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + ); + const endaomentDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + undefined, + undefined, + true, + ); + assert.equal(totalDonationInTimeFrame, 2); + assert.equal(endaomentDonationInTimeFrame, 1); + }); +} + +function donationsTotalAmountPerDateRangeByMonthTestCases() { + it('should return per month donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationStart = moment().add(10, 'months'); + const donationStart1month = moment().add(11, 'month'); + const donationStart2month = moment().add(12, 'month'); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const donationValueToNonEndaomentinUSD1 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + const donationValueToNonEndaomentinUSD2 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 40, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + const donationValueToNonEndaomentinUSD3 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + const donationValueToEndaomentinUSD1 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const donationValueToEndaomentinUSD2 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 30, + }), + user.id, + endaomentProject.id, + ); + + const donationValueToEndaomentinUSD3 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 40, + }), + user.id, + endaomentProject.id, + ); + const expectedReturnForAllProjects = [ + donationValueToNonEndaomentinUSD1.valueUsd + + donationValueToEndaomentinUSD1.valueUsd, + donationValueToEndaomentinUSD2.valueUsd + + donationValueToNonEndaomentinUSD2.valueUsd, + donationValueToEndaomentinUSD3.valueUsd + + donationValueToNonEndaomentinUSD3.valueUsd, + ]; + + const expectedReturnEndaomentProjects = [ + donationValueToEndaomentinUSD1.valueUsd, + donationValueToEndaomentinUSD2.valueUsd, + donationValueToEndaomentinUSD3.valueUsd, + ]; + const fromDate = donationStart.toISOString(true); + const toDate = donationStart2month.toISOString(true); + const actualReturnEndaomentProjects = + await donationsTotalAmountPerDateRangeByMonth( + fromDate, + toDate, + undefined, + undefined, + true, + ); + const actualReturnAllProjects = + await donationsTotalAmountPerDateRangeByMonth(fromDate, toDate); + + assert.deepEqual( + expectedReturnEndaomentProjects, + actualReturnEndaomentProjects.map( + donationPerDate => donationPerDate.total, + ), + ); + assert.deepEqual( + expectedReturnForAllProjects, + actualReturnAllProjects.map(donationPerDate => donationPerDate.total), + ); + }); +} + +function donationsTotalAmountPerDateRangeTestCases() { + it('should return total donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const donationValueToNonEndaomentinUSD = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(66, 'days').toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + const donationValueToEndaomentinUSD = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(66, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const fromDate = moment().add(65, 'days').format('YYYY/MM/DD'); + const toDate = moment().add(67, 'days').toDate().toDateString(); + const totalDonationInTimeFrame = await donationsTotalAmountPerDateRange( + fromDate, + toDate, + ); + const endaomentDonationInTimeFrame = await donationsTotalAmountPerDateRange( + fromDate, + toDate, + undefined, + undefined, + true, + ); + assert.equal( + totalDonationInTimeFrame, + donationValueToEndaomentinUSD.valueUsd + + donationValueToNonEndaomentinUSD.valueUsd, + ); + assert.equal( + endaomentDonationInTimeFrame, + donationValueToEndaomentinUSD.valueUsd, + ); + }); +} + function fillQfRoundDonationsUserScoresTestCases() { let qfRound: QfRound; let qfRoundProject: Project; @@ -388,7 +863,7 @@ function createDonationTestCases() { const newDonation = await createDonation({ donationAnonymous: false, donorUser: user, - isProjectVerified: false, + isProjectGivbackEligible: false, isTokenEligibleForGivback: false, project, segmentNotified: false, @@ -1492,7 +1967,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1505,7 +1980,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1517,7 +1992,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, @@ -1535,7 +2010,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd3 * givbackFactor3, ); }); - it('should return correct value for specific round, exclude donations with isProjectVerified:false', async () => { + it('should return correct value for specific round, exclude donations with isProjectGivbackEligible:false', async () => { // 3 donations with 2 different donor const project = await saveProjectDirectlyToDb({ ...createProjectData(), @@ -1563,7 +2038,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1576,7 +2051,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1588,7 +2063,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: false, + isProjectGivbackEligible: false, }, donor2.id, project.id, @@ -1632,7 +2107,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1645,7 +2120,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1657,7 +2132,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound: 31234231, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, @@ -1700,7 +2175,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1713,7 +2188,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1736,7 +2211,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor3.id, project.id, @@ -1781,7 +2256,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1794,7 +2269,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1807,7 +2282,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound: 1231, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 4094b531b..4b649faaf 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -6,6 +6,7 @@ import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; import { logger } from '../utils/logger'; import { QfRound } from '../entities/qfRound'; import { ChainType } from '../types/network'; +import { ORGANIZATION_LABELS } from '../entities/organization'; import { AppDataSource } from '../orm'; import { getPowerRound } from './powerRoundRepository'; @@ -71,7 +72,7 @@ export const createDonation = async (data: { fromWalletAddress: string; transactionId: string; tokenAddress: string; - isProjectVerified: boolean; + isProjectGivbackEligible: boolean; donorUser: any; isTokenEligibleForGivback: boolean; segmentNotified: boolean; @@ -99,7 +100,7 @@ export const createDonation = async (data: { tokenAddress, project, isTokenEligibleForGivback, - isProjectVerified, + isProjectGivbackEligible, donationAnonymous, toWalletAddress, fromWalletAddress, @@ -128,7 +129,7 @@ export const createDonation = async (data: { tokenAddress, project, isTokenEligibleForGivback, - isProjectVerified, + isProjectGivbackEligible, createdAt: new Date(), segmentNotified: true, toWalletAddress, @@ -177,6 +178,7 @@ export const donationsTotalAmountPerDateRange = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select(`COALESCE(SUM(donation."valueUsd"), 0)`, 'sum') @@ -200,12 +202,23 @@ export const donationsTotalAmountPerDateRange = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + const donationsUsdAmount = await query.getRawOne(); query.cache( `donationsTotalAmountPerDateRange-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }-${onlyVerified || 'all'}`, + }-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -217,6 +230,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -243,6 +257,17 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -250,7 +275,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( query.cache( `donationsTotalAmountPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${networkId || 'all'}-${onlyVerified || 'all'}`, + }-${networkId || 'all'}-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -262,6 +287,7 @@ export const donationsNumberPerDateRange = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select(`COALESCE(COUNT(donation.id), 0)`, 'count') @@ -285,12 +311,23 @@ export const donationsNumberPerDateRange = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + const donationsUsdAmount = await query.getRawOne(); query.cache( `donationsTotalNumberPerDateRange-${fromDate || ''}-${toDate || ''}--${ networkId || 'all' - }-${onlyVerified || 'all'}`, + }-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -302,6 +339,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -327,6 +365,17 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -334,7 +383,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( query.cache( `donationsTotalNumberPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${networkId || 'all'}-${onlyVerified || 'all'}`, + }-${networkId || 'all'}-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -345,6 +394,7 @@ export const donorsCountPerDate = async ( fromDate?: string, toDate?: string, networkId?: number, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -364,11 +414,18 @@ export const donorsCountPerDate = async ( if (networkId) { query.andWhere(`donation."transactionNetworkId" = ${networkId}`); } + if (onlyEndaoment) { + query.leftJoin('donation.project', 'project'); + query.leftJoin('project.organization', 'organization'); + query.andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } query.cache( `donorsCountPerDate-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }`, + }-${onlyEndaoment || 'all'}`, 300000, ); @@ -413,6 +470,7 @@ export const donorsCountPerDateByMonthAndYear = async ( fromDate?: string, toDate?: string, networkId?: number, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -432,6 +490,14 @@ export const donorsCountPerDateByMonthAndYear = async ( query.andWhere(`donation."transactionNetworkId" = ${networkId}`); } + if (onlyEndaoment) { + query.leftJoin('donation.project', 'project'); + query.leftJoin('project.organization', 'organization'); + query + .andWhere('organization."label" = :label') + .setParameter('label', ORGANIZATION_LABELS.ENDAOMENT); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -439,7 +505,7 @@ export const donorsCountPerDateByMonthAndYear = async ( query.cache( `donorsCountPerDateByMonthAndYear-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }`, + } - ${onlyEndaoment || 'all'}`, 300000, ); @@ -501,7 +567,7 @@ export const getSumOfGivbackEligibleDonationsForSpecificRound = async (params: { SUM("donation"."valueUsd" * "donation"."givbackFactor") AS "totalUsdWithGivbackFactor" FROM "donation" WHERE "donation"."status" = 'verified' - AND "donation"."isProjectVerified" = true + AND "donation"."isProjectGivbackEligible" = true AND "donation"."powerRound" = $1 AND NOT EXISTS ( SELECT 1 diff --git a/src/repositories/projectGivbackViewRepository.test.ts b/src/repositories/projectGivbackViewRepository.test.ts new file mode 100644 index 000000000..736d3fe6b --- /dev/null +++ b/src/repositories/projectGivbackViewRepository.test.ts @@ -0,0 +1,209 @@ +import { assert } from 'chai'; +import { AppDataSource } from '../orm'; +import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; +import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; +import { + createProjectData, + generateRandomEtheriumAddress, + saveProjectDirectlyToDb, + saveUserDirectlyToDb, +} from '../../test/testUtils'; +import { + insertSinglePowerBoosting, + takePowerBoostingSnapshot, +} from './powerBoostingRepository'; +import { findPowerSnapshots } from './powerSnapshotRepository'; +import { addOrUpdatePowerSnapshotBalances } from './powerBalanceSnapshotRepository'; +import { setPowerRound } from './powerRoundRepository'; +import { + findProjectGivbackRankViewByProjectId, + getBottomGivbackRank, + refreshProjectGivbackRankView, +} from './projectGivbackViewRepository'; + +describe( + 'findProjectGivbackRankViewByProjectId test', + findProjectGivbackRankViewByProjectIdTestCases, +); + +describe('getBottomGivbackRank test cases', getBottomGivbackRankTestCases); + +function getBottomGivbackRankTestCases() { + beforeEach(async () => { + await AppDataSource.getDataSource().query( + 'truncate power_snapshot cascade', + ); + await PowerBalanceSnapshot.clear(); + await PowerBoostingSnapshot.clear(); + }); + + it('should return bottomPowerRank correctly', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project1 = await saveProjectDirectlyToDb(createProjectData()); + const project2 = await saveProjectDirectlyToDb(createProjectData()); + await saveProjectDirectlyToDb(createProjectData()); + await saveProjectDirectlyToDb(createProjectData()); + + const roundNumber = project1.id * 10; + + await insertSinglePowerBoosting({ + user, + project: project1, + percentage: 10, + }); + await insertSinglePowerBoosting({ + user, + project: project2, + percentage: 20, + }); + + await takePowerBoostingSnapshot(); + const [powerSnapshots] = await findPowerSnapshots(); + const snapshot = powerSnapshots[0]; + + snapshot.blockNumber = 1; + snapshot.roundNumber = roundNumber; + await snapshot.save(); + + await addOrUpdatePowerSnapshotBalances({ + userId: user.id, + powerSnapshotId: snapshot.id, + balance: 100, + }); + + await setPowerRound(roundNumber); + await refreshProjectGivbackRankView(); + + const bottomPowerRank = await getBottomGivbackRank(); + assert.equal(bottomPowerRank, 3); + }); + it('should return bottomPowerRank correctly and not consider project that are not isGivbackEligible but are verified', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + isGivbackEligible: false, + verified: true, + }); + const project2 = await saveProjectDirectlyToDb(createProjectData()); + await saveProjectDirectlyToDb(createProjectData()); + await saveProjectDirectlyToDb(createProjectData()); + + const roundNumber = project1.id * 10; + + await insertSinglePowerBoosting({ + user, + project: project1, + percentage: 10, + }); + await insertSinglePowerBoosting({ + user, + project: project2, + percentage: 20, + }); + + await takePowerBoostingSnapshot(); + const [powerSnapshots] = await findPowerSnapshots(); + const snapshot = powerSnapshots[0]; + + snapshot.blockNumber = 1; + snapshot.roundNumber = roundNumber; + await snapshot.save(); + + await addOrUpdatePowerSnapshotBalances({ + userId: user.id, + powerSnapshotId: snapshot.id, + balance: 100, + }); + + await setPowerRound(roundNumber); + await refreshProjectGivbackRankView(); + + const bottomPowerRank = await getBottomGivbackRank(); + assert.equal(bottomPowerRank, 2); + }); +} + +function findProjectGivbackRankViewByProjectIdTestCases() { + beforeEach(async () => { + await AppDataSource.getDataSource().query( + 'truncate power_snapshot cascade', + ); + await PowerBalanceSnapshot.clear(); + await PowerBoostingSnapshot.clear(); + }); + + it('Return project rank correctly', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project1 = await saveProjectDirectlyToDb(createProjectData()); + + const roundNumber = project1.id * 10; + + await insertSinglePowerBoosting({ + user, + project: project1, + percentage: 10, + }); + + await takePowerBoostingSnapshot(); + const [powerSnapshots] = await findPowerSnapshots(); + const snapshot = powerSnapshots[0]; + + snapshot.blockNumber = 1; + snapshot.roundNumber = roundNumber; + await snapshot.save(); + + await addOrUpdatePowerSnapshotBalances({ + userId: user.id, + powerSnapshotId: snapshot.id, + balance: 100, + }); + + await setPowerRound(roundNumber); + await refreshProjectGivbackRankView(); + const projectPower = await findProjectGivbackRankViewByProjectId( + project1.id, + ); + assert.isOk(projectPower); + assert.equal(projectPower?.powerRank, 1); + assert.equal(projectPower?.totalPower, 10); + }); + it('Return project rank correctly and not consider project that are not isGivbackEligible but are verified', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + isGivbackEligible: false, + verified: true, + }); + + const roundNumber = project1.id * 10; + + await insertSinglePowerBoosting({ + user, + project: project1, + percentage: 10, + }); + + await takePowerBoostingSnapshot(); + const [powerSnapshots] = await findPowerSnapshots(); + const snapshot = powerSnapshots[0]; + + snapshot.blockNumber = 1; + snapshot.roundNumber = roundNumber; + await snapshot.save(); + + await addOrUpdatePowerSnapshotBalances({ + userId: user.id, + powerSnapshotId: snapshot.id, + balance: 100, + }); + + await setPowerRound(roundNumber); + await refreshProjectGivbackRankView(); + const projectPower = await findProjectGivbackRankViewByProjectId( + project1.id, + ); + assert.isOk(projectPower); + assert.equal(projectPower?.powerRank, 1); + assert.equal(projectPower?.totalPower, 0); + }); +} diff --git a/src/repositories/projectGivbackViewRepository.ts b/src/repositories/projectGivbackViewRepository.ts new file mode 100644 index 000000000..ed20e75be --- /dev/null +++ b/src/repositories/projectGivbackViewRepository.ts @@ -0,0 +1,34 @@ +import { logger } from '../utils/logger'; +import { AppDataSource } from '../orm'; +import { ProjectGivbackRankView } from '../entities/ProjectGivbackRankView'; + +export const refreshProjectGivbackRankView = async (): Promise => { + logger.debug('Refresh project_givback_rank_view materialized view'); + try { + return AppDataSource.getDataSource().query( + ` + REFRESH MATERIALIZED VIEW CONCURRENTLY project_givback_rank_view + `, + ); + } catch (e) { + logger.error('refreshProjectGivbackRankView() error', e); + } +}; + +export const getBottomGivbackRank = async (): Promise => { + try { + const powerRank = await AppDataSource.getDataSource().query(` + SELECT MAX("powerRank") FROM project_givback_rank_view + `); + return Number(powerRank[0].max); + } catch (e) { + logger.error('getBottomGivbackRank error', e); + throw new Error('Error in getting last power rank'); + } +}; + +export const findProjectGivbackRankViewByProjectId = async ( + projectId: number, +): Promise => { + return ProjectGivbackRankView.findOne({ where: { projectId } }); +}; diff --git a/src/repositories/projectRepository.test.ts b/src/repositories/projectRepository.test.ts index 6af02d8ea..7ca545787 100644 --- a/src/repositories/projectRepository.test.ts +++ b/src/repositories/projectRepository.test.ts @@ -558,7 +558,6 @@ function removeProjectAndRelatedEntitiesTestCase() { FeaturedUpdate.create({ projectId: project.id }).save(), SocialProfile.create({ projectId: project.id }).save(), ]); - const relatedEntitiesBefore = await Promise.all([ Donation.findOne({ where: { projectId: project.id } }), Reaction.findOne({ where: { projectId: project.id } }), diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index b314051ef..34bb4f907 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -22,6 +22,7 @@ import { ProjectSocialMedia } from '../entities/projectSocialMedia'; import { ProjectStatusHistory } from '../entities/projectStatusHistory'; import { Reaction } from '../entities/reaction'; import { SocialProfile } from '../entities/socialProfile'; +import { PreviousRoundRank } from '../entities/previousRoundRank'; export const findProjectById = (projectId: number): Promise => { // return Project.findOne({ id: projectId }); @@ -198,7 +199,8 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { break; case SortingField.GIVPower: query - .orderBy(`project.verified`, OrderDirection.DESC) + .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition + .addOrderBy('project.verified', 'DESC') // Secondary sorting condition .addOrderBy( 'projectPower.totalPower', OrderDirection.DESC, @@ -207,7 +209,8 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { break; case SortingField.InstantBoosting: // This is our default sorting query - .orderBy(`project.verified`, OrderDirection.DESC) + .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition + .addOrderBy('project.verified', 'DESC') // Secondary sorting condition .addOrderBy( 'projectInstantPower.totalPower', OrderDirection.DESC, @@ -232,7 +235,8 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { OrderDirection.DESC, 'NULLS LAST', ) - .addOrderBy(`project.verified`, OrderDirection.DESC); + .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition + .addOrderBy('project.verified', 'DESC'); // Secondary sorting condition } break; case SortingField.EstimatedMatching: @@ -244,13 +248,18 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { OrderDirection.DESC, 'NULLS LAST', ) - .addOrderBy(`project.verified`, OrderDirection.DESC); + .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition + .addOrderBy('project.verified', 'DESC'); // Secondary sorting condition } break; + case SortingField.BestMatch: + break; + default: query - .orderBy('projectInstantPower.totalPower', OrderDirection.DESC) - .addOrderBy(`project.verified`, OrderDirection.DESC); + .addOrderBy('projectInstantPower.totalPower', OrderDirection.DESC) + .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition + .addOrderBy('project.verified', 'DESC'); // Secondary sorting condition break; } @@ -270,7 +279,7 @@ export const projectsWithoutUpdateAfterTimeFrame = async ( 'project.title', ]) .where('project.isImported = false') - .andWhere('project.verified = true') + .andWhere('project.isGivbackEligible = true') .andWhere( '(project.verificationStatus NOT IN (:...statuses) OR project.verificationStatus IS NULL)', { @@ -324,14 +333,6 @@ export const verifyMultipleProjects = async (params: { verified: boolean; projectsIds: string[] | number[]; }): Promise => { - if (params.verified) { - await Project.query(` - UPDATE project - SET "verificationStatus" = NULL - WHERE id IN (${params.projectsIds?.join(',')}) - `); - } - return Project.createQueryBuilder('project') .update(Project, { verified: params.verified, @@ -381,7 +382,6 @@ export const verifyProject = async (params: { throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); project.verified = params.verified; - if (params.verified) project.verificationStatus = null; // reset this field return project.save(); }; @@ -600,6 +600,11 @@ export const removeProjectAndRelatedEntities = async ( .where('projectId = :projectId', { projectId }) .execute(); + await PreviousRoundRank.createQueryBuilder() + .delete() + .where('projectId = :projectId', { projectId }) + .execute(); + await Project.createQueryBuilder() .delete() .where('id = :id', { id: projectId }) diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index df600f776..7324b22e8 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -9,6 +9,7 @@ import { ProjectRegistry, ProjectVerificationForm, } from '../entities/projectVerificationForm'; +import { Project } from '../entities/project'; import { findProjectById } from './projectRepository'; import { findUserById } from './userRepository'; import { i18n, translationErrorMessagesKeys } from '../utils/errorMessages'; @@ -376,3 +377,41 @@ export const getVerificationFormByProjectId = async ( .leftJoinAndSelect('project_verification_form.user', 'user') .getOne(); }; + +export const approveProject = async (params: { + approved: boolean; + projectId: number; +}): Promise => { + const project = await Project.findOne({ where: { id: params.projectId } }); + + if (!project) + throw new Error(i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND)); + + project.isGivbackEligible = params.approved; + if (params.approved) project.verificationStatus = null; // reset this field + + return project.save(); +}; + +export const approveMultipleProjects = async (params: { + approved: boolean; + projectsIds: string[] | number[]; +}): Promise => { + if (params.approved) { + await Project.query(` + UPDATE project + SET "verificationStatus" = NULL + WHERE id IN (${params.projectsIds?.join(',')}) + `); + } + + return Project.createQueryBuilder('project') + .update(Project, { + isGivbackEligible: params.approved, + }) + .where('project.id IN (:...ids)') + .setParameter('ids', params.projectsIds) + .returning('*') + .updateEntity(true) + .execute(); +}; diff --git a/src/repositories/qfRoundRepository.test.ts b/src/repositories/qfRoundRepository.test.ts index beacac09f..adab54b26 100644 --- a/src/repositories/qfRoundRepository.test.ts +++ b/src/repositories/qfRoundRepository.test.ts @@ -17,6 +17,7 @@ import { getProjectDonationsSqrtRootSum, getQfRoundTotalSqrtRootSumSquared, getQfRoundStats, + findUsersWithoutMBDScoreInActiveAround, } from './qfRoundRepository'; import { Project } from '../entities/project'; import { refreshProjectEstimatedMatchingView } from '../services/projectViewsService'; @@ -26,6 +27,11 @@ describe( 'getProjectDonationsSqrtRootSum test cases', getProjectDonationsSqrRootSumTests, ); + +describe( + 'findUsersWithoutMBDScoreInActiveAround test cases', + findUsersWithoutMBDScoreInActiveAroundTestCases, +); describe( 'getQfRoundTotalProjectsDonationsSum test cases', getQfRoundTotalProjectsDonationsSumTestCases, @@ -41,6 +47,56 @@ describe( describe('findQfRoundById test cases', findQfRoundByIdTestCases); describe('findQfRoundBySlug test cases', findQfRoundBySlugTestCases); +function findUsersWithoutMBDScoreInActiveAroundTestCases() { + it('should find users without score that donated in the round', async () => { + await QfRound.update({}, { isActive: false }); + const qfRound = QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + minimumPassportScore: 8, + slug: new Date().getTime().toString(), + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }); + await qfRound.save(); + const project = await saveProjectDirectlyToDb(createProjectData()); + project.qfRounds = [qfRound]; + await project.save(); + + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveDonationDirectlyToDb( + { + ...createDonationData(), + segmentNotified: false, + qfRoundId: qfRound.id, + status: 'verified', + }, + user.id, + project.id, + ); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + segmentNotified: false, + qfRoundId: qfRound.id, + status: 'verified', + }, + user2.id, + project.id, + ); + + const userIds = await findUsersWithoutMBDScoreInActiveAround(); + assert.equal(userIds.length, 2); + assert.isTrue(userIds.includes(user.id) && userIds.includes(user2.id)); + + qfRound.isActive = false; + await qfRound.save(); + }); +} + function getProjectDonationsSqrRootSumTests() { let qfRound: QfRound; let project: Project; diff --git a/src/repositories/qfRoundRepository.ts b/src/repositories/qfRoundRepository.ts index 219371c85..91c42749b 100644 --- a/src/repositories/qfRoundRepository.ts +++ b/src/repositories/qfRoundRepository.ts @@ -15,6 +15,10 @@ const qfRoundEstimatedMatchingParamsCacheDuration = Number( process.env.QF_ROUND_ESTIMATED_MATCHING_CACHE_DURATION || 60000, ); +const qfRoundUsersMissedMBDScore = Number( + process.env.QF_ROUND_USERS_MISSED_SCORE || 0, +); + const qfRoundsCacheDuration = (config.get('QF_ROUND_AND_MAIN_CATEGORIES_CACHE_DURATION') as number) || 1000 * 60 * 2; @@ -172,6 +176,31 @@ export const findActiveQfRound = async ( return query.cache('findActiveQfRound', qfRoundsCacheDuration).getOne(); }; +export const findUsersWithoutMBDScoreInActiveAround = async (): Promise< + number[] +> => { + const activeQfRoundId = + (await findActiveQfRound())?.id || qfRoundUsersMissedMBDScore; + + if (!activeQfRoundId || activeQfRoundId === 0) return []; + + const usersMissingMDBScore = await QfRound.query( + ` + SELECT DISTINCT d."userId" + FROM public.donation d + LEFT JOIN user_qf_round_model_score uqrms ON d."userId" = uqrms."userId" AND uqrms."qfRoundId" = $1 + WHERE d."qfRoundId" = $1 + AND d.status = 'verified' + AND uqrms.id IS NULL + AND d."userId" IS NOT NULL + ORDER BY d."userId"; + `, + [activeQfRoundId], + ); + + return usersMissingMDBScore.map(user => user.userId); +}; + export const findQfRoundById = async (id: number): Promise => { return QfRound.createQueryBuilder('qf_round').where(`id = ${id}`).getOne(); }; diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 270703001..11b80dace 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -49,7 +49,6 @@ import { takePowerBoostingSnapshot, } from '../repositories/powerBoostingRepository'; import { setPowerRound } from '../repositories/powerRoundRepository'; -import { refreshProjectPowerView } from '../repositories/projectPowerViewRepository'; import { PowerBalanceSnapshot } from '../entities/powerBalanceSnapshot'; import { PowerBoostingSnapshot } from '../entities/powerBoostingSnapshot'; import { AppDataSource } from '../orm'; @@ -68,6 +67,7 @@ import { import { addNewAnchorAddress } from '../repositories/anchorContractAddressRepository'; import { createNewRecurringDonation } from '../repositories/recurringDonationRepository'; import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; +import { refreshProjectGivbackRankView } from '../repositories/projectGivbackViewRepository'; // eslint-disable-next-line @typescript-eslint/no-var-requires const moment = require('moment'); @@ -167,6 +167,113 @@ function totalDonationsPerCategoryPerDateTestCases() { ); assert.equal(foodTotal.totalUsd, donationToVerified.valueUsd); }); + + it('should return donation count as per category per time range for endaoment projects', async () => { + const allDonationResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + }); + const allEndaomentDonationResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + onlyEndaoment: true, + }, + }); + assert.isOk(allEndaomentDonationResponse); + assert.isOk(allDonationResponse); + const allEndaomentFoodDonations = + allEndaomentDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const allFoodDonations = + allDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const amountFoodDonation = allFoodDonations.totalUsd; + const amountEndaomentFoodDonation = allEndaomentFoodDonations.totalUsd; + + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationValueToEndaomentinUSD = 20; + const donationValueToNonEndaomentinUSD = 30; + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(45, 'days').toDate(), + valueUsd: donationValueToEndaomentinUSD, + }), + user.id, + endaomentProject.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(45, 'days').toDate(), + valueUsd: donationValueToNonEndaomentinUSD, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + const afterUpdateAllDonationResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + }); + const afterUpdateAllEndaomentDonationResponse = await axios.post( + graphqlUrl, + { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + onlyEndaoment: true, + }, + }, + ); + assert.isOk(afterUpdateAllDonationResponse); + assert.isOk(afterUpdateAllEndaomentDonationResponse); + const allEndaomentFoodDonationsAfterUpdate = + afterUpdateAllEndaomentDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const allFoodDonationsAfterUpdate = + afterUpdateAllDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const amountFoodDonationAfterUpdate = allFoodDonationsAfterUpdate.totalUsd; + const amountEndaomentFoodDonationAfterUpdate = + allEndaomentFoodDonationsAfterUpdate.totalUsd; + + assert.equal( + amountFoodDonation + + donationValueToNonEndaomentinUSD + + donationValueToEndaomentinUSD, + amountFoodDonationAfterUpdate, + ); + assert.equal( + amountEndaomentFoodDonation + donationValueToEndaomentinUSD, + amountEndaomentFoodDonationAfterUpdate, + ); + + const totalDonationsToEndaomentInTimeFrame = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + fromDate: moment().add(44, 'days').toDate(), + toDate: moment().add(46, 'days').toDate(), + onlyEndaoment: true, + }, + }); + + const foodTotal = + totalDonationsToEndaomentInTimeFrame.data.data.totalDonationsPerCategory.find( + d => d.title === 'food', + ); + + assert.equal(foodTotal.totalUsd, donationValueToEndaomentinUSD); + }); } function totalDonationsNumberPerDateTestCases() { @@ -220,6 +327,108 @@ function totalDonationsNumberPerDateTestCases() { 1, ); }); + it('should return donations count for endaoment projects per time range', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(202, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(202, 'days').toDate(), + valueUsd: 20, + }), + SEED_DATA.FIRST_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(202, 'days').toDate(), + valueUsd: 30, + }), + SEED_DATA.THIRD_USER.id, + SEED_DATA.NON_VERIFIED_PROJECT.id, + ); + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment() + .add(201, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(203, 'days').toDate().toISOString().split('T')[0], + }, + }); + const donationsResponseToVerifiedandEndaoment = await axios.post( + graphqlUrl, + { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment() + .add(201, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment() + .add(203, 'days') + .toDate() + .toISOString() + .split('T')[0], + onlyVerified: true, + onlyEndaoment: true, + }, + }, + ); + + const donationsResponseToVerified = await axios.post(graphqlUrl, { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment() + .add(201, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(203, 'days').toDate().toISOString().split('T')[0], + onlyVerified: true, + }, + }); + + assert.isNumber( + donationsResponse.data.data.totalDonationsNumberPerDate.total, + ); + assert.isTrue( + donationsResponse.data.data.totalDonationsNumberPerDate + .totalPerMonthAndYear.length > 0, + ); + assert.equal( + donationsResponse.data.data.totalDonationsNumberPerDate.total, + 3, + ); + assert.equal( + donationsResponseToVerified.data.data.totalDonationsNumberPerDate.total, + 2, + ); + assert.equal( + donationsResponseToVerifiedandEndaoment.data.data + .totalDonationsNumberPerDate.total, + 1, + ); + }); } function donorsCountPerDateTestCases() { @@ -307,6 +516,127 @@ function donorsCountPerDateTestCases() { total, ); }); + it('should return donors unique total count for endaoment projects in a time range', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as it's the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + }), + SEED_DATA.FIRST_USER.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + // anonymous donations count as separate + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + undefined, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + undefined, + SEED_DATA.SECOND_PROJECT.id, + ); + + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonors, + variables: { + fromDate: moment() + .add(509, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(511, 'days').toDate().toISOString().split('T')[0], + }, + }); + const donationsResponseForEndaoment = await axios.post(graphqlUrl, { + query: fetchTotalDonors, + variables: { + fromDate: moment() + .add(509, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(511, 'days').toDate().toISOString().split('T')[0], + onlyEndaoment: true, + }, + }); + assert.isOk(donationsResponse); + assert.isOk(donationsResponseForEndaoment); + // 2 unique donor and 2 anonymous + assert.equal(donationsResponse.data.data.totalDonorsCountPerDate.total, 4); + const total = + donationsResponse.data.data.totalDonorsCountPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + const totalForEndaoment = + donationsResponseForEndaoment.data.data.totalDonorsCountPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + assert.equal( + donationsResponse.data.data.totalDonorsCountPerDate.total, + total, + ); + // 2 donors : User Created and First User + assert.equal( + donationsResponseForEndaoment.data.data.totalDonorsCountPerDate.total, + 2, + ); + assert.equal( + donationsResponseForEndaoment.data.data.totalDonorsCountPerDate.total, + totalForEndaoment, + ); + }); } function newDonorsCountAndTotalDonationPerDateTestCases() { @@ -626,6 +956,79 @@ function donationsUsdAmountTestCases() { donationToVerified.valueUsd, ); }); + + it('should return total usd amount for donations to endaoment project made in a time range', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donationToNonEndaoment = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(250, 'days').toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + const donationToEndaoment = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(250, 'days').toDate(), + valueUsd: 10, + }), + user.id, + endaomentProject.id, + ); + + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsUsdAmount, + variables: { + fromDate: moment() + .add(249, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(251, 'days').toDate().toISOString().split('T')[0], + }, + }); + + const donationsResponseToEndaoment = await axios.post(graphqlUrl, { + query: fetchTotalDonationsUsdAmount, + variables: { + fromDate: moment() + .add(249, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(251, 'days').toDate().toISOString().split('T')[0], + onlyEndaoment: true, + }, + }); + + assert.isOk(donationsResponse.data.data); + assert.isOk(donationsResponseToEndaoment.data.data); + assert.equal( + donationsResponse.data.data.donationsTotalUsdPerDate.total, + donationToNonEndaoment.valueUsd + donationToEndaoment.valueUsd, + ); + const total = + donationsResponse.data.data.donationsTotalUsdPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + assert.equal( + donationsResponse.data.data.donationsTotalUsdPerDate.total, + total, + ); + assert.equal( + donationsResponseToEndaoment.data.data.donationsTotalUsdPerDate.total, + donationToEndaoment.valueUsd, + ); + }); } function donationsTestCases() { @@ -1572,7 +1975,7 @@ function createDonationTestCases() { assert.isTrue(donation?.isTokenEligibleForGivback); assert.equal(donation?.amount, amount); }); - it('should create GIV donation and fill averageGivbackFactor', async () => { + it(' should create GIV donation and fill averageGivbackFactor', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); const project2 = await saveProjectDirectlyToDb(createProjectData()); const user = await User.create({ @@ -1614,7 +2017,7 @@ function createDonationTestCases() { balance: 100, }); await setPowerRound(roundNumber); - await refreshProjectPowerView(); + await refreshProjectGivbackRankView(); const accessToken = await generateTestAccessToken(user.id); const saveDonationResponse = await axios.post( @@ -2388,7 +2791,7 @@ function createDonationTestCases() { errorMessages.PROJECT_NOT_FOUND, ); }); - it('should isProjectVerified be true after create donation for verified projects', async () => { + it('should isProjectGivbackEligible be true after create donation for verified projects', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), verified: true, @@ -2425,12 +2828,13 @@ function createDonationTestCases() { }, }); assert.isOk(donation); - assert.isTrue(donation?.isProjectVerified); + assert.isTrue(donation?.isProjectGivbackEligible); }); - it('should isProjectVerified be true after create donation for unVerified projects', async () => { + it('should isProjectGivbackEligible be true after create donation for unVerified projects', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), verified: false, + isGivbackEligible: true, }); const user = await User.create({ walletAddress: generateRandomEtheriumAddress(), @@ -2464,7 +2868,7 @@ function createDonationTestCases() { }, }); assert.isOk(donation); - assert.isFalse(donation?.isProjectVerified); + assert.isTrue(donation?.isProjectGivbackEligible); }); it('should throw exception when donating to draft projects', async () => { const project = await saveProjectDirectlyToDb({ @@ -3799,6 +4203,7 @@ function donationsByUserIdTestCases() { walletAddress: generateRandomEtheriumAddress(), categories: ['food1'], verified: true, + isGivbackEligible: true, listed: true, reviewStatus: ReviewStatus.Listed, giveBacks: false, @@ -3883,6 +4288,7 @@ function donationsByUserIdTestCases() { listed: true, reviewStatus: ReviewStatus.Listed, giveBacks: false, + isGivbackEligible: false, creationDate: new Date(), updatedAt: new Date(), latestUpdateCreationDate: new Date(), @@ -4999,7 +5405,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5012,7 +5418,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5025,7 +5431,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd3, powerRound: 1231, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, @@ -5075,7 +5481,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5088,7 +5494,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5119,7 +5525,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 4214a5e72..bb7207810 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -72,6 +72,7 @@ import { DraftDonation, } from '../entities/draftDonation'; import { nonZeroRecurringDonationsByProjectId } from '../repositories/recurringDonationRepository'; +import { ORGANIZATION_LABELS } from '../entities/organization'; import { getTokenPrice } from '../services/priceService'; import { findTokenByNetworkAndSymbol } from '../utils/tokenUtils'; @@ -315,6 +316,7 @@ export class DonationResolver { @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( @@ -355,6 +357,13 @@ export class DonationResolver { query.andWhere('projects.verified = true'); } + if (onlyEndaoment) { + query + .leftJoin('projects.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } return await query.getRawMany(); } catch (e) { logger.error('totalDonationsPerCategory query error', e); @@ -369,6 +378,7 @@ export class DonationResolver { @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( @@ -380,6 +390,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); const totalPerMonthAndYear = await donationsTotalAmountPerDateRangeByMonth( @@ -387,6 +398,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); return { @@ -406,6 +418,7 @@ export class DonationResolver { @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( @@ -417,6 +430,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); const totalPerMonthAndYear = await donationsTotalNumberPerDateRangeByMonth( @@ -424,6 +438,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); return { @@ -498,17 +513,24 @@ export class DonationResolver { @Arg('fromDate', { nullable: true }) fromDate?: string, @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( { fromDate, toDate }, resourcePerDateReportValidator, ); - const total = await donorsCountPerDate(fromDate, toDate, networkId); + const total = await donorsCountPerDate( + fromDate, + toDate, + networkId, + onlyEndaoment, + ); const totalPerMonthAndYear = await donorsCountPerDateByMonthAndYear( fromDate, toDate, networkId, + onlyEndaoment, ); return { total, @@ -919,7 +941,7 @@ export class DonationResolver { project, isTokenEligibleForGivback, isCustomToken, - isProjectVerified: project.verified, + isProjectGivbackEligible: project.isGivbackEligible, createdAt: new Date(), segmentNotified: false, toWalletAddress: toAddress, diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index e7235cd42..533e61209 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -220,9 +220,10 @@ function allProjectsTestCases() { ); assert.isTrue(firstProjectIsOlder); }); - it('should return projects, filter by verified, true', async () => { + + it('should return projects, filter by verified, true #1', async () => { // There is two verified projects so I just need to create a project with verified: false and listed:true - await saveProjectDirectlyToDb({ + const unverifiedProject = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), @@ -239,7 +240,97 @@ function allProjectsTestCases() { result.data.data.allProjects.projects.forEach(project => assert.isTrue(project.verified), ); + + // should not include unverified project in the response + assert.notExists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === unverifiedProject.id, + ), + ); + }); + it('should return projects, filter by verified, true #2', async () => { + const verified = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['Verified'], + sortingBy: SortingField.Newest, + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.verified), + ); + + // should not include unverified project in the response + assert.exists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === verified.id, + ), + ); }); + + it('should return projects, filter by isGivbackEligible, true #1', async () => { + // There is two isGivbackEligible projects so I just need to create a project with isGivbackEligible: false and listed:true + const notGivbackEligibleProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + isGivbackEligible: false, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['IsGivbackEligible'], + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.isGivbackEligible), + ); + + // should not include unisGivbackEligible project in the response + assert.notExists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === notGivbackEligibleProject.id, + ), + ); + }); + it('should return projects, filter by isGivbackEligible, true #2', async () => { + const givbackEligibleProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + isGivbackEligible: true, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['IsGivbackEligible'], + sortingBy: SortingField.Newest, + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.isGivbackEligible), + ); + + // should not include unisGivbackEligible project in the response + assert.exists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === givbackEligibleProject.id, + ), + ); + }); + it('should return projects, filter by acceptGiv, true', async () => { await saveProjectDirectlyToDb({ ...createProjectData(), diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index bafb581a1..74915f8ac 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -328,19 +328,22 @@ export class ProjectResolver { // .addSelect('similarity(project.description, :searchTerm)', 'desc_slm') // .addSelect('similarity(project.impactLocation, :searchTerm)', 'loc_slm') // .setParameter('searchTerm', searchTerm) + .addSelect( + `(CASE + WHEN project.title %> :searchTerm THEN 1 + ELSE 2 + END)`, + 'title_priority', + ) .andWhere( new Brackets(qb => { - qb.where('project.title %> :searchTerm ', { - searchTerm, - }) - .orWhere('project.description %> :searchTerm ', { - searchTerm, - }) - .orWhere('project.impactLocation %> :searchTerm', { - searchTerm, - }); + qb.where('project.title %> :searchTerm', { searchTerm }) + .orWhere('project.description %> :searchTerm', { searchTerm }) + .orWhere('project.impactLocation %> :searchTerm', { searchTerm }); }), ) + .orderBy('title_priority', 'ASC') + .setParameter('searchTerm', searchTerm) ); } diff --git a/src/resolvers/projectVerificationFormResolver.test.ts b/src/resolvers/projectVerificationFormResolver.test.ts index f13c94e67..c36c6ba75 100644 --- a/src/resolvers/projectVerificationFormResolver.test.ts +++ b/src/resolvers/projectVerificationFormResolver.test.ts @@ -75,6 +75,7 @@ function createProjectVerificationFormMutationTestCases() { statusId: ProjStatus.deactive, adminUserId: user.id, verified: false, + isGivbackEligible: false, listed: false, reviewStatus: ReviewStatus.NotListed, }); @@ -93,7 +94,6 @@ function createProjectVerificationFormMutationTestCases() { }, }, ); - assert.equal( result.data.data.createProjectVerificationForm.status, PROJECT_VERIFICATION_STATUSES.DRAFT, @@ -210,7 +210,8 @@ function createProjectVerificationFormMutationTestCases() { ...createProjectData(), statusId: ProjStatus.deactive, adminUserId: user.id, - verified: false, + verified: true, + isGivbackEligible: false, listed: false, reviewStatus: ReviewStatus.NotListed, }); diff --git a/src/resolvers/projectVerificationFormResolver.ts b/src/resolvers/projectVerificationFormResolver.ts index b5a6e170d..2217bc99c 100644 --- a/src/resolvers/projectVerificationFormResolver.ts +++ b/src/resolvers/projectVerificationFormResolver.ts @@ -224,7 +224,7 @@ export class ProjectVerificationFormResolver { ), ); } - if (project.verified) { + if (project.isGivbackEligible) { throw new Error( i18n.__(translationErrorMessagesKeys.PROJECT_IS_ALREADY_VERIFIED), ); diff --git a/src/routers/apiGivRoutes.ts b/src/routers/apiGivRoutes.ts index b05cbdc16..e7bf250cd 100644 --- a/src/routers/apiGivRoutes.ts +++ b/src/routers/apiGivRoutes.ts @@ -86,7 +86,7 @@ apiGivRouter.post( toWalletAddress, user: donor, anonymous, - isProjectVerified: project.verified, + isProjectGivbackEligible: project.isGivbackEligible, project, amount, valueUsd, diff --git a/src/server/adminJs/adminJs-types.ts b/src/server/adminJs/adminJs-types.ts index 4a13ece68..952f60738 100644 --- a/src/server/adminJs/adminJs-types.ts +++ b/src/server/adminJs/adminJs-types.ts @@ -43,7 +43,7 @@ export interface AdminJsDonationsQuery { createdAt?: string; currency?: string; transactionNetworkId?: string; - isProjectVerified?: string; + isProjectGivbackEligible?: string; qfRoundId?: string; } @@ -76,7 +76,7 @@ export const donationHeaders = [ 'id', 'transactionId', 'transactionNetworkId', - 'isProjectVerified', + 'isProjectGivbackEligible', 'status', 'toWalletAddress', 'fromWalletAddress', diff --git a/src/server/adminJs/adminJs.ts b/src/server/adminJs/adminJs.ts index 17e45c5f0..56a21ce4f 100644 --- a/src/server/adminJs/adminJs.ts +++ b/src/server/adminJs/adminJs.ts @@ -215,7 +215,7 @@ const getadminJsInstance = async () => { properties: { transactionNetworkId: 'Network', transactionId: 'txHash', - isProjectVerified: 'Givback Eligible', + isProjectGivbackEligible: 'Givback Eligible', disperseTxHash: 'disperseTxHash, this is optional, just for disperse transactions', }, diff --git a/src/server/adminJs/adminJsPermissions.test.ts b/src/server/adminJs/adminJsPermissions.test.ts index 6481f3076..b742275bd 100644 --- a/src/server/adminJs/adminJsPermissions.test.ts +++ b/src/server/adminJs/adminJsPermissions.test.ts @@ -88,10 +88,10 @@ const actionsPerRole = Object.freeze({ 'delete', 'edit', 'show', - 'verifyProject', + 'approveProject', 'makeEditableByUser', 'rejectProject', - 'verifyProjects', + 'approveProjects', 'rejectProjects', ], mainCategory: ['list', 'show', 'new', 'edit'], @@ -156,10 +156,10 @@ const actionsPerRole = Object.freeze({ 'delete', 'edit', 'show', - 'verifyProject', + 'approveProject', 'makeEditableByUser', 'rejectProject', - 'verifyProjects', + 'approveProjects', 'rejectProjects', ], mainCategory: ['list', 'show'], diff --git a/src/server/adminJs/adminJsPermissions.ts b/src/server/adminJs/adminJsPermissions.ts index 1fc29d0f1..3fbbe0510 100644 --- a/src/server/adminJs/adminJsPermissions.ts +++ b/src/server/adminJs/adminJsPermissions.ts @@ -12,6 +12,7 @@ export enum ResourceActions { LIST_PROJECT = 'listProject', UNLIST_PROJECT = 'unlistProject', VERIFY_PROJECT = 'verifyProject', + APPROVE_PROJECT = 'approveProject', REJECT_PROJECT = 'rejectProject', REVOKE_BADGE = 'revokeBadge', ACTIVATE_PROJECT = 'activateProject', @@ -20,6 +21,7 @@ export enum ResourceActions { ADD_FEATURED_PROJECT_UPDATE = 'addFeaturedProjectUpdate', MAKE_EDITABLE_BY_USER = 'makeEditableByUser', VERIFY_PROJECTS = 'verifyProjects', + APPROVE_PROJECTS = 'approveProjects', REJECT_PROJECTS = 'rejectProjects', ADD_PROJECT_TO_QF_ROUND = 'addToQfRound', REMOVE_PROJECT_FROM_QF_ROUND = 'removeFromQfRound', @@ -407,10 +409,10 @@ const projectVerificationFormPermissions = { delete: true, edit: true, show: true, - verifyProject: true, + approveProject: true, makeEditableByUser: true, rejectProject: true, - verifyProjects: true, + approveProjects: true, rejectProjects: true, }, [UserRole.OPERATOR]: { @@ -422,10 +424,10 @@ const projectVerificationFormPermissions = { delete: true, edit: true, show: true, - verifyProject: true, + approveProject: true, makeEditableByUser: true, rejectProject: true, - verifyProjects: true, + approveProjects: true, rejectProjects: true, }, [UserRole.CAMPAIGN_MANAGER]: { diff --git a/src/server/adminJs/tabs/components/CustomIdFilterComponent.tsx b/src/server/adminJs/tabs/components/CustomIdFilterComponent.tsx new file mode 100644 index 000000000..9c2e852e0 --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomIdFilterComponent.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { FormGroup, Label, Input } from '@adminjs/design-system'; + +const CustomIdFilterComponent = props => { + const { onChange, property, filter } = props; + const handleChange = event => { + onChange(property.path, event.target.value); + }; + + return ( + + + + + ); +}; + +export default CustomIdFilterComponent; diff --git a/src/server/adminJs/tabs/components/CustomProjectReferenceComponent.tsx b/src/server/adminJs/tabs/components/CustomProjectReferenceComponent.tsx new file mode 100644 index 000000000..eb2f71dfd --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomProjectReferenceComponent.tsx @@ -0,0 +1,14 @@ +// components/CustomProjectReferenceListComponent.jsx +import React from 'react'; +import { Link } from '@adminjs/design-system'; + +const CustomProjectReferenceListComponent = props => { + const { record } = props; + const projectId = + record.params.project?.id || record.params.projectId || 'N/A'; + const href = `/admin/resources/Project/records/${projectId}/show`; + + return Project {projectId}; +}; + +export default CustomProjectReferenceListComponent; diff --git a/src/server/adminJs/tabs/components/CustomProjectReferenceShowComponent.tsx b/src/server/adminJs/tabs/components/CustomProjectReferenceShowComponent.tsx new file mode 100644 index 000000000..4fd6baa84 --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomProjectReferenceShowComponent.tsx @@ -0,0 +1,20 @@ +// components/CustomProjectReferenceShowComponent.jsx +import React from 'react'; +import { Link, ValueGroup } from '@adminjs/design-system'; + +const CustomProjectReferenceShowComponent = props => { + const { record } = props; + const projectId = + record.params.project?.id || record.params.projectId || 'N/A'; + const href = `/admin/resources/Project/records/${projectId}/show`; + + return ( + + + {projectId} + + + ); +}; + +export default CustomProjectReferenceShowComponent; diff --git a/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx new file mode 100644 index 000000000..c86aa78ac --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx @@ -0,0 +1,191 @@ +// customQfRoundMultiUpdateComponent.js +import React, { useState } from 'react'; +import { Box, Button, Text, DatePicker, Select } from '@adminjs/design-system'; +import { FormGroup, Label, Input } from '@adminjs/design-system'; +import { ApiClient } from 'adminjs'; + +const RecordInput = ({ index, record, updateRecord, removeRecord }) => ( + + + + updateRecord(index, 'projectId', e.target.value)} + required + /> + + + + updateRecord(index, 'qfRoundId', e.target.value)} + required + /> + + + + updateRecord(index, 'matchingFund', e.target.value)} + required + /> + + + + + updateRecord(index, 'matchingFundAmount', e.target.value) + } + required + /> + + + + + updateRecord(index, 'matchingFundPriceUsd', e.target.value) + } + required + /> + + + + + updateRecord(index, 'matchingFundCurrency', selected.value) + } + /> + + + + + updateRecord(index, 'distributedFundTxHash', e.target.value) + } + /> + + + + + updateRecord(index, 'distributedFundNetwork', e.target.value) + } + /> + + + + updateRecord(index, 'distributedFundTxDate', date)} + /> + + + +); + +const CustomQfRoundMultiUpdateComponent = props => { + const [records, setRecords] = useState([ + { + projectId: '', + qfRoundId: '', + matchingFund: '', + matchingFundAmount: '', + matchingFundPriceUsd: '', + matchingFundCurrency: '', + distributedFundTxHash: '', + distributedFundNetwork: '', + distributedFundTxDate: null, + }, + ]); + const [message, setMessage] = useState(''); + + const api = new ApiClient(); + + const addRecord = () => { + setRecords([ + ...records, + { + projectId: '', + qfRoundId: '', + matchingFund: '', + matchingFundAmount: '', + matchingFundPriceUsd: '', + matchingFundCurrency: '', + distributedFundTxHash: '', + distributedFundNetwork: '', + distributedFundTxDate: null, + }, + ]); + }; + + const updateRecord = (index, field, value) => { + const updatedRecords = [...records]; + updatedRecords[index][field] = value; + setRecords(updatedRecords); + }; + + const removeRecord = index => { + const updatedRecords = records.filter((_, i) => i !== index); + setRecords(updatedRecords); + }; + + const handleSubmit = async event => { + event.preventDefault(); + setMessage(''); + + try { + const response = await api.resourceAction({ + resourceId: 'QfRoundHistory', + actionName: 'bulkUpdateQfRound', + data: { records }, + }); + + if (response.data.notice) { + if (typeof response.data.notice === 'string') { + setMessage(response.data.notice); + } else if (typeof response.data.notice.message === 'string') { + setMessage(response.data.notice.message); + } else { + setMessage('Update successful'); + } + } else { + setMessage('Update successful'); + } + } catch (error) { + setMessage(`Error: ${error.message}`); + } + }; + + return ( + + + Update Multiple QfRoundHistory Records + + {records.map((record, index) => ( + + ))} + + + {message && {message}} + + ); +}; + +export default CustomQfRoundMultiUpdateComponent; diff --git a/src/server/adminJs/tabs/components/CustomQfRoundReferenceComponent.tsx b/src/server/adminJs/tabs/components/CustomQfRoundReferenceComponent.tsx new file mode 100644 index 000000000..5887ed9db --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomQfRoundReferenceComponent.tsx @@ -0,0 +1,14 @@ +// components/CustomQfRoundReferenceListComponent.jsx +import React from 'react'; +import { Link } from '@adminjs/design-system'; + +const CustomQfRoundReferenceListComponent = props => { + const { record } = props; + const qfRoundId = + record.params.qfRound?.id || record.params.qfRoundId || 'N/A'; + const href = `/admin/resources/QfRound/records/${qfRoundId}/show`; + + return QF Round {qfRoundId}; +}; + +export default CustomQfRoundReferenceListComponent; diff --git a/src/server/adminJs/tabs/components/CustomQfRoundReferenceShowComponent.tsx b/src/server/adminJs/tabs/components/CustomQfRoundReferenceShowComponent.tsx new file mode 100644 index 000000000..976247555 --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomQfRoundReferenceShowComponent.tsx @@ -0,0 +1,20 @@ +// components/CustomQfRoundReferenceShowComponent.jsx +import React from 'react'; +import { Link, ValueGroup } from '@adminjs/design-system'; + +const CustomQfRoundReferenceShowComponent = props => { + const { record } = props; + const qfRoundId = + record.params.qfRound?.id || record.params.qfRoundId || 'N/A'; + const href = `/admin/resources/QfRound/records/${qfRoundId}/show`; + + return ( + + + {qfRoundId} + + + ); +}; + +export default CustomQfRoundReferenceShowComponent; diff --git a/src/server/adminJs/tabs/components/ProjectCategories.tsx b/src/server/adminJs/tabs/components/ProjectCategories.tsx new file mode 100644 index 000000000..5706131cb --- /dev/null +++ b/src/server/adminJs/tabs/components/ProjectCategories.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { withTheme } from 'styled-components'; +import { Section, Label } from '@adminjs/design-system'; + +const ProjectUpdates = props => { + const categories = props?.record?.params?.categories; + return ( +
+ +
+ {categories?.map(category => { + return ( +
+
+
+ +

+ {category.name || ''} - Id: {category.id} +

+
+
+ ); + })} +
+
+
+ ); +}; + +export default withTheme(ProjectUpdates); diff --git a/src/server/adminJs/tabs/donationTab.test.ts b/src/server/adminJs/tabs/donationTab.test.ts index 7d1980822..1f546bc10 100644 --- a/src/server/adminJs/tabs/donationTab.test.ts +++ b/src/server/adminJs/tabs/donationTab.test.ts @@ -218,7 +218,7 @@ function createDonationTestCases() { priceUsd: tokenPrice, txType: 'gnosisSafe', segmentNotified: true, - isProjectVerified: true, + isProjectGivbackEligible: true, }, }, { @@ -248,7 +248,7 @@ function createDonationTestCases() { assert.equal(donation.status, DONATION_STATUS.VERIFIED); assert.equal(donation.priceUsd, tokenPrice); assert.equal(donation.segmentNotified, true); - assert.equal(donation.isProjectVerified, true); + assert.equal(donation.isProjectGivbackEligible, true); assert.equal(donation.amount, 5); assert.equal( donation.fromWalletAddress.toLowerCase(), diff --git a/src/server/adminJs/tabs/donationTab.ts b/src/server/adminJs/tabs/donationTab.ts index e99fa5f9b..46b26b465 100644 --- a/src/server/adminJs/tabs/donationTab.ts +++ b/src/server/adminJs/tabs/donationTab.ts @@ -66,7 +66,7 @@ export const createDonation = async ( currency, priceUsd, txType, - isProjectVerified, + isProjectGivbackEligible, segmentNotified, } = request.payload; if (!priceUsd) { @@ -144,7 +144,7 @@ export const createDonation = async ( amount: transactionInfo?.amount, valueUsd: (transactionInfo?.amount as number) * priceUsd, status: DONATION_STATUS.VERIFIED, - isProjectVerified, + isProjectGivbackEligible, donationType, createdAt: new Date(transactionInfo?.timestamp * 1000), anonymous: true, @@ -251,10 +251,14 @@ export const buildDonationsQuery = ( referrerWallet: `%${queryStrings.referrerWallet}%`, }); - if (queryStrings.isProjectVerified) - query.andWhere('donation.isProjectVerified = :isProjectVerified', { - isProjectVerified: queryStrings.isProjectVerified === 'true', - }); + if (queryStrings.isProjectGivbackEligible) + query.andWhere( + 'donation.isProjectGivbackEligible = :isProjectGivbackEligible', + { + isProjectGivbackEligible: + queryStrings.isProjectGivbackEligible === 'true', + }, + ); if (queryStrings['createdAt~~from']) query.andWhere('donation."createdAt" >= :createdFrom', { @@ -402,7 +406,7 @@ const sendDonationsToGoogleSheet = async ( id: donation.id, transactionId: donation.transactionId, transactionNetworkId: donation.transactionNetworkId, - isProjectVerified: Boolean(donation.isProjectVerified), + isProjectGivbackEligible: Boolean(donation.isProjectGivbackEligible), status: donation.status, toWalletAddress: donation.toWalletAddress, fromWalletAddress: donation.fromWalletAddress, @@ -619,7 +623,7 @@ export const donationTab = { new: false, }, }, - isProjectVerified: { + isProjectGivbackEligible: { isVisible: { list: false, filter: false, @@ -765,7 +769,7 @@ export const donationTab = { isVisible: true, before: async (request: AdminJsRequestInterface) => { const availableFieldsForEdit = [ - 'isProjectVerified', + 'isProjectGivbackEligible', 'status', 'valueUsd', 'priceUsd', diff --git a/src/server/adminJs/tabs/projectVerificationTab.ts b/src/server/adminJs/tabs/projectVerificationTab.ts index 169cf453e..41abe11a5 100644 --- a/src/server/adminJs/tabs/projectVerificationTab.ts +++ b/src/server/adminJs/tabs/projectVerificationTab.ts @@ -18,6 +18,8 @@ import { AdminJsRequestInterface, } from '../adminJs-types'; import { + approveMultipleProjects, + approveProject, findProjectVerificationFormById, makeFormDraft, verifyForm, @@ -30,8 +32,6 @@ import { import { findProjectById, updateProjectWithVerificationForm, - verifyMultipleProjects, - verifyProject, } from '../../../repositories/projectRepository'; import { getNotificationAdapter } from '../../../adapters/adaptersFactory'; import { logger } from '../../../utils/logger'; @@ -82,12 +82,12 @@ export const setCommentEmailAndTimeStamps: After = async ( export const verifySingleVerificationForm = async ( context: AdminJsContextInterface, request: AdminJsRequestInterface, - verified: boolean, + approved: boolean, ) => { const { currentAdmin } = context; let responseMessage = ''; let responseType = 'success'; - const verificationStatus = verified + const verificationStatus = approved ? PROJECT_VERIFICATION_STATUSES.VERIFIED : PROJECT_VERIFICATION_STATUSES.REJECTED; const formId = Number(request?.params?.recordId); @@ -95,7 +95,7 @@ export const verifySingleVerificationForm = async ( try { if ( - verified && + approved && ![ PROJECT_VERIFICATION_STATUSES.REJECTED, PROJECT_VERIFICATION_STATUSES.SUBMITTED, @@ -108,7 +108,7 @@ export const verifySingleVerificationForm = async ( ); } if ( - !verified && + !approved && PROJECT_VERIFICATION_STATUSES.SUBMITTED !== verificationFormInDb?.status ) { throw new Error( @@ -124,9 +124,9 @@ export const verifySingleVerificationForm = async ( adminId: currentAdmin.id, }); const projectId = verificationForm.projectId; - const project = await verifyProject({ verified, projectId }); + const project = await approveProject({ approved, projectId }); - if (verified) { + if (approved) { await updateProjectWithVerificationForm(verificationForm, project); await getNotificationAdapter().projectVerified({ project, @@ -140,7 +140,7 @@ export const verifySingleVerificationForm = async ( } responseMessage = `Project(s) successfully ${ - verified ? 'verified' : 'rejected' + approved ? 'approved' : 'rejected' }`; } catch (error) { logger.error('verifyVerificationForm() error', error); @@ -226,16 +226,16 @@ export const makeEditableByUser = async ( }; }; -export const verifyVerificationForms = async ( +export const approveVerificationForms = async ( context: AdminJsContextInterface, request: AdminJsRequestInterface, - verified: boolean, + approved: boolean, ) => { const { records, currentAdmin } = context; let responseMessage = ''; let responseType = 'success'; try { - const verificationStatus = verified + const verificationStatus = approved ? PROJECT_VERIFICATION_STATUSES.VERIFIED : PROJECT_VERIFICATION_STATUSES.REJECTED; const formIds = request?.query?.recordIds?.split(','); @@ -248,7 +248,7 @@ export const verifyVerificationForms = async ( const projectsIds = projectsForms.raw.map(projectForm => { return projectForm.projectId; }); - const projects = await verifyMultipleProjects({ verified, projectsIds }); + const projects = await approveMultipleProjects({ approved, projectsIds }); const projectIds = projects.raw.map(project => { return project.id; @@ -270,7 +270,7 @@ export const verifyVerificationForms = async ( verificationForm.project, ); const { project } = verificationForm; - if (verified) { + if (approved) { await getNotificationAdapter().projectVerified({ project, }); @@ -283,7 +283,7 @@ export const verifyVerificationForms = async ( } } responseMessage = `Project(s) successfully ${ - verified ? 'verified' : 'rejected' + approved ? 'approved' : 'rejected' }`; } catch (error) { logger.error('verifyVerificationForm() error', error); @@ -613,13 +613,13 @@ export const projectVerificationTab = { ResourceActions.NEW, ), }, - verifyProject: { + approveProject: { actionType: 'record', isVisible: true, isAccessible: ({ currentAdmin }) => canAccessProjectVerificationFormAction( { currentAdmin }, - ResourceActions.VERIFY_PROJECT, + ResourceActions.APPROVE_PROJECT, ), handler: async (request, response, context) => { return verifySingleVerificationForm(context, request, true); @@ -652,16 +652,16 @@ export const projectVerificationTab = { }, component: false, }, - verifyProjects: { + approveProjects: { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => canAccessProjectVerificationFormAction( { currentAdmin }, - ResourceActions.VERIFY_PROJECTS, + ResourceActions.APPROVE_PROJECTS, ), handler: async (request, response, context) => { - return verifyVerificationForms(context, request, true); + return approveVerificationForms(context, request, true); }, component: false, }, @@ -674,7 +674,7 @@ export const projectVerificationTab = { ResourceActions.REJECT_PROJECTS, ), handler: async (request, response, context) => { - return verifyVerificationForms(context, request, false); + return approveVerificationForms(context, request, false); }, component: false, }, diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 832c5d930..b1009f4ef 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -35,6 +35,7 @@ import { addFeaturedProjectUpdate, exportProjectsWithFiltersToCsv, listDelist, + revokeGivbacksEligibility, updateStatusOfProjects, verifyProjects, } from './projectsTab'; @@ -452,8 +453,20 @@ function verifyProjectsTestCases() { recordIds: String(project.id), }, }, - true, // give priority to revoke badge - true, // revoke badge + false, + ); + await revokeGivbacksEligibility( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(project.id), + }, + }, ); const updatedProject = await findProjectById(project.id); @@ -527,15 +540,20 @@ function verifyProjectsTestCases() { assert.isTrue(updatedProject?.listed); assert.equal(updatedProject?.reviewStatus, ReviewStatus.Listed); assert.isTrue(project!.verificationStatus === RevokeSteps.Revoked); - assert.isTrue(updatedProject!.verificationStatus === null); + assert.isTrue( + updatedProject!.verificationStatus === project.verificationStatus, + ); assert.equal( updatedVerificationForm!.status, - PROJECT_VERIFICATION_STATUSES.VERIFIED, + PROJECT_VERIFICATION_STATUSES.DRAFT, + ); + assert.equal( + updatedVerificationForm!.isTermAndConditionsAccepted, + projectVerificationForm.isTermAndConditionsAccepted, ); - assert.equal(updatedVerificationForm!.isTermAndConditionsAccepted, true); assert.equal( updatedVerificationForm!.lastStep, - PROJECT_VERIFICATION_STEPS.SUBMIT, + projectVerificationForm.lastStep, ); }); @@ -616,12 +634,15 @@ function verifyProjectsTestCases() { assert.isTrue(updatedProject!.verificationStatus === RevokeSteps.Revoked); assert.equal( updatedVerificationForm!.status, - PROJECT_VERIFICATION_STATUSES.DRAFT, + PROJECT_VERIFICATION_STATUSES.VERIFIED, + ); + assert.equal( + updatedVerificationForm!.isTermAndConditionsAccepted, + projectVerificationForm.isTermAndConditionsAccepted, ); - assert.equal(updatedVerificationForm!.isTermAndConditionsAccepted, false); assert.equal( updatedVerificationForm!.lastStep, - PROJECT_VERIFICATION_STEPS.MANAGING_FUNDS, + projectVerificationForm.lastStep, ); }); @@ -921,13 +942,13 @@ function verifyMultipleProjectsTestCases() { where: { id: project2.id }, }); - assert.notEqual(project1Updated?.verificationStatus, 'revoked'); - assert.equal(project1Updated?.verificationStatus, null); + assert.equal(project1Updated?.verificationStatus, 'revoked'); + assert.notEqual(project1Updated?.verified, false); assert.equal(project1Updated?.verified, true); - assert.notEqual(project2Updated?.verificationStatus, 'reminder'); - assert.equal(project2Updated?.verificationStatus, null); + assert.equal(project2Updated?.verificationStatus, 'reminder'); + assert.notEqual(project2Updated?.verified, false); assert.equal(project2Updated?.verified, true); }); diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index d0cf6fbe2..49e2e29b9 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -55,6 +55,7 @@ import { User } from '../../../entities/user'; import { refreshProjectEstimatedMatchingView } from '../../../services/projectViewsService'; import { extractAdminJsReferrerUrlParams } from '../adminJs'; import { relateManyProjectsToQfRound } from '../../../repositories/qfRoundRepository2'; +import { Category } from '../../../entities/category'; // add queries depending on which filters were selected export const buildProjectsQuery = ( @@ -183,29 +184,72 @@ export const addFeaturedProjectUpdate = async ( }; }; +export const revokeGivbacksEligibility = async ( + context: AdminJsContextInterface, + request: AdminJsRequestInterface, +) => { + const { records, currentAdmin } = context; + try { + const projectIds = request?.query?.recordIds + ?.split(',') + ?.map(strId => Number(strId)) as number[]; + const updateParams = { isGivbackEligible: false }; + const projects = await Project.createQueryBuilder('project') + .update(Project, updateParams) + .where('project.id IN (:...ids)') + .setParameter('ids', projectIds) + .returning('*') + .updateEntity(true) + .execute(); + + for (const project of projects.raw) { + const projectWithAdmin = (await findProjectById(project.id)) as Project; + projectWithAdmin.verificationStatus = RevokeSteps.Revoked; + await projectWithAdmin.save(); + await getNotificationAdapter().projectBadgeRevoked({ + project: projectWithAdmin, + }); + const verificationForm = await getVerificationFormByProjectId(project.id); + if (verificationForm) { + await makeFormDraft({ + formId: verificationForm.id, + adminId: currentAdmin.id, + }); + } + } + await Promise.all([ + refreshUserProjectPowerView(), + refreshProjectPowerView(), + refreshProjectFuturePowerView(), + ]); + } catch (error) { + logger.error('revokeGivbacksEligibility() error', error); + throw error; + } + return { + redirectUrl: '/admin/resources/Project', + records: records.map(record => { + record.toJSON(context.currentAdmin); + }), + notice: { + message: 'Project(s) successfully revoked from Givbacks eligibility', + type: 'success', + }, + }; +}; + export const verifyProjects = async ( context: AdminJsContextInterface, request: AdminJsRequestInterface, - verified: boolean = true, - revokeBadge: boolean = false, + vouchedStatus: boolean = true, ) => { const { records, currentAdmin } = context; - // prioritize revokeBadge - const verificationStatus = revokeBadge ? false : verified; try { const projectIds = request?.query?.recordIds ?.split(',') ?.map(strId => Number(strId)) as number[]; const projectsBeforeUpdating = await findProjectsByIdArray(projectIds); - const updateParams = { verified: verificationStatus }; - - if (verificationStatus) { - await Project.query(` - UPDATE project - SET "verificationStatus" = NULL - WHERE id IN (${request?.query?.recordIds}) - `); - } + const updateParams = { verified: vouchedStatus }; const projects = await Project.createQueryBuilder('project') .update(Project, updateParams) @@ -218,11 +262,11 @@ export const verifyProjects = async ( for (const project of projects.raw) { if ( projectsBeforeUpdating.find(p => p.id === project.id)?.verified === - verificationStatus + vouchedStatus ) { logger.debug('verifying/unVerifying project but no changes happened', { projectId: project.id, - verificationStatus, + verificationStatus: vouchedStatus, }); // if project.verified have not changed, so we should not execute rest of the codes continue; @@ -231,42 +275,10 @@ export const verifyProjects = async ( project, status: project.status, userId: currentAdmin.id, - description: verified + description: vouchedStatus ? HISTORY_DESCRIPTIONS.CHANGED_TO_VERIFIED : HISTORY_DESCRIPTIONS.CHANGED_TO_UNVERIFIED, }); - const projectWithAdmin = (await findProjectById(project.id)) as Project; - - if (revokeBadge) { - projectWithAdmin.verificationStatus = RevokeSteps.Revoked; - await projectWithAdmin.save(); - await getNotificationAdapter().projectBadgeRevoked({ - project: projectWithAdmin, - }); - } else if (verificationStatus) { - await getNotificationAdapter().projectVerified({ - project: projectWithAdmin, - }); - } else { - await getNotificationAdapter().projectUnVerified({ - project: projectWithAdmin, - }); - } - - const verificationForm = await getVerificationFormByProjectId(project.id); - if (verificationForm) { - if (verificationStatus) { - await makeFormVerified({ - formId: verificationForm.id, - adminId: currentAdmin.id, - }); - } else { - await makeFormDraft({ - formId: verificationForm.id, - adminId: currentAdmin.id, - }); - } - } } await Promise.all([ @@ -285,7 +297,7 @@ export const verifyProjects = async ( }), notice: { message: `Project(s) successfully ${ - verificationStatus ? 'verified' : 'unverified' + vouchedStatus ? 'vouched' : 'unvouched' }`, type: 'success', }, @@ -446,6 +458,13 @@ export const addProjectsToQfRound = async ( }; }; +export const extractCategoryIds = (payload: any) => { + if (!payload) return; + return Object.keys(payload) + .filter(key => key.startsWith('categoryIds.')) + .map(key => payload[key]); +}; + export const addSingleProjectToQfRound = async ( context: AdminJsContextInterface, request: AdminJsRequestInterface, @@ -486,6 +505,14 @@ export const fillSocialProfileAndQfRounds: After< const projectUpdates = await findProjectUpdatesByProjectId(projectId); const project = await findProjectById(projectId); const adminJsBaseUrl = process.env.SERVER_URL; + let categories; + if (project) { + categories = await Category.createQueryBuilder('category') + .innerJoin('category.projects', 'projects') + .where('projects.id = :id', { id: project.id }) + .orderBy('category.name', 'ASC') + .getMany(); + } response.record = { ...record, params: { @@ -499,6 +526,11 @@ export const fillSocialProfileAndQfRounds: After< adminJsBaseUrl, }, }; + + if (categories) { + response.record.params.categoryIds = categories; + response.record.params.categories = categories; + } return response; }; @@ -660,7 +692,7 @@ export const projectsTab = { id: { isVisible: { list: false, - filter: false, + filter: true, show: true, edit: false, }, @@ -831,12 +863,36 @@ export const projectsTab = { edit: false, }, }, + categoryIds: { + type: 'reference', + isArray: true, + reference: 'Category', + isVisible: { + list: false, + filter: false, + show: true, + edit: true, + }, + components: { + show: adminJs.bundle('./components/ProjectCategories'), + }, + availableValues: async _record => { + const categories = await Category.createQueryBuilder('category') + .where('category.isActive = :isActive', { isActive: true }) + .orderBy('category.name', 'ASC') + .getMany(); + return categories.map(category => ({ + value: category.id, + label: `${category.id} - ${category.name}`, + })); + }, + }, isImported: { isVisible: { list: false, filter: true, show: true, - edit: false, + edit: true, }, }, totalReactions: { @@ -924,6 +980,24 @@ export const projectsTab = { isVisible: false, isAccessible: ({ currentAdmin }) => canAccessProjectAction({ currentAdmin }, ResourceActions.NEW), + before: async request => { + if (request.payload.categories) { + request.payload.categories = ( + request.payload.categories as string[] + ).map(id => ({ id: parseInt(id, 10) })); + } + return request; + }, + after: async response => { + const { request } = response; + const project = await Project.findOne({ + where: { id: request?.record?.id }, + }); + const categoryIds = extractCategoryIds(request.record.params); + await saveCategories(project!, categoryIds || []); + + return response; + }, }, bulkDelete: { isVisible: false, @@ -952,6 +1026,16 @@ export const projectsTab = { } const project = await findProjectById(Number(request.payload.id)); + if (project) { + await Category.query( + ` + DELETE FROM project_categories_category + WHERE "projectId" = $1 + `, + [project.id], + ); + } + if ( project && Number(request?.payload?.statusId) !== project?.status?.id @@ -1014,6 +1098,7 @@ export const projectsTab = { // We put these status changes in payload, so in after hook we would know to send notification for users request.payload.statusChanges = statusChanges.join(','); } + return request; }, after: async ( @@ -1151,10 +1236,13 @@ export const projectsTab = { }); } } + const categoryIds = extractCategoryIds(request.record.params); + await Promise.all([ refreshUserProjectPowerView(), refreshProjectFuturePowerView(), refreshProjectPowerView(), + saveCategories(project!, categoryIds || []), ]); return request; }, @@ -1196,7 +1284,7 @@ export const projectsTab = { }, component: false, }, - verify: { + approveVouched: { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => @@ -1209,7 +1297,7 @@ export const projectsTab = { }, component: false, }, - reject: { + removeVouched: { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => @@ -1222,8 +1310,7 @@ export const projectsTab = { }, component: false, }, - // the difference is that it sends another segment event - revokeBadge: { + revokeGivbacksEligible: { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => @@ -1231,8 +1318,8 @@ export const projectsTab = { { currentAdmin }, ResourceActions.REVOKE_BADGE, ), - handler: async (request, response, context) => { - return verifyProjects(context, request, false, true); + handler: async (request, _response, context) => { + return revokeGivbacksEligibility(context, request); }, component: false, }, @@ -1350,3 +1437,23 @@ export const projectsTab = { }, }, }; + +async function saveCategories(project: Project, categoryIds?: string[]) { + if (!project) return; + if (!categoryIds || categoryIds?.length === 0) return; + + await Category.query( + ` + DELETE FROM project_categories_category + WHERE "projectId" = $1 + `, + [project.id], + ); + + const categories = await Category.createQueryBuilder('category') + .where('category.id IN (:...ids)', { ids: categoryIds }) + .getMany(); + + project.categories = categories; + await project.save(); +} diff --git a/src/server/adminJs/tabs/qfRoundHistoryTab.ts b/src/server/adminJs/tabs/qfRoundHistoryTab.ts index d4fadb5cf..ce91e38d1 100644 --- a/src/server/adminJs/tabs/qfRoundHistoryTab.ts +++ b/src/server/adminJs/tabs/qfRoundHistoryTab.ts @@ -1,8 +1,8 @@ +import adminJs from 'adminjs'; import { canAccessQfRoundHistoryAction, ResourceActions, } from '../adminJsPermissions'; - import { QfRoundHistory } from '../../../entities/qfRoundHistory'; import { AdminJsContextInterface, @@ -48,21 +48,57 @@ export const qfRoundHistoryTab = { resource: QfRoundHistory, options: { properties: { - project: { + projectId: { isVisible: { - list: false, + list: true, edit: false, filter: true, show: true, }, + reference: 'Project', + position: 100, + type: 'reference', + custom: { + getValue: record => { + return record.params.project?.id || record.params.projectId; + }, + renderValue: (value, _record) => { + return value ? `Project ${value}` : 'N/A'; + }, + }, + components: { + list: adminJs.bundle('./components/CustomProjectReferenceComponent'), + show: adminJs.bundle( + './components/CustomProjectReferenceShowComponent', + ), + filter: adminJs.bundle('./components/CustomIdFilterComponent'), + }, }, - qfRound: { + qfRoundId: { isVisible: { - list: false, + list: true, edit: false, filter: true, show: true, }, + reference: 'QfRound', + position: 101, + type: 'reference', + custom: { + getValue: record => { + return record.params.qfRound?.id || record.params.qfRoundId; + }, + renderValue: (value, _record) => { + return value ? `QF Round ${value}` : 'N/A'; + }, + }, + components: { + list: adminJs.bundle('./components/CustomQfRoundReferenceComponent'), + show: adminJs.bundle( + './components/CustomQfRoundReferenceShowComponent', + ), + filter: adminJs.bundle('./components/CustomIdFilterComponent'), + }, }, uniqueDonors: { isVisible: true, @@ -135,6 +171,63 @@ export const qfRoundHistoryTab = { isAccessible: ({ currentAdmin }) => canAccessQfRoundHistoryAction({ currentAdmin }, ResourceActions.SHOW), }, + bulkUpdateQfRound: { + component: adminJs.bundle( + './components/CustomQfRoundMultiUpdateComponent', + ), + handler: async (request, _response, _context) => { + const { records } = request.payload; + const results: string[] = []; + + for (const record of records) { + const { + projectId, + qfRoundId, + matchingFund, + matchingFundAmount, + matchingFundPriceUsd, + matchingFundCurrency, + distributedFundTxHash, + distributedFundNetwork, + distributedFundTxDate, + } = record; + + const existingRecord = await QfRoundHistory.findOne({ + where: { projectId, qfRoundId }, + }); + + if (existingRecord) { + await QfRoundHistory.createQueryBuilder() + .update(QfRoundHistory) + .set({ + matchingFund, + matchingFundAmount, + matchingFundPriceUsd, + matchingFundCurrency, + distributedFundTxHash, + distributedFundNetwork, + distributedFundTxDate: new Date(distributedFundTxDate), + }) + .where('id = :id', { id: existingRecord.id }) + .execute(); + results.push( + `Updated: Project ${projectId}, Round ${qfRoundId}, Matching Fund: ${matchingFund}`, + ); + } else { + results.push( + `Project QfRoundHistory Not found for Project ${projectId}, Round ${qfRoundId}.`, + ); + } + } + + return { + notice: { + message: `Operations completed:\n${results.join('\n')}`, + type: 'success', + }, + }; + }, + }, updateQfRoundHistories: { actionType: 'resource', isVisible: true, diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index 129a288a3..26b02cecc 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -129,6 +129,43 @@ export const createToken = async ( organizations, } = request.payload; try { + if (!address || !decimals || !name || !networkId || !symbol) { + message = 'Please fill all required fields'; + type = 'danger'; + return { + notice: { + message, + type, + }, + }; + } + const duplicateAddress = await Token.createQueryBuilder('token') + .where('LOWER(token.address) = LOWER(:address)', { address }) + .andWhere('token.networkId = :networkId', { + networkId: Number(networkId), + }) + .getOne(); + + const duplicateSymbol = await Token.createQueryBuilder('token') + .where('LOWER(token.symbol) = LOWER(:symbol)', { symbol }) + .andWhere('token.networkId = :networkId', { + networkId: Number(networkId), + }) + .getOne(); + + if (duplicateSymbol || duplicateAddress) { + message = `Token ${ + duplicateAddress ? 'address' : 'symbol' + } already exists!`; + type = 'danger'; + return { + record: {}, + notice: { + message, + type, + }, + }; + } newToken = Token.create({ name, symbol, diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 3ca543943..9c55eac75 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -69,6 +69,7 @@ import { runCheckUserSuperTokenBalancesJob } from '../services/cronJobs/checkUse import { runCheckPendingRecurringDonationsCronJob } from '../services/cronJobs/syncRecurringDonationsWithNetwork'; import { runCheckQRTransactionJob } from '../services/cronJobs/checkQRTransactionJob'; import { addClient } from '../services/sse/sse'; +import { runCheckPendingUserModelScoreCronjob } from '../services/cronJobs/syncUsersModelScore'; Resource.validate = validate; @@ -195,7 +196,6 @@ export async function bootstrap() { if (process.env.DISABLE_SERVER_CORS !== 'true') { app.use(cors(corsOptions)); } - app.use(bodyParserJson); if (process.env.DISABLE_SERVER_RATE_LIMITER !== 'true') { const limiter = rateLimit({ @@ -277,6 +277,9 @@ export async function bootstrap() { }, }), ); + // AdminJs! + app.use(adminJsRootPath, await getAdminJsRouter()); + app.use(bodyParserJson); app.use('/apigive', apiGivRouter); app.use(SOCIAL_PROFILES_PREFIX, oauth2CallbacksRouter); app.post( @@ -311,9 +314,6 @@ export async function bootstrap() { reject(err); // Reject the Promise if there's an error starting the server }); }); - - // AdminJs! - app.use(adminJsRootPath, await getAdminJsRouter()); } catch (err) { logger.fatal('bootstrap() error', err); } @@ -367,6 +367,11 @@ export async function bootstrap() { runCheckProjectVerificationStatus(); } + // If we need to deactivate the process use the env var NO MORE + if (process.env.SYNC_USERS_MBD_SCORE_ACTIVE === 'true') { + runCheckPendingUserModelScoreCronjob(); + } + // If we need to deactivate the process use the env var NO MORE // if (process.env.GIVING_BLOCKS_SERVICE_ACTIVE === 'true') { // runGivingBlocksProjectSynchronization(); diff --git a/src/services/Idriss/contractDonations.ts b/src/services/Idriss/contractDonations.ts index feb540782..e14a89857 100644 --- a/src/services/Idriss/contractDonations.ts +++ b/src/services/Idriss/contractDonations.ts @@ -215,7 +215,7 @@ export const createIdrissTwitterDonation = async ( origin: DONATION_ORIGINS.IDRISS_TWITTER, isTokenEligibleForGivback, isCustomToken: false, - isProjectVerified: project.verified, + isProjectGivbackEligible: project.isGivbackEligible, createdAt: moment(idrissDonation.blockTimestamp).toDate(), segmentNotified: false, isExternal: true, diff --git a/src/services/campaignService.ts b/src/services/campaignService.ts index fd22e2947..03735f39d 100644 --- a/src/services/campaignService.ts +++ b/src/services/campaignService.ts @@ -55,32 +55,37 @@ export const getAllProjectsRelatedToActiveCampaigns = async (): Promise<{ }; export const cacheProjectCampaigns = async (): Promise => { - logger.debug('cacheProjectCampaigns() has been called'); - const newProjectCampaignCache = {}; - const activeCampaigns = await findAllActiveCampaigns(); - for (const campaign of activeCampaigns) { - const projectsQueryParams = createFetchCampaignProjectsQuery(campaign); - if (!projectsQueryParams) { - continue; - } - const projectsQuery = filterProjectsQuery(projectsQueryParams); - const projects = await projectsQuery.getMany(); - for (const project of projects) { - newProjectCampaignCache[project.id] - ? newProjectCampaignCache[project.id].push(campaign.slug) - : (newProjectCampaignCache[project.id] = [campaign.slug]); + try { + logger.debug('cacheProjectCampaigns() has been called'); + const newProjectCampaignCache = {}; + const activeCampaigns = await findAllActiveCampaigns(); + for (const campaign of activeCampaigns) { + const projectsQueryParams = createFetchCampaignProjectsQuery(campaign); + if (!projectsQueryParams) { + continue; + } + const projectsQuery = filterProjectsQuery(projectsQueryParams); + const projects = await projectsQuery.getMany(); + for (const project of projects) { + newProjectCampaignCache[project.id] + ? newProjectCampaignCache[project.id].push(campaign.slug) + : (newProjectCampaignCache[project.id] = [campaign.slug]); + } } + await setObjectInRedis({ + key: PROJECT_CAMPAIGN_CACHE_REDIS_KEY, + value: newProjectCampaignCache, + // cronjob would fill it every 10 minutes so the expiration doesnt matter + expirationInSeconds: 60 * 60 * 24 * 1, // 1 day + }); + logger.debug( + 'cacheProjectCampaigns() ended successfully, projectCampaignCache size ', + Object.keys(newProjectCampaignCache).length, + ); + } catch (e) { + logger.error('cacheProjectCampaigns() failed with error: ', e); + throw e; } - await setObjectInRedis({ - key: PROJECT_CAMPAIGN_CACHE_REDIS_KEY, - value: newProjectCampaignCache, - // cronjob would fill it every 10 minutes so the expiration doesnt matter - expirationInSeconds: 60 * 60 * 24 * 1, // 1 day - }); - logger.debug( - 'cacheProjectCampaigns() ended successfully, projectCampaignCache size ', - Object.keys(newProjectCampaignCache).length, - ); }; export const fillCampaignProjects = async (params: { diff --git a/src/services/cronJobs/checkProjectVerificationStatus.test.ts b/src/services/cronJobs/checkProjectVerificationStatus.test.ts index 3b1d2d66e..998ac3b46 100644 --- a/src/services/cronJobs/checkProjectVerificationStatus.test.ts +++ b/src/services/cronJobs/checkProjectVerificationStatus.test.ts @@ -24,7 +24,7 @@ function checkProjectVerificationStatusTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), - verified: true, + isGivbackEligible: true, latestUpdateCreationDate: moment() .subtract(46, 'days') .endOf('day') @@ -36,7 +36,7 @@ function checkProjectVerificationStatusTestCases() { const warnableProjectUpdate = await findProjectById(warnableProject.id); - assert.isTrue(warnableProjectUpdate!.verified); + assert.isTrue(warnableProjectUpdate!.isGivbackEligible); assert.equal( warnableProjectUpdate!.verificationStatus, RevokeSteps.Warning, @@ -47,7 +47,7 @@ function checkProjectVerificationStatusTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), - verified: true, + isGivbackEligible: true, latestUpdateCreationDate: moment().subtract(91, 'days').endOf('day'), verificationStatus: RevokeSteps.Warning, }); @@ -56,7 +56,7 @@ function checkProjectVerificationStatusTestCases() { const warnableProjectUpdate = await findProjectById(warnableProject.id); - assert.isTrue(warnableProjectUpdate!.verified); + assert.isTrue(warnableProjectUpdate!.isGivbackEligible); assert.equal( warnableProjectUpdate!.verificationStatus, RevokeSteps.LastChance, @@ -67,7 +67,7 @@ function checkProjectVerificationStatusTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), - verified: true, + isGivbackEligible: true, latestUpdateCreationDate: moment().subtract(105, 'days').endOf('day'), verificationStatus: RevokeSteps.LastChance, }); @@ -78,7 +78,7 @@ function checkProjectVerificationStatusTestCases() { lastWarningProject.id, ); - assert.isTrue(lastWarningProjectUpdated!.verified); + assert.isTrue(lastWarningProjectUpdated!.isGivbackEligible); assert.equal( lastWarningProjectUpdated!.verificationStatus, RevokeSteps.UpForRevoking, @@ -89,7 +89,7 @@ function checkProjectVerificationStatusTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), - verified: true, + isGivbackEligible: true, latestUpdateCreationDate: moment().subtract(105, 'days').endOf('day'), isImported: true, }); @@ -98,7 +98,7 @@ function checkProjectVerificationStatusTestCases() { const importedProjectUpdated = await findProjectById(importedProject.id); - assert.isTrue(importedProjectUpdated!.verified); + assert.isTrue(importedProjectUpdated!.isGivbackEligible); assert.equal(importedProjectUpdated!.verificationStatus, null); }); // it('should revoke project verification after last chance time frame expired', async () => { diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index e33b5003b..8e1005cca 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -158,7 +158,7 @@ export async function checkTransactions( fromWalletAddress: transaction.source_account, transactionId: transaction.transaction_hash, tokenAddress: donation.tokenAddress, - isProjectVerified: project.verified, + isProjectGivbackEligible: project.isGivbackEligible, donorUser: donor, isTokenEligibleForGivback: token.isGivbackEligible, segmentNotified: false, diff --git a/src/services/cronJobs/importLostDonationsJob.ts b/src/services/cronJobs/importLostDonationsJob.ts index 5438cc6c5..250229558 100644 --- a/src/services/cronJobs/importLostDonationsJob.ts +++ b/src/services/cronJobs/importLostDonationsJob.ts @@ -233,7 +233,7 @@ export const importLostDonations = async () => { anonymous: false, segmentNotified: true, isTokenEligibleForGivback: tokenInDB?.isGivbackEligible, - isProjectVerified: project?.verified, + isProjectGivbackEligible: project?.isGivbackEligible, qfRoundId: qfRound?.id, }); diff --git a/src/services/cronJobs/syncUsersModelScore.test.ts b/src/services/cronJobs/syncUsersModelScore.test.ts new file mode 100644 index 000000000..321109845 --- /dev/null +++ b/src/services/cronJobs/syncUsersModelScore.test.ts @@ -0,0 +1,85 @@ +import { assert } from 'chai'; +import moment from 'moment'; +import { + createDonationData, + createProjectData, + generateRandomEtheriumAddress, + saveDonationDirectlyToDb, + saveProjectDirectlyToDb, + saveUserDirectlyToDb, +} from '../../../test/testUtils'; +import { QfRound } from '../../entities/qfRound'; +import { updateUsersWithoutMBDScoreInRound } from './syncUsersModelScore'; +import { UserQfRoundModelScore } from '../../entities/userQfRoundModelScore'; + +describe( + 'updateUsersWithoutMBDScoreInRound() test cases', + updateUsersWithoutMBDScoreInRoundTestCases, +); + +function updateUsersWithoutMBDScoreInRoundTestCases() { + // for tests it return 1, useful to test cronjob logic and worker + it('should save the score for users that donated in the round', async () => { + await QfRound.update({}, { isActive: false }); + const qfRound = QfRound.create({ + isActive: true, + name: 'test', + allocatedFund: 100, + minimumPassportScore: 8, + slug: new Date().getTime().toString(), + beginDate: new Date(), + endDate: moment().add(10, 'days').toDate(), + }); + await qfRound.save(); + const project = await saveProjectDirectlyToDb(createProjectData()); + project.qfRounds = [qfRound]; + await project.save(); + + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + await saveDonationDirectlyToDb( + { + ...createDonationData(), + segmentNotified: false, + qfRoundId: qfRound.id, + status: 'verified', + }, + user.id, + project.id, + ); + + await saveDonationDirectlyToDb( + { + ...createDonationData(), + segmentNotified: false, + qfRoundId: qfRound.id, + status: 'verified', + }, + user2.id, + project.id, + ); + + await updateUsersWithoutMBDScoreInRound(); + + const user1ModelScore = await UserQfRoundModelScore.createQueryBuilder( + 'score', + ) + .where('score."userId" = :userId', { userId: user.id }) + .andWhere('score."qfRoundId" = :qfRoundId', { qfRoundId: qfRound.id }) + .getOne(); + + const user2ModelScore = await UserQfRoundModelScore.createQueryBuilder( + 'score', + ) + .where('score."userId" = :userId', { userId: user2.id }) + .andWhere('score."qfRoundId" = :qfRoundId', { qfRoundId: qfRound.id }) + .getOne(); + + // base values for mocks + assert.equal(user1ModelScore?.score, 1); + assert.equal(user2ModelScore?.score, 1); + + qfRound.isActive = false; + await qfRound.save(); + }); +} diff --git a/src/services/cronJobs/syncUsersModelScore.ts b/src/services/cronJobs/syncUsersModelScore.ts new file mode 100644 index 000000000..944363e6a --- /dev/null +++ b/src/services/cronJobs/syncUsersModelScore.ts @@ -0,0 +1,63 @@ +import { schedule } from 'node-cron'; +import { spawn, Worker, Thread } from 'threads'; +import config from '../../config'; +import { logger } from '../../utils/logger'; +import { + findActiveQfRound, + findUsersWithoutMBDScoreInActiveAround, +} from '../../repositories/qfRoundRepository'; +import { findUserById } from '../../repositories/userRepository'; +import { UserQfRoundModelScore } from '../../entities/userQfRoundModelScore'; + +const cronJobTime = + (config.get('MAKE_UNREVIEWED_PROJECT_LISTED_CRONJOB_EXPRESSION') as string) || + '0 0 * * * *'; + +const qfRoundUsersMissedMBDScore = Number( + process.env.QF_ROUND_USERS_MISSED_SCORE || 0, +); + +export const runCheckPendingUserModelScoreCronjob = () => { + logger.debug( + 'runCheckPendingUserModelScoreCronjob() has been called, cronJobTime', + cronJobTime, + ); + schedule(cronJobTime, async () => { + await updateUsersWithoutMBDScoreInRound(); + }); +}; + +export const updateUsersWithoutMBDScoreInRound = async () => { + const worker = await spawn( + new Worker('../../workers/userMBDScoreSyncWorker'), + ); + const userIds = await findUsersWithoutMBDScoreInActiveAround(); + const activeQfRoundId = + (await findActiveQfRound())?.id || qfRoundUsersMissedMBDScore; + if (!activeQfRoundId || activeQfRoundId === 0) return; + + if (userIds.length === 0) return; + + for (const userId of userIds) { + try { + const user = await findUserById(userId); + if (!user) continue; + + const userScore = await worker.syncUserScore({ + userWallet: user?.walletAddress, + }); + if (userScore) { + const userScoreInRound = UserQfRoundModelScore.create({ + userId, + qfRoundId: activeQfRoundId, + score: userScore, + }); + + await userScoreInRound.save(); + } + } catch (e) { + logger.info(`User with Id ${userId} did not sync MBD score this batch`); + } + } + await Thread.terminate(worker); +}; diff --git a/src/services/cronJobs/updatePowerRoundJob.ts b/src/services/cronJobs/updatePowerRoundJob.ts index d36f94c5d..6e6e26096 100644 --- a/src/services/cronJobs/updatePowerRoundJob.ts +++ b/src/services/cronJobs/updatePowerRoundJob.ts @@ -19,6 +19,7 @@ import { import { getNotificationAdapter } from '../../adapters/adaptersFactory'; import { sleep } from '../../utils/utils'; import { fillIncompletePowerSnapshots } from '../powerSnapshotServices'; +import { refreshProjectGivbackRankView } from '../../repositories/projectGivbackViewRepository'; const cronJobTime = (config.get('UPDATE_POWER_ROUND_CRONJOB_EXPRESSION') as string) || @@ -55,6 +56,7 @@ export const runUpdatePowerRoundCronJob = () => { refreshProjectPowerView(), refreshProjectFuturePowerView(), refreshUserProjectPowerView(), + refreshProjectGivbackRankView(), ]); if (powerRound !== currentRound?.round) { // Refreshing views need time to refresh tables, so I wait for 1 minute and after that check project rank changes diff --git a/src/services/givbackService.ts b/src/services/givbackService.ts index f94db6f1a..5871535cc 100644 --- a/src/services/givbackService.ts +++ b/src/services/givbackService.ts @@ -1,8 +1,8 @@ -import { - findProjectPowerViewByProjectId, - getBottomRank, -} from '../repositories/projectPowerViewRepository'; import { getPowerRound } from '../repositories/powerRoundRepository'; +import { + findProjectGivbackRankViewByProjectId, + getBottomGivbackRank, +} from '../repositories/projectGivbackViewRepository'; export const calculateGivbackFactor = async ( projectId: number, @@ -14,21 +14,21 @@ export const calculateGivbackFactor = async ( }> => { const minGivFactor = Number(process.env.GIVBACK_MIN_FACTOR); const maxGivFactor = Number(process.env.GIVBACK_MAX_FACTOR); - const [projectPowerView, bottomRank, powerRound] = await Promise.all([ - findProjectPowerViewByProjectId(projectId), - getBottomRank(), + const [projectGivbackRankView, bottomRank, powerRound] = await Promise.all([ + findProjectGivbackRankViewByProjectId(projectId), + getBottomGivbackRank(), getPowerRound(), ]); const eachRoundImpact = (maxGivFactor - minGivFactor) / (bottomRank - 1); - const givbackFactor = projectPowerView?.powerRank + const givbackFactor = projectGivbackRankView?.powerRank ? minGivFactor + - eachRoundImpact * (bottomRank - projectPowerView?.powerRank) + eachRoundImpact * (bottomRank - projectGivbackRankView?.powerRank) : minGivFactor; return { givbackFactor: givbackFactor || 0, - projectRank: projectPowerView?.powerRank, + projectRank: projectGivbackRankView?.powerRank, bottomRankInRound: bottomRank, powerRound: powerRound?.round as number, }; diff --git a/src/services/googleSheets.ts b/src/services/googleSheets.ts index a888a32b6..a9843e364 100644 --- a/src/services/googleSheets.ts +++ b/src/services/googleSheets.ts @@ -55,7 +55,7 @@ interface DonationExport { id: number; transactionId: string; transactionNetworkId: number; - isProjectVerified: boolean; + isProjectGivbackEligible: boolean; status: string; toWalletAddress: string; fromWalletAddress: string; @@ -175,6 +175,8 @@ export const addQfRoundDonationsSheetToSpreadsheet = async (params: { 'totalValuesOfUserDonationsAfterAnalysis', 'uniqueUserIdsAfterAnalysis', 'projectOwnerEmail', + 'projectId', + 'qfRoundId', ]; const { rows, qfRoundId } = params; diff --git a/src/services/onramper/donationService.ts b/src/services/onramper/donationService.ts index bc51cd8d8..0008077fd 100644 --- a/src/services/onramper/donationService.ts +++ b/src/services/onramper/donationService.ts @@ -108,7 +108,7 @@ export const createFiatDonationFromOnramper = async ( project, isTokenEligibleForGivback, isCustomToken, - isProjectVerified: project.verified, + isProjectGivbackEligible: project.isGivbackEligible, createdAt: new Date(fiatTransaction.payload.timestamp), segmentNotified: false, toWalletAddress: toAddress.toString().toLowerCase(), diff --git a/src/services/projectViewsService.ts b/src/services/projectViewsService.ts index b317e8d8c..50532411d 100644 --- a/src/services/projectViewsService.ts +++ b/src/services/projectViewsService.ts @@ -119,6 +119,8 @@ export const getQfRoundActualDonationDetails = async ( row?.totalValuesOfUserDonationsAfterAnalysis?.join('-'), uniqueUserIdsAfterAnalysis: row?.uniqueUserIdsAfterAnalysis?.join('-'), projectOwnerEmail: row?.email, // can be empty for new users + projectId: row?.projectId, + qfRoundId: row?.qfRoundId, }; }); logger.debug( diff --git a/src/services/recurringDonationService.test.ts b/src/services/recurringDonationService.test.ts index c6bd6e943..c723cba0b 100644 --- a/src/services/recurringDonationService.test.ts +++ b/src/services/recurringDonationService.test.ts @@ -170,7 +170,8 @@ function updateRecurringDonationStatusWithNetworkTestCases() { await RecurringDonation.delete({ id: recurringDonation.id }); await AnchorContractAddress.delete({ id: anchorContractAddress.id }); }); - it('should remain pending, different toAddress from OP Sepolia', async () => { + it.skip('should remain pending, different toAddress from OP Sepolia', async () => { + //Because in mock adapter we always the sender address, so it should not remain pending and we have to skip this test case // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 const projectOwner = await saveUserDirectlyToDb( generateRandomEtheriumAddress(), @@ -216,7 +217,8 @@ function updateRecurringDonationStatusWithNetworkTestCases() { await RecurringDonation.delete({ id: recurringDonation.id }); await AnchorContractAddress.delete({ id: anchorContractAddress.id }); }); - it('should donation remain pending, different amount from OP Sepolia', async () => { + it.skip('should donation remain pending, different amount from OP Sepolia', async () => { + //Because in mock adapter we always the sender address, so it should not remain pending and we have to skip this test case // https://sepolia-optimism.etherscan.io/tx/0x516567c51c3506afe1291f7055fa0e858cc2ca9ed4079625c747fe92bd125a10 const projectOwner = await saveUserDirectlyToDb( generateRandomEtheriumAddress(), diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 3e2d52d41..4124d2c01 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -196,7 +196,7 @@ export const createRelatedDonationsToStream = async ( status: DONATION_STATUS.VERIFIED, isTokenEligibleForGivback, isCustomToken, - isProjectVerified: project.verified, + isProjectGivbackEligible: project.isGivbackEligible, createdAt: new Date(), segmentNotified: false, toWalletAddress: toAddress, diff --git a/src/workers/userMBDScoreSyncWorker.ts b/src/workers/userMBDScoreSyncWorker.ts new file mode 100644 index 000000000..0bcc65d4c --- /dev/null +++ b/src/workers/userMBDScoreSyncWorker.ts @@ -0,0 +1,17 @@ +// workers/auth.js +import { expose } from 'threads/worker'; +import { WorkerModule } from 'threads/dist/types/worker'; +import { getGitcoinAdapter } from '../adapters/adaptersFactory'; + +type UsersMBDScoreSyncWorkerFunctions = 'syncUserScore'; + +export type UserMBDScoreSyncWorker = + WorkerModule; + +const worker: UserMBDScoreSyncWorker = { + async syncUserScore(args: { userWallet: string }) { + return await getGitcoinAdapter().getUserAnalysisScore(args.userWallet); + }, +}; + +expose(worker); diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index b3c4e8d14..4f6dd6ca3 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -570,12 +570,14 @@ export const fetchTotalDonationsPerCategoryPerDate = ` $toDate: String $networkId: Float $onlyVerified: Boolean + $onlyEndaoment: Boolean ) { totalDonationsPerCategory( fromDate: $fromDate toDate: $toDate networkId: $networkId onlyVerified: $onlyVerified + onlyEndaoment: $onlyEndaoment ) { id title @@ -611,11 +613,13 @@ export const fetchTotalDonors = ` $fromDate: String $toDate: String $networkId: Float + $onlyEndaoment: Boolean ) { totalDonorsCountPerDate( fromDate: $fromDate toDate: $toDate networkId: $networkId + onlyEndaoment: $onlyEndaoment ) { total totalPerMonthAndYear { @@ -632,12 +636,14 @@ export const fetchTotalDonationsUsdAmount = ` $toDate: String $networkId: Float $onlyVerified: Boolean + $onlyEndaoment: Boolean ) { donationsTotalUsdPerDate ( fromDate: $fromDate toDate: $toDate networkId: $networkId onlyVerified: $onlyVerified + onlyEndaoment: $onlyEndaoment ) { total totalPerMonthAndYear { @@ -650,24 +656,26 @@ export const fetchTotalDonationsUsdAmount = ` export const fetchTotalDonationsNumberPerDateRange = ` query ( - $fromDate: String - $toDate: String - $networkId: Float - $onlyVerified: Boolean + $fromDate: String + $toDate: String + $networkId: Float + $onlyVerified: Boolean + $onlyEndaoment: Boolean +) { + totalDonationsNumberPerDate ( + fromDate: $fromDate + toDate: $toDate + networkId: $networkId + onlyVerified: $onlyVerified + onlyEndaoment: $onlyEndaoment ) { - totalDonationsNumberPerDate ( - fromDate: $fromDate - toDate: $toDate - networkId: $networkId - onlyVerified: $onlyVerified - ) { + total + totalPerMonthAndYear { total - totalPerMonthAndYear { - total - date - } + date } } +} `; export const fetchNewDonorsCount = ` @@ -927,6 +935,7 @@ export const fetchMultiFilterAllProjectsQuery = ` impactLocation qualityScore verified + isGivbackEligible traceCampaignId listed reviewStatus diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index 3156bb1df..6c5cfa285 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -42,6 +42,7 @@ import { ProjectFuturePowerViewV21717643016553 } from '../migration/171764301655 import { ProjectUserInstantPowerViewV21717644442966 } from '../migration/1717644442966-ProjectUserInstantPowerView_V2'; import { ProjectInstantPowerViewV21717648653115 } from '../migration/1717648653115-ProjectInstantPowerView_V2'; import { UserProjectPowerViewV21717645768886 } from '../migration/1717645768886-UserProjectPowerView_V2'; +import { ProjectGivbackRankViewV31725260193333 } from '../migration/1725260193333-projectGivbackRankView'; async function seedDb() { await seedUsers(); @@ -551,6 +552,7 @@ async function runMigrations() { await new ProjectActualMatchingViewV161717646612482().up(queryRunner); await new EnablePgTrgmExtension1713859866338().up(queryRunner); await new AddPgTrgmIndexes1715086559930().up(queryRunner); + await new ProjectGivbackRankViewV31725260193333().up(queryRunner); } finally { await queryRunner.release(); } diff --git a/test/testUtils.ts b/test/testUtils.ts index 38587eb5f..9cd3c21f6 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -151,6 +151,7 @@ export interface CreateProjectData { image?: string; networkId?: number; chainType?: ChainType; + isGivbackEligible: boolean; } export const saveUserDirectlyToDb = async ( @@ -321,6 +322,7 @@ export const createProjectData = (name?: string): CreateProjectData => { walletAddress, categories: ['food1'], verified: true, + isGivbackEligible: true, listed: true, reviewStatus: ReviewStatus.Listed, giveBacks: false, @@ -1954,7 +1956,7 @@ export interface CreateDonationData { donationPercentage?: number; powerRound?: number; givbackFactor?: number; - isProjectVerified?: boolean; + isProjectGivbackEligible?: boolean; } export interface CategoryData { From 7676e676a56f5a839645fb6c14cd13ef52b55dd4 Mon Sep 17 00:00:00 2001 From: Griff Green Date: Wed, 16 Oct 2024 18:52:45 +0200 Subject: [PATCH 2/9] Update funding.json --- funding.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/funding.json b/funding.json index 668d35837..a08e0685c 100644 --- a/funding.json +++ b/funding.json @@ -1,5 +1,5 @@ { "opRetro": { - "projectId": "0xe434930e189c807b137ff0d8e2fa6a95eaa57dde574143a02ca0d7fb31a40bea" + "projectId": "0x5decc7c7bb5ac6448be3408fd18e5c75738725d739711985e2c55026d2fa1391" } } From b4fada022e3fde89206e416fed87b2c81fcdadf9 Mon Sep 17 00:00:00 2001 From: mohammadranjbarz Date: Fri, 18 Oct 2024 12:48:37 +0300 Subject: [PATCH 3/9] hotfix 4844 fix instant boosting sort (#1863) * Fix instant boosting sort related to #Giveth/giveth-dapps-v2/4844 * Remove .only from test cases --- src/repositories/projectRepository.ts | 7 ++++--- .../projectResolver.allProject.test.ts | 19 ++++++++++++++++--- test/pre-test-scripts.ts | 4 ++-- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 34bb4f907..d500b41bb 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -209,13 +209,14 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { break; case SortingField.InstantBoosting: // This is our default sorting query - .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition - .addOrderBy('project.verified', 'DESC') // Secondary sorting condition .addOrderBy( 'projectInstantPower.totalPower', OrderDirection.DESC, 'NULLS LAST', - ); + ) + .addOrderBy('project.isGivbackEligible', 'DESC') // Primary sorting condition + .addOrderBy('project.verified', 'DESC'); // Secondary sorting condition + if (isFilterByQF) { query.addOrderBy( 'project.sumDonationValueUsdForActiveQfRound', diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 533e61209..c6543637d 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -550,12 +550,25 @@ function allProjectsTestCases() { const user1 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const user2 = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const project1 = await saveProjectDirectlyToDb(createProjectData()); - const project2 = await saveProjectDirectlyToDb(createProjectData()); - const project3 = await saveProjectDirectlyToDb(createProjectData()); + const project1 = await saveProjectDirectlyToDb({ + ...createProjectData(), + verified: true, + isGivbackEligible: true, + }); + const project2 = await saveProjectDirectlyToDb({ + ...createProjectData(), + verified: true, + isGivbackEligible: false, + }); + const project3 = await saveProjectDirectlyToDb({ + ...createProjectData(), + verified: true, + isGivbackEligible: true, + }); const project4 = await saveProjectDirectlyToDb({ ...createProjectData(), verified: false, + isGivbackEligible: false, }); // Not boosted -Not verified project await saveProjectDirectlyToDb(createProjectData()); // Not boosted project diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index 6c5cfa285..da482f3a5 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -40,9 +40,9 @@ import { ProjectActualMatchingViewV161717646612482 } from '../migration/17176466 import { LastSnapshotProjectPowerViewV21717648491606 } from '../migration/1717648491606-LastSnapshotProjectPowerView_V2'; import { ProjectFuturePowerViewV21717643016553 } from '../migration/1717643016553-ProjectFuturePowerView_V2'; import { ProjectUserInstantPowerViewV21717644442966 } from '../migration/1717644442966-ProjectUserInstantPowerView_V2'; -import { ProjectInstantPowerViewV21717648653115 } from '../migration/1717648653115-ProjectInstantPowerView_V2'; import { UserProjectPowerViewV21717645768886 } from '../migration/1717645768886-UserProjectPowerView_V2'; import { ProjectGivbackRankViewV31725260193333 } from '../migration/1725260193333-projectGivbackRankView'; +import { ProjectInstantPowerViewV31724223781248 } from '../migration/1724223781248-ProjectInstantPowerViewV3'; async function seedDb() { await seedUsers(); @@ -538,7 +538,7 @@ async function runMigrations() { queryRunner, ); await new createOrganisatioTokenTable1646302349926().up(queryRunner); - await new ProjectInstantPowerViewV21717648653115().up(queryRunner); + await new ProjectInstantPowerViewV31724223781248().up(queryRunner); await new ProjectEstimatedMatchingViewV21717646357435().up(queryRunner); await new ProjectUserInstantPowerViewV21717644442966().up(queryRunner); await new TakePowerBoostingSnapshotProcedureSecondVersion1690723242749().up( From f3b067d998b82c17f9ec774936abd939b8695a7a Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Fri, 18 Oct 2024 14:03:39 +0330 Subject: [PATCH 4/9] Skip isWalletAddressSmartContract test cases --- src/utils/validators/projectValidator.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/validators/projectValidator.test.ts b/src/utils/validators/projectValidator.test.ts index 1b541ad27..3d56e86ff 100644 --- a/src/utils/validators/projectValidator.test.ts +++ b/src/utils/validators/projectValidator.test.ts @@ -29,7 +29,9 @@ describe( ); // describe('validateProjectTitleForEdit() test cases', validateProjectTitleForEditTestCases); describe('validateProjectTitleTestCases', validateProjectTitleTestCases); -describe( + +// It's failing because of network issues, so I skip them temporarily +describe.skip( 'isWalletAddressSmartContract() test cases', isWalletAddressSmartContractTestCases, ); From 2e229ee1cbe903f6328210cf5877cd9717eeee0b Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Wed, 6 Nov 2024 11:29:08 +0100 Subject: [PATCH 5/9] change recurringdonation status to exclude failed (#1869) --- src/services/recurringDonationService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 4124d2c01..647b187cd 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -428,8 +428,8 @@ export const recurringDonationsCountPerDateRange = async ( ): Promise => { const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('COALESCE(COUNT(recurringDonation.id), 0)', 'count') - .where('recurringDonation.status = :status', { - status: RECURRING_DONATION_STATUS.ACTIVE, + .where('recurringDonation.status != :status', { + status: RECURRING_DONATION_STATUS.FAILED, }); if (fromDate) { @@ -477,8 +477,8 @@ export const recurringDonationsCountPerDateRangePerMonth = async ( const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('COUNT(recurringDonation.id)', 'total') .addSelect("TO_CHAR(recurringDonation.createdAt, 'YYYY/MM')", 'date') - .where('recurringDonation.status = :status', { - status: RECURRING_DONATION_STATUS.ACTIVE, + .where('recurringDonation.status != :status', { + status: RECURRING_DONATION_STATUS.FAILED, }); if (fromDate) { @@ -672,8 +672,8 @@ export const recurringDonationsCountPerToken = async (params: { const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('recurringDonation.currency', 'token') .addSelect('COALESCE(COUNT(recurringDonation.id), 0)', 'total') - .where('recurringDonation.status = :status', { - status: RECURRING_DONATION_STATUS.ACTIVE, + .where('recurringDonation.status != :status', { + status: RECURRING_DONATION_STATUS.FAILED, }) .groupBy('recurringDonation.currency') .having('COUNT(recurringDonation.id) > 0'); From 69d0bb111d9f612cb86bd20ac09dc194e2a900ba Mon Sep 17 00:00:00 2001 From: divine-comedian Date: Tue, 12 Nov 2024 10:26:42 +0700 Subject: [PATCH 6/9] update recurring donation token info --- src/provider.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/provider.ts b/src/provider.ts index 93b190f0c..85a9c66aa 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -121,13 +121,46 @@ export const superTokens = [ underlyingToken: { decimals: 6, id: '0x7f5c764cbc14f9669b88837ca1490cca17c31607', + name: 'Bridged USD Coin', + symbol: 'USDC.e', + coingeckoId: 'usd-coin', + }, + decimals: 18, + id: '0x8430f084b939208e2eded1584889c9a66b90562f', + name: 'Super Bridged USD Coin', + symbol: 'USDC.ex', + isSuperToken: true, + coingeckoId: 'usd-coin', + }, + { + underlyingToken: { + decimals: 6, + id: '0x0b2c639c533813f4aa9d7837caf62653d097ff85', name: 'USD Coin', symbol: 'USDC', + coingeckoId: 'usd-coin', }, decimals: 18, - id: '0x8430f084b939208e2eded1584889c9a66b90562f', + id: '0x35Adeb0638EB192755B6E52544650603Fe65A006', name: 'Super USD Coin', symbol: 'USDCx', + isSuperToken: true, + coingeckoId: 'usd-coin', + }, + { + underlyingToken: { + decimals: 18, + id: '0x4F604735c1cF31399C6E711D5962b2B3E0225AD3', + name: 'Glo Dollar', + symbol: 'USDGLO', + coingeckoId: 'glo-dollar', + }, + decimals: 18, + id: '0x9F41d0AA24E599fd8D0c180Ee3C0F609dc41c622', + name: 'Super Glo Dollar', + symbol: 'USDGLOx', + isSuperToken: true, + coingeckoId: 'glo-dollar', }, ]; From 3865ebe72a4bef5db39b4fb102724c782f87ac2b Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Tue, 12 Nov 2024 22:57:57 +0100 Subject: [PATCH 7/9] fix date filters for recurringdonation stats (#1872) --- src/repositories/recurringDonationRepository.ts | 3 ++- src/services/recurringDonationService.ts | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/repositories/recurringDonationRepository.ts b/src/repositories/recurringDonationRepository.ts index 7dec0dea1..d7b05cb99 100644 --- a/src/repositories/recurringDonationRepository.ts +++ b/src/repositories/recurringDonationRepository.ts @@ -106,7 +106,8 @@ export const updateRecurringDonationFromTheStreamDonations = async ( SELECT COALESCE(SUM(d."amount"), 0) FROM donation as d WHERE d."recurringDonationId" = $1 - ) + ), + "updatedAt" = NOW() WHERE "id" = $1 `, [recurringDonationId], diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 647b187cd..5241fd2c9 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -529,13 +529,13 @@ export const recurringDonationsStreamedCUsdTotal = async ( ).select('COALESCE(SUM(recurringDonation.totalUsdStreamed), 0)', 'total'); if (fromDate) { - query.andWhere('recurringDonation.createdAt >= :fromDate', { + query.andWhere('recurringDonation.updatedAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.createdAt <= :toDate', { + query.andWhere('recurringDonation.updatedAt <= :toDate', { toDate: new Date(toDate), }); } @@ -572,16 +572,16 @@ export const recurringDonationsStreamedCUsdTotalPerMonth = async ( ): Promise => { const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('SUM(recurringDonation.totalUsdStreamed)', 'total') - .addSelect("TO_CHAR(recurringDonation.createdAt, 'YYYY/MM')", 'date'); + .addSelect("TO_CHAR(recurringDonation.updatedAt, 'YYYY/MM')", 'date'); if (fromDate) { - query.andWhere('recurringDonation.createdAt >= :fromDate', { + query.andWhere('recurringDonation.updatedAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.createdAt <= :toDate', { + query.andWhere('recurringDonation.updatedAt <= :toDate', { toDate: new Date(toDate), }); } @@ -627,13 +627,13 @@ export const recurringDonationsTotalPerToken = async (params: { .having('SUM(recurringDonation.totalUsdStreamed) > 0'); if (fromDate) { - query.andWhere('recurringDonation.createdAt >= :fromDate', { + query.andWhere('recurringDonation.updatedAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.createdAt <= :toDate', { + query.andWhere('recurringDonation.updatedAt <= :toDate', { toDate: new Date(toDate), }); } From 8bd4c7cbad7815eb85a0ffb08ced63cb27d7e5c8 Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Tue, 19 Nov 2024 22:26:02 +0100 Subject: [PATCH 8/9] Change recurring donation stat queries totalUSD (#1873) * change recurring donation stat queries totalUSD * fix count for giveth stats recurring donations --- src/services/recurringDonationService.ts | 57 +++++++++++++----------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 5241fd2c9..3f800ed11 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -428,18 +428,19 @@ export const recurringDonationsCountPerDateRange = async ( ): Promise => { const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('COALESCE(COUNT(recurringDonation.id), 0)', 'count') + .innerJoin('recurringDonation.donations', 'donations') .where('recurringDonation.status != :status', { status: RECURRING_DONATION_STATUS.FAILED, }); if (fromDate) { - query.andWhere('recurringDonation.createdAt >= :fromDate', { + query.andWhere('donations.createdAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.createdAt <= :toDate', { + query.andWhere('donations.createdAt <= :toDate', { toDate: new Date(toDate), }); } @@ -477,18 +478,19 @@ export const recurringDonationsCountPerDateRangePerMonth = async ( const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('COUNT(recurringDonation.id)', 'total') .addSelect("TO_CHAR(recurringDonation.createdAt, 'YYYY/MM')", 'date') + .innerJoin('recurringDonation.donations', 'donations') .where('recurringDonation.status != :status', { status: RECURRING_DONATION_STATUS.FAILED, }); if (fromDate) { - query.andWhere('recurringDonation.createdAt >= :fromDate', { + query.andWhere('donations.createdAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.createdAt <= :toDate', { + query.andWhere('donations.createdAt <= :toDate', { toDate: new Date(toDate), }); } @@ -524,24 +526,24 @@ export const recurringDonationsStreamedCUsdTotal = async ( networkId?: number, onlyVerified?: boolean, ): Promise => { - const query = RecurringDonation.createQueryBuilder( - 'recurringDonation', - ).select('COALESCE(SUM(recurringDonation.totalUsdStreamed), 0)', 'total'); + const query = RecurringDonation.createQueryBuilder('recurringDonation') + .select('COALESCE(SUM(donations.valueUsd), 0)', 'total') + .innerJoin('recurringDonation.donations', 'donations'); if (fromDate) { - query.andWhere('recurringDonation.updatedAt >= :fromDate', { + query.andWhere('donations.createdAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.updatedAt <= :toDate', { + query.andWhere('donations.createdAt <= :toDate', { toDate: new Date(toDate), }); } if (networkId) { - query.andWhere('recurringDonation.networkId = :networkId', { + query.andWhere('donations.transactionNetworkId = :networkId', { networkId, }); } @@ -571,23 +573,24 @@ export const recurringDonationsStreamedCUsdTotalPerMonth = async ( onlyVerified?: boolean, ): Promise => { const query = RecurringDonation.createQueryBuilder('recurringDonation') - .select('SUM(recurringDonation.totalUsdStreamed)', 'total') - .addSelect("TO_CHAR(recurringDonation.updatedAt, 'YYYY/MM')", 'date'); + .select('SUM(donations.valueUsd)', 'total') + .addSelect("TO_CHAR(donations.createdAt, 'YYYY/MM')", 'date') + .innerJoin('recurringDonation.donations', 'donations'); if (fromDate) { - query.andWhere('recurringDonation.updatedAt >= :fromDate', { + query.andWhere('donations.createdAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.updatedAt <= :toDate', { + query.andWhere('donations.createdAt <= :toDate', { toDate: new Date(toDate), }); } if (networkId) { - query.andWhere('recurringDonation.networkId = :networkId', { + query.andWhere('donations.transactionNetworkId = :networkId', { networkId, }); } @@ -621,25 +624,26 @@ export const recurringDonationsTotalPerToken = async (params: { }): Promise<{ token: string; total: number }[]> => { const { fromDate, toDate, networkId, onlyVerified } = params; const query = RecurringDonation.createQueryBuilder('recurringDonation') - .select('recurringDonation.currency', 'token') - .addSelect('COALESCE(SUM(recurringDonation.totalUsdStreamed), 0)', 'total') - .groupBy('recurringDonation.currency') - .having('SUM(recurringDonation.totalUsdStreamed) > 0'); + .select('donations.currency', 'token') + .addSelect('COALESCE(SUM(donations.valueUsd), 0)', 'total') + .innerJoin('recurringDonation.donations', 'donations') + .groupBy('donations.currency') + .having('SUM(donations.valueUsd) > 0'); if (fromDate) { - query.andWhere('recurringDonation.updatedAt >= :fromDate', { + query.andWhere('donations.createdAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.updatedAt <= :toDate', { + query.andWhere('donations.createdAt <= :toDate', { toDate: new Date(toDate), }); } if (networkId) { - query.andWhere('recurringDonation.networkId = :networkId', { + query.andWhere('donations.transactionNetworkId = :networkId', { networkId, }); } @@ -670,22 +674,23 @@ export const recurringDonationsCountPerToken = async (params: { }): Promise<{ token: string; total: number }[]> => { const { fromDate, toDate, networkId, onlyVerified } = params; const query = RecurringDonation.createQueryBuilder('recurringDonation') - .select('recurringDonation.currency', 'token') + .select('donations.currency', 'token') .addSelect('COALESCE(COUNT(recurringDonation.id), 0)', 'total') + .innerJoin('recurringDonation.donations', 'donations') .where('recurringDonation.status != :status', { status: RECURRING_DONATION_STATUS.FAILED, }) - .groupBy('recurringDonation.currency') + .groupBy('donations.currency') .having('COUNT(recurringDonation.id) > 0'); if (fromDate) { - query.andWhere('recurringDonation.createdAt >= :fromDate', { + query.andWhere('donations.createdAt >= :fromDate', { fromDate: new Date(fromDate), }); } if (toDate) { - query.andWhere('recurringDonation.createdAt <= :toDate', { + query.andWhere('donations.createdAt <= :toDate', { toDate: new Date(toDate), }); } From 37c81c72aea65f3edc7b6b83ce98cf5e917237f4 Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 20 Nov 2024 00:00:15 +0100 Subject: [PATCH 9/9] add distinct to recurring donation count --- src/services/recurringDonationService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/recurringDonationService.ts b/src/services/recurringDonationService.ts index 3f800ed11..cb1f286e3 100644 --- a/src/services/recurringDonationService.ts +++ b/src/services/recurringDonationService.ts @@ -427,7 +427,7 @@ export const recurringDonationsCountPerDateRange = async ( onlyVerified?: boolean, ): Promise => { const query = RecurringDonation.createQueryBuilder('recurringDonation') - .select('COALESCE(COUNT(recurringDonation.id), 0)', 'count') + .select('COALESCE(COUNT(DISTINCT recurringDonation.id), 0)', 'count') .innerJoin('recurringDonation.donations', 'donations') .where('recurringDonation.status != :status', { status: RECURRING_DONATION_STATUS.FAILED, @@ -476,7 +476,7 @@ export const recurringDonationsCountPerDateRangePerMonth = async ( onlyVerified?: boolean, ): Promise => { const query = RecurringDonation.createQueryBuilder('recurringDonation') - .select('COUNT(recurringDonation.id)', 'total') + .select('COUNT(DISTINCT recurringDonation.id)', 'total') .addSelect("TO_CHAR(recurringDonation.createdAt, 'YYYY/MM')", 'date') .innerJoin('recurringDonation.donations', 'donations') .where('recurringDonation.status != :status', { @@ -675,7 +675,7 @@ export const recurringDonationsCountPerToken = async (params: { const { fromDate, toDate, networkId, onlyVerified } = params; const query = RecurringDonation.createQueryBuilder('recurringDonation') .select('donations.currency', 'token') - .addSelect('COALESCE(COUNT(recurringDonation.id), 0)', 'total') + .addSelect('COALESCE(COUNT(DISTINCT recurringDonation.id), 0)', 'total') .innerJoin('recurringDonation.donations', 'donations') .where('recurringDonation.status != :status', { status: RECURRING_DONATION_STATUS.FAILED,