diff --git a/.DS_Store b/.DS_Store index 58d2ae234..eff655779 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/src/entities/project.ts b/src/entities/project.ts index f10daa91b..8a1a386c0 100644 --- a/src/entities/project.ts +++ b/src/entities/project.ts @@ -77,10 +77,12 @@ export enum SortingField { InstantBoosting = 'InstantBoosting', ActiveQfRoundRaisedFunds = 'ActiveQfRoundRaisedFunds', EstimatedMatching = 'EstimatedMatching', + BestMatch = 'BestMatch', } export enum FilterField { Verified = 'verified', + IsGivbackEligible = 'isGivbackEligible', AcceptGiv = 'givingBlocksId', AcceptFundOnGnosis = 'acceptFundOnGnosis', AcceptFundOnMainnet = 'acceptFundOnMainnet', diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index 1aed8882f..589cf4318 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -15,6 +15,11 @@ import { User, UserRole } from '../entities/user'; import { countUniqueDonorsAndSumDonationValueUsd, createDonation, + donationsNumberPerDateRange, + donationsTotalAmountPerDateRange, + donationsTotalAmountPerDateRangeByMonth, + donationsTotalNumberPerDateRangeByMonth, + donorsCountPerDateByMonthAndYear, fillQfRoundDonationsUserScores, findDonationById, findDonationsByProjectIdWhichUseDonationBox, @@ -29,6 +34,7 @@ import { QfRound } from '../entities/qfRound'; import { Project } from '../entities/project'; import { refreshProjectEstimatedMatchingView } from '../services/projectViewsService'; import { calculateEstimateMatchingForProjectById } from '../utils/qfUtils'; +import { ORGANIZATION_LABELS } from '../entities/organization'; import { setPowerRound } from './powerRoundRepository'; describe('createDonation test cases', createDonationTestCases); @@ -62,12 +68,481 @@ describe( 'isVerifiedDonationExistsInQfRound() test cases', isVerifiedDonationExistsInQfRoundTestCases, ); +describe( + 'donationsTotalAmountPerDateRange() test cases', + donationsTotalAmountPerDateRangeTestCases, +); describe('findDonationsToGiveth() test cases', findDonationsToGivethTestCases); +describe( + 'donationsTotalAmountPerDateRangeByMonth() test cases', + donationsTotalAmountPerDateRangeByMonthTestCases, +); +describe( + 'donationsTotalNumberPerDateRangeByMonth() test cases', + donationsTotalNumberPerDateRangeByMonthTestCase, +); +describe( + 'donationsNumberPerDateRange() test cases', + donationsNumberPerDateRangeTestCases, +); +describe( + 'donorsCountPerDateByMonthAndYear() test cases', + donorsCountPerDateByMonthAndYearTestCase, +); +describe('donorsCountPerDate() test cases', donorsCountPerDateTestCases); + describe( 'getSumOfGivbackEligibleDonationsForSpecificRound() test cases', getSumOfGivbackEligibleDonationsForSpecificRoundTestCases, ); +function donorsCountPerDateByMonthAndYearTestCase() { + it('should return per month number of donations for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationStart = moment().add(30, 'months'); + const donationStart1month = moment().add(31, 'month'); + const donationStart2month = moment().add(32, 'month'); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 30, + }), + SEED_DATA.FIRST_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 30, + }), + SEED_DATA.THIRD_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 20, + }), + SEED_DATA.THIRD_USER.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 30, + }), + SEED_DATA.FIRST_USER.id, + endaomentProject.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 40, + }), + user.id, + endaomentProject.id, + ); + const expectedReturnForAllProjects: number[] = [2, 2, 2]; + const expectedReturnEndaomentProjects: number[] = [1, 1, 1]; + + const fromDate = donationStart.toISOString(true); + const toDate = donationStart2month.toISOString(true); + const actualReturnEndaomentProjects = + await donorsCountPerDateByMonthAndYear(fromDate, toDate, undefined, true); + const actualReturnAllProjects = await donorsCountPerDateByMonthAndYear( + fromDate, + toDate, + ); + const endaomentProjectsReturns: number[] = actualReturnEndaomentProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + const allProjectsReturns: number[] = actualReturnAllProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + + assert.deepEqual(expectedReturnForAllProjects, allProjectsReturns); + assert.deepEqual(expectedReturnEndaomentProjects, endaomentProjectsReturns); + }); +} + +function donorsCountPerDateTestCases() { + it('should return total donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(221, 'days').toDate(), + valueUsd: 30, + }), + SEED_DATA.FIRST_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(221, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const fromDate = moment().add(220, 'days').format('YYYY/MM/DD'); + const toDate = moment().add(222, 'days').toDate().toDateString(); + const totalDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + ); + const endaomentDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + undefined, + undefined, + true, + ); + assert.equal(totalDonationInTimeFrame, 2); + assert.equal(endaomentDonationInTimeFrame, 1); + }); +} + +function donationsTotalNumberPerDateRangeByMonthTestCase() { + it('should return per month number of donations for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationStart = moment().add(20, 'months'); + const donationStart1month = moment().add(21, 'month'); + const donationStart2month = moment().add(22, 'month'); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 30, + }), + user.id, + endaomentProject.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 40, + }), + user.id, + endaomentProject.id, + ); + const expectedReturnForAllProjects: number[] = [2, 2, 2]; + const expectedReturnEndaomentProjects: number[] = [1, 1, 1]; + + const fromDate = donationStart.toISOString(true); + const toDate = donationStart2month.toISOString(true); + const actualReturnEndaomentProjects = + await donationsTotalNumberPerDateRangeByMonth( + fromDate, + toDate, + undefined, + undefined, + true, + ); + const actualReturnAllProjects = + await donationsTotalNumberPerDateRangeByMonth(fromDate, toDate); + const endaomentProjectsReturns: number[] = actualReturnEndaomentProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + const allProjectsReturns: number[] = actualReturnAllProjects + .filter(donationPerDate => donationPerDate.total !== undefined) + .map(donationPerDate => Number(donationPerDate.total!)); + + assert.deepEqual(expectedReturnForAllProjects, allProjectsReturns); + assert.deepEqual(expectedReturnEndaomentProjects, endaomentProjectsReturns); + }); +} + +function donationsNumberPerDateRangeTestCases() { + it('should return total donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(445, 'days').toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(445, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const fromDate = moment().add(444, 'days').format('YYYY/MM/DD'); + const toDate = moment().add(446, 'days').toDate().toDateString(); + const totalDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + ); + const endaomentDonationInTimeFrame = await donationsNumberPerDateRange( + fromDate, + toDate, + undefined, + undefined, + true, + ); + assert.equal(totalDonationInTimeFrame, 2); + assert.equal(endaomentDonationInTimeFrame, 1); + }); +} + +function donationsTotalAmountPerDateRangeByMonthTestCases() { + it('should return per month donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationStart = moment().add(10, 'months'); + const donationStart1month = moment().add(11, 'month'); + const donationStart2month = moment().add(12, 'month'); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const donationValueToNonEndaomentinUSD1 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + const donationValueToNonEndaomentinUSD2 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 40, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + const donationValueToNonEndaomentinUSD3 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + const donationValueToEndaomentinUSD1 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart.toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const donationValueToEndaomentinUSD2 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart1month.toDate(), + valueUsd: 30, + }), + user.id, + endaomentProject.id, + ); + + const donationValueToEndaomentinUSD3 = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: donationStart2month.toDate(), + valueUsd: 40, + }), + user.id, + endaomentProject.id, + ); + const expectedReturnForAllProjects = [ + donationValueToNonEndaomentinUSD1.valueUsd + + donationValueToEndaomentinUSD1.valueUsd, + donationValueToEndaomentinUSD2.valueUsd + + donationValueToNonEndaomentinUSD2.valueUsd, + donationValueToEndaomentinUSD3.valueUsd + + donationValueToNonEndaomentinUSD3.valueUsd, + ]; + + const expectedReturnEndaomentProjects = [ + donationValueToEndaomentinUSD1.valueUsd, + donationValueToEndaomentinUSD2.valueUsd, + donationValueToEndaomentinUSD3.valueUsd, + ]; + const fromDate = donationStart.toISOString(true); + const toDate = donationStart2month.toISOString(true); + const actualReturnEndaomentProjects = + await donationsTotalAmountPerDateRangeByMonth( + fromDate, + toDate, + undefined, + undefined, + true, + ); + const actualReturnAllProjects = + await donationsTotalAmountPerDateRangeByMonth(fromDate, toDate); + + assert.deepEqual( + expectedReturnEndaomentProjects, + actualReturnEndaomentProjects.map( + donationPerDate => donationPerDate.total, + ), + ); + assert.deepEqual( + expectedReturnForAllProjects, + actualReturnAllProjects.map(donationPerDate => donationPerDate.total), + ); + }); +} + +function donationsTotalAmountPerDateRangeTestCases() { + it('should return total donations amount for endaoment projects', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const donationValueToNonEndaomentinUSD = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(66, 'days').toDate(), + valueUsd: 30, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + const donationValueToEndaomentinUSD = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(66, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + const fromDate = moment().add(65, 'days').format('YYYY/MM/DD'); + const toDate = moment().add(67, 'days').toDate().toDateString(); + const totalDonationInTimeFrame = await donationsTotalAmountPerDateRange( + fromDate, + toDate, + ); + const endaomentDonationInTimeFrame = await donationsTotalAmountPerDateRange( + fromDate, + toDate, + undefined, + undefined, + true, + ); + assert.equal( + totalDonationInTimeFrame, + donationValueToEndaomentinUSD.valueUsd + + donationValueToNonEndaomentinUSD.valueUsd, + ); + assert.equal( + endaomentDonationInTimeFrame, + donationValueToEndaomentinUSD.valueUsd, + ); + }); +} + function fillQfRoundDonationsUserScoresTestCases() { let qfRound: QfRound; let qfRoundProject: Project; diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 731bcebdc..4b649faaf 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -6,6 +6,7 @@ import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; import { logger } from '../utils/logger'; import { QfRound } from '../entities/qfRound'; import { ChainType } from '../types/network'; +import { ORGANIZATION_LABELS } from '../entities/organization'; import { AppDataSource } from '../orm'; import { getPowerRound } from './powerRoundRepository'; @@ -177,6 +178,7 @@ export const donationsTotalAmountPerDateRange = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select(`COALESCE(SUM(donation."valueUsd"), 0)`, 'sum') @@ -200,12 +202,23 @@ export const donationsTotalAmountPerDateRange = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + const donationsUsdAmount = await query.getRawOne(); query.cache( `donationsTotalAmountPerDateRange-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }-${onlyVerified || 'all'}`, + }-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -217,6 +230,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -243,6 +257,17 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -250,7 +275,7 @@ export const donationsTotalAmountPerDateRangeByMonth = async ( query.cache( `donationsTotalAmountPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${networkId || 'all'}-${onlyVerified || 'all'}`, + }-${networkId || 'all'}-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -262,6 +287,7 @@ export const donationsNumberPerDateRange = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select(`COALESCE(COUNT(donation.id), 0)`, 'count') @@ -285,12 +311,23 @@ export const donationsNumberPerDateRange = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + const donationsUsdAmount = await query.getRawOne(); query.cache( `donationsTotalNumberPerDateRange-${fromDate || ''}-${toDate || ''}--${ networkId || 'all' - }-${onlyVerified || 'all'}`, + }-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -302,6 +339,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( toDate?: string, networkId?: number, onlyVerified?: boolean, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -327,6 +365,17 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( .andWhere('project.verified = true'); } + if (onlyEndaoment) { + if (!onlyVerified) { + query.leftJoin('donation.project', 'project'); + } + query + .leftJoin('project.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -334,7 +383,7 @@ export const donationsTotalNumberPerDateRangeByMonth = async ( query.cache( `donationsTotalNumberPerDateRangeByMonth-${fromDate || ''}-${ toDate || '' - }-${networkId || 'all'}-${onlyVerified || 'all'}`, + }-${networkId || 'all'}-${onlyVerified || 'all'}-${onlyEndaoment || 'all'}`, 300000, ); @@ -345,6 +394,7 @@ export const donorsCountPerDate = async ( fromDate?: string, toDate?: string, networkId?: number, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -364,11 +414,18 @@ export const donorsCountPerDate = async ( if (networkId) { query.andWhere(`donation."transactionNetworkId" = ${networkId}`); } + if (onlyEndaoment) { + query.leftJoin('donation.project', 'project'); + query.leftJoin('project.organization', 'organization'); + query.andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } query.cache( `donorsCountPerDate-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }`, + }-${onlyEndaoment || 'all'}`, 300000, ); @@ -413,6 +470,7 @@ export const donorsCountPerDateByMonthAndYear = async ( fromDate?: string, toDate?: string, networkId?: number, + onlyEndaoment?: boolean, ): Promise => { const query = Donation.createQueryBuilder('donation') .select( @@ -432,6 +490,14 @@ export const donorsCountPerDateByMonthAndYear = async ( query.andWhere(`donation."transactionNetworkId" = ${networkId}`); } + if (onlyEndaoment) { + query.leftJoin('donation.project', 'project'); + query.leftJoin('project.organization', 'organization'); + query + .andWhere('organization."label" = :label') + .setParameter('label', ORGANIZATION_LABELS.ENDAOMENT); + } + query.groupBy('year, month'); query.orderBy('year', 'ASC'); query.addOrderBy('month', 'ASC'); @@ -439,7 +505,7 @@ export const donorsCountPerDateByMonthAndYear = async ( query.cache( `donorsCountPerDateByMonthAndYear-${fromDate || ''}-${toDate || ''}-${ networkId || 'all' - }`, + } - ${onlyEndaoment || 'all'}`, 300000, ); 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 diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index df0810600..11b80dace 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -167,6 +167,113 @@ function totalDonationsPerCategoryPerDateTestCases() { ); assert.equal(foodTotal.totalUsd, donationToVerified.valueUsd); }); + + it('should return donation count as per category per time range for endaoment projects', async () => { + const allDonationResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + }); + const allEndaomentDonationResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + onlyEndaoment: true, + }, + }); + assert.isOk(allEndaomentDonationResponse); + assert.isOk(allDonationResponse); + const allEndaomentFoodDonations = + allEndaomentDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const allFoodDonations = + allDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const amountFoodDonation = allFoodDonations.totalUsd; + const amountEndaomentFoodDonation = allEndaomentFoodDonations.totalUsd; + + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const donationValueToEndaomentinUSD = 20; + const donationValueToNonEndaomentinUSD = 30; + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(45, 'days').toDate(), + valueUsd: donationValueToEndaomentinUSD, + }), + user.id, + endaomentProject.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(45, 'days').toDate(), + valueUsd: donationValueToNonEndaomentinUSD, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + const afterUpdateAllDonationResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + }); + const afterUpdateAllEndaomentDonationResponse = await axios.post( + graphqlUrl, + { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + onlyEndaoment: true, + }, + }, + ); + assert.isOk(afterUpdateAllDonationResponse); + assert.isOk(afterUpdateAllEndaomentDonationResponse); + const allEndaomentFoodDonationsAfterUpdate = + afterUpdateAllEndaomentDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const allFoodDonationsAfterUpdate = + afterUpdateAllDonationResponse.data.data.totalDonationsPerCategory.find( + donation => donation.title === 'food', + ); + const amountFoodDonationAfterUpdate = allFoodDonationsAfterUpdate.totalUsd; + const amountEndaomentFoodDonationAfterUpdate = + allEndaomentFoodDonationsAfterUpdate.totalUsd; + + assert.equal( + amountFoodDonation + + donationValueToNonEndaomentinUSD + + donationValueToEndaomentinUSD, + amountFoodDonationAfterUpdate, + ); + assert.equal( + amountEndaomentFoodDonation + donationValueToEndaomentinUSD, + amountEndaomentFoodDonationAfterUpdate, + ); + + const totalDonationsToEndaomentInTimeFrame = await axios.post(graphqlUrl, { + query: fetchTotalDonationsPerCategoryPerDate, + variables: { + fromDate: moment().add(44, 'days').toDate(), + toDate: moment().add(46, 'days').toDate(), + onlyEndaoment: true, + }, + }); + + const foodTotal = + totalDonationsToEndaomentInTimeFrame.data.data.totalDonationsPerCategory.find( + d => d.title === 'food', + ); + + assert.equal(foodTotal.totalUsd, donationValueToEndaomentinUSD); + }); } function totalDonationsNumberPerDateTestCases() { @@ -220,6 +327,108 @@ function totalDonationsNumberPerDateTestCases() { 1, ); }); + it('should return donations count for endaoment projects per time range', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(202, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(202, 'days').toDate(), + valueUsd: 20, + }), + SEED_DATA.FIRST_USER.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(202, 'days').toDate(), + valueUsd: 30, + }), + SEED_DATA.THIRD_USER.id, + SEED_DATA.NON_VERIFIED_PROJECT.id, + ); + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment() + .add(201, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(203, 'days').toDate().toISOString().split('T')[0], + }, + }); + const donationsResponseToVerifiedandEndaoment = await axios.post( + graphqlUrl, + { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment() + .add(201, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment() + .add(203, 'days') + .toDate() + .toISOString() + .split('T')[0], + onlyVerified: true, + onlyEndaoment: true, + }, + }, + ); + + const donationsResponseToVerified = await axios.post(graphqlUrl, { + query: fetchTotalDonationsNumberPerDateRange, + variables: { + fromDate: moment() + .add(201, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(203, 'days').toDate().toISOString().split('T')[0], + onlyVerified: true, + }, + }); + + assert.isNumber( + donationsResponse.data.data.totalDonationsNumberPerDate.total, + ); + assert.isTrue( + donationsResponse.data.data.totalDonationsNumberPerDate + .totalPerMonthAndYear.length > 0, + ); + assert.equal( + donationsResponse.data.data.totalDonationsNumberPerDate.total, + 3, + ); + assert.equal( + donationsResponseToVerified.data.data.totalDonationsNumberPerDate.total, + 2, + ); + assert.equal( + donationsResponseToVerifiedandEndaoment.data.data + .totalDonationsNumberPerDate.total, + 1, + ); + }); } function donorsCountPerDateTestCases() { @@ -307,6 +516,127 @@ function donorsCountPerDateTestCases() { total, ); }); + it('should return donors unique total count for endaoment projects in a time range', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const walletAddress = generateRandomEtheriumAddress(); + const user = await saveUserDirectlyToDb(walletAddress); + // should count as 1 as it's the same user + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + }), + SEED_DATA.FIRST_USER.id, + endaomentProject.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + }), + user.id, + endaomentProject.id, + ); + // anonymous donations count as separate + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + undefined, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(510, 'days').toDate(), + valueUsd: 20, + anonymous: true, + }), + undefined, + SEED_DATA.SECOND_PROJECT.id, + ); + + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonors, + variables: { + fromDate: moment() + .add(509, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(511, 'days').toDate().toISOString().split('T')[0], + }, + }); + const donationsResponseForEndaoment = await axios.post(graphqlUrl, { + query: fetchTotalDonors, + variables: { + fromDate: moment() + .add(509, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(511, 'days').toDate().toISOString().split('T')[0], + onlyEndaoment: true, + }, + }); + assert.isOk(donationsResponse); + assert.isOk(donationsResponseForEndaoment); + // 2 unique donor and 2 anonymous + assert.equal(donationsResponse.data.data.totalDonorsCountPerDate.total, 4); + const total = + donationsResponse.data.data.totalDonorsCountPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + const totalForEndaoment = + donationsResponseForEndaoment.data.data.totalDonorsCountPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + assert.equal( + donationsResponse.data.data.totalDonorsCountPerDate.total, + total, + ); + // 2 donors : User Created and First User + assert.equal( + donationsResponseForEndaoment.data.data.totalDonorsCountPerDate.total, + 2, + ); + assert.equal( + donationsResponseForEndaoment.data.data.totalDonorsCountPerDate.total, + totalForEndaoment, + ); + }); } function newDonorsCountAndTotalDonationPerDateTestCases() { @@ -626,6 +956,79 @@ function donationsUsdAmountTestCases() { donationToVerified.valueUsd, ); }); + + it('should return total usd amount for donations to endaoment project made in a time range', async () => { + const endaomentProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + organizationLabel: ORGANIZATION_LABELS.ENDAOMENT, + }); + const user = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + const donationToNonEndaoment = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(250, 'days').toDate(), + valueUsd: 20, + }), + user.id, + SEED_DATA.SECOND_PROJECT.id, + ); + const donationToEndaoment = await saveDonationDirectlyToDb( + createDonationData({ + status: DONATION_STATUS.VERIFIED, + createdAt: moment().add(250, 'days').toDate(), + valueUsd: 10, + }), + user.id, + endaomentProject.id, + ); + + const donationsResponse = await axios.post(graphqlUrl, { + query: fetchTotalDonationsUsdAmount, + variables: { + fromDate: moment() + .add(249, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(251, 'days').toDate().toISOString().split('T')[0], + }, + }); + + const donationsResponseToEndaoment = await axios.post(graphqlUrl, { + query: fetchTotalDonationsUsdAmount, + variables: { + fromDate: moment() + .add(249, 'days') + .toDate() + .toISOString() + .split('T')[0], + toDate: moment().add(251, 'days').toDate().toISOString().split('T')[0], + onlyEndaoment: true, + }, + }); + + assert.isOk(donationsResponse.data.data); + assert.isOk(donationsResponseToEndaoment.data.data); + assert.equal( + donationsResponse.data.data.donationsTotalUsdPerDate.total, + donationToNonEndaoment.valueUsd + donationToEndaoment.valueUsd, + ); + const total = + donationsResponse.data.data.donationsTotalUsdPerDate.totalPerMonthAndYear.reduce( + (sum, value) => sum + value.total, + 0, + ); + assert.equal( + donationsResponse.data.data.donationsTotalUsdPerDate.total, + total, + ); + assert.equal( + donationsResponseToEndaoment.data.data.donationsTotalUsdPerDate.total, + donationToEndaoment.valueUsd, + ); + }); } function donationsTestCases() { diff --git a/src/resolvers/donationResolver.ts b/src/resolvers/donationResolver.ts index 211fa7bf7..2e1b4f8fb 100644 --- a/src/resolvers/donationResolver.ts +++ b/src/resolvers/donationResolver.ts @@ -72,6 +72,7 @@ import { DraftDonation, } from '../entities/draftDonation'; import { nonZeroRecurringDonationsByProjectId } from '../repositories/recurringDonationRepository'; +import { ORGANIZATION_LABELS } from '../entities/organization'; import { getTokenPrice } from '../services/priceService'; import { findTokenByNetworkAndSymbol } from '../utils/tokenUtils'; @@ -315,6 +316,7 @@ export class DonationResolver { @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( @@ -355,6 +357,13 @@ export class DonationResolver { query.andWhere('projects.verified = true'); } + if (onlyEndaoment) { + query + .leftJoin('projects.organization', 'organization') + .andWhere('organization."label" = :label', { + label: ORGANIZATION_LABELS.ENDAOMENT, + }); + } return await query.getRawMany(); } catch (e) { logger.error('totalDonationsPerCategory query error', e); @@ -369,6 +378,7 @@ export class DonationResolver { @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( @@ -380,6 +390,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); const totalPerMonthAndYear = await donationsTotalAmountPerDateRangeByMonth( @@ -387,6 +398,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); return { @@ -406,6 +418,7 @@ export class DonationResolver { @Arg('toDate', { nullable: true }) toDate?: string, @Arg('networkId', { nullable: true }) networkId?: number, @Arg('onlyVerified', { nullable: true }) onlyVerified?: boolean, + @Arg('onlyEndaoment', { nullable: true }) onlyEndaoment?: boolean, ): Promise { try { validateWithJoiSchema( @@ -417,6 +430,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); const totalPerMonthAndYear = await donationsTotalNumberPerDateRangeByMonth( @@ -424,6 +438,7 @@ export class DonationResolver { toDate, networkId, onlyVerified, + onlyEndaoment, ); return { @@ -495,17 +510,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, diff --git a/src/resolvers/projectResolver.allProject.test.ts b/src/resolvers/projectResolver.allProject.test.ts index e7235cd42..533e61209 100644 --- a/src/resolvers/projectResolver.allProject.test.ts +++ b/src/resolvers/projectResolver.allProject.test.ts @@ -220,9 +220,10 @@ function allProjectsTestCases() { ); assert.isTrue(firstProjectIsOlder); }); - it('should return projects, filter by verified, true', async () => { + + it('should return projects, filter by verified, true #1', async () => { // There is two verified projects so I just need to create a project with verified: false and listed:true - await saveProjectDirectlyToDb({ + const unverifiedProject = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), slug: String(new Date().getTime()), @@ -239,7 +240,97 @@ function allProjectsTestCases() { result.data.data.allProjects.projects.forEach(project => assert.isTrue(project.verified), ); + + // should not include unverified project in the response + assert.notExists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === unverifiedProject.id, + ), + ); + }); + it('should return projects, filter by verified, true #2', async () => { + const verified = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + verified: true, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['Verified'], + sortingBy: SortingField.Newest, + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.verified), + ); + + // should not include unverified project in the response + assert.exists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === verified.id, + ), + ); }); + + it('should return projects, filter by isGivbackEligible, true #1', async () => { + // There is two isGivbackEligible projects so I just need to create a project with isGivbackEligible: false and listed:true + const notGivbackEligibleProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + isGivbackEligible: false, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['IsGivbackEligible'], + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.isGivbackEligible), + ); + + // should not include unisGivbackEligible project in the response + assert.notExists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === notGivbackEligibleProject.id, + ), + ); + }); + it('should return projects, filter by isGivbackEligible, true #2', async () => { + const givbackEligibleProject = await saveProjectDirectlyToDb({ + ...createProjectData(), + title: String(new Date().getTime()), + slug: String(new Date().getTime()), + isGivbackEligible: true, + qualityScore: 0, + }); + const result = await axios.post(graphqlUrl, { + query: fetchMultiFilterAllProjectsQuery, + variables: { + filters: ['IsGivbackEligible'], + sortingBy: SortingField.Newest, + }, + }); + assert.isNotEmpty(result.data.data.allProjects.projects); + result.data.data.allProjects.projects.forEach(project => + assert.isTrue(project.isGivbackEligible), + ); + + // should not include unisGivbackEligible project in the response + assert.exists( + result.data.data.allProjects.projects.find( + project => Number(project.id) === givbackEligibleProject.id, + ), + ); + }); + it('should return projects, filter by acceptGiv, true', async () => { await saveProjectDirectlyToDb({ ...createProjectData(), diff --git a/src/resolvers/projectResolver.ts b/src/resolvers/projectResolver.ts index bafb581a1..74915f8ac 100644 --- a/src/resolvers/projectResolver.ts +++ b/src/resolvers/projectResolver.ts @@ -328,19 +328,22 @@ export class ProjectResolver { // .addSelect('similarity(project.description, :searchTerm)', 'desc_slm') // .addSelect('similarity(project.impactLocation, :searchTerm)', 'loc_slm') // .setParameter('searchTerm', searchTerm) + .addSelect( + `(CASE + WHEN project.title %> :searchTerm THEN 1 + ELSE 2 + END)`, + 'title_priority', + ) .andWhere( new Brackets(qb => { - qb.where('project.title %> :searchTerm ', { - searchTerm, - }) - .orWhere('project.description %> :searchTerm ', { - searchTerm, - }) - .orWhere('project.impactLocation %> :searchTerm', { - searchTerm, - }); + qb.where('project.title %> :searchTerm', { searchTerm }) + .orWhere('project.description %> :searchTerm', { searchTerm }) + .orWhere('project.impactLocation %> :searchTerm', { searchTerm }); }), ) + .orderBy('title_priority', 'ASC') + .setParameter('searchTerm', searchTerm) ); } diff --git a/src/server/adminJs/tabs/components/ProjectCategories.tsx b/src/server/adminJs/tabs/components/ProjectCategories.tsx new file mode 100644 index 000000000..5706131cb --- /dev/null +++ b/src/server/adminJs/tabs/components/ProjectCategories.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { withTheme } from 'styled-components'; +import { Section, Label } from '@adminjs/design-system'; + +const ProjectUpdates = props => { + const categories = props?.record?.params?.categories; + return ( +
+ +
+ {categories?.map(category => { + return ( +
+
+
+ +

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

+
+
+ ); + })} +
+
+
+ ); +}; + +export default withTheme(ProjectUpdates); diff --git a/src/server/adminJs/tabs/projectsTab.ts b/src/server/adminJs/tabs/projectsTab.ts index d0cf6fbe2..a61aedfa6 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 = ( @@ -446,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, @@ -486,6 +494,14 @@ export const fillSocialProfileAndQfRounds: After< const projectUpdates = await findProjectUpdatesByProjectId(projectId); const project = await findProjectById(projectId); const adminJsBaseUrl = process.env.SERVER_URL; + let categories; + if (project) { + categories = await Category.createQueryBuilder('category') + .innerJoin('category.projects', 'projects') + .where('projects.id = :id', { id: project.id }) + .orderBy('category.name', 'ASC') + .getMany(); + } response.record = { ...record, params: { @@ -499,6 +515,11 @@ export const fillSocialProfileAndQfRounds: After< adminJsBaseUrl, }, }; + + if (categories) { + response.record.params.categoryIds = categories; + response.record.params.categories = categories; + } return response; }; @@ -660,7 +681,7 @@ export const projectsTab = { id: { isVisible: { list: false, - filter: false, + filter: true, show: true, edit: false, }, @@ -831,12 +852,36 @@ export const projectsTab = { edit: false, }, }, + categoryIds: { + type: 'reference', + isArray: true, + reference: 'Category', + isVisible: { + list: false, + filter: false, + show: true, + edit: true, + }, + components: { + show: adminJs.bundle('./components/ProjectCategories'), + }, + availableValues: async _record => { + const categories = await Category.createQueryBuilder('category') + .where('category.isActive = :isActive', { isActive: true }) + .orderBy('category.name', 'ASC') + .getMany(); + return categories.map(category => ({ + value: category.id, + label: `${category.id} - ${category.name}`, + })); + }, + }, isImported: { isVisible: { list: false, filter: true, show: true, - edit: false, + edit: true, }, }, totalReactions: { @@ -924,6 +969,24 @@ export const projectsTab = { isVisible: false, isAccessible: ({ currentAdmin }) => canAccessProjectAction({ currentAdmin }, ResourceActions.NEW), + before: async request => { + if (request.payload.categories) { + request.payload.categories = ( + request.payload.categories as string[] + ).map(id => ({ id: parseInt(id, 10) })); + } + return request; + }, + after: async response => { + const { request } = response; + const project = await Project.findOne({ + where: { id: request?.record?.id }, + }); + const categoryIds = extractCategoryIds(request.record.params); + await saveCategories(project!, categoryIds || []); + + return response; + }, }, bulkDelete: { isVisible: false, @@ -952,6 +1015,16 @@ export const projectsTab = { } const project = await findProjectById(Number(request.payload.id)); + if (project) { + await Category.query( + ` + DELETE FROM project_categories_category + WHERE "projectId" = $1 + `, + [project.id], + ); + } + if ( project && Number(request?.payload?.statusId) !== project?.status?.id @@ -1014,6 +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 ( @@ -1151,10 +1225,13 @@ export const projectsTab = { }); } } + const categoryIds = extractCategoryIds(request.record.params); + await Promise.all([ refreshUserProjectPowerView(), refreshProjectFuturePowerView(), refreshProjectPowerView(), + saveCategories(project!, categoryIds || []), ]); return request; }, @@ -1350,3 +1427,23 @@ export const projectsTab = { }, }, }; + +async function saveCategories(project: Project, categoryIds?: string[]) { + if (!project) return; + if (!categoryIds || categoryIds?.length === 0) return; + + await Category.query( + ` + DELETE FROM project_categories_category + WHERE "projectId" = $1 + `, + [project.id], + ); + + const categories = await Category.createQueryBuilder('category') + .where('category.id IN (:...ids)', { ids: categoryIds }) + .getMany(); + + project.categories = categories; + await project.save(); +} diff --git a/src/server/adminJs/tabs/qfRoundTab.ts b/src/server/adminJs/tabs/qfRoundTab.ts index 5555f9cb1..2e7197d28 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, diff --git a/test/graphqlQueries.ts b/test/graphqlQueries.ts index 3977856f0..4f6dd6ca3 100644 --- a/test/graphqlQueries.ts +++ b/test/graphqlQueries.ts @@ -570,12 +570,14 @@ export const fetchTotalDonationsPerCategoryPerDate = ` $toDate: String $networkId: Float $onlyVerified: Boolean + $onlyEndaoment: Boolean ) { totalDonationsPerCategory( fromDate: $fromDate toDate: $toDate networkId: $networkId onlyVerified: $onlyVerified + onlyEndaoment: $onlyEndaoment ) { id title @@ -611,11 +613,13 @@ export const fetchTotalDonors = ` $fromDate: String $toDate: String $networkId: Float + $onlyEndaoment: Boolean ) { totalDonorsCountPerDate( fromDate: $fromDate toDate: $toDate networkId: $networkId + onlyEndaoment: $onlyEndaoment ) { total totalPerMonthAndYear { @@ -632,12 +636,14 @@ export const fetchTotalDonationsUsdAmount = ` $toDate: String $networkId: Float $onlyVerified: Boolean + $onlyEndaoment: Boolean ) { donationsTotalUsdPerDate ( fromDate: $fromDate toDate: $toDate networkId: $networkId onlyVerified: $onlyVerified + onlyEndaoment: $onlyEndaoment ) { total totalPerMonthAndYear { @@ -650,24 +656,26 @@ export const fetchTotalDonationsUsdAmount = ` export const fetchTotalDonationsNumberPerDateRange = ` query ( - $fromDate: String - $toDate: String - $networkId: Float - $onlyVerified: Boolean + $fromDate: String + $toDate: String + $networkId: Float + $onlyVerified: Boolean + $onlyEndaoment: Boolean +) { + totalDonationsNumberPerDate ( + fromDate: $fromDate + toDate: $toDate + networkId: $networkId + onlyVerified: $onlyVerified + onlyEndaoment: $onlyEndaoment ) { - totalDonationsNumberPerDate ( - fromDate: $fromDate - toDate: $toDate - networkId: $networkId - onlyVerified: $onlyVerified - ) { + total + totalPerMonthAndYear { total - totalPerMonthAndYear { - total - date - } + date } } +} `; export const fetchNewDonorsCount = `