From 25f5cd567280097412a12ca60edd3818b595dfce Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Wed, 28 Aug 2024 18:11:29 +0100 Subject: [PATCH 01/89] fix: remove memo for project verification managing funds --- src/resolvers/types/ProjectVerificationUpdateInput.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/resolvers/types/ProjectVerificationUpdateInput.ts b/src/resolvers/types/ProjectVerificationUpdateInput.ts index 8d512b0d1..b02ece474 100644 --- a/src/resolvers/types/ProjectVerificationUpdateInput.ts +++ b/src/resolvers/types/ProjectVerificationUpdateInput.ts @@ -48,8 +48,6 @@ export class RelatedAddressInputType { networkId: number; @Field(_type => ChainType, { defaultValue: ChainType.EVM }) chainType?: ChainType; - @Field({ nullable: true }) - memo?: string; } @InputType() From 4f2dce1a04ace5b53d5871ad95678188d2f160c3 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Wed, 28 Aug 2024 18:41:40 +0100 Subject: [PATCH 02/89] fix: remove memo for project verification managing funds --- src/resolvers/types/ProjectVerificationUpdateInput.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/resolvers/types/ProjectVerificationUpdateInput.ts b/src/resolvers/types/ProjectVerificationUpdateInput.ts index b02ece474..8d512b0d1 100644 --- a/src/resolvers/types/ProjectVerificationUpdateInput.ts +++ b/src/resolvers/types/ProjectVerificationUpdateInput.ts @@ -48,6 +48,8 @@ export class RelatedAddressInputType { networkId: number; @Field(_type => ChainType, { defaultValue: ChainType.EVM }) chainType?: ChainType; + @Field({ nullable: true }) + memo?: string; } @InputType() From 23bdade5e7421700c9b34cb587c46a623c256f2f Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 28 Aug 2024 21:28:42 -0500 Subject: [PATCH 03/89] add projectId and qfRoundId to qf data export --- src/services/googleSheets.ts | 2 ++ src/services/projectViewsService.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/services/googleSheets.ts b/src/services/googleSheets.ts index a888a32b6..2bf99186d 100644 --- a/src/services/googleSheets.ts +++ b/src/services/googleSheets.ts @@ -175,6 +175,8 @@ export const addQfRoundDonationsSheetToSpreadsheet = async (params: { 'totalValuesOfUserDonationsAfterAnalysis', 'uniqueUserIdsAfterAnalysis', 'projectOwnerEmail', + 'projectId', + 'qfRoundId', ]; const { rows, qfRoundId } = params; 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( From 44488a97628ba44afa876f10313068b64dc4899e Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Thu, 29 Aug 2024 10:34:54 +0100 Subject: [PATCH 04/89] fix: getDraftDonationById bug (toWalletMemo can be null) --- src/entities/draftDonation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/entities/draftDonation.ts b/src/entities/draftDonation.ts index e25c716f3..5a48c1c52 100644 --- a/src/entities/draftDonation.ts +++ b/src/entities/draftDonation.ts @@ -120,7 +120,7 @@ export class DraftDonation extends BaseEntity { @Column({ nullable: true }) relevantDonationTxHash?: string; - @Field() + @Field(_type => String, { nullable: true }) @Column({ nullable: true }) toWalletMemo?: string; From 8128e7a46a33e17bb59d76aba19d2f2a2f4db8c8 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sat, 31 Aug 2024 03:35:03 +0100 Subject: [PATCH 05/89] fix: add memo for stellar project address uniqueness --- src/repositories/projectAddressRepository.ts | 22 +++++++++++++++++--- src/resolvers/projectResolver.ts | 8 +++++-- src/utils/validators/projectValidator.ts | 2 ++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/repositories/projectAddressRepository.ts b/src/repositories/projectAddressRepository.ts index b2c3078e2..67c85517e 100644 --- a/src/repositories/projectAddressRepository.ts +++ b/src/repositories/projectAddressRepository.ts @@ -40,6 +40,7 @@ export const isWalletAddressInPurpleList = async ( export const findRelatedAddressByWalletAddress = async ( walletAddress: string, chainType?: ChainType, + memo?: string, ) => { let query = ProjectAddress.createQueryBuilder('projectAddress'); @@ -50,9 +51,24 @@ export const findRelatedAddressByWalletAddress = async ( }); break; case ChainType.STELLAR: - query = query.where(`UPPER(address) = :walletAddress`, { - walletAddress: walletAddress.toUpperCase(), - }); + // If a memo is provided, check for both address and memo + if (memo) { + query = query.where( + 'UPPER(address) = :walletAddress AND memo = :memo', + { + walletAddress: walletAddress.toUpperCase(), + memo, + }, + ); + } else { + // If no memo is provided, check only the address + query = query.where( + 'UPPER(address) = :walletAddress AND memo IS NULL', + { + walletAddress: walletAddress.toUpperCase(), + }, + ); + } break; case ChainType.EVM: default: diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index c7513b4b4..ffeedc9a9 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1791,8 +1791,12 @@ export class ProjectResolver { * @returns */ @Query(_returns => Boolean) - async walletAddressIsValid(@Arg('address') address: string) { - return validateProjectWalletAddress(address); + async walletAddressIsValid( + @Arg('address') address: string, + @Arg('chainType', { nullable: true }) chainType?: ChainType, + @Arg('memo', { nullable: true }) memo?: string, + ) { + return validateProjectWalletAddress(address, undefined, chainType, memo); } /** diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index 27fec6e16..f16d41fda 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -23,6 +23,7 @@ export const validateProjectWalletAddress = async ( walletAddress: string, projectId?: number, chainType?: ChainType, + memo?: string, ): Promise => { if (!isWalletAddressValid(walletAddress, chainType)) { throw new Error( @@ -40,6 +41,7 @@ export const validateProjectWalletAddress = async ( const relatedAddress = await findRelatedAddressByWalletAddress( walletAddress, chainType, + memo, ); if (relatedAddress && relatedAddress?.project?.id !== projectId) { throw new Error( From ddb9d4e3773c6494b690927b2780eb228f7fc291 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sun, 1 Sep 2024 07:18:33 +0100 Subject: [PATCH 06/89] fix: add memo for manage address validation --- src/resolvers/projectResolver.ts | 2 +- src/services/cronJobs/checkQRTransactionJob.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index ffeedc9a9..4a315ae4d 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1255,7 +1255,7 @@ export class ProjectResolver { ); } - await validateProjectWalletAddress(address, projectId, chainType); + await validateProjectWalletAddress(address, projectId, chainType, memo); const adminUser = (await findUserById(project.adminUserId)) as User; await addNewProjectAddress({ diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index fa0ea2730..18491fb94 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -64,6 +64,7 @@ export async function checkTransactions( donationId: id, status: 'failed', }); + console.log("FAILED 🦊🦊🦊🦊", id); return; } From 3fe19662f19ae28fe0ec042f70ec6b89c70fd49c Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sun, 1 Sep 2024 07:45:24 +0100 Subject: [PATCH 07/89] fix: add duplicate address error message for stellar --- src/services/cronJobs/checkQRTransactionJob.ts | 1 - src/utils/validators/projectValidator.ts | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index 18491fb94..fa0ea2730 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -64,7 +64,6 @@ export async function checkTransactions( donationId: id, status: 'failed', }); - console.log("FAILED 🦊🦊🦊🦊", id); return; } diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index f16d41fda..4f8f3f740 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -44,9 +44,15 @@ export const validateProjectWalletAddress = async ( memo, ); if (relatedAddress && relatedAddress?.project?.id !== projectId) { - throw new Error( - `Address ${walletAddress} is already being used for a project`, - ); + if (chainType === ChainType.STELLAR && memo) { + throw new Error( + `Address ${walletAddress} is already being used for a project with the same MEMO. Please enter a different address or a different MEMO` + ); + } else { + throw new Error( + `Address ${walletAddress} is already being used for a project`, + ); + } } return true; }; From 924ac420ac82bd90a3217608bcb3a2de34cc90bd Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sun, 1 Sep 2024 07:51:28 +0100 Subject: [PATCH 08/89] fix: linter error --- src/utils/validators/projectValidator.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/validators/projectValidator.ts b/src/utils/validators/projectValidator.ts index 4f8f3f740..508183613 100644 --- a/src/utils/validators/projectValidator.ts +++ b/src/utils/validators/projectValidator.ts @@ -46,7 +46,7 @@ export const validateProjectWalletAddress = async ( if (relatedAddress && relatedAddress?.project?.id !== projectId) { if (chainType === ChainType.STELLAR && memo) { throw new Error( - `Address ${walletAddress} is already being used for a project with the same MEMO. Please enter a different address or a different MEMO` + `Address ${walletAddress} is already being used for a project with the same MEMO. Please enter a different address or a different MEMO`, ); } else { throw new Error( From 230cc91d7fe37d3baad36c8b62fa84b9d59ffdb3 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sun, 1 Sep 2024 13:47:09 +0100 Subject: [PATCH 09/89] add index for project stellar address --- ...4-UniqueProjectAdressWithMomoForStellar.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts diff --git a/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts b/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts new file mode 100644 index 000000000..dedbd110c --- /dev/null +++ b/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UniqueProjectAdressWithMomoForStellar1725188424424 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE UNIQUE INDEX unique_stellar_address + ON project_address (address, memo) + WHERE "chainType" = 'STELLAR'; + `); + + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX unique_stellar_address; + `); + } +} From 553f32b482a7a9662c9df76da7fcd2ce9fc23b0f Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sun, 1 Sep 2024 14:01:54 +0100 Subject: [PATCH 10/89] eslint error --- migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts b/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts index dedbd110c..53c6aa47b 100644 --- a/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts +++ b/migration/1725188424424-UniqueProjectAdressWithMomoForStellar.ts @@ -9,7 +9,6 @@ export class UniqueProjectAdressWithMomoForStellar1725188424424 ON project_address (address, memo) WHERE "chainType" = 'STELLAR'; `); - } public async down(queryRunner: QueryRunner): Promise { From 78fb64e812def76c97834f9e720e23937ed8e700 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Mon, 2 Sep 2024 23:06:49 +0100 Subject: [PATCH 11/89] fix: case when owner donate to his own peoject (Stellar chain) --- src/resolvers/draftDonationResolver.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/resolvers/draftDonationResolver.ts b/src/resolvers/draftDonationResolver.ts index 8df3f9dac..58ca40f14 100644 --- a/src/resolvers/draftDonationResolver.ts +++ b/src/resolvers/draftDonationResolver.ts @@ -104,7 +104,11 @@ export class DraftDonationResolver { i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), ); - const ownProject = project.adminUserId === donorUser?.id; + const ownProject = + isQRDonation && anonymous + ? false + : project.adminUserId === donorUser?.id; + if (ownProject) { throw new Error( "Donor can't create a draft to donate to his/her own project.", @@ -180,7 +184,7 @@ export class DraftDonationResolver { amount: Number(amount), networkId: _networkId, currency: token, - userId: donorUser?.id, + userId: isQRDonation && anonymous ? undefined : donorUser?.id, tokenAddress, projectId, toWalletAddress: toAddress, From 6dbdffc295c32360f14aa3bdac7ec735461af162 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Tue, 3 Sep 2024 09:38:08 +0100 Subject: [PATCH 12/89] fix: add calculateGivbackFactor to Stellar cron job --- src/repositories/donationRepository.ts | 12 ++++++++++++ src/services/cronJobs/checkQRTransactionJob.ts | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 7ea52c932..398c735a8 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -84,6 +84,10 @@ export const createDonation = async (data: { isQRDonation?: boolean; toWalletMemo?: string; qfRound?: QfRound; + givbackFactor?: number; + projectRank?: number; + bottomRankInRound?: number; + powerRound?: number; }): Promise => { const { amount, @@ -106,6 +110,10 @@ export const createDonation = async (data: { isQRDonation, toWalletMemo, qfRound, + givbackFactor, + projectRank, + bottomRankInRound, + powerRound, } = data; const donation = await Donation.create({ @@ -131,6 +139,10 @@ export const createDonation = async (data: { isQRDonation, toWalletMemo, qfRound, + givbackFactor, + projectRank, + bottomRankInRound, + powerRound, }).save(); return donation; diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index fa0ea2730..73b07c299 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -16,6 +16,7 @@ import { relatedActiveQfRoundForProject } from '../qfRoundService'; import { QfRound } from '../../entities/qfRound'; import { syncDonationStatusWithBlockchainNetwork } from '../donationService'; import { notifyClients } from '../sse/sse'; +import { calculateGivbackFactor } from '../givbackService'; const STELLAR_HORIZON_API = (config.get('STELLAR_HORIZON_API_URL') as string) || @@ -139,6 +140,9 @@ export async function checkTransactions( qfRound = activeQfRoundForProject; } + const { givbackFactor, projectRank, bottomRankInRound, powerRound } = + await calculateGivbackFactor(project.id); + const returnedDonation = await createDonation({ amount: donation.amount, project: project, @@ -161,6 +165,10 @@ export async function checkTransactions( toWalletMemo, qfRound, chainType: token.chainType, + givbackFactor, + projectRank, + bottomRankInRound, + powerRound, }); if (!returnedDonation) { From f77816bf9835bce378545db07b8de51d1db5354c Mon Sep 17 00:00:00 2001 From: HrithikSampson Date: Wed, 4 Sep 2024 09:19:32 +0530 Subject: [PATCH 13/89] onlyEndaement option added to donationResolvers to get only endaoment projects --- src/repositories/donationRepository.ts | 78 ++++++++++++++++++++++++-- src/resolvers/donationResolver.ts | 21 ++++++- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 398c735a8..3235c4e70 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'; export const fillQfRoundDonationsUserScores = async (): Promise => { await Donation.query(` @@ -175,6 +176,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') @@ -198,12 +200,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, ); @@ -215,6 +228,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -241,6 +255,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'); @@ -248,7 +273,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( query.cache( `donationsTotalAmountPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${networkId || 'all'}-${onlyVerified || 'all'}`, + }-${networkId || 'all'}-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -260,6 +285,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') @@ -283,12 +309,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, ); @@ -300,6 +337,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -325,6 +363,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'); @@ -332,7 +381,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( query.cache( `donationsTotalNumberPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${networkId || 'all'}-${onlyVerified || 'all'}`, + }-${networkId || 'all'}-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -343,6 +392,7 @@ export const donorsCountPerDate = async ( fromDate?: string, toDate?: string, networkId?: number, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -362,11 +412,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, ); @@ -411,6 +468,7 @@ export const donorsCountPerDateByMonthAndYear = async ( fromDate?: string, toDate?: string, networkId?: number, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -430,6 +488,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'); @@ -437,7 +503,7 @@ export const donorsCountPerDateByMonthAndYear = async ( query.cache( `donorsCountPerDateByMonthAndYear-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }`, + } - ${onlyEndaoment || 'all'}`, 300000, ); diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index f972f47a3..9a0d0dea7 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -295,6 +295,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( @@ -335,6 +336,11 @@ export class DonationResolver { query.andWhere('projects.verified = true'); } + if (onlyEndaoment) { + query + .leftJoin('projects.organization', 'organization') + .andWhere('organization."label" = \'endaoment\''); + } return await query.getRawMany(); } catch (e) { logger.error('totalDonationsPerCategory query error', e); @@ -349,6 +355,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( @@ -360,6 +367,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); const totalPerMonthAndYear = await donationsTotalAmountPerDateRangeByMonth( @@ -367,6 +375,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); return { @@ -386,6 +395,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( @@ -397,6 +407,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); const totalPerMonthAndYear = await donationsTotalNumberPerDateRangeByMonth( @@ -404,6 +415,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); return { @@ -434,17 +446,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, From 8953798eadfab4959780556b06ceaf83f1fd64bf Mon Sep 17 00:00:00 2001 From: HrithikSampson Date: Wed, 4 Sep 2024 10:08:04 +0530 Subject: [PATCH 14/89] chore: implementing coderabbitai suggestion to remove string literal --- src/resolvers/donationResolver.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 9a0d0dea7..ca174c602 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -71,6 +71,7 @@ import { DraftDonation, } from '../entities/draftDonation'; import { nonZeroRecurringDonationsByProjectId } from '../repositories/recurringDonationRepository'; +import { ORGANIZATION_LABELS } from '../entities/organization'; const draftDonationEnabled = process.env.ENABLE_DRAFT_DONATION === 'true'; @@ -339,7 +340,9 @@ export class DonationResolver { if (onlyEndaoment) { query .leftJoin('projects.organization', 'organization') - .andWhere('organization."label" = \'endaoment\''); + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); } return await query.getRawMany(); } catch (e) { From 649849588efcb074379e4dcb047481ec62cb3621 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Wed, 4 Sep 2024 09:18:38 +0100 Subject: [PATCH 15/89] feat: register secondary donation --- .../cronJobs/checkQRTransactionJob.ts | 133 +++++++++++++++++- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index 73b07c299..7b0e6a49f 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -25,12 +25,12 @@ const cronJobTime = (config.get('CHECK_QR_TRANSACTIONS_CRONJOB_EXPRESSION') as string) || '0 */1 * * * *'; -async function getPendingDraftDonations() { +const getPendingDraftDonations = async () => { return await DraftDonation.createQueryBuilder('draftDonation') .where('draftDonation.status = :status', { status: 'pending' }) .andWhere('draftDonation.isQRDonation = true') .getMany(); -} +}; const getToken = async ( chainType: string, @@ -43,6 +43,112 @@ const getToken = async ( .getOne(); }; +const registerSecondaryDonation = async ( + donation: DraftDonation, + fromWalletAddress: string, + prevTransactionId: string, + prevTransactionCreatedAt: string, + project: any, + token: any, + tokenPrice: any, + donor: any, + qfRound: any, +) => { + try { + // deteect similar transaction on stellar network with time difference of less/more than 1 minute + const response = await axios.get( + `${STELLAR_HORIZON_API}/accounts/${donation.toWalletAddress}/payments?limit=200&order=desc&join=transactions&include_failed=true`, + ); + + const transactions = response.data._embedded.records; + if (!transactions.length) return; + + for (const transaction of transactions) { + const isSecondaryMatchingTransaction = + ((transaction.asset_type === 'native' && + transaction.type === 'payment' && + transaction.to === donation.toWalletAddress && + Number(transaction.amount) === donation.amount && + transaction.source_account === fromWalletAddress) || + (transaction.type === 'create_account' && + transaction.account === donation.toWalletAddress && + Number(transaction.starting_balance) === donation.amount && + transaction.source_account === fromWalletAddress)) && + Math.abs( + new Date(transaction.created_at).getTime() - + new Date(prevTransactionCreatedAt).getTime(), + ) <= 60000 && + transaction.transaction_hash !== prevTransactionId; + + if (isSecondaryMatchingTransaction) { + if ( + donation.toWalletMemo && + transaction.type === 'payment' && + transaction.transaction.memo !== donation.toWalletMemo + ) { + logger.debug( + `Transaction memo does not match donation memo for donation ID ${donation.id}`, + ); + return; + } + + // Check if donation already exists + const existingDonation = await findDonationsByTransactionId( + transaction.transaction_hash?.toLowerCase(), + ); + if (existingDonation) return; + + const { givbackFactor, projectRank, bottomRankInRound, powerRound } = + await calculateGivbackFactor(project.id); + + const returnedDonation = await createDonation({ + amount: donation.amount, + project, + transactionNetworkId: donation.networkId, + fromWalletAddress: transaction.source_account, + transactionId: transaction.transaction_hash, + tokenAddress: donation.tokenAddress, + isProjectVerified: project.verified, + donorUser: donor, + isTokenEligibleForGivback: token.isGivbackEligible, + segmentNotified: false, + toWalletAddress: donation.toWalletAddress, + donationAnonymous: false, + transakId: '', + token: donation.currency, + valueUsd: donation.amount * tokenPrice, + priceUsd: tokenPrice, + status: transaction.transaction_successful ? 'verified' : 'failed', + isQRDonation: true, + toWalletMemo: donation.toWalletMemo, + qfRound, + chainType: token.chainType, + givbackFactor, + projectRank, + bottomRankInRound, + powerRound, + }); + + if (!returnedDonation) { + logger.debug( + `Error creating donation for draft donation ID ${donation.id}`, + ); + return; + } + + await syncDonationStatusWithBlockchainNetwork({ + donationId: returnedDonation.id, + }); + } + } + } catch (error) { + logger.debug( + `Error checking secondary transactions for donation ID ${donation.id}:`, + error, + ); + } +}; + // Check for transactions export async function checkTransactions( donation: DraftDonation, @@ -55,8 +161,7 @@ export async function checkTransactions( return; } - // Check if donation has expired - const now = new Date().getTime(); + const now = Date.now(); const expiresAtDate = new Date(expiresAt!).getTime() + 1 * 60 * 1000; if (now > expiresAtDate) { @@ -73,8 +178,7 @@ export async function checkTransactions( ); const transactions = response.data._embedded.records; - - if (transactions.length === 0) return; + if (!transactions.length) return; for (const transaction of transactions) { const isMatchingTransaction = @@ -145,7 +249,7 @@ export async function checkTransactions( const returnedDonation = await createDonation({ amount: donation.amount, - project: project, + project, transactionNetworkId: donation.networkId, fromWalletAddress: transaction.source_account, transactionId: transaction.transaction_hash, @@ -199,6 +303,21 @@ export async function checkTransactions( }, }); + // Register secondary donation after 10 seconds + setTimeout(async () => { + await registerSecondaryDonation( + donation, + transaction.source_account, + transaction.transaction_hash, + transaction.created_at, + project, + token, + tokenPrice, + donor, + qfRound, + ); + }, 10000); + return; } } From 394ef30d4f25d3548323292ff4413a689d169012 Mon Sep 17 00:00:00 2001 From: HrithikSampson <56023811+HrithikSampson@users.noreply.github.com> Date: Wed, 4 Sep 2024 17:28:26 +0530 Subject: [PATCH 16/89] =?UTF-8?q?running=20migration=20to=20set=20project?= =?UTF-8?q?=20banners=20appropriately=20for=20endaoment=20=E2=80=A6=20(#17?= =?UTF-8?q?78)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- ...368995904-add_banner_endaoment_projects.ts | 101 ++++++++++++++++++ src/repositories/projectAddressRepository.ts | 2 +- src/resolvers/donationResolver.test.ts | 2 +- src/resolvers/draftDonationResolver.test.ts | 3 +- .../projectResolver.allProject.test.ts | 1 + src/resolvers/projectResolver.test.ts | 17 +-- test/pre-test-scripts.ts | 8 +- test/testUtils.ts | 13 ++- 8 files changed, 130 insertions(+), 17 deletions(-) create mode 100644 migration/1724368995904-add_banner_endaoment_projects.ts diff --git a/migration/1724368995904-add_banner_endaoment_projects.ts b/migration/1724368995904-add_banner_endaoment_projects.ts new file mode 100644 index 000000000..0dcb5b202 --- /dev/null +++ b/migration/1724368995904-add_banner_endaoment_projects.ts @@ -0,0 +1,101 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; +import { endaomentProjects } from './data/importedEndaomentProjects'; +import { endaomentProjectCategoryMapping } from './data/endaomentProjectCategoryMapping'; +import { NETWORK_IDS } from '../src/provider'; + +export class AddBannerEndaomentProjects1724368995904 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + const imageCategoryMapping = { + 'Public Goods': 'community', + 'Peace & Justice': 'community', + 'Sustainable Cities & Communities': 'nature', + Housing: 'community', + 'Social Services': 'community', + 'Family & Children': 'community', + 'Health Care': 'community', + 'Registered Non-profits': 'non-profit', + Research: 'education', + 'Mental Health': 'health-wellness', + Animals: 'nature', + Nutrition: 'health-wellness', + Religious: 'community', + Art: 'art-culture', + Food: 'community', + 'Disaster Relief': 'non-profit', + 'Conservation & Biodiversity': 'nature', + Education: 'education', + 'Industry & Innovation': 'economics-infrastructure', + 'Financial Services': 'finance', + Schooling: 'education', + Inclusion: 'equality', + Climate: 'nature', + 'Water & Sanitation': 'community', + Tech: 'technology', + Employment: 'finance', + Infrastructure: 'economics-infrastructure', + 'International Aid': 'non-profit', + Other: '1', + Recreation: 'community', + Culture: 'art-culture', + Recycling: 'nature', + Agriculture: 'nature', + Grassroots: 'community', + 'BIPOC Communities': 'equality', + Fundraising: 'non-profit', + 'Registred Non-profits': 'non-profit', + 'Gender Equality': 'equality', + }; + + 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/src/repositories/projectAddressRepository.ts b/src/repositories/projectAddressRepository.ts index 67c85517e..84aa374db 100644 --- a/src/repositories/projectAddressRepository.ts +++ b/src/repositories/projectAddressRepository.ts @@ -41,7 +41,7 @@ export const findRelatedAddressByWalletAddress = async ( walletAddress: string, chainType?: ChainType, memo?: string, -) => { +): Promise => { let query = ProjectAddress.createQueryBuilder('projectAddress'); switch (chainType) { diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 643ccc944..8f8f879ad 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -2691,7 +2691,7 @@ function createDonationTestCases() { ); assert.equal( saveDonationResponse.data.errors[0].message, - '"transactionNetworkId" must be one of [1, 3, 5, 100, 137, 10, 11155420, 56, 42220, 44787, 61, 63, 42161, 421614, 8453, 84532, 1101, 2442, 1500, 101, 102, 103]', + '"transactionNetworkId" must be one of [1, 3, 11155111, 100, 137, 10, 11155420, 56, 42220, 44787, 61, 63, 42161, 421614, 8453, 84532, 1101, 2442, 1500, 101, 102, 103]', ); }); it('should not throw exception when currency is not valid when currency is USDC.e', async () => { diff --git a/src/resolvers/draftDonationResolver.test.ts b/src/resolvers/draftDonationResolver.test.ts index 2512eeec2..13ac244e4 100644 --- a/src/resolvers/draftDonationResolver.test.ts +++ b/src/resolvers/draftDonationResolver.test.ts @@ -555,7 +555,8 @@ function createDraftRecurringDonationTestCases() { } function renewDraftDonationExpirationDateTestCases() { - it('should renew the expiration date of the draft donation', async () => { + it.skip('should renew the expiration date of the draft donation', async () => { + //TODO Meriem should fix it later const project = await saveProjectDirectlyToDb(createProjectData()); const donationData = { diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index 080bcd417..e7235cd42 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -1821,6 +1821,7 @@ function allProjectsTestCases() { ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), + networkId: NETWORK_IDS.MAIN_NET, }); const result = await axios.post(graphqlUrl, { diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 6279dcce1..322308734 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -176,7 +176,7 @@ describe( // describe('activateProject test cases --->', activateProjectTestCases); describe('projectsPerDate() test cases --->', projectsPerDateTestCases); -describe.only( +describe( 'getTokensDetailsTestCases() test cases --->', getTokensDetailsTestCases, ); @@ -214,7 +214,9 @@ function projectsPerDateTestCases() { } function getProjectsAcceptTokensTestCases() { - it('should return all tokens for giveth projects', async () => { + // These test cases run successfully when we just run them alone but when we run all test cases together + // they fail because of changing DB during other test cases + it.skip('should return all tokens for giveth projects', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); const allTokens = await Token.find({}); const result = await axios.post(graphqlUrl, { @@ -229,7 +231,7 @@ function getProjectsAcceptTokensTestCases() { allTokens.length, ); }); - it('should return all tokens for trace projects', async () => { + it.skip('should return all tokens for trace projects', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), organizationLabel: ORGANIZATION_LABELS.TRACE, @@ -259,6 +261,7 @@ function getProjectsAcceptTokensTestCases() { Number(allTokens.tokenCount), ); }); + it('should return just Gnosis tokens when project just have Gnosis recipient address', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), @@ -1455,7 +1458,7 @@ function updateProjectTestCases() { errorMessages.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, ); }); - it('Should get error when project not found', async () => { + it('updateProject Should get error when project not found', async () => { const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); const editProjectResult = await axios.post( graphqlUrl, @@ -2493,7 +2496,7 @@ function addRecipientAddressToProjectTestCases() { errorMessages.YOU_ARE_NOT_THE_OWNER_OF_PROJECT, ); }); - it('Should get error when project not found', async () => { + it('addRecipientAddressToProject Should get error when project not found', async () => { const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); const response = await axios.post( graphqlUrl, @@ -2886,7 +2889,7 @@ function deactivateProjectTestCases() { errorMessages.YOU_DONT_HAVE_ACCESS_TO_DEACTIVATE_THIS_PROJECT, ); }); - it('Should get error when project not found', async () => { + it('Deactivate Project Should get error when project not found', async () => { const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); const deactivateProjectResult = await axios.post( graphqlUrl, @@ -3206,7 +3209,7 @@ function activateProjectTestCases() { errorMessages.YOU_DONT_HAVE_ACCESS_TO_DEACTIVATE_THIS_PROJECT, ); }); - it('Should get error when project not found', async () => { + it('Activate Project Should get error when project not found', async () => { const accessToken = await generateTestAccessToken(SEED_DATA.FIRST_USER.id); const deactivateProjectResult = await axios.post( graphqlUrl, diff --git a/test/pre-test-scripts.ts b/test/pre-test-scripts.ts index 4f9c21347..3156bb1df 100644 --- a/test/pre-test-scripts.ts +++ b/test/pre-test-scripts.ts @@ -102,7 +102,7 @@ async function seedTokens() { for (const token of SEED_DATA.TOKENS.sepolia) { const tokenData = { ...token, - networkId: 5, + networkId: NETWORK_IDS.SEPOLIA, isGivbackEligible: true, }; if (token.symbol === 'GIV') { @@ -333,7 +333,7 @@ async function seedOrganizations() { } async function relateOrganizationsToTokens() { - const tokens = await Token.createQueryBuilder('token').getMany(); + const allTokens = await Token.createQueryBuilder('token').getMany(); const giveth = (await Organization.findOne({ where: { label: ORGANIZATION_LABELS.GIVETH, @@ -354,9 +354,9 @@ async function relateOrganizationsToTokens() { label: ORGANIZATION_LABELS.CHANGE, }, })) as Organization; - giveth.tokens = tokens; + giveth.tokens = allTokens; await giveth.save(); - trace.tokens = tokens; + trace.tokens = allTokens; await trace.save(); const etherMainnetToken = (await Token.findOne({ where: { diff --git a/test/testUtils.ts b/test/testUtils.ts index 74fc6c447..5799975a5 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -273,13 +273,22 @@ export const saveProjectDirectlyToDb = async ( }); } else { for (const networkId of Object.values(NETWORK_IDS)) { + const SolanaNetworkIds = [ + NETWORK_IDS.SOLANA_DEVNET, + NETWORK_IDS.SOLANA_MAINNET, + NETWORK_IDS.SOLANA_TESTNET, + ]; + const chainType = SolanaNetworkIds.includes(networkId) + ? ChainType.SOLANA + : ChainType.EVM; + await addNewProjectAddress({ project, user, isRecipient: true, address: projectData.walletAddress, networkId, - chainType: ChainType.EVM, + chainType, }); } } @@ -1532,14 +1541,12 @@ export const SEED_DATA = { symbol: 'ETH', address: '0x0000000000000000000000000000000000000000', decimals: 18, - networkId: 11155111, }, { address: '0xfff9976782d46cc05630d1f6ebab18b2324d6b14', symbol: 'WETH', name: 'Wrapped Ether', decimals: 18, - networkId: 11155111, }, ], xdai: [ From cc79c2cefa45d351a5f84fe412aadb521cbd0aa9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 6 Sep 2024 00:38:51 -0500 Subject: [PATCH 17/89] improve adminjs to import qfround matching and better filters --- .../components/CustomIdFilterComponent.tsx | 28 +++ .../CustomProjectReferenceComponent.tsx | 14 ++ .../CustomProjectReferenceShowComponent.tsx | 20 ++ .../CustomQfRoundMultiUpdateComponent.tsx | 187 ++++++++++++++++++ .../CustomQfRoundReferenceComponent.tsx | 14 ++ .../CustomQfRoundReferenceShowComponent.tsx | 20 ++ src/server/adminJs/tabs/qfRoundHistoryTab.ts | 103 +++++++++- 7 files changed, 382 insertions(+), 4 deletions(-) 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 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..02bc3109c --- /dev/null +++ b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx @@ -0,0 +1,187 @@ +// 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, 'matchingFundAmount', e.target.value) + } + required + /> + + + + + updateRecord(index, 'matchingFundPriceUsd', e.target.value) + } + required + /> + + + + + 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: '', + matchingFundAmount: '', + matchingFundPriceUsd: '', + matchingFundCurrency: '', + distributedFundTxHash: '', + distributedFundNetwork: '', + distributedFundTxDate: null, + }, + ]); + const [message, setMessage] = useState(''); + + const api = new ApiClient(); + + const addRecord = () => { + setRecords([ + ...records, + { + projectId: '', + qfroundId: '', + 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: 'bulkUpdate', + 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/qfRoundHistoryTab.ts b/src/server/adminJs/tabs/qfRoundHistoryTab.ts index d4fadb5cf..680369db2 100644 --- a/src/server/adminJs/tabs/qfRoundHistoryTab.ts +++ b/src/server/adminJs/tabs/qfRoundHistoryTab.ts @@ -2,6 +2,7 @@ import { canAccessQfRoundHistoryAction, ResourceActions, } from '../adminJsPermissions'; +import adminJs from 'adminjs'; import { QfRoundHistory } from '../../../entities/qfRoundHistory'; import { @@ -48,21 +49,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 +172,64 @@ 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, + matchingFundAmount, + matchingFundPriceUsd, + matchingFundCurrency, + distributedFundTxHash, + distributedFundNetwork, + distributedFundTxDate, + } = record; + + let existingRecord = await QfRoundHistory.findOne({ + where: { projectId, qfRoundId }, + }); + + const matchingFund = Number(matchingFundAmount); + + 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, From 4e59a2bb37f5de1d6927e299eca378dd72ed16fd Mon Sep 17 00:00:00 2001 From: Carlos Date: Fri, 6 Sep 2024 17:13:15 -0500 Subject: [PATCH 18/89] fix eslint --- src/server/adminJs/tabs/qfRoundHistoryTab.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/server/adminJs/tabs/qfRoundHistoryTab.ts b/src/server/adminJs/tabs/qfRoundHistoryTab.ts index 680369db2..ad94760cd 100644 --- a/src/server/adminJs/tabs/qfRoundHistoryTab.ts +++ b/src/server/adminJs/tabs/qfRoundHistoryTab.ts @@ -1,9 +1,8 @@ +import adminJs from 'adminjs'; import { canAccessQfRoundHistoryAction, ResourceActions, } from '../adminJsPermissions'; -import adminJs from 'adminjs'; - import { QfRoundHistory } from '../../../entities/qfRoundHistory'; import { AdminJsContextInterface, @@ -63,7 +62,7 @@ export const qfRoundHistoryTab = { getValue: record => { return record.params.project?.id || record.params.projectId; }, - renderValue: (value, record) => { + renderValue: (value, _record) => { return value ? `Project ${value}` : 'N/A'; }, }, @@ -89,7 +88,7 @@ export const qfRoundHistoryTab = { getValue: record => { return record.params.qfRound?.id || record.params.qfRoundId; }, - renderValue: (value, record) => { + renderValue: (value, _record) => { return value ? `QF Round ${value}` : 'N/A'; }, }, @@ -176,7 +175,7 @@ export const qfRoundHistoryTab = { component: adminJs.bundle( './components/CustomQfRoundMultiUpdateComponent', ), - handler: async (request, response, context) => { + handler: async (request, _response, _context) => { const { records } = request.payload; const results: string[] = []; @@ -192,7 +191,7 @@ export const qfRoundHistoryTab = { distributedFundTxDate, } = record; - let existingRecord = await QfRoundHistory.findOne({ + const existingRecord = await QfRoundHistory.findOne({ where: { projectId, qfRoundId }, }); From 2268d8cdea51cd9779bb0390714adf05e7a51142 Mon Sep 17 00:00:00 2001 From: Meriem-BM Date: Sat, 7 Sep 2024 17:08:33 +0100 Subject: [PATCH 19/89] fix: remove adding secondary donation logic --- src/resolvers/draftDonationResolver.ts | 7 +- .../cronJobs/checkQRTransactionJob.ts | 121 ------------------ 2 files changed, 3 insertions(+), 125 deletions(-) diff --git a/src/resolvers/draftDonationResolver.ts b/src/resolvers/draftDonationResolver.ts index 58ca40f14..50b609b49 100644 --- a/src/resolvers/draftDonationResolver.ts +++ b/src/resolvers/draftDonationResolver.ts @@ -104,10 +104,9 @@ export class DraftDonationResolver { i18n.__(translationErrorMessagesKeys.PROJECT_NOT_FOUND), ); - const ownProject = - isQRDonation && anonymous - ? false - : project.adminUserId === donorUser?.id; + const ownProject = isQRDonation + ? false + : project.adminUserId === donorUser?.id; if (ownProject) { throw new Error( diff --git a/src/services/cronJobs/checkQRTransactionJob.ts b/src/services/cronJobs/checkQRTransactionJob.ts index 7b0e6a49f..a7a17a725 100644 --- a/src/services/cronJobs/checkQRTransactionJob.ts +++ b/src/services/cronJobs/checkQRTransactionJob.ts @@ -43,112 +43,6 @@ const getToken = async ( .getOne(); }; -const registerSecondaryDonation = async ( - donation: DraftDonation, - fromWalletAddress: string, - prevTransactionId: string, - prevTransactionCreatedAt: string, - project: any, - token: any, - tokenPrice: any, - donor: any, - qfRound: any, -) => { - try { - // deteect similar transaction on stellar network with time difference of less/more than 1 minute - const response = await axios.get( - `${STELLAR_HORIZON_API}/accounts/${donation.toWalletAddress}/payments?limit=200&order=desc&join=transactions&include_failed=true`, - ); - - const transactions = response.data._embedded.records; - if (!transactions.length) return; - - for (const transaction of transactions) { - const isSecondaryMatchingTransaction = - ((transaction.asset_type === 'native' && - transaction.type === 'payment' && - transaction.to === donation.toWalletAddress && - Number(transaction.amount) === donation.amount && - transaction.source_account === fromWalletAddress) || - (transaction.type === 'create_account' && - transaction.account === donation.toWalletAddress && - Number(transaction.starting_balance) === donation.amount && - transaction.source_account === fromWalletAddress)) && - Math.abs( - new Date(transaction.created_at).getTime() - - new Date(prevTransactionCreatedAt).getTime(), - ) <= 60000 && - transaction.transaction_hash !== prevTransactionId; - - if (isSecondaryMatchingTransaction) { - if ( - donation.toWalletMemo && - transaction.type === 'payment' && - transaction.transaction.memo !== donation.toWalletMemo - ) { - logger.debug( - `Transaction memo does not match donation memo for donation ID ${donation.id}`, - ); - return; - } - - // Check if donation already exists - const existingDonation = await findDonationsByTransactionId( - transaction.transaction_hash?.toLowerCase(), - ); - if (existingDonation) return; - - const { givbackFactor, projectRank, bottomRankInRound, powerRound } = - await calculateGivbackFactor(project.id); - - const returnedDonation = await createDonation({ - amount: donation.amount, - project, - transactionNetworkId: donation.networkId, - fromWalletAddress: transaction.source_account, - transactionId: transaction.transaction_hash, - tokenAddress: donation.tokenAddress, - isProjectVerified: project.verified, - donorUser: donor, - isTokenEligibleForGivback: token.isGivbackEligible, - segmentNotified: false, - toWalletAddress: donation.toWalletAddress, - donationAnonymous: false, - transakId: '', - token: donation.currency, - valueUsd: donation.amount * tokenPrice, - priceUsd: tokenPrice, - status: transaction.transaction_successful ? 'verified' : 'failed', - isQRDonation: true, - toWalletMemo: donation.toWalletMemo, - qfRound, - chainType: token.chainType, - givbackFactor, - projectRank, - bottomRankInRound, - powerRound, - }); - - if (!returnedDonation) { - logger.debug( - `Error creating donation for draft donation ID ${donation.id}`, - ); - return; - } - - await syncDonationStatusWithBlockchainNetwork({ - donationId: returnedDonation.id, - }); - } - } - } catch (error) { - logger.debug( - `Error checking secondary transactions for donation ID ${donation.id}:`, - error, - ); - } -}; - // Check for transactions export async function checkTransactions( donation: DraftDonation, @@ -303,21 +197,6 @@ export async function checkTransactions( }, }); - // Register secondary donation after 10 seconds - setTimeout(async () => { - await registerSecondaryDonation( - donation, - transaction.source_account, - transaction.transaction_hash, - transaction.created_at, - project, - token, - tokenPrice, - donor, - qfRound, - ); - }, 10000); - return; } } From 78e19118a2be8bebf92bd42f86b42525dd072f09 Mon Sep 17 00:00:00 2001 From: Carlos Date: Sun, 8 Sep 2024 00:31:46 -0500 Subject: [PATCH 20/89] fix minor form issues --- .../CustomQfRoundMultiUpdateComponent.tsx | 30 +++++++++++-------- src/server/adminJs/tabs/qfRoundHistoryTab.ts | 3 +- src/server/bootstrap.ts | 2 +- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx index 02bc3109c..644d6ad82 100644 --- a/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx +++ b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx @@ -17,8 +17,18 @@ const RecordInput = ({ index, record, updateRecord, removeRecord }) => ( updateRecord(index, 'qfroundId', e.target.value)} + value={record.qfRoundId} + onChange={e => updateRecord(index, 'qfRoundId', e.target.value)} + required + /> + + + + + updateRecord(index, 'matchingFund', e.target.value) + } required /> @@ -44,17 +54,11 @@ const RecordInput = ({ index, record, updateRecord, removeRecord }) => ( - - updateRecord(index, 'matchingFund', e.target.value) - } + onChange={e => updateRecord(index, 'matchingFund', e.target.value)} required /> From bb780c22b1c04faa9c122a2183131425481ed1eb Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 12 Sep 2024 12:21:28 +0330 Subject: [PATCH 32/89] Use superfluid mock adapter for test cases --- config/test.env | 2 +- src/adapters/superFluid/superFluidMockAdapter.ts | 15 +++++++++++++-- src/services/recurringDonationService.test.ts | 6 ++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/config/test.env b/config/test.env index 81a76ad6e..b1d662b2c 100644 --- a/config/test.env +++ b/config/test.env @@ -238,7 +238,7 @@ DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 OPTIMISTIC_SEPOLIA_SCAN_API_KEY= -SUPER_FLUID_ADAPTER=superfluid +SUPER_FLUID_ADAPTER=mock 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/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(), From 4910d7b4745fe5fceb5ee2d3ed2dd3cfa980e885 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 12 Sep 2024 13:11:12 +0330 Subject: [PATCH 33/89] Use superfluid adapter on test env again --- config/test.env | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test.env b/config/test.env index b1d662b2c..81a76ad6e 100644 --- a/config/test.env +++ b/config/test.env @@ -238,7 +238,7 @@ DRAFT_RECURRING_DONATION_MATCH_EXPIRATION_HOURS=24 OPTIMISTIC_SEPOLIA_SCAN_API_KEY= -SUPER_FLUID_ADAPTER=mock +SUPER_FLUID_ADAPTER=superfluid From b54b825ce616d2620f7e6b4800e316c6267d4ecb Mon Sep 17 00:00:00 2001 From: Cherik Date: Thu, 12 Sep 2024 17:17:13 +0330 Subject: [PATCH 34/89] 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, From ffdda5abcc17897b830711f5a1a24474b7a4d4e6 Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Thu, 12 Sep 2024 17:45:58 +0330 Subject: [PATCH 35/89] Fix test cases related to isProjectVerified --- src/repositories/donationRepository.test.ts | 32 ++++++++++----------- src/repositories/donationRepository.ts | 2 +- src/resolvers/donationResolver.test.ts | 12 ++++---- test/testUtils.ts | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index a40887d28..1aed8882f 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -1492,7 +1492,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1505,7 +1505,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1517,7 +1517,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, @@ -1535,7 +1535,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 +1563,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1576,7 +1576,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1588,7 +1588,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: false, + isProjectGivbackEligible: false, }, donor2.id, project.id, @@ -1632,7 +1632,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1645,7 +1645,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1657,7 +1657,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound: 31234231, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, @@ -1700,7 +1700,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1713,7 +1713,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1736,7 +1736,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor3.id, project.id, @@ -1781,7 +1781,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1794,7 +1794,7 @@ function getSumOfGivbackEligibleDonationsForSpecificRoundTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -1807,7 +1807,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 5505d6d7b..731bcebdc 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -501,7 +501,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/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 13e67e3ed..df0810600 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -5002,7 +5002,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5015,7 +5015,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5028,7 +5028,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd3, powerRound: 1231, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, @@ -5078,7 +5078,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd1, powerRound, givbackFactor: givbackFactor1, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5091,7 +5091,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd2, powerRound, givbackFactor: givbackFactor2, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor1.id, project.id, @@ -5122,7 +5122,7 @@ async function allocatedGivbacksQueryTestCases() { valueUsd: valueUsd3, powerRound, givbackFactor: givbackFactor3, - isProjectVerified: true, + isProjectGivbackEligible: true, }, donor2.id, project.id, diff --git a/test/testUtils.ts b/test/testUtils.ts index a05eea665..9cd3c21f6 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -1956,7 +1956,7 @@ export interface CreateDonationData { donationPercentage?: number; powerRound?: number; givbackFactor?: number; - isProjectVerified?: boolean; + isProjectGivbackEligible?: boolean; } export interface CategoryData { From 8d30b1ebe785f98fad5adffc1e143aa9f681a636 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 12 Sep 2024 13:44:00 -0500 Subject: [PATCH 36/89] add isImported And categories to project tab --- src/server/adminJs/tabs/projectsTab.ts | 79 +++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 2 deletions(-) diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index d0cf6fbe2..fdbbcab40 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 = ( @@ -486,6 +487,15 @@ export const fillSocialProfileAndQfRounds: After< const projectUpdates = await findProjectUpdatesByProjectId(projectId); const project = await findProjectById(projectId); const adminJsBaseUrl = process.env.SERVER_URL; + let categories; + if (project) { + const categoryIds = project!.categories.map(cat => cat.id); + categories = await Category + .createQueryBuilder('category') + .where('category.id IN (:...ids)', { ids: categoryIds }) + .orderBy('category.name', 'ASC') + .getMany(); + } response.record = { ...record, params: { @@ -499,6 +509,10 @@ export const fillSocialProfileAndQfRounds: After< adminJsBaseUrl, }, }; + + if (categories) { + response.record.params.categories = categories.map(cat => `${cat.id} - ${cat.name}`); + } return response; }; @@ -660,7 +674,7 @@ export const projectsTab = { id: { isVisible: { list: false, - filter: false, + filter: true, show: true, edit: false, }, @@ -831,12 +845,34 @@ export const projectsTab = { edit: false, }, }, + categoryIds: { + type: 'reference', + isArray: true, + reference: 'Category', + isVisible: { + list: false, + filter: false, + show: true, + edit: true, + }, + 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 +960,19 @@ 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 { record, request } = response; + if (request.payload.categoryIds) { + await saveCategories(record.params.id, request.payload.categoryIds); + } + return response; + }, }, bulkDelete: { isVisible: false, @@ -1014,6 +1063,9 @@ 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(','); } + if (request.payload.categories) { + request.payload.categories = (request.payload.categories as string[]).map(id => ({ id: parseInt(id, 10) })); + } return request; }, after: async ( @@ -1155,6 +1207,7 @@ export const projectsTab = { refreshUserProjectPowerView(), refreshProjectFuturePowerView(), refreshProjectPowerView(), + saveCategories(project!.id, request?.payload?.categoryIds || []) ]); return request; }, @@ -1350,3 +1403,25 @@ export const projectsTab = { }, }, }; + +async function saveCategories(projectId: number, categoryIds: string[]) { + if (categoryIds?.length === 0) return; + + const project = await Project + .createQueryBuilder('project') + .leftJoinAndSelect('project.categories', 'category') + .where('project.id = :id', { id: projectId }) + .getOne(); + + if (!project) { + throw new Error('Project not found'); + } + + const categories = await Category + .createQueryBuilder('category') + .where('category.id IN (:...ids)', { ids: categoryIds }) + .getMany(); + + project.categories = categories; + await project.save(); +} From 2a564a38dc285bf5e97098122e92c35fec14f0f9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 12 Sep 2024 13:51:43 -0500 Subject: [PATCH 37/89] fix isProjectGivbackEligible Migration in wrong folder --- ...402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {migrations => migration-old-backup}/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts (100%) diff --git a/migrations/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts b/migration-old-backup/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts similarity index 100% rename from migrations/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts rename to migration-old-backup/1724061402220-RenameIsProjectVerifiedToIsGivbackEligibleInDonation.ts From 2674708da9c196147dd7886be577dbe267f9f92d Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 16 Sep 2024 20:29:18 -0500 Subject: [PATCH 38/89] add chaintype and solana networks to tokenTab --- package-lock.json | 4 ++-- src/server/adminJs/tabs/tokenTab.ts | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index a3a11ed57..d7e750829 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "giveth-graphql-api", - "version": "1.25.3", + "version": "1.25.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "giveth-graphql-api", - "version": "1.25.3", + "version": "1.25.0", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/src/server/adminJs/tabs/tokenTab.ts b/src/server/adminJs/tabs/tokenTab.ts index 9c328557b..129a288a3 100644 --- a/src/server/adminJs/tabs/tokenTab.ts +++ b/src/server/adminJs/tabs/tokenTab.ts @@ -7,6 +7,7 @@ import { AdminJsRequestInterface } from '../adminJs-types'; import { Organization } from '../../../entities/organization'; import { logger } from '../../../utils/logger'; import { findTokenByTokenId } from '../../../repositories/tokenRepository'; +import { ChainType } from '../../../types/network'; // generates orderly permutations and maps then into an array which is later flatten into 1 dimension // Current length is the length of selected items from the total items @@ -210,6 +211,18 @@ export const generateTokenTab = async () => { value: NETWORK_IDS.MORDOR_ETC_TESTNET, label: 'Ethereum Classic Testnet', }, + { + value: NETWORK_IDS.SOLANA_MAINNET, + label: 'SOLANA MAINNET', + }, + { + value: NETWORK_IDS.SOLANA_TESTNET, + label: 'SOLANA TESTNET', + }, + { + value: NETWORK_IDS.SOLANA_DEVNET, + label: 'SOLANA DEVNET', + }, ], }, symbol: { isVisible: true }, @@ -225,6 +238,14 @@ export const generateTokenTab = async () => { filter: true, }, }, + chainType: { + isVisible: true, + availableValues: [ + { value: ChainType.EVM, label: 'EVM' }, + { value: ChainType.SOLANA, label: 'SOLANA' }, + { value: ChainType.STELLAR, label: 'STELLAR' }, + ], + }, coingeckoId: { isVisible: { show: true, From 1d33d4093195a808443e87172f88deafa9a7307a Mon Sep 17 00:00:00 2001 From: HrithikSampson Date: Tue, 17 Sep 2024 09:03:49 +0530 Subject: [PATCH 39/89] update branch --- src/repositories/donationRepository.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index fc9f18612..589cf4318 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -91,7 +91,8 @@ describe( ); describe('donorsCountPerDate() test cases', donorsCountPerDateTestCases); -describe('getSumOfGivbackEligibleDonationsForSpecificRound() test cases', +describe( + 'getSumOfGivbackEligibleDonationsForSpecificRound() test cases', getSumOfGivbackEligibleDonationsForSpecificRoundTestCases, ); From ee8617cd96b3ebf61bed12ab68df51906bb734c9 Mon Sep 17 00:00:00 2001 From: HrithikSampson Date: Tue, 17 Sep 2024 10:31:07 +0530 Subject: [PATCH 40/89] add environment and energy image mapping --- migration/1726069430594-add_endaoment_project_banners.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/migration/1726069430594-add_endaoment_project_banners.ts b/migration/1726069430594-add_endaoment_project_banners.ts index 1b7f2089a..7118dc76a 100644 --- a/migration/1726069430594-add_endaoment_project_banners.ts +++ b/migration/1726069430594-add_endaoment_project_banners.ts @@ -7,7 +7,7 @@ export class AddEndaomentProjectBanners1726069430594 { public async up(queryRunner: QueryRunner): Promise { const mainCategorySlugToBannerMapping = { - 'environment-and-energy': 'nature', + 'environment-and-energy': 'environment-energy', 'health-and-wellness': 'health-wellness', 'art-and-culture': 'art-culture', nature: 'nature', From 3d10bbf1976107b36ba04524e8a7f3d4ad9462dd Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 18 Sep 2024 01:58:40 -0500 Subject: [PATCH 41/89] add categories to show and edit forms in adminjs for projects --- src/entities/project.ts | 1 + .../tabs/components/ProjectCategories.tsx | 28 +++++++++ src/server/adminJs/tabs/projectsTab.ts | 61 ++++++++++++------- 3 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 src/server/adminJs/tabs/components/ProjectCategories.tsx diff --git a/src/entities/project.ts b/src/entities/project.ts index c0b02bcf6..ae768c263 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -217,6 +217,7 @@ export class Project extends BaseEntity { @Field(_type => [Category], { nullable: true }) @ManyToMany(_type => Category, category => category.projects, { nullable: true, + eager: true, }) @JoinTable() categories: Category[]; diff --git a/src/server/adminJs/tabs/components/ProjectCategories.tsx b/src/server/adminJs/tabs/components/ProjectCategories.tsx new file mode 100644 index 000000000..27a6e8893 --- /dev/null +++ b/src/server/adminJs/tabs/components/ProjectCategories.tsx @@ -0,0 +1,28 @@ +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/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index fdbbcab40..3e8ad00dd 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -447,6 +447,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, @@ -489,10 +496,10 @@ export const fillSocialProfileAndQfRounds: After< const adminJsBaseUrl = process.env.SERVER_URL; let categories; if (project) { - const categoryIds = project!.categories.map(cat => cat.id); categories = await Category .createQueryBuilder('category') - .where('category.id IN (:...ids)', { ids: categoryIds }) + .innerJoin('category.projects', 'projects') + .where('projects.id = :id', { id: project.id }) .orderBy('category.name', 'ASC') .getMany(); } @@ -511,7 +518,8 @@ export const fillSocialProfileAndQfRounds: After< }; if (categories) { - response.record.params.categories = categories.map(cat => `${cat.id} - ${cat.name}`); + response.record.params.categoryIds = categories; + response.record.params.categories = categories; } return response; }; @@ -855,6 +863,9 @@ export const projectsTab = { show: true, edit: true, }, + components: { + show: adminJs.bundle('./components/ProjectCategories'), + }, availableValues: async (_record) => { const categories = await Category .createQueryBuilder('category') @@ -968,9 +979,12 @@ export const projectsTab = { }, after: async (response) => { const { record, request } = response; - if (request.payload.categoryIds) { - await saveCategories(record.params.id, request.payload.categoryIds); - } + const project = await Project.findOne({ + where: { id: request?.record?.id }, + }); + const categoryIds = extractCategoryIds(request.record.params); + await saveCategories(project!, categoryIds || []); + return response; }, }, @@ -1001,6 +1015,13 @@ 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 @@ -1063,9 +1084,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(','); } - if (request.payload.categories) { - request.payload.categories = (request.payload.categories as string[]).map(id => ({ id: parseInt(id, 10) })); - } + return request; }, after: async ( @@ -1203,11 +1222,13 @@ export const projectsTab = { }); } } + const categoryIds = extractCategoryIds(request.record.params); + await Promise.all([ refreshUserProjectPowerView(), refreshProjectFuturePowerView(), refreshProjectPowerView(), - saveCategories(project!.id, request?.payload?.categoryIds || []) + saveCategories(project!, categoryIds || []), ]); return request; }, @@ -1404,18 +1425,14 @@ export const projectsTab = { }, }; -async function saveCategories(projectId: number, categoryIds: string[]) { - if (categoryIds?.length === 0) return; - - const project = await Project - .createQueryBuilder('project') - .leftJoinAndSelect('project.categories', 'category') - .where('project.id = :id', { id: projectId }) - .getOne(); - - if (!project) { - throw new Error('Project not found'); - } +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') From 4b8d3082933aa9d2483d561a4f4018e3a684eeba Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 18 Sep 2024 19:17:28 -0500 Subject: [PATCH 42/89] fix eslint --- .../CustomQfRoundMultiUpdateComponent.tsx | 4 +- .../tabs/components/ProjectCategories.tsx | 4 +- src/server/adminJs/tabs/projectsTab.ts | 63 ++++++++++--------- 3 files changed, 38 insertions(+), 33 deletions(-) diff --git a/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx index 644d6ad82..c86aa78ac 100644 --- a/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx +++ b/src/server/adminJs/tabs/components/CustomQfRoundMultiUpdateComponent.tsx @@ -26,9 +26,7 @@ const RecordInput = ({ index, record, updateRecord, removeRecord }) => ( - updateRecord(index, 'matchingFund', e.target.value) - } + onChange={e => updateRecord(index, 'matchingFund', e.target.value)} required /> diff --git a/src/server/adminJs/tabs/components/ProjectCategories.tsx b/src/server/adminJs/tabs/components/ProjectCategories.tsx index 27a6e8893..5706131cb 100644 --- a/src/server/adminJs/tabs/components/ProjectCategories.tsx +++ b/src/server/adminJs/tabs/components/ProjectCategories.tsx @@ -14,7 +14,9 @@ const ProjectUpdates = props => {
-

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

+

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

); diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index 3e8ad00dd..a61aedfa6 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -447,12 +447,12 @@ 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 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, @@ -496,12 +496,11 @@ export const fillSocialProfileAndQfRounds: After< 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(); + categories = await Category.createQueryBuilder('category') + .innerJoin('category.projects', 'projects') + .where('projects.id = :id', { id: project.id }) + .orderBy('category.name', 'ASC') + .getMany(); } response.record = { ...record, @@ -866,12 +865,11 @@ export const projectsTab = { components: { show: adminJs.bundle('./components/ProjectCategories'), }, - availableValues: async (_record) => { - const categories = await Category - .createQueryBuilder('category') + availableValues: async _record => { + const categories = await Category.createQueryBuilder('category') .where('category.isActive = :isActive', { isActive: true }) .orderBy('category.name', 'ASC') - .getMany(); + .getMany(); return categories.map(category => ({ value: category.id, label: `${category.id} - ${category.name}`, @@ -971,18 +969,20 @@ export const projectsTab = { isVisible: false, isAccessible: ({ currentAdmin }) => canAccessProjectAction({ currentAdmin }, ResourceActions.NEW), - before: async (request) => { + before: async request => { if (request.payload.categories) { - request.payload.categories = (request.payload.categories as string[]).map(id => ({ id: parseInt(id, 10) })); + request.payload.categories = ( + request.payload.categories as string[] + ).map(id => ({ id: parseInt(id, 10) })); } return request; }, - after: async (response) => { - const { record, request } = response; + after: async response => { + const { request } = response; const project = await Project.findOne({ where: { id: request?.record?.id }, }); - const categoryIds = extractCategoryIds(request.record.params); + const categoryIds = extractCategoryIds(request.record.params); await saveCategories(project!, categoryIds || []); return response; @@ -1016,10 +1016,13 @@ export const projectsTab = { const project = await findProjectById(Number(request.payload.id)); if (project) { - await Category.query(` + await Category.query( + ` DELETE FROM project_categories_category WHERE "projectId" = $1 - `, [project.id]); + `, + [project.id], + ); } if ( @@ -1084,7 +1087,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 ( @@ -1222,7 +1225,7 @@ export const projectsTab = { }); } } - const categoryIds = extractCategoryIds(request.record.params); + const categoryIds = extractCategoryIds(request.record.params); await Promise.all([ refreshUserProjectPowerView(), @@ -1429,13 +1432,15 @@ async function saveCategories(project: Project, categoryIds?: string[]) { if (!project) return; if (!categoryIds || categoryIds?.length === 0) return; - await Category.query(` + await Category.query( + ` DELETE FROM project_categories_category WHERE "projectId" = $1 - `, [project.id]); + `, + [project.id], + ); - const categories = await Category - .createQueryBuilder('category') + const categories = await Category.createQueryBuilder('category') .where('category.id IN (:...ids)', { ids: categoryIds }) .getMany(); From 70079874a0f0c0187c05c8d4cf941da8ca879769 Mon Sep 17 00:00:00 2001 From: Cherik Date: Thu, 19 Sep 2024 12:04:43 +0330 Subject: [PATCH 43/89] add best match sort option --- src/entities/project.ts | 1 + src/repositories/projectRepository.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/entities/project.ts b/src/entities/project.ts index 711e1334b..ab6f6a8a8 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -77,6 +77,7 @@ export enum SortingField { InstantBoosting = 'InstantBoosting', ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds', EstimatedMatching = 'EstimatedMatching', + BestMatch = 'BestMatch', } export enum FilterField { diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 1edcc8485..a17703158 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -252,6 +252,8 @@ export const filterProjectsQuery = (params: FilterProjectQueryInputParams) => { .addOrderBy('project.verified', 'DESC'); // Secondary sorting condition } break; + case SortingField.BestMatch: + break; default: query From 61524cdd05f1cf1488a0b99cccb196931650e258 Mon Sep 17 00:00:00 2001 From: Cherik Date: Thu, 19 Sep 2024 12:06:00 +0330 Subject: [PATCH 44/89] update addSearchQuery to prioritize the title --- src/resolvers/projectResolver.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) 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) ); } From 93f13b6f523164084c519a5c1c10e431940631a6 Mon Sep 17 00:00:00 2001 From: HrithikSampson Date: Thu, 19 Sep 2024 23:57:08 +0530 Subject: [PATCH 45/89] Add Stellar to QFRound --- src/server/adminJs/tabs/qfRoundTab.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 5555f9cb1..867a8aa56 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -211,6 +211,7 @@ const availableNetworkValues = [ label: 'MORDOR ETC TESTNET', }, { value: NETWORK_IDS.OPTIMISM_SEPOLIA, label: 'OPTIMISM SEPOLIA' }, + { value: NETWORK_IDS.STELLAR_MAINNET, label: 'STELLAR MAINNET'}, { value: NETWORK_IDS.CELO, label: 'CELO' }, { value: NETWORK_IDS.CELO_ALFAJORES, From 69dd31aa117afe34f3f7a8258eea13e1f26dbff6 Mon Sep 17 00:00:00 2001 From: HrithikSampson Date: Fri, 20 Sep 2024 00:12:41 +0530 Subject: [PATCH 46/89] run linter --- .DS_Store | Bin 10244 -> 10244 bytes src/server/adminJs/tabs/qfRoundTab.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) 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/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 867a8aa56..2e7197d28 100644 --- a/src/server/adminJs/tabs/qfRoundTab.ts +++ b/src/server/adminJs/tabs/qfRoundTab.ts @@ -211,7 +211,7 @@ const availableNetworkValues = [ label: 'MORDOR ETC TESTNET', }, { value: NETWORK_IDS.OPTIMISM_SEPOLIA, label: 'OPTIMISM SEPOLIA' }, - { value: NETWORK_IDS.STELLAR_MAINNET, label: 'STELLAR MAINNET'}, + { value: NETWORK_IDS.STELLAR_MAINNET, label: 'STELLAR MAINNET' }, { value: NETWORK_IDS.CELO, label: 'CELO' }, { value: NETWORK_IDS.CELO_ALFAJORES, From d0728d8cbd5c674e9490abc5c2d0a321af73f890 Mon Sep 17 00:00:00 2001 From: Carlos Date: Thu, 19 Sep 2024 21:11:19 -0500 Subject: [PATCH 47/89] remove eager from project categories in entity --- src/entities/project.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/entities/project.ts b/src/entities/project.ts index 711e1334b..f10daa91b 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -217,7 +217,6 @@ export class Project extends BaseEntity { @Field(_type => [Category], { nullable: true }) @ManyToMany(_type => Category, category => category.projects, { nullable: true, - eager: true, }) @JoinTable() categories: Category[]; From 3ee817a9cc0823e05bcf91d2bccbd6073953f9ef Mon Sep 17 00:00:00 2001 From: Mohammad Ranjbar Z Date: Tue, 24 Sep 2024 11:33:41 +0330 Subject: [PATCH 48/89] Add isGivbackEligible filter --- src/entities/project.ts | 1 + .../projectResolver.allProject.test.ts | 95 ++++++++++++++++++- 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/entities/project.ts b/src/entities/project.ts index fee212f20..8a1a386c0 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -82,6 +82,7 @@ export enum SortingField { export enum FilterField { Verified = 'verified', + IsGivbackEligible = 'isGivbackEligible', AcceptGiv = 'givingBlocksId', AcceptFundOnGnosis = 'acceptFundOnGnosis', AcceptFundOnMainnet = 'acceptFundOnMainnet', 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(), From de13f3c0c1767d4413ee0c21251c70acd8324042 Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Tue, 1 Oct 2024 19:36:40 +0200 Subject: [PATCH 49/89] 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 --- package.json | 1 + src/repositories/qfRoundRepository.test.ts | 56 ++++++++++++ src/repositories/qfRoundRepository.ts | 29 +++++++ src/server/bootstrap.ts | 6 ++ .../cronJobs/syncUsersModelScore.test.ts | 85 +++++++++++++++++++ src/services/cronJobs/syncUsersModelScore.ts | 63 ++++++++++++++ src/workers/userMBDScoreSyncWorker.ts | 17 ++++ 7 files changed, 257 insertions(+) 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/package.json b/package.json index 36d4c96ac..4555ea968 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "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: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", 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/server/bootstrap.ts b/src/server/bootstrap.ts index 0311fe0d0..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; @@ -366,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/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/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); From e4d210fca4804c3be2209305e4e9c98bf322a003 Mon Sep 17 00:00:00 2001 From: Ramin Date: Wed, 2 Oct 2024 14:36:50 +0330 Subject: [PATCH 50/89] prevent duplicate tokens being added in adminJS --- src/server/adminJs/tabs/tokenTab.ts | 37 +++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) 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, From 9cfdfc56569e80427af0615f3c8cc3fa3e56f09b Mon Sep 17 00:00:00 2001 From: Ramin Date: Thu, 3 Oct 2024 02:06:04 +0330 Subject: [PATCH 51/89] Ensure correct emails are sent for project status changes related to decentralized verification --- src/server/adminJs/tabs/projectsTab.ts | 120 +++++++++++++------------ 1 file changed, 65 insertions(+), 55 deletions(-) diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index a61aedfa6..49e2e29b9 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -184,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) @@ -219,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; @@ -232,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([ @@ -286,7 +297,7 @@ export const verifyProjects = async ( }), notice: { message: `Project(s) successfully ${ - verificationStatus ? 'verified' : 'unverified' + vouchedStatus ? 'vouched' : 'unvouched' }`, type: 'success', }, @@ -1273,7 +1284,7 @@ export const projectsTab = { }, component: false, }, - verify: { + approveVouched: { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => @@ -1286,7 +1297,7 @@ export const projectsTab = { }, component: false, }, - reject: { + removeVouched: { actionType: 'bulk', isVisible: true, isAccessible: ({ currentAdmin }) => @@ -1299,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 }) => @@ -1308,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, }, From e8a0aaa00f3bc4296f01a610e19aeedbf20a9f78 Mon Sep 17 00:00:00 2001 From: Ramin Date: Thu, 3 Oct 2024 02:21:59 +0330 Subject: [PATCH 52/89] fix test --- src/server/adminJs/tabs/projectsTab.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 8b53726e3..d2979c26c 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -453,7 +453,6 @@ function verifyProjectsTestCases() { }, }, true, // give priority to revoke badge - true, // revoke badge ); const updatedProject = await findProjectById(project.id); From bd7c70e2a67eb985624f6e1dd8110a44f6ed8d9d Mon Sep 17 00:00:00 2001 From: Ramin Date: Thu, 3 Oct 2024 03:17:32 +0330 Subject: [PATCH 53/89] fix test cases --- package.json | 1 + src/server/adminJs/tabs/projectsTab.test.ts | 38 ++++++++++++++++----- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 4555ea968..17349eadc 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "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", diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index d2979c26c..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,7 +453,20 @@ function verifyProjectsTestCases() { recordIds: String(project.id), }, }, - true, // give priority to revoke badge + false, + ); + await revokeGivbacksEligibility( + { + currentAdmin: adminUser as User, + h: {}, + resource: {}, + records: [], + }, + { + query: { + recordIds: String(project.id), + }, + }, ); const updatedProject = await findProjectById(project.id); @@ -526,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, ); }); @@ -615,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, ); }); From a4ae0886e6f20180dc55c21000da02dcdac14e69 Mon Sep 17 00:00:00 2001 From: Ramin Date: Thu, 3 Oct 2024 12:47:16 +0330 Subject: [PATCH 54/89] fix test cases --- src/repositories/projectRepository.ts | 2 +- .../checkProjectVerificationStatus.test.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index a17703158..34bb4f907 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -279,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)', { 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 () => { From 682de08a29cf7ff4a05ffe3699b324193b2a1e4f Mon Sep 17 00:00:00 2001 From: Ramin Date: Sun, 6 Oct 2024 23:42:27 +0330 Subject: [PATCH 55/89] move revoke badge to project verification form section in adminJS --- .../adminJs/tabs/projectVerificationTab.ts | 76 ++++++++++++++++++- src/server/adminJs/tabs/projectsTab.test.ts | 2 +- src/server/adminJs/tabs/projectsTab.ts | 68 ----------------- 3 files changed, 76 insertions(+), 70 deletions(-) diff --git a/src/server/adminJs/tabs/projectVerificationTab.ts b/src/server/adminJs/tabs/projectVerificationTab.ts index 41abe11a5..f06cfbf1c 100644 --- a/src/server/adminJs/tabs/projectVerificationTab.ts +++ b/src/server/adminJs/tabs/projectVerificationTab.ts @@ -9,6 +9,7 @@ import { ProjectVerificationForm, } from '../../../entities/projectVerificationForm'; import { + canAccessProjectAction, canAccessProjectVerificationFormAction, ResourceActions, } from '../adminJsPermissions'; @@ -21,6 +22,7 @@ import { approveMultipleProjects, approveProject, findProjectVerificationFormById, + getVerificationFormByProjectId, makeFormDraft, verifyForm, verifyMultipleForms, @@ -35,8 +37,13 @@ import { } from '../../../repositories/projectRepository'; import { getNotificationAdapter } from '../../../adapters/adaptersFactory'; import { logger } from '../../../utils/logger'; -import { Project } from '../../../entities/project'; +import { Project, RevokeSteps } from '../../../entities/project'; import { fillSocialProfileAndQfRounds } from './projectsTab'; +import { refreshUserProjectPowerView } from '../../../repositories/userProjectPowerViewRepository'; +import { + refreshProjectFuturePowerView, + refreshProjectPowerView, +} from '../../../repositories/projectPowerViewRepository'; const extractLastComment = (verificationForm: ProjectVerificationForm) => { const commentsSorted = verificationForm.commentsSection?.comments?.sort( @@ -303,6 +310,60 @@ export const approveVerificationForms = 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 projectVerificationTab = { resource: ProjectVerificationForm, options: { @@ -678,6 +739,19 @@ export const projectVerificationTab = { }, component: false, }, + revokeGivbacksEligible: { + actionType: 'bulk', + isVisible: true, + isAccessible: ({ currentAdmin }) => + canAccessProjectAction( + { currentAdmin }, + ResourceActions.REVOKE_BADGE, + ), + handler: async (request, _response, context) => { + return revokeGivbacksEligibility(context, request); + }, + component: false, + }, }, }, }; diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index b1009f4ef..68664cf1a 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -35,12 +35,12 @@ import { addFeaturedProjectUpdate, exportProjectsWithFiltersToCsv, listDelist, - revokeGivbacksEligibility, updateStatusOfProjects, verifyProjects, } from './projectsTab'; import { messages } from '../../../utils/messages'; import { ProjectStatus } from '../../../entities/projectStatus'; +import { revokeGivbacksEligibility } from './projectVerificationTab'; describe( 'verifyMultipleProjects() test cases', diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index 49e2e29b9..b52aa85b8 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -12,7 +12,6 @@ import { ProjectUpdate, ProjStatus, ReviewStatus, - RevokeSteps, } from '../../../entities/project'; import { canAccessProjectAction, ResourceActions } from '../adminJsPermissions'; import { @@ -184,60 +183,6 @@ 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, @@ -1310,19 +1255,6 @@ export const projectsTab = { }, component: false, }, - revokeGivbacksEligible: { - actionType: 'bulk', - isVisible: true, - isAccessible: ({ currentAdmin }) => - canAccessProjectAction( - { currentAdmin }, - ResourceActions.REVOKE_BADGE, - ), - handler: async (request, _response, context) => { - return revokeGivbacksEligibility(context, request); - }, - component: false, - }, activate: { actionType: 'bulk', isVisible: true, From 3b060384648d60ffc8401620569300e57c64d0d5 Mon Sep 17 00:00:00 2001 From: Ramin Date: Mon, 7 Oct 2024 00:07:45 +0330 Subject: [PATCH 56/89] fix test cases --- src/server/adminJs/tabs/projectVerificationTab.ts | 11 +++++++++-- src/server/adminJs/tabs/projectsTab.test.ts | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/server/adminJs/tabs/projectVerificationTab.ts b/src/server/adminJs/tabs/projectVerificationTab.ts index f06cfbf1c..5ba5d14f1 100644 --- a/src/server/adminJs/tabs/projectVerificationTab.ts +++ b/src/server/adminJs/tabs/projectVerificationTab.ts @@ -316,9 +316,16 @@ export const revokeGivbacksEligibility = async ( ) => { const { records, currentAdmin } = context; try { - const projectIds = request?.query?.recordIds + const projectFormIds = request?.query?.recordIds ?.split(',') ?.map(strId => Number(strId)) as number[]; + const projectIds: number[] = []; + for (const formId of projectFormIds) { + const verificationForm = await findProjectVerificationFormById(formId); + if (verificationForm) { + projectIds.push(verificationForm.projectId); + } + } const updateParams = { isGivbackEligible: false }; const projects = await Project.createQueryBuilder('project') .update(Project, updateParams) @@ -353,7 +360,7 @@ export const revokeGivbacksEligibility = async ( throw error; } return { - redirectUrl: '/admin/resources/Project', + redirectUrl: '/admin/resources/ProjectVerificationForm', records: records.map(record => { record.toJSON(context.currentAdmin); }), diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 68664cf1a..9e2f89767 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -464,7 +464,7 @@ function verifyProjectsTestCases() { }, { query: { - recordIds: String(project.id), + recordIds: String(projectVerificationForm.projectId), }, }, ); From d4c8068e14c80d6ea425aabb877f8c22ceadbc04 Mon Sep 17 00:00:00 2001 From: Ramin Date: Mon, 7 Oct 2024 00:23:23 +0330 Subject: [PATCH 57/89] fix test cases --- src/server/adminJs/tabs/projectsTab.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 9e2f89767..49083065c 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -464,7 +464,7 @@ function verifyProjectsTestCases() { }, { query: { - recordIds: String(projectVerificationForm.projectId), + recordIds: String(projectVerificationForm.id), }, }, ); From 1afe41cf01da3e3c14f86ef22932726eeb1e345b Mon Sep 17 00:00:00 2001 From: Carlos Date: Mon, 7 Oct 2024 16:04:09 +0200 Subject: [PATCH 58/89] fix env var for model score sync --- config/test.env | 1 + src/config.ts | 1 + src/services/cronJobs/syncUsersModelScore.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/config/test.env b/config/test.env index 81a76ad6e..8a3d288fa 100644 --- a/config/test.env +++ b/config/test.env @@ -257,3 +257,4 @@ STELLAR_HORIZON_API_URL=https://horizon.stellar.org STELLAR_SCAN_API_URL=https://stellar.expert/explorer/public ENDAOMENT_ADMIN_WALLET_ADDRESS=0xfE3524e04E4e564F9935D34bB5e80c5CaB07F5b4 +SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION=0 0 */3 * * \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index be3f52e62..d94dba840 100644 --- a/src/config.ts +++ b/src/config.ts @@ -56,6 +56,7 @@ const envVars = [ 'GIVBACK_MAX_FACTOR', 'GIVBACK_MIN_FACTOR', 'DONATION_VERIFICAITON_EXPIRATION_HOURS', + 'SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION', ]; interface requiredEnv { diff --git a/src/services/cronJobs/syncUsersModelScore.ts b/src/services/cronJobs/syncUsersModelScore.ts index 944363e6a..ea0d39a52 100644 --- a/src/services/cronJobs/syncUsersModelScore.ts +++ b/src/services/cronJobs/syncUsersModelScore.ts @@ -10,7 +10,7 @@ import { findUserById } from '../../repositories/userRepository'; import { UserQfRoundModelScore } from '../../entities/userQfRoundModelScore'; const cronJobTime = - (config.get('MAKE_UNREVIEWED_PROJECT_LISTED_CRONJOB_EXPRESSION') as string) || + (config.get('SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION') as string) || '0 0 * * * *'; const qfRoundUsersMissedMBDScore = Number( From c2a18ae07279f38c622e22f4ca94e7ff3e9af3c5 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 8 Oct 2024 15:54:51 +0200 Subject: [PATCH 59/89] add logs for sync score --- src/adapters/gitcoin/gitcoinAdapter.ts | 5 ++++- src/services/cronJobs/syncUsersModelScore.ts | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/adapters/gitcoin/gitcoinAdapter.ts b/src/adapters/gitcoin/gitcoinAdapter.ts index c8f950f9a..09830ea87 100644 --- a/src/adapters/gitcoin/gitcoinAdapter.ts +++ b/src/adapters/gitcoin/gitcoinAdapter.ts @@ -42,7 +42,10 @@ export class GitcoinAdapter implements GitcoinAdapterInterface { }, }, ); - return result.data?.details?.models?.ethereum_activity?.score; + return ( + result.data?.details?.models?.ethereum_activity?.score || + result.data?.details?.models?.aggregate?.score + ); } catch (e) { logger.error('getUserAnalysisScore error', e); throw new Error( diff --git a/src/services/cronJobs/syncUsersModelScore.ts b/src/services/cronJobs/syncUsersModelScore.ts index ea0d39a52..7f5a51ccf 100644 --- a/src/services/cronJobs/syncUsersModelScore.ts +++ b/src/services/cronJobs/syncUsersModelScore.ts @@ -11,7 +11,7 @@ import { UserQfRoundModelScore } from '../../entities/userQfRoundModelScore'; const cronJobTime = (config.get('SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION') as string) || - '0 0 * * * *'; + '0 * * * * *'; const qfRoundUsersMissedMBDScore = Number( process.env.QF_ROUND_USERS_MISSED_SCORE || 0, @@ -39,13 +39,17 @@ export const updateUsersWithoutMBDScoreInRound = async () => { if (userIds.length === 0) return; for (const userId of userIds) { + logger.debug(`User with ${userId} is being processed`); try { const user = await findUserById(userId); + logger.debug(`User with ${user?.id} fetched from Db`); if (!user) continue; + logger.debug(`User with ${user?.id} fetching its score`); const userScore = await worker.syncUserScore({ userWallet: user?.walletAddress, }); + logger.debug(`User with ${user?.id} has score of ${userScore}`); if (userScore) { const userScoreInRound = UserQfRoundModelScore.create({ userId, @@ -54,6 +58,7 @@ export const updateUsersWithoutMBDScoreInRound = async () => { }); await userScoreInRound.save(); + logger.debug(`${userScore?.id} saved!`); } } catch (e) { logger.info(`User with Id ${userId} did not sync MBD score this batch`); From e2e4369c294dde8c4c12b1169d9d9d1f48d5d7a3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 8 Oct 2024 17:06:16 +0200 Subject: [PATCH 60/89] add better logs to sync model job --- src/adapters/gitcoin/gitcoinAdapter.ts | 5 +---- src/services/cronJobs/syncUsersModelScore.ts | 7 ++++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/adapters/gitcoin/gitcoinAdapter.ts b/src/adapters/gitcoin/gitcoinAdapter.ts index 09830ea87..c8f950f9a 100644 --- a/src/adapters/gitcoin/gitcoinAdapter.ts +++ b/src/adapters/gitcoin/gitcoinAdapter.ts @@ -42,10 +42,7 @@ export class GitcoinAdapter implements GitcoinAdapterInterface { }, }, ); - return ( - result.data?.details?.models?.ethereum_activity?.score || - result.data?.details?.models?.aggregate?.score - ); + return result.data?.details?.models?.ethereum_activity?.score; } catch (e) { logger.error('getUserAnalysisScore error', e); throw new Error( diff --git a/src/services/cronJobs/syncUsersModelScore.ts b/src/services/cronJobs/syncUsersModelScore.ts index 7f5a51ccf..295aec1ab 100644 --- a/src/services/cronJobs/syncUsersModelScore.ts +++ b/src/services/cronJobs/syncUsersModelScore.ts @@ -44,10 +44,11 @@ export const updateUsersWithoutMBDScoreInRound = async () => { const user = await findUserById(userId); logger.debug(`User with ${user?.id} fetched from Db`); if (!user) continue; - - logger.debug(`User with ${user?.id} fetching its score`); + logger.debug( + `User ${user.id} with wallet ${user.walletAddress} fetching score`, + ); const userScore = await worker.syncUserScore({ - userWallet: user?.walletAddress, + userWallet: user?.walletAddress?.toLowerCase(), }); logger.debug(`User with ${user?.id} has score of ${userScore}`); if (userScore) { From 490024e81dfa24faaf8afee0152f582359fd1301 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 8 Oct 2024 18:15:02 +0200 Subject: [PATCH 61/89] change insertion query for userQfRoundModelScore --- src/services/cronJobs/syncUsersModelScore.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/services/cronJobs/syncUsersModelScore.ts b/src/services/cronJobs/syncUsersModelScore.ts index 295aec1ab..e487682df 100644 --- a/src/services/cronJobs/syncUsersModelScore.ts +++ b/src/services/cronJobs/syncUsersModelScore.ts @@ -52,14 +52,11 @@ export const updateUsersWithoutMBDScoreInRound = async () => { }); logger.debug(`User with ${user?.id} has score of ${userScore}`); if (userScore) { - const userScoreInRound = UserQfRoundModelScore.create({ - userId, - qfRoundId: activeQfRoundId, - score: userScore, - }); - - await userScoreInRound.save(); - logger.debug(`${userScore?.id} saved!`); + await UserQfRoundModelScore.query(` + INSERT INTO "user_qf_round_model_score" ("userId", "qfRoundId", "score", "createdAt", "updatedAt") + VALUES ('${userId}', '${activeQfRoundId}', ${userScore}, NOW(), NOW()); + `); + logger.debug(`${user.id} score saved!`); } } catch (e) { logger.info(`User with Id ${userId} did not sync MBD score this batch`); From fc9d85aba60cd7ba391c2ea723712d6e91879a12 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 8 Oct 2024 19:17:31 +0200 Subject: [PATCH 62/89] remove conditional for userscore model sync --- src/services/cronJobs/syncUsersModelScore.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/services/cronJobs/syncUsersModelScore.ts b/src/services/cronJobs/syncUsersModelScore.ts index e487682df..86e619761 100644 --- a/src/services/cronJobs/syncUsersModelScore.ts +++ b/src/services/cronJobs/syncUsersModelScore.ts @@ -51,13 +51,11 @@ export const updateUsersWithoutMBDScoreInRound = async () => { userWallet: user?.walletAddress?.toLowerCase(), }); logger.debug(`User with ${user?.id} has score of ${userScore}`); - if (userScore) { - await UserQfRoundModelScore.query(` - INSERT INTO "user_qf_round_model_score" ("userId", "qfRoundId", "score", "createdAt", "updatedAt") - VALUES ('${userId}', '${activeQfRoundId}', ${userScore}, NOW(), NOW()); - `); - logger.debug(`${user.id} score saved!`); - } + await UserQfRoundModelScore.query(` + INSERT INTO "user_qf_round_model_score" ("userId", "qfRoundId", "score", "createdAt", "updatedAt") + VALUES ('${userId}', '${activeQfRoundId}', ${userScore}, NOW(), NOW()); + `); + logger.debug(`${user.id} score saved!`); } catch (e) { logger.info(`User with Id ${userId} did not sync MBD score this batch`); } From 5e9e255545efdc5fcb2f6d75b6b1cd8a56f16e04 Mon Sep 17 00:00:00 2001 From: Griff Green Date: Wed, 16 Oct 2024 18:52:01 +0200 Subject: [PATCH 63/89] 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 f6d7b3f78c35b9bd55b770ecbc7117f828e5be41 Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:05:36 +0100 Subject: [PATCH 64/89] add redirect util function for project custom actions (#1865) --- src/config.ts | 1 - src/server/adminJs/adminJs-types.ts | 1 + src/server/adminJs/adminJsUtils.ts | 33 ++++++++++++++++++++++++++ src/server/adminJs/tabs/projectsTab.ts | 12 +++++++--- 4 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 src/server/adminJs/adminJsUtils.ts diff --git a/src/config.ts b/src/config.ts index d94dba840..be3f52e62 100644 --- a/src/config.ts +++ b/src/config.ts @@ -56,7 +56,6 @@ const envVars = [ 'GIVBACK_MAX_FACTOR', 'GIVBACK_MIN_FACTOR', 'DONATION_VERIFICAITON_EXPIRATION_HOURS', - 'SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION', ]; interface requiredEnv { diff --git a/src/server/adminJs/adminJs-types.ts b/src/server/adminJs/adminJs-types.ts index 952f60738..a24cd2e2a 100644 --- a/src/server/adminJs/adminJs-types.ts +++ b/src/server/adminJs/adminJs-types.ts @@ -13,6 +13,7 @@ export interface AdminJsContextInterface { export interface AdminJsRequestInterface { payload?: any; record?: any; + rawHeaders?: any; query?: { recordIds?: string; }; diff --git a/src/server/adminJs/adminJsUtils.ts b/src/server/adminJs/adminJsUtils.ts new file mode 100644 index 000000000..ce800ca0e --- /dev/null +++ b/src/server/adminJs/adminJsUtils.ts @@ -0,0 +1,33 @@ +import { logger } from '../../utils/logger'; + +/** + * Extracts the redirect URL from request headers for AdminJS actions + * @param request - The AdminJS action request object + * @param resourceName - The name of the resource (e.g., 'Project', 'User') + * @returns The URL to redirect to after action completion + */ +export const getRedirectUrl = (request: any, resourceName: string): string => { + const refererIndex = + request?.rawHeaders?.findIndex(h => h.toLowerCase() === 'referer') || -1; + const referrerUrl = + refererIndex !== -1 ? request.rawHeaders[refererIndex + 1] : false; + + // Default fallback URL if no referer is found + const defaultUrl = `/admin/resources/${resourceName}`; + + try { + if (referrerUrl) { + const url = new URL(referrerUrl); + // If it's the main list view (no search params), add a timestamp to force refresh + if (url.pathname === `/admin/resources/${resourceName}` && !url.search) { + return `${url.pathname}?timestamp=${Date.now()}`; + } + return url.pathname + url.search; + } + } catch (error) { + logger.error('Error parsing referrer URL:', error); + } + + // Add timestamp to default URL as well + return `${defaultUrl}?timestamp=${Date.now()}`; +}; diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index b52aa85b8..604c2d5ae 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -55,6 +55,7 @@ import { refreshProjectEstimatedMatchingView } from '../../../services/projectVi import { extractAdminJsReferrerUrlParams } from '../adminJs'; import { relateManyProjectsToQfRound } from '../../../repositories/qfRoundRepository2'; import { Category } from '../../../entities/category'; +import { getRedirectUrl } from '../adminJsUtils'; // add queries depending on which filters were selected export const buildProjectsQuery = ( @@ -188,6 +189,7 @@ export const verifyProjects = async ( request: AdminJsRequestInterface, vouchedStatus: boolean = true, ) => { + const redirectUrl = getRedirectUrl(request, 'Project'); const { records, currentAdmin } = context; try { const projectIds = request?.query?.recordIds @@ -236,7 +238,7 @@ export const verifyProjects = async ( throw error; } return { - redirectUrl: '/admin/resources/Project', + redirectUrl: redirectUrl, records: records.map(record => { record.toJSON(context.currentAdmin); }), @@ -254,6 +256,7 @@ export const updateStatusOfProjects = async ( request: AdminJsRequestInterface, status, ) => { + const redirectUrl = getRedirectUrl(request, 'Project'); const { records, currentAdmin } = context; const projectIds = request?.query?.recordIds ?.split(',') @@ -319,7 +322,7 @@ export const updateStatusOfProjects = async ( ]); } return { - redirectUrl: '/admin/resources/Project', + redirectUrl: redirectUrl, records: records.map(record => { record.toJSON(context.currentAdmin); }), @@ -415,6 +418,7 @@ export const addSingleProjectToQfRound = async ( request: AdminJsRequestInterface, add: boolean = true, ) => { + const redirectUrl = getRedirectUrl(request, 'Project'); const { record, currentAdmin } = context; let message = messages.PROJECTS_RELATED_TO_ACTIVE_QF_ROUND_SUCCESSFULLY; const projectId = Number(request?.params?.recordId); @@ -431,6 +435,7 @@ export const addSingleProjectToQfRound = async ( message = messages.THERE_IS_NOT_ANY_ACTIVE_QF_ROUND; } return { + redirectUrl: redirectUrl, record: record.toJSON(currentAdmin), notice: { message, @@ -523,6 +528,7 @@ export const listDelist = async ( request, reviewStatus: ReviewStatus = ReviewStatus.Listed, ) => { + const redirectUrl = getRedirectUrl(request, 'Project'); const { records, currentAdmin } = context; let listed; switch (reviewStatus) { @@ -586,7 +592,7 @@ export const listDelist = async ( throw error; } return { - redirectUrl: '/admin/resources/Project', + redirectUrl: redirectUrl, records: records.map(record => { record.toJSON(context.currentAdmin); }), From d2b614d6d835130eacc4504ed5071e4ce8da09d1 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 65/89] 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 be46140f1c14adfc1eef4508ee5e4d2fa4a4c665 Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Fri, 8 Nov 2024 12:15:45 +0100 Subject: [PATCH 66/89] Add date to qf round donation existance condition (#1870) * add date to qf round donation existance condition * fix test donation to fit into the qfround timeframe --- src/repositories/donationRepository.ts | 12 +++++++----- src/resolvers/donationResolver.test.ts | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 4b649faaf..5ec0e093d 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -661,12 +661,14 @@ export async function isVerifiedDonationExistsInQfRound(params: { ` SELECT EXISTS ( SELECT 1 - FROM donation + FROM donation as d + INNER JOIN "qf_round" as qr on qr.id = $1 WHERE - status = 'verified' AND - "qfRoundId" = $1 AND - "projectId" = $2 AND - "userId" = $3 + d.status = 'verified' AND + d."qfRoundId" = $1 AND + d."projectId" = $2 AND + d."userId" = $3 AND + d."createdAt" >= qr."beginDate" AND d."createdAt" <= qr."endDate" ) AS exists; `, [params.qfRoundId, params.projectId, params.userId], diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 11b80dace..dfb19d3f1 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -715,7 +715,7 @@ function doesDonatedToProjectInQfRoundTestCases() { await saveDonationDirectlyToDb( createDonationData({ status: DONATION_STATUS.VERIFIED, - createdAt: moment().add(50, 'days').toDate(), + createdAt: moment().add(8, 'days').toDate(), valueUsd: 20, qfRoundId: qfRound.id, }), From 6dc5c77238427ff2ea2257ed3a6aa9fedccc9931 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 67/89] 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 0c4082d654faed11bc4440d83c5fdc9ec1052452 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 14 Nov 2024 14:57:45 +0100 Subject: [PATCH 68/89] Feat/User email verification --- ...1653657-addUserEmailVerificationColumns.ts | 21 ++++++++++ .../notifications/MockNotificationAdapter.ts | 8 ++++ .../NotificationAdapterInterface.ts | 5 +++ .../NotificationCenterAdapter.ts | 22 +++++++++++ src/analytics/analytics.ts | 2 + src/entities/user.ts | 8 ++++ src/resolvers/userResolver.ts | 38 +++++++++++++++++++ src/utils/errorMessages.ts | 1 + src/utils/locales/en.json | 5 ++- src/utils/locales/es.json | 3 +- src/utils/utils.ts | 16 ++++++++ 11 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 migration/1731071653657-addUserEmailVerificationColumns.ts diff --git a/migration/1731071653657-addUserEmailVerificationColumns.ts b/migration/1731071653657-addUserEmailVerificationColumns.ts new file mode 100644 index 000000000..1127cc0a9 --- /dev/null +++ b/migration/1731071653657-addUserEmailVerificationColumns.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddUserEmailVerificationColumns1731071653657 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + queryRunner.query(` + ALTER TABLE "user" + ADD COLUMN IF NOT EXISTS "emailVerificationCode" VARCHAR, + ADD COLUMN IF NOT EXISTS "isEmailVerified" BOOLEAN DEFAULT false; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + queryRunner.query(` + ALTER TABLE "user" + DROP COLUMN IF EXISTS "emailVerificationCode", + DROP COLUMN IF EXISTS "isEmailVerified"; + `); + } +} diff --git a/src/adapters/notifications/MockNotificationAdapter.ts b/src/adapters/notifications/MockNotificationAdapter.ts index e47a3825c..0b4727974 100644 --- a/src/adapters/notifications/MockNotificationAdapter.ts +++ b/src/adapters/notifications/MockNotificationAdapter.ts @@ -35,6 +35,14 @@ export class MockNotificationAdapter implements NotificationAdapterInterface { return Promise.resolve(undefined); } + sendUserEmailConfirmationCodeFlow(params: { email: string }): Promise { + logger.debug( + 'MockNotificationAdapter sendUserEmailConfirmationCodeFlow', + params, + ); + return Promise.resolve(undefined); + } + userSuperTokensCritical(): Promise { return Promise.resolve(undefined); } diff --git a/src/adapters/notifications/NotificationAdapterInterface.ts b/src/adapters/notifications/NotificationAdapterInterface.ts index 7e19aaacb..ef09758cf 100644 --- a/src/adapters/notifications/NotificationAdapterInterface.ts +++ b/src/adapters/notifications/NotificationAdapterInterface.ts @@ -72,6 +72,11 @@ export interface NotificationAdapterInterface { networkName: string; }): Promise; + sendUserEmailConfirmationCodeFlow(params: { + email: string; + user: User; + }): Promise; + projectVerified(params: { project: Project }): Promise; projectBoosted(params: { projectId: number; userId: number }): Promise; projectBoostedBatch(params: { diff --git a/src/adapters/notifications/NotificationCenterAdapter.ts b/src/adapters/notifications/NotificationCenterAdapter.ts index 1cc873ba6..5eb4c9661 100644 --- a/src/adapters/notifications/NotificationCenterAdapter.ts +++ b/src/adapters/notifications/NotificationCenterAdapter.ts @@ -94,6 +94,28 @@ export class NotificationCenterAdapter implements NotificationAdapterInterface { } } + async sendUserEmailConfirmationCodeFlow(params: { + email: string; + user: User; + }): Promise { + const { email, user } = params; + try { + await callSendNotification({ + eventName: + NOTIFICATIONS_EVENT_NAMES.SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW, + segment: { + payload: { + email, + verificationCode: user.emailVerificationCode, + userId: user.id, + }, + }, + }); + } catch (e) { + logger.error('sendUserEmailConfirmationCodeFlow >> error', e); + } + } + async userSuperTokensCritical(params: { user: User; eventName: UserStreamBalanceWarning; diff --git a/src/analytics/analytics.ts b/src/analytics/analytics.ts index 44b40ffc4..798436b64 100644 --- a/src/analytics/analytics.ts +++ b/src/analytics/analytics.ts @@ -49,4 +49,6 @@ export enum NOTIFICATIONS_EVENT_NAMES { SUBSCRIBE_ONBOARDING = 'Subscribe onboarding', CREATE_ORTTO_PROFILE = 'Create Ortto profile', SEND_EMAIL_CONFIRMATION = 'Send email confirmation', + + SEND_USER_EMAIL_CONFIRMATION_CODE_FLOW = 'Send email confirmation code flow', } diff --git a/src/entities/user.ts b/src/entities/user.ts index 12ca19950..da7d6fac1 100644 --- a/src/entities/user.ts +++ b/src/entities/user.ts @@ -34,6 +34,7 @@ export const publicSelectionFields = [ 'user.totalReceived', 'user.passportScore', 'user.passportStamps', + 'user.isEmailVerified', ]; export enum UserRole { @@ -195,6 +196,13 @@ export class User extends BaseEntity { @Field(_type => Float, { nullable: true }) activeQFMBDScore?: number; + @Field(_type => Boolean, { nullable: true }) + @Column('bool', { default: false }) + isEmailVerified: boolean; + + @Column('varchar', { nullable: true, default: null }) + emailVerificationCode?: string | null; + @Field(_type => Int, { nullable: true }) async donationsCount() { // Count for non-recurring donations diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 487f9ac28..5eee77bb7 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -30,6 +30,8 @@ import { isWalletAddressInPurpleList } from '../repositories/projectAddressRepos import { addressHasDonated } from '../repositories/donationRepository'; import { getOrttoPersonAttributes } from '../adapters/notifications/NotificationCenterAdapter'; import { retrieveActiveQfRoundUserMBDScore } from '../repositories/qfRoundRepository'; +import { getLoggedInUser } from '../services/authorizationServices'; +import { generateRandomNumericCode } from '../utils/utils'; @ObjectType() class UserRelatedAddressResponse { @@ -230,4 +232,40 @@ export class UserResolver { return true; } + + @Mutation(_returns => String) + async sendUserEmailConfirmationCodeFlow( + @Arg('email') email: string, + @Ctx() ctx: ApolloContext, + ): Promise { + const user = await getLoggedInUser(ctx); + + // Check if email aready veriffied + if (user.isEmailVerified) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), + ); + } + + // Check do we have an email already in the database + const isEmailAlreadyUsed = await User.findOne({ + where: { email: email }, + }); + + if (isEmailAlreadyUsed && isEmailAlreadyUsed.id !== user.id) { + return 'EMAIL_EXIST'; + } + + // Send verification code + const code = generateRandomNumericCode(5).toString(); + + user.emailVerificationCode = code; + + await getNotificationAdapter().sendUserEmailConfirmationCodeFlow({ + email: email, + user: user, + }); + + return 'VERIFICATION_SENT'; + } } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index a6584da2a..0fc4b1ee7 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -379,4 +379,5 @@ export const translationErrorMessagesKeys = { DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED: 'DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED', QR_CODE_DATA_URL_REQUIRED: 'QR_CODE_DATA_URL_REQUIRED', + USER_EMAIL_ALREADY_VERIFIED: 'USER_EMAIL_ALREADY_VERIFIED', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 70645329d..1647cff6a 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -117,5 +117,6 @@ "TX_NOT_FOUND": "TX_NOT_FOUND", "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION": "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION", "Project does not accept recurring donation": "Project does not accept recurring donation", - "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED" -} \ No newline at end of file + "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED", + "USER_EMAIL_ALREADY_VERIFIED": "User email already verified" +} diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index 7f8d184f5..ca78b3b4e 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -106,5 +106,6 @@ "PROJECT_UPDATE_CONTENT_LENGTH_SIZE_EXCEEDED": "El contenido es demasiado largo", "DRAFT_DONATION_DISABLED": "El borrador de donación está deshabilitado", "EVM_SUPPORT_ONLY": "Solo se admite EVM", - "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar" + "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar", + "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado" } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index d50f467e9..52a3e34dc 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -466,3 +466,19 @@ export const isSocialMediaEqual = ( .sort(), ); }; + +/** + * Generates a random numeric code with the specified number of digits. + * + * @param {number} digits - The number of digits for the generated code. Defaults to 6 if not provided. + * @returns {number} A random numeric code with the specified number of digits. + * + * Example: + * generateRandomNumericCode(4) // Returns a 4-digit number, e.g., 3741 + * generateRandomNumericCode(8) // Returns an 8-digit number, e.g., 29384756 + */ +export const generateRandomNumericCode = (digits: number = 6): number => { + const min = Math.pow(10, digits - 1); + const max = Math.pow(10, digits) - 1; + return Math.floor(min + Math.random() * (max - min + 1)); +}; From d8897fac3feb5163d2b68c3284c211b941c25a82 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Mon, 18 Nov 2024 14:59:48 +0100 Subject: [PATCH 69/89] added confirmation flow for use inputed code --- src/resolvers/userResolver.ts | 99 ++++++++++++++++++++++++++++++++++- src/utils/errorMessages.ts | 2 + src/utils/locales/en.json | 4 +- src/utils/locales/es.json | 4 +- 4 files changed, 106 insertions(+), 3 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 5eee77bb7..1f931f5f0 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -233,6 +233,33 @@ export class UserResolver { return true; } + /** + * Mutation to handle the process of sending a user email confirmation code. + * + * This function performs the following steps: + * 1. **Retrieve Logged-In User**: Fetches the currently logged-in user using the context (`ctx`). + * 2. **Check Email Verification Status**: + * - If the user's email is already verified, it throws an error with an appropriate message. + * 3. **Check for Email Usage**: + * - Verifies if the provided email is already in use by another user in the database. + * - If the email exists and belongs to a different user, it returns `'EMAIL_EXIST'`. + * 4. **Generate Verification Code**: + * - Creates a random 5-digit numeric code for email verification. + * - Updates the logged-in user's email verification code and email in the database. + * 5. **Send Verification Code**: + * - Uses the notification adapter to send the generated verification code to the provided email. + * 6. **Save User Record**: + * - Saves the updated user information (email and verification code) to the database. + * 7. **Return Status**: + * - If the verification code is successfully sent, it returns `'VERIFICATION_SENT'`. + * + * @param {string} email - The email address to verify. + * @param {ApolloContext} ctx - The GraphQL context containing user and other relevant information. + * @returns {Promise} - A status string indicating the result of the operation: + * - `'EMAIL_EXIST'`: The email is already used by another user. + * - `'VERIFICATION_SENT'`: The verification code has been sent successfully. + * @throws {Error} - If the user's email is already verified. + */ @Mutation(_returns => String) async sendUserEmailConfirmationCodeFlow( @Arg('email') email: string, @@ -240,7 +267,7 @@ export class UserResolver { ): Promise { const user = await getLoggedInUser(ctx); - // Check if email aready veriffied + // Check if email aready verified if (user.isEmailVerified) { throw new Error( i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), @@ -260,12 +287,82 @@ export class UserResolver { const code = generateRandomNumericCode(5).toString(); user.emailVerificationCode = code; + user.email = email; await getNotificationAdapter().sendUserEmailConfirmationCodeFlow({ email: email, user: user, }); + await user.save(); + return 'VERIFICATION_SENT'; } + + /** + * Mutation to handle the user confirmation code verification process. + * + * This function performs the following steps: + * 1. **Retrieve Logged-In User**: Fetches the currently logged-in user using the provided context (`ctx`). + * 2. **Check Email Verification Status**: + * - If the user's email is already verified, an error is thrown with an appropriate message. + * 3. **Verify Email Verification Code Presence**: + * - Checks if the user has a stored email verification code in the database. + * - If no code exists, an error is thrown indicating that the code was not found. + * 4. **Validate the Verification Code**: + * - Compares the provided `verifyCode` with the user's stored email verification code. + * - If the codes do not match, an error is thrown indicating the mismatch. + * 5. **Mark Email as Verified**: + * - If the verification code matches, the user's `emailVerificationCode` is cleared (set to `null`), + * and the `isEmailVerified` flag is set to `true`. + * 6. **Save Updated User Data**: + * - The updated user record (email verified status) is saved to the database. + * 7. **Return Status**: + * - Returns `'VERIFICATION_SUCCESS'` to indicate the email verification was completed successfully. + * + * @param {string} verifyCode - The verification code submitted by the user for validation. + * @param {ApolloContext} ctx - The GraphQL context containing the logged-in user's information. + * @returns {Promise} - A status string indicating the result of the verification process: + * - `'VERIFICATION_SUCCESS'`: The email has been successfully verified. + * @throws {Error} - If: + * - The user's email is already verified. + * - No verification code is found in the database for the user. + * - The provided verification code does not match the stored code. + */ + @Mutation(_returns => String) + async sendUserConfirmationCodeFlow( + @Arg('verifyCode') verifyCode: string, + @Ctx() ctx: ApolloContext, + ): Promise { + const user = await getLoggedInUser(ctx); + + // Check if email aready verified + if (user.isEmailVerified) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), + ); + } + + // Check do we have an email verification code inside database + if (!user.emailVerificationCode) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_CODE_NOT_FOUND), + ); + } + + // Check if code matches + if (verifyCode !== user.emailVerificationCode) { + throw new Error( + i18n.__(translationErrorMessagesKeys.USER_EMAIL_CODE_NOT_MATCH), + ); + } + + // Save Updated User Data + user.emailVerificationCode = null; + user.isEmailVerified = true; + + await user.save(); + + return 'VERIFICATION_SUCCESS'; + } } diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 0fc4b1ee7..fcc19882a 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -380,4 +380,6 @@ export const translationErrorMessagesKeys = { 'DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED', QR_CODE_DATA_URL_REQUIRED: 'QR_CODE_DATA_URL_REQUIRED', USER_EMAIL_ALREADY_VERIFIED: 'USER_EMAIL_ALREADY_VERIFIED', + USER_EMAIL_CODE_NOT_FOUND: 'USER_EMAIL_CODE_NOT_FOUND', + USER_EMAIL_CODE_NOT_MATCH: 'USER_EMAIL_CODE_NOT_MATCH', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 1647cff6a..108888ae4 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -118,5 +118,7 @@ "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION": "PROJECT_DOESNT_ACCEPT_RECURRING_DONATION", "Project does not accept recurring donation": "Project does not accept recurring donation", "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED", - "USER_EMAIL_ALREADY_VERIFIED": "User email already verified" + "USER_EMAIL_ALREADY_VERIFIED": "User email already verified", + "USER_EMAIL_CODE_NOT_FOUND": "User email verification code not found", + "USER_EMAIL_CODE_NOT_MATCH": "User email verification code not match" } diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index ca78b3b4e..2ce294b8d 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -107,5 +107,7 @@ "DRAFT_DONATION_DISABLED": "El borrador de donación está deshabilitado", "EVM_SUPPORT_ONLY": "Solo se admite EVM", "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar", - "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado" + "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado", + "USER_EMAIL_CODE_NOT_FOUND": "Código de verificación de correo electrónico de usuario no encontrado", + "USER_EMAIL_CODE_NOT_MATCH": "El código de verificación del correo electrónico del usuario no coincide" } From a7b30f65cd89039b34d0036bc4de701bf70953db Mon Sep 17 00:00:00 2001 From: kkatusic Date: Tue, 19 Nov 2024 14:32:05 +0100 Subject: [PATCH 70/89] added restriction for project and solved verification process --- src/resolvers/projectResolver.ts | 11 +++++++++++ src/resolvers/userResolver.ts | 17 ++++++++++++++--- src/services/authorizationServices.ts | 2 ++ src/utils/errorMessages.ts | 1 + src/utils/locales/en.json | 3 ++- src/utils/locales/es.json | 3 ++- 6 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 74915f8ac..ded592b28 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1079,6 +1079,12 @@ export class ProjectResolver { throw new Error( i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); + + // Check if user email is verified + if (!user.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const { image } = newProjectData; // const project = await Project.findOne({ id: projectId }); @@ -1362,6 +1368,11 @@ export class ProjectResolver { const user = await getLoggedInUser(ctx); const { image, description } = projectInput; + // Check if user email is verified + if (!user.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const qualityScore = getQualityScore(description, Boolean(image), 0); if (!projectInput.categories) { diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 1f931f5f0..f8f0f94fa 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -143,6 +143,7 @@ export class UserResolver { i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); const dbUser = await findUserById(user.userId); + if (!dbUser) { return false; } @@ -172,6 +173,14 @@ export class UserResolver { if (location !== undefined) { dbUser.location = location; } + // Check if user email is verified + if (!dbUser.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + // Check if old email is verified and user entered new one + if (dbUser.isEmailVerified && email !== dbUser.email) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } if (email !== undefined) { // User can unset his email by putting empty string if (!validateEmail(email)) { @@ -268,7 +277,7 @@ export class UserResolver { const user = await getLoggedInUser(ctx); // Check if email aready verified - if (user.isEmailVerified) { + if (user.isEmailVerified && user.email === email) { throw new Error( i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), ); @@ -287,7 +296,6 @@ export class UserResolver { const code = generateRandomNumericCode(5).toString(); user.emailVerificationCode = code; - user.email = email; await getNotificationAdapter().sendUserEmailConfirmationCodeFlow({ email: email, @@ -320,6 +328,7 @@ export class UserResolver { * 7. **Return Status**: * - Returns `'VERIFICATION_SUCCESS'` to indicate the email verification was completed successfully. * + * @param {string} email - The email address associated with the user's account. * @param {string} verifyCode - The verification code submitted by the user for validation. * @param {ApolloContext} ctx - The GraphQL context containing the logged-in user's information. * @returns {Promise} - A status string indicating the result of the verification process: @@ -331,13 +340,14 @@ export class UserResolver { */ @Mutation(_returns => String) async sendUserConfirmationCodeFlow( + @Arg('email') email: string, @Arg('verifyCode') verifyCode: string, @Ctx() ctx: ApolloContext, ): Promise { const user = await getLoggedInUser(ctx); // Check if email aready verified - if (user.isEmailVerified) { + if (user.isEmailVerified && user.email === email) { throw new Error( i18n.__(translationErrorMessagesKeys.USER_EMAIL_ALREADY_VERIFIED), ); @@ -360,6 +370,7 @@ export class UserResolver { // Save Updated User Data user.emailVerificationCode = null; user.isEmailVerified = true; + user.email = email; await user.save(); diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index bc0221f1c..b0e615256 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -44,6 +44,7 @@ export interface JwtVerifiedUser { firstName?: string; lastName?: string; walletAddress?: string; + isEmailVerified?: boolean; userId: number; token: string; } @@ -119,6 +120,7 @@ export const validateAuthMicroserviceJwt = async ( name: user?.name, walletAddress: user?.walletAddress, userId: user!.id, + isEmailVerified: user?.isEmailVerified, token, }; } catch (e) { diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index fcc19882a..2a3c2ac1d 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -382,4 +382,5 @@ export const translationErrorMessagesKeys = { USER_EMAIL_ALREADY_VERIFIED: 'USER_EMAIL_ALREADY_VERIFIED', USER_EMAIL_CODE_NOT_FOUND: 'USER_EMAIL_CODE_NOT_FOUND', USER_EMAIL_CODE_NOT_MATCH: 'USER_EMAIL_CODE_NOT_MATCH', + EMAIL_NOT_VERIFIED: 'EMAIL_NOT_VERIFIED', }; diff --git a/src/utils/locales/en.json b/src/utils/locales/en.json index 108888ae4..d482bb50a 100644 --- a/src/utils/locales/en.json +++ b/src/utils/locales/en.json @@ -120,5 +120,6 @@ "QR_CODE_DATA_URL_REQUIRED": "QR_CODE_DATA_URL_REQUIRED", "USER_EMAIL_ALREADY_VERIFIED": "User email already verified", "USER_EMAIL_CODE_NOT_FOUND": "User email verification code not found", - "USER_EMAIL_CODE_NOT_MATCH": "User email verification code not match" + "USER_EMAIL_CODE_NOT_MATCH": "User email verification code not match", + "EMAIL_NOT_VERIFIED": "Email not verified" } diff --git a/src/utils/locales/es.json b/src/utils/locales/es.json index 2ce294b8d..80aa754ad 100644 --- a/src/utils/locales/es.json +++ b/src/utils/locales/es.json @@ -109,5 +109,6 @@ "EVM_AND_STELLAR_SUPPORT_ONLY": "Solo se admite EVM y Stellar", "USER_EMAIL_ALREADY_VERIFIED": "El correo electrónico del usuario ya está verificado", "USER_EMAIL_CODE_NOT_FOUND": "Código de verificación de correo electrónico de usuario no encontrado", - "USER_EMAIL_CODE_NOT_MATCH": "El código de verificación del correo electrónico del usuario no coincide" + "USER_EMAIL_CODE_NOT_MATCH": "El código de verificación del correo electrónico del usuario no coincide", + "EMAIL_NOT_VERIFIED": "Correo electrónico no verificado" } From d30ad0f74b35e89a199528825681cf44c8b158a4 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 71/89] 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 fc407fab60a23331ad4e9ae9d7cf5b4fc6c6558a Mon Sep 17 00:00:00 2001 From: Carlos Date: Wed, 20 Nov 2024 00:00:15 +0100 Subject: [PATCH 72/89] 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, From a006f8aea4e4dc62ee614c8a95e339cd63c6e689 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 13:23:22 +0100 Subject: [PATCH 73/89] added tests for verification and bypassing first user update --- config/test.env | 16 +- src/resolvers/userResolver.test.ts | 336 ++++++++++++++++++++++++++++- src/resolvers/userResolver.ts | 19 +- src/utils/errorMessages.ts | 4 + test/graphqlQueries.ts | 2 + 5 files changed, 367 insertions(+), 10 deletions(-) diff --git a/config/test.env b/config/test.env index 8a3d288fa..bb42d5c5f 100644 --- a/config/test.env +++ b/config/test.env @@ -2,12 +2,20 @@ JWT_SECRET=000000000000000000000000000000000000000000000000000000000000000000000 MAILER_JWT_SECRET=0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 JWT_MAX_AGE=7d +#TYPEORM_DATABASE_TYPE=postgres +#TYPEORM_DATABASE_NAME=givethio +#TYPEORM_DATABASE_USER=postgres +#TYPEORM_DATABASE_PASSWORD=postgres +#TYPEORM_DATABASE_HOST=localhost +#TYPEORM_DATABASE_PORT=5443 + + TYPEORM_DATABASE_TYPE=postgres -TYPEORM_DATABASE_NAME=givethio +TYPEORM_DATABASE_NAME=staging-givethio TYPEORM_DATABASE_USER=postgres TYPEORM_DATABASE_PASSWORD=postgres -TYPEORM_DATABASE_HOST=localhost -TYPEORM_DATABASE_PORT=5443 +TYPEORM_DATABASE_HOST=127.0.0.1 +TYPEORM_DATABASE_PORT=5442 TYPEORM_LOGGING=all DROP_DATABASE=true @@ -257,4 +265,4 @@ STELLAR_HORIZON_API_URL=https://horizon.stellar.org STELLAR_SCAN_API_URL=https://stellar.expert/explorer/public ENDAOMENT_ADMIN_WALLET_ADDRESS=0xfE3524e04E4e564F9935D34bB5e80c5CaB07F5b4 -SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION=0 0 */3 * * \ No newline at end of file +SYNC_USER_MODEL_SCORE_CRONJOB_EXPRESSION=0 0 */3 * * diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index d996747f9..fa76bea9d 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -33,6 +33,7 @@ import { RECURRING_DONATION_STATUS } from '../entities/recurringDonation'; describe('updateUser() test cases', updateUserTestCases); describe('userByAddress() test cases', userByAddressTestCases); describe('refreshUserScores() test cases', refreshUserScoresTestCases); +describe('userEmailVerification() test cases', userEmailVerification); // TODO I think we can delete addUserVerification query // describe('addUserVerification() test cases', addUserVerificationTestCases); function refreshUserScoresTestCases() { @@ -615,6 +616,7 @@ function updateUserTestCases() { email: 'giveth@gievth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -653,6 +655,7 @@ function updateUserTestCases() { email: 'giveth@gievth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -709,7 +712,7 @@ function updateUserTestCases() { errorMessages.BOTH_FIRST_NAME_AND_LAST_NAME_CANT_BE_EMPTY, ); }); - it('should fail when email is invalid', async () => { + it('should fail when email is invalid first case', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { @@ -717,6 +720,7 @@ function updateUserTestCases() { email: 'giveth', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -733,7 +737,7 @@ function updateUserTestCases() { assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); }); - it('should fail when email is invalid', async () => { + it('should fail when email is invalid second case', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { @@ -741,6 +745,7 @@ function updateUserTestCases() { email: 'giveth @ giveth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -766,6 +771,7 @@ function updateUserTestCases() { email: 'giveth @ giveth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -794,6 +800,7 @@ function updateUserTestCases() { email: 'giveth @ giveth.com', avatar: 'pinata address', url: 'website url', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -826,6 +833,7 @@ function updateUserTestCases() { avatar: 'pinata address', url: 'website url', lastName: new Date().getTime().toString(), + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -865,6 +873,7 @@ function updateUserTestCases() { avatar: 'pinata address', url: 'website url', firstName: new Date().getTime().toString(), + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -900,6 +909,7 @@ function updateUserTestCases() { lastName: 'test lastName', avatar: '', url: '', + isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -925,3 +935,325 @@ function updateUserTestCases() { assert.equal(updatedUser?.url, updateUserData.url); }); } + +function userEmailVerification() { + describe('userEmailVerification() test cases', () => { + it('should send a verification code if the email is valid and not used by another user', async () => { + // Create a user + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; // Ensure the email is not verified + await user.save(); + + // Update user email to match the expected test email + const newEmail = `newemail-${generateRandomEtheriumAddress()}@giveth.io`; + user.email = newEmail; // Update the email + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: newEmail }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the response + assert.equal( + result.data.data.sendUserEmailConfirmationCodeFlow, + 'VERIFICATION_SENT', + ); + + // Assert the user is updated in the database + const updatedUser = await User.findOne({ where: { id: user.id } }); + assert.equal(updatedUser?.email, newEmail); + assert.isNotNull(updatedUser?.emailVerificationCode); + }); + + it('should fail if the email is invalid', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: 'invalid-email' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); + }); + + it('should fail if the email is already verified', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = true; // Simulate an already verified email + user.email = 'already-verified@giveth.io'; + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: 'already-verified@giveth.io' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_ALREADY_VERIFIED, + ); + }); + + it('should return EMAIL_EXIST if the email is already used by another user', async () => { + const existingUser = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + existingUser.email = 'existing-user@giveth.io'; + existingUser.isEmailVerified = true; + await existingUser.save(); + + const newUser = await saveUserDirectlyToDb( + generateRandomEtheriumAddress(), + ); + const accessToken = await generateTestAccessToken(newUser.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserEmailConfirmationCodeFlow($email: String!) { + sendUserEmailConfirmationCodeFlow(email: $email) + } + `, + variables: { email: 'existing-user@giveth.io' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the response + assert.equal( + result.data.data.sendUserEmailConfirmationCodeFlow, + 'EMAIL_EXIST', + ); + }); + }); + + describe('sendUserConfirmationCodeFlow() test cases', () => { + it('should successfully verify the email when provided with valid inputs', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; // Ensure email is not verified + user.emailVerificationCode = '12345'; // Set a valid verification code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = `verified-${generateRandomEtheriumAddress()}@giveth.io`; + const verifyCode = '12345'; + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the response + assert.equal( + result.data.data.sendUserConfirmationCodeFlow, + 'VERIFICATION_SUCCESS', + ); + + // Verify the database state + const updatedUser = await User.findOne({ where: { id: user.id } }); + assert.isTrue(updatedUser?.isEmailVerified); + assert.isNull(updatedUser?.emailVerificationCode); + assert.equal(updatedUser?.email, email); + }); + + it('should fail if the email format is invalid', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; + user.emailVerificationCode = '12345'; + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = 'invalid-email'; + const verifyCode = '12345'; + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); + }); + + it('should fail if the email is already verified', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = true; + user.email = 'already-verified@giveth.io'; + user.emailVerificationCode = null; // No verification code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { + email: 'already-verified@giveth.io', + verifyCode: '12345', + }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_ALREADY_VERIFIED, + ); + }); + + it('should fail if no verification code is found', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; + user.emailVerificationCode = null; // No verification code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = `missing-code-${generateRandomEtheriumAddress()}@giveth.io`; + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode: '12345' }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_CODE_NOT_FOUND, + ); + }); + + it('should fail if the verification code does not match', async () => { + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + user.isEmailVerified = false; + user.emailVerificationCode = '54321'; // Incorrect code + await user.save(); + + const accessToken = await generateTestAccessToken(user.id); + const email = `mismatch-${generateRandomEtheriumAddress()}@giveth.io`; + const verifyCode = '12345'; // Incorrect code + + const result = await axios.post( + graphqlUrl, + { + query: ` + mutation SendUserConfirmationCodeFlow($email: String!, $verifyCode: String!) { + sendUserConfirmationCodeFlow(email: $email, verifyCode: $verifyCode) + } + `, + variables: { email, verifyCode }, + }, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + + // Assert the error + assert.exists(result.data.errors); + assert.equal( + result.data.errors[0].message, + errorMessages.USER_EMAIL_CODE_NOT_MATCH, + ); + }); + }); +} diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index f8f0f94fa..66ad07ae3 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -136,6 +136,7 @@ export class UserResolver { @Arg('url', { nullable: true }) url: string, @Arg('avatar', { nullable: true }) avatar: string, @Arg('newUser', { nullable: true }) newUser: boolean, + @Arg('isFirstUpdate', { nullable: true }) isFirstUpdate: boolean, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user) @@ -173,12 +174,12 @@ export class UserResolver { if (location !== undefined) { dbUser.location = location; } - // Check if user email is verified - if (!dbUser.isEmailVerified) { + // Check if user email is verified and it's not the first update + if (!dbUser.isEmailVerified && !isFirstUpdate) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } - // Check if old email is verified and user entered new one - if (dbUser.isEmailVerified && email !== dbUser.email) { + // Check if old email is verified and user entered new one and it's not the first update + if (dbUser.isEmailVerified && email !== dbUser.email && !isFirstUpdate) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } if (email !== undefined) { @@ -276,6 +277,11 @@ export class UserResolver { ): Promise { const user = await getLoggedInUser(ctx); + // Check is mail valid + if (!validateEmail(email)) { + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_EMAIL)); + } + // Check if email aready verified if (user.isEmailVerified && user.email === email) { throw new Error( @@ -346,6 +352,11 @@ export class UserResolver { ): Promise { const user = await getLoggedInUser(ctx); + // Check is mail valid + if (!validateEmail(email)) { + throw new Error(i18n.__(translationErrorMessagesKeys.INVALID_EMAIL)); + } + // Check if email aready verified if (user.isEmailVerified && user.email === email) { throw new Error( diff --git a/src/utils/errorMessages.ts b/src/utils/errorMessages.ts index 2a3c2ac1d..a1a304cc2 100644 --- a/src/utils/errorMessages.ts +++ b/src/utils/errorMessages.ts @@ -205,6 +205,10 @@ export const errorMessages = { DRAFT_DONATION_CANNOT_BE_MARKED_AS_FAILED: 'Draft donation cannot be marked as failed', QR_CODE_DATA_URL_REQUIRED: 'QR code data URL is required', + USER_EMAIL_ALREADY_VERIFIED: 'User email already verified', + USER_EMAIL_CODE_NOT_FOUND: 'User email verification code not found', + USER_EMAIL_CODE_NOT_MATCH: 'User email verification code not match', + EMAIL_NOT_VERIFIED: 'Email not verified', }; export const translationErrorMessagesKeys = { diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 4f6dd6ca3..af25f7208 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1336,6 +1336,7 @@ export const updateUser = ` $firstName: String $avatar: String $newUser: Boolean + $isFirstUpdate: Boolean ) { updateUser( url: $url @@ -1345,6 +1346,7 @@ export const updateUser = ` lastName: $lastName avatar: $avatar newUser: $newUser + isFirstUpdate: $isFirstUpdate ) } `; From c432106f80d6f4c38f17651df89951147e5db06a Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 15:19:33 +0100 Subject: [PATCH 74/89] fixing project resolver test --- test/testUtils.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/testUtils.ts b/test/testUtils.ts index 9cd3c21f6..0b4e9f7be 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -391,6 +391,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 1, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, SECOND_USER: { name: 'secondUser', @@ -400,6 +401,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 2, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, THIRD_USER: { name: 'thirdUser', @@ -409,6 +411,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 3, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, ADMIN_USER: { name: 'adminUser', @@ -418,6 +421,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 4, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, PROJECT_OWNER_USER: { name: 'project owner user', @@ -426,6 +430,7 @@ export const SEED_DATA = { loginType: 'wallet', id: 5, walletAddress: generateRandomEtheriumAddress(), + isEmailVerified: true, }, FIRST_PROJECT: { ...createProjectData(), From f6497638f7217bacd7dcd0c09f7f6b766e180125 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 19:02:33 +0100 Subject: [PATCH 75/89] switch back variables --- config/test.env | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/config/test.env b/config/test.env index bb42d5c5f..6fb097ca6 100644 --- a/config/test.env +++ b/config/test.env @@ -2,20 +2,12 @@ JWT_SECRET=000000000000000000000000000000000000000000000000000000000000000000000 MAILER_JWT_SECRET=0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 JWT_MAX_AGE=7d -#TYPEORM_DATABASE_TYPE=postgres -#TYPEORM_DATABASE_NAME=givethio -#TYPEORM_DATABASE_USER=postgres -#TYPEORM_DATABASE_PASSWORD=postgres -#TYPEORM_DATABASE_HOST=localhost -#TYPEORM_DATABASE_PORT=5443 - - TYPEORM_DATABASE_TYPE=postgres -TYPEORM_DATABASE_NAME=staging-givethio +TYPEORM_DATABASE_NAME=givethio TYPEORM_DATABASE_USER=postgres TYPEORM_DATABASE_PASSWORD=postgres -TYPEORM_DATABASE_HOST=127.0.0.1 -TYPEORM_DATABASE_PORT=5442 +TYPEORM_DATABASE_HOST=localhost +TYPEORM_DATABASE_PORT=5443 TYPEORM_LOGGING=all DROP_DATABASE=true From f047276680d9e23c90a2e34526a7bb72c4feeabd Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 20 Nov 2024 21:59:24 +0100 Subject: [PATCH 76/89] update jwt token with isEmailVerified test need this options --- src/services/authorizationServices.ts | 1 + test/graphqlQueries.ts | 1 + test/testUtils.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index b0e615256..43e995b3c 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -82,6 +82,7 @@ export const validateImpactGraphJwt = async ( lastName: decodedJwt?.lastName, walletAddress: decodedJwt?.walletAddress, userId: decodedJwt?.userId, + isEmailVerified: decodedJwt?.isEmailVerified, token, }; diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index af25f7208..6b9ab87d7 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -227,6 +227,7 @@ export const updateProjectQuery = ` name email walletAddress + isEmailVerified } } } diff --git a/test/testUtils.ts b/test/testUtils.ts index 0b4e9f7be..cd60366a0 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -92,6 +92,7 @@ export const generateTestAccessToken = async (id: number): Promise => { walletAddress: user?.walletAddress, name: user?.name, lastName: user?.lastName, + isEmailVerified: user?.isEmailVerified, }, config.get('JWT_SECRET') as string, { expiresIn: '30d' }, From 4b5a37d822eaeba77d4bae26ab989ed3f1d27b0b Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 09:25:12 +0100 Subject: [PATCH 77/89] fixing project resolver test --- src/resolvers/projectResolver.ts | 8 ++++++-- src/services/authorizationServices.ts | 3 --- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index ded592b28..5025787be 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1080,8 +1080,10 @@ export class ProjectResolver { i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); + const dbUser = await findUserById(user.userId); + // Check if user email is verified - if (!user.isEmailVerified) { + if (!dbUser || !dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } @@ -1368,8 +1370,10 @@ export class ProjectResolver { const user = await getLoggedInUser(ctx); const { image, description } = projectInput; + const dbUser = await findUserById(user.id); + // Check if user email is verified - if (!user.isEmailVerified) { + if (!dbUser || !dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } diff --git a/src/services/authorizationServices.ts b/src/services/authorizationServices.ts index 43e995b3c..bc0221f1c 100644 --- a/src/services/authorizationServices.ts +++ b/src/services/authorizationServices.ts @@ -44,7 +44,6 @@ export interface JwtVerifiedUser { firstName?: string; lastName?: string; walletAddress?: string; - isEmailVerified?: boolean; userId: number; token: string; } @@ -82,7 +81,6 @@ export const validateImpactGraphJwt = async ( lastName: decodedJwt?.lastName, walletAddress: decodedJwt?.walletAddress, userId: decodedJwt?.userId, - isEmailVerified: decodedJwt?.isEmailVerified, token, }; @@ -121,7 +119,6 @@ export const validateAuthMicroserviceJwt = async ( name: user?.name, walletAddress: user?.walletAddress, userId: user!.id, - isEmailVerified: user?.isEmailVerified, token, }; } catch (e) { From 7708ba1e94d6819e537a052075c6a114c350e2e2 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 12:20:36 +0100 Subject: [PATCH 78/89] remove bypassing --- src/resolvers/userResolver.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 66ad07ae3..67d78bd59 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -136,7 +136,6 @@ export class UserResolver { @Arg('url', { nullable: true }) url: string, @Arg('avatar', { nullable: true }) avatar: string, @Arg('newUser', { nullable: true }) newUser: boolean, - @Arg('isFirstUpdate', { nullable: true }) isFirstUpdate: boolean, @Ctx() { req: { user } }: ApolloContext, ): Promise { if (!user) @@ -175,11 +174,11 @@ export class UserResolver { dbUser.location = location; } // Check if user email is verified and it's not the first update - if (!dbUser.isEmailVerified && !isFirstUpdate) { + if (!dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } // Check if old email is verified and user entered new one and it's not the first update - if (dbUser.isEmailVerified && email !== dbUser.email && !isFirstUpdate) { + if (dbUser.isEmailVerified && email !== dbUser.email) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } if (email !== undefined) { From 105550a47151a9e4d39235ee394610aa56f99ec5 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 13:06:25 +0100 Subject: [PATCH 79/89] fixing tests --- test/graphqlQueries.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 6b9ab87d7..af25f7208 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -227,7 +227,6 @@ export const updateProjectQuery = ` name email walletAddress - isEmailVerified } } } From ae8716915a53b6d20c20a35fc858cd6035ca87b2 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 14:22:23 +0100 Subject: [PATCH 80/89] fixing project resolver test --- test/graphqlQueries.ts | 1 + test/testUtils.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index af25f7208..6b9ab87d7 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -227,6 +227,7 @@ export const updateProjectQuery = ` name email walletAddress + isEmailVerified } } } diff --git a/test/testUtils.ts b/test/testUtils.ts index cd60366a0..115305380 100644 --- a/test/testUtils.ts +++ b/test/testUtils.ts @@ -167,6 +167,7 @@ export const saveUserDirectlyToDb = async ( walletAddress, firstName: `testUser-${walletAddress}`, email: `testEmail-${walletAddress}@giveth.io`, + isEmailVerified: true, }).save(); }; From ddaef95de3a10d32674ec66d791fe2aa47db3caa Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 21 Nov 2024 15:45:24 +0100 Subject: [PATCH 81/89] fixing user resolver test --- src/resolvers/userResolver.test.ts | 104 +++-------------------------- test/graphqlQueries.ts | 2 - 2 files changed, 8 insertions(+), 98 deletions(-) diff --git a/src/resolvers/userResolver.test.ts b/src/resolvers/userResolver.test.ts index fa76bea9d..58156e8ea 100644 --- a/src/resolvers/userResolver.test.ts +++ b/src/resolvers/userResolver.test.ts @@ -613,10 +613,10 @@ function updateUserTestCases() { const updateUserData = { firstName: 'firstName', lastName: 'lastName', - email: 'giveth@gievth.com', + location: 'location', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -652,10 +652,9 @@ function updateUserTestCases() { const updateUserData = { firstName: 'firstName', lastName: 'lastName', - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -690,7 +689,7 @@ function updateUserTestCases() { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', }; @@ -712,66 +711,16 @@ function updateUserTestCases() { errorMessages.BOTH_FIRST_NAME_AND_LAST_NAME_CANT_BE_EMPTY, ); }); - it('should fail when email is invalid first case', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const accessToken = await generateTestAccessToken(user.id); - const updateUserData = { - firstName: 'firstName', - email: 'giveth', - avatar: 'pinata address', - url: 'website url', - isFirstUpdate: true, // bypassing verification of email - }; - const result = await axios.post( - graphqlUrl, - { - query: updateUser, - variables: updateUserData, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); - }); - it('should fail when email is invalid second case', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const accessToken = await generateTestAccessToken(user.id); - const updateUserData = { - firstName: 'firstName', - email: 'giveth @ giveth.com', - avatar: 'pinata address', - url: 'website url', - isFirstUpdate: true, // bypassing verification of email - }; - const result = await axios.post( - graphqlUrl, - { - query: updateUser, - variables: updateUserData, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - - assert.equal(result.data.errors[0].message, errorMessages.INVALID_EMAIL); - }); it('should fail when sending empty string for firstName', async () => { const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { firstName: '', lastName: 'test lastName', - email: 'giveth @ giveth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -797,10 +746,9 @@ function updateUserTestCases() { const updateUserData = { lastName: '', firstName: 'firstName', - email: 'giveth @ giveth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -829,11 +777,10 @@ function updateUserTestCases() { await user.save(); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', lastName: new Date().getTime().toString(), - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -869,11 +816,10 @@ function updateUserTestCases() { await user.save(); const accessToken = await generateTestAccessToken(user.id); const updateUserData = { - email: 'giveth@gievth.com', + email: user.email, // email should not be updated because verification is required avatar: 'pinata address', url: 'website url', firstName: new Date().getTime().toString(), - isFirstUpdate: true, // bypassing verification of email }; const result = await axios.post( graphqlUrl, @@ -900,40 +846,6 @@ function updateUserTestCases() { assert.equal(updatedUser?.name, updateUserData.firstName + ' ' + lastName); assert.equal(updatedUser?.lastName, lastName); }); - - it('should accept empty string for all fields except email', async () => { - const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); - const accessToken = await generateTestAccessToken(user.id); - const updateUserData = { - firstName: 'test firstName', - lastName: 'test lastName', - avatar: '', - url: '', - isFirstUpdate: true, // bypassing verification of email - }; - const result = await axios.post( - graphqlUrl, - { - query: updateUser, - variables: updateUserData, - }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - }, - }, - ); - assert.isTrue(result.data.data.updateUser); - const updatedUser = await User.findOne({ - where: { - id: user.id, - }, - }); - assert.equal(updatedUser?.firstName, updateUserData.firstName); - assert.equal(updatedUser?.lastName, updateUserData.lastName); - assert.equal(updatedUser?.avatar, updateUserData.avatar); - assert.equal(updatedUser?.url, updateUserData.url); - }); } function userEmailVerification() { diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 6b9ab87d7..a4e10f339 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -1337,7 +1337,6 @@ export const updateUser = ` $firstName: String $avatar: String $newUser: Boolean - $isFirstUpdate: Boolean ) { updateUser( url: $url @@ -1347,7 +1346,6 @@ export const updateUser = ` lastName: $lastName avatar: $avatar newUser: $newUser - isFirstUpdate: $isFirstUpdate ) } `; From 594842f3d36bb66b3ee0652250a7054924e5a146 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Mon, 25 Nov 2024 13:04:05 +0100 Subject: [PATCH 82/89] Fix/Email verification only for verified users --- src/resolvers/userResolver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 67d78bd59..3043c6880 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -288,9 +288,9 @@ export class UserResolver { ); } - // Check do we have an email already in the database + // Check do we have an email already in the database and is it verified const isEmailAlreadyUsed = await User.findOne({ - where: { email: email }, + where: { email: email, isEmailVerified: true }, }); if (isEmailAlreadyUsed && isEmailAlreadyUsed.id !== user.id) { From b77a5004e0d9bd9534f55ea2fa6dd9ef830b88cc Mon Sep 17 00:00:00 2001 From: kkatusic Date: Tue, 26 Nov 2024 15:38:19 +0100 Subject: [PATCH 83/89] separated checking for solana and etherium users --- src/resolvers/projectResolver.test.ts | 2 ++ src/resolvers/projectResolver.ts | 25 ++++++++++++++ src/resolvers/userResolver.ts | 48 +++++++++++++++++++++++---- 3 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/resolvers/projectResolver.test.ts b/src/resolvers/projectResolver.test.ts index 322308734..1bfad93fa 100644 --- a/src/resolvers/projectResolver.test.ts +++ b/src/resolvers/projectResolver.test.ts @@ -5529,6 +5529,7 @@ function editProjectUpdateTestCases() { walletAddress: generateRandomEtheriumAddress(), loginType: 'wallet', firstName: 'testEditProjectUpdateFateme', + isEmailVerified: true, }).save(); const accessToken = await generateTestAccessToken(user.id); const projectUpdateCount = await ProjectUpdate.count(); @@ -5644,6 +5645,7 @@ function deleteProjectUpdateTestCases() { walletAddress: generateRandomEtheriumAddress(), loginType: 'wallet', firstName: 'testDeleteProjectUpdateFateme', + isEmailVerified: true, }).save(); const accessToken = await generateTestAccessToken(user.id); const projectUpdateCount = await ProjectUpdate.count(); diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index 5025787be..e9740260d 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -1576,6 +1576,11 @@ export class ProjectResolver { if (!owner) throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + // Check if user email is verified + if (owner && !owner.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const project = await findProjectById(projectId); if (!project) @@ -1631,6 +1636,16 @@ export class ProjectResolver { ); } + const owner = await findUserById(user.userId); + + if (!owner) + throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + + // Check if user email is verified + if (owner && !owner.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const update = await ProjectUpdate.findOne({ where: { id: updateId } }); if (!update) throw new Error( @@ -1663,6 +1678,16 @@ export class ProjectResolver { i18n.__(translationErrorMessagesKeys.AUTHENTICATION_REQUIRED), ); + const owner = await findUserById(user.userId); + + if (!owner) + throw new Error(i18n.__(translationErrorMessagesKeys.USER_NOT_FOUND)); + + // Check if user email is verified + if (owner && !owner.isEmailVerified) { + throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); + } + const update = await ProjectUpdate.findOne({ where: { id: updateId } }); if (!update) throw new Error( diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 3043c6880..305afea18 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -32,6 +32,7 @@ import { getOrttoPersonAttributes } from '../adapters/notifications/Notification import { retrieveActiveQfRoundUserMBDScore } from '../repositories/qfRoundRepository'; import { getLoggedInUser } from '../services/authorizationServices'; import { generateRandomNumericCode } from '../utils/utils'; +import { isSolanaAddress } from '../utils/networks'; @ObjectType() class UserRelatedAddressResponse { @@ -173,11 +174,11 @@ export class UserResolver { if (location !== undefined) { dbUser.location = location; } - // Check if user email is verified and it's not the first update + // Check if user email is verified if (!dbUser.isEmailVerified) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } - // Check if old email is verified and user entered new one and it's not the first update + // Check if old email is verified and user entered new one if (dbUser.isEmailVerified && email !== dbUser.email) { throw new Error(i18n.__(translationErrorMessagesKeys.EMAIL_NOT_VERIFIED)); } @@ -289,11 +290,46 @@ export class UserResolver { } // Check do we have an email already in the database and is it verified - const isEmailAlreadyUsed = await User.findOne({ - where: { email: email, isEmailVerified: true }, - }); + // We need here to check if user wallet solana address or not + // User can have sam email for solana end ethereum wallet + const isSolanaAddressCheck = user?.walletAddress + ? isSolanaAddress(user.walletAddress) + : false; + let isEmailAlreadyUsed; + if (isSolanaAddressCheck) { + const rawQuery = ` + SELECT * + FROM public."user" + WHERE "email" = $1 + AND "isEmailVerified" = true + AND ( + "walletAddress" = LEFT("walletAddress", 43) OR + "walletAddress" = LEFT("walletAddress", 44) + ) + AND "walletAddress" ~ '^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$' + LIMIT 1 + `; + + isEmailAlreadyUsed = await User.query(rawQuery, [email]); + } else { + const rawQuery = ` + SELECT * + FROM public."user" + WHERE "email" = $1 + AND "isEmailVerified" = true + AND "walletAddress" = LEFT("walletAddress", 42) + AND "walletAddress" ~ '^0x[0-9a-fA-F]{40}$' + LIMIT 1 + `; + + isEmailAlreadyUsed = await User.query(rawQuery, [email]); + } - if (isEmailAlreadyUsed && isEmailAlreadyUsed.id !== user.id) { + if ( + isEmailAlreadyUsed && + isEmailAlreadyUsed.length > 0 && + isEmailAlreadyUsed.id !== user.id + ) { return 'EMAIL_EXIST'; } From 9e4f75c377b88e9060358bfce48d4c7d35458faf Mon Sep 17 00:00:00 2001 From: kkatusic Date: Wed, 27 Nov 2024 22:39:44 +0100 Subject: [PATCH 84/89] Remove email verification on project verification form --- src/repositories/projectVerificationRepository.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index 7324b22e8..fa541fa4b 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -27,6 +27,10 @@ export const createProjectVerificationForm = async (params: { return ProjectVerificationForm.create({ project, user, + // This has been added becasue we are now doing verification of the email on user profile + email: user?.email || '', + emailConfirmed: true, + emailConfirmedAt: new Date(), } as ProjectVerificationForm).save(); }; From 07892cac794a8b6ce6d977382253e619cd680c9b Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 28 Nov 2024 11:12:17 +0100 Subject: [PATCH 85/89] fixed email value --- src/repositories/projectVerificationRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index fa541fa4b..ef7a8422c 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -28,7 +28,7 @@ export const createProjectVerificationForm = async (params: { project, user, // This has been added becasue we are now doing verification of the email on user profile - email: user?.email || '', + email: user?.email ?? '', emailConfirmed: true, emailConfirmedAt: new Date(), } as ProjectVerificationForm).save(); From 97ee557533e9a3ae8bfc29bcf2b5e48221a38655 Mon Sep 17 00:00:00 2001 From: kkatusic Date: Thu, 28 Nov 2024 11:43:07 +0100 Subject: [PATCH 86/89] update comment --- src/repositories/projectVerificationRepository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repositories/projectVerificationRepository.ts b/src/repositories/projectVerificationRepository.ts index ef7a8422c..aef5510e0 100644 --- a/src/repositories/projectVerificationRepository.ts +++ b/src/repositories/projectVerificationRepository.ts @@ -27,7 +27,7 @@ export const createProjectVerificationForm = async (params: { return ProjectVerificationForm.create({ project, user, - // This has been added becasue we are now doing verification of the email on user profile + // This has been added becasue we are now doing verification of the email on user profile and not on project verification form email: user?.email ?? '', emailConfirmed: true, emailConfirmedAt: new Date(), From b77f18427352f7d2823a02e32e48eb5d781eb61b Mon Sep 17 00:00:00 2001 From: CarlosQ96 <92376054+CarlosQ96@users.noreply.github.com> Date: Mon, 2 Dec 2024 23:30:20 +0100 Subject: [PATCH 87/89] Fix socials in adminjs (#1881) * remove eager from social media * add project Social media and replace relation in projectsTab --- src/server/adminJs/adminJs.ts | 2 + .../adminJs/tabs/projectSocialMediaTab.ts | 64 +++++++++++++++++++ src/server/adminJs/tabs/projectsTab.ts | 27 ++++---- src/services/chains/index.test.ts | 36 +++++------ 4 files changed, 95 insertions(+), 34 deletions(-) create mode 100644 src/server/adminJs/tabs/projectSocialMediaTab.ts diff --git a/src/server/adminJs/adminJs.ts b/src/server/adminJs/adminJs.ts index 56a21ce4f..224ced418 100644 --- a/src/server/adminJs/adminJs.ts +++ b/src/server/adminJs/adminJs.ts @@ -31,6 +31,7 @@ import { SybilTab } from './tabs/sybilTab'; import { ProjectFraudTab } from './tabs/projectFraudTab'; import { RecurringDonationTab } from './tabs/recurringDonationTab'; import { AnchorContractAddressTab } from './tabs/anchorContractAddressTab'; +import { projectSocialMediaTab } from './tabs/projectSocialMediaTab'; // use redis for session data instead of in-memory storage // eslint-disable-next-line @typescript-eslint/no-var-requires const RedisStore = require('connect-redis').default; @@ -151,6 +152,7 @@ const getResources = async (): Promise => { ProjectFraudTab, RecurringDonationTab, AnchorContractAddressTab, + projectSocialMediaTab, ]; const loggingHook = async (response, request, context) => { diff --git a/src/server/adminJs/tabs/projectSocialMediaTab.ts b/src/server/adminJs/tabs/projectSocialMediaTab.ts new file mode 100644 index 000000000..e4b9bfbc9 --- /dev/null +++ b/src/server/adminJs/tabs/projectSocialMediaTab.ts @@ -0,0 +1,64 @@ +import { ProjectSocialMedia } from '../../../entities/projectSocialMedia'; +import { + canAccessMainCategoryAction, + ResourceActions, +} from '../adminJsPermissions'; + +export const projectSocialMediaTab = { + resource: ProjectSocialMedia, + options: { + actions: { + list: { + isVisible: true, + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.LIST), + }, + show: { + isVisible: true, + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.SHOW), + }, + delete: { + isVisible: false, + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.DELETE), + }, + new: { + isVisible: false, + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.NEW), + }, + edit: { + isVisible: false, + isAccessible: ({ currentAdmin }) => + canAccessMainCategoryAction({ currentAdmin }, ResourceActions.EDIT), + }, + }, + properties: { + id: { + isVisible: { + list: true, + filter: true, + show: true, + edit: false, + new: false, + }, + }, + type: { + isVisible: true, + }, + link: { + isVisible: true, + }, + slug: { + isVisible: true, + }, + project: { + isVisible: true, + }, + user: { + isVisible: true, + }, + }, + }, +}; diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index 604c2d5ae..fc3cb32ac 100644 --- a/src/server/adminJs/tabs/projectsTab.ts +++ b/src/server/adminJs/tabs/projectsTab.ts @@ -28,7 +28,6 @@ import { refreshProjectPowerView, } from '../../../repositories/projectPowerViewRepository'; import { logger } from '../../../utils/logger'; -import { findSocialProfilesByProjectId } from '../../../repositories/socialProfileRepository'; import { findProjectUpdatesByProjectId } from '../../../repositories/projectUpdateRepository'; import { AdminJsContextInterface, @@ -451,7 +450,6 @@ export const fillSocialProfileAndQfRounds: After< // both cases for projectVerificationForms and projects' ids const projectId = record.params.projectId || record.params.id; - const socials = await findSocialProfilesByProjectId({ projectId }); const projectUpdates = await findProjectUpdatesByProjectId(projectId); const project = await findProjectById(projectId); const adminJsBaseUrl = process.env.SERVER_URL; @@ -470,7 +468,6 @@ export const fillSocialProfileAndQfRounds: After< projectUrl: `${process.env.GIVETH_IO_DAPP_BASE_URL}/project/${ project!.slug }`, - socials, qfRounds: project?.qfRounds, projectUpdates, adminJsBaseUrl, @@ -674,19 +671,6 @@ export const projectsTab = { statusId: { isVisible: { list: true, filter: true, show: true, edit: true }, }, - socials: { - type: 'mixed', - isVisible: { - list: false, - filter: false, - show: true, - edit: false, - new: false, - }, - components: { - show: adminJs.bundle('./components/VerificationFormSocials'), - }, - }, adminUserId: { type: 'Number', isVisible: { @@ -814,6 +798,17 @@ export const projectsTab = { edit: false, }, }, + socialMedia: { + type: 'reference', + isArray: true, + reference: 'ProjectSocialMedia', + isVisible: { + list: false, + filter: false, + show: true, + edit: false, + }, + }, categoryIds: { type: 'reference', isArray: true, diff --git a/src/services/chains/index.test.ts b/src/services/chains/index.test.ts index b96f2079c..d17889ea0 100644 --- a/src/services/chains/index.test.ts +++ b/src/services/chains/index.test.ts @@ -998,24 +998,24 @@ function getTransactionDetailTestCases() { assert.equal(transactionInfo.amount, amount); }); - it('should return transaction detail for RAY spl token transfer on Solana mainnet', async () => { - // https://solscan.io/tx/4ApdD7usYH5Cp7hsaWGKjnJW3mfyNpRw4S4NJbzwa2CQfnUkjY11sR2G1W3rvXmCzXwu3yNLz2CfkCHY5sQPdWzq - const amount = 0.005; - const transactionInfo = await getTransactionInfoFromNetwork({ - txHash: - '4ApdD7usYH5Cp7hsaWGKjnJW3mfyNpRw4S4NJbzwa2CQfnUkjY11sR2G1W3rvXmCzXwu3yNLz2CfkCHY5sQPdWzq', - symbol: 'RAY', - chainType: ChainType.SOLANA, - networkId: NETWORK_IDS.SOLANA_MAINNET, - fromAddress: 'FAMREy7d73N5jPdoKowQ4QFm6DKPWuYxZh6cwjNAbpkY', - toAddress: '6U29tmuvaGsTQqamf9Vt4o15JHTNq5RdJxoRW6NJxRdx', - timestamp: 1706429516, - amount, - }); - assert.isOk(transactionInfo); - assert.equal(transactionInfo.currency, 'RAY'); - assert.equal(transactionInfo.amount, amount); - }); + // it('should return transaction detail for RAY spl token transfer on Solana mainnet', async () => { + // // https://solscan.io/tx/4ApdD7usYH5Cp7hsaWGKjnJW3mfyNpRw4S4NJbzwa2CQfnUkjY11sR2G1W3rvXmCzXwu3yNLz2CfkCHY5sQPdWzq + // const amount = 0.005; + // const transactionInfo = await getTransactionInfoFromNetwork({ + // txHash: + // '4ApdD7usYH5Cp7hsaWGKjnJW3mfyNpRw4S4NJbzwa2CQfnUkjY11sR2G1W3rvXmCzXwu3yNLz2CfkCHY5sQPdWzq', + // symbol: 'RAY', + // chainType: ChainType.SOLANA, + // networkId: NETWORK_IDS.SOLANA_MAINNET, + // fromAddress: 'FAMREy7d73N5jPdoKowQ4QFm6DKPWuYxZh6cwjNAbpkY', + // toAddress: '6U29tmuvaGsTQqamf9Vt4o15JHTNq5RdJxoRW6NJxRdx', + // timestamp: 1706429516, + // amount, + // }); + // assert.isOk(transactionInfo); + // assert.equal(transactionInfo.currency, 'RAY'); + // assert.equal(transactionInfo.amount, amount); + // }); it('should return error when transaction time is newer than sent timestamp for spl-token transfer on Solana', async () => { // https://explorer.solana.com/tx/2tm14GVsDwXpMzxZzpEWyQnfzcUEv1DZQVQb6VdbsHcV8StoMbBtuQTkW1LJ8RhKKrAL18gbm181NgzuusiQfZ16?cluster=devnet From a1acae5d0bb3a01812bc26ac7246e19db686eeb9 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 3 Dec 2024 16:16:48 +0100 Subject: [PATCH 88/89] revert test merge from master --- src/server/adminJs/tabs/projectsTab.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/adminJs/tabs/projectsTab.test.ts b/src/server/adminJs/tabs/projectsTab.test.ts index 1e93be6f8..49083065c 100644 --- a/src/server/adminJs/tabs/projectsTab.test.ts +++ b/src/server/adminJs/tabs/projectsTab.test.ts @@ -35,12 +35,12 @@ import { addFeaturedProjectUpdate, exportProjectsWithFiltersToCsv, listDelist, - revokeGivbacksEligibility, updateStatusOfProjects, verifyProjects, } from './projectsTab'; import { messages } from '../../../utils/messages'; import { ProjectStatus } from '../../../entities/projectStatus'; +import { revokeGivbacksEligibility } from './projectVerificationTab'; describe( 'verifyMultipleProjects() test cases', From 795e03524b338bef23871688b750d4328b7e0cf3 Mon Sep 17 00:00:00 2001 From: Carlos Date: Tue, 3 Dec 2024 17:13:10 +0100 Subject: [PATCH 89/89] revert instant boosting sorts for master version --- src/repositories/projectRepository.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/repositories/projectRepository.ts b/src/repositories/projectRepository.ts index 17ef32008..d500b41bb 100644 --- a/src/repositories/projectRepository.ts +++ b/src/repositories/projectRepository.ts @@ -209,8 +209,6 @@ 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,