From b54b825ce616d2620f7e6b4800e316c6267d4ecb Mon Sep 17 00:00:00 2001 From: Cherik Date: Thu, 12 Sep 2024 17:17:13 +0330 Subject: [PATCH] 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 --- 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 ++++++ ...ctVerifiedToIsGivbackEligibleInDonation.ts | 19 ++ package.json | 1 + src/entities/ProjectGivbackRankView.ts | 53 +++++ src/entities/donation.ts | 2 +- src/entities/entities.ts | 3 + src/entities/project.ts | 4 + src/repositories/donationRepository.test.ts | 2 +- src/repositories/donationRepository.ts | 6 +- .../projectGivbackViewRepository.test.ts | 209 ++++++++++++++++++ .../projectGivbackViewRepository.ts | 34 +++ src/repositories/projectRepository.ts | 27 +-- .../projectVerificationRepository.ts | 39 ++++ src/resolvers/donationResolver.test.ts | 17 +- src/resolvers/donationResolver.ts | 2 +- .../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 +- 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 | 8 +- src/services/Idriss/contractDonations.ts | 2 +- src/services/campaignService.ts | 53 +++-- .../cronJobs/checkQRTransactionJob.ts | 2 +- .../cronJobs/importLostDonationsJob.ts | 2 +- src/services/cronJobs/updatePowerRoundJob.ts | 2 + src/services/givbackService.ts | 20 +- src/services/googleSheets.ts | 2 +- src/services/onramper/donationService.ts | 2 +- src/services/recurringDonationService.ts | 2 +- test/graphqlQueries.ts | 1 + test/pre-test-scripts.ts | 2 + test/testUtils.ts | 2 + 44 files changed, 684 insertions(+), 120 deletions(-) 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 migrations/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts create mode 100644 src/entities/ProjectGivbackRankView.ts create mode 100644 src/repositories/projectGivbackViewRepository.test.ts create mode 100644 src/repositories/projectGivbackViewRepository.ts 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/migrations/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts b/migrations/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts new file mode 100644 index 000000000..94abf6ad5 --- /dev/null +++ b/migrations/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/package.json b/package.json index 075cfa477..36d4c96ac 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,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/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..f10daa91b 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -408,6 +408,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..a40887d28 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -388,7 +388,7 @@ function createDonationTestCases() { const newDonation = await createDonation({ donationAnonymous: false, donorUser: user, - isProjectVerified: false, + isProjectGivbackEligible: false, isTokenEligibleForGivback: false, project, segmentNotified: false, diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 4094b531b..5505d6d7b 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -71,7 +71,7 @@ export const createDonation = async (data: { fromWalletAddress: string; transactionId: string; tokenAddress: string; - isProjectVerified: boolean; + isProjectGivbackEligible: boolean; donorUser: any; isTokenEligibleForGivback: boolean; segmentNotified: boolean; @@ -99,7 +99,7 @@ export const createDonation = async (data: { tokenAddress, project, isTokenEligibleForGivback, - isProjectVerified, + isProjectGivbackEligible, donationAnonymous, toWalletAddress, fromWalletAddress, @@ -128,7 +128,7 @@ export const createDonation = async (data: { tokenAddress, project, isTokenEligibleForGivback, - isProjectVerified, + isProjectGivbackEligible, createdAt: new Date(), segmentNotified: true, toWalletAddress, 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.ts b/src/repositories/projectRepository.ts index 013d3d0e9..1edcc8485 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -199,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, @@ -208,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, @@ -233,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: @@ -245,13 +248,16 @@ 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; + 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; } @@ -325,14 +331,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, @@ -382,7 +380,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(); }; 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/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 270703001..13e67e3ed 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'); @@ -1572,7 +1572,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 +1614,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 +2388,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 +2425,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 +2465,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 +3800,7 @@ function donationsByUserIdTestCases() { walletAddress: generateRandomEtheriumAddress(), categories: ['food1'], verified: true, + isGivbackEligible: true, listed: true, reviewStatus: ReviewStatus.Listed, giveBacks: false, @@ -3883,6 +3885,7 @@ function donationsByUserIdTestCases() { listed: true, reviewStatus: ReviewStatus.Listed, giveBacks: false, + isGivbackEligible: false, creationDate: new Date(), updatedAt: new Date(), latestUpdateCreationDate: new Date(), diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 750a7eaf0..211fa7bf7 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -916,7 +916,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/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/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..8b53726e3 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -921,13 +921,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/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/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index a7a17a725..05b1e7abf 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -148,7 +148,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/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 2bf99186d..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; 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/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/test/graphqlQueries.ts b/test/graphqlQueries.ts index b3c4e8d14..3977856f0 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -927,6 +927,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..a05eea665 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,