diff --git a/migration/1719887968639-addUseDonationBoxToDraftDonations.ts b/migration/1719887968639-addUseDonationBoxToDraftDonations.ts index c25570b74..e2235a8a5 100644 --- a/migration/1719887968639-addUseDonationBoxToDraftDonations.ts +++ b/migration/1719887968639-addUseDonationBoxToDraftDonations.ts @@ -4,34 +4,24 @@ export class AddUseDonationBoxToDraftDonations1719887968639 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - const table = await queryRunner.getTable('draft_donation'); - const useDonationBoxColumn = table?.findColumnByName('useDonationBox'); - const relevantDonationTxHashColumn = table?.findColumnByName( - 'relevantDonationTxHash', + await queryRunner.addColumn( + 'draft_donation', + new TableColumn({ + name: 'useDonationBox', + type: 'boolean', + isNullable: true, + default: false, + }), ); - if (!useDonationBoxColumn) { - await queryRunner.addColumn( - 'draft_donation', - new TableColumn({ - name: 'useDonationBox', - type: 'boolean', - isNullable: true, - default: false, - }), - ); - } - - if (!relevantDonationTxHashColumn) { - await queryRunner.addColumn( - 'draft_donation', - new TableColumn({ - name: 'relevantDonationTxHash', - type: 'varchar', - isNullable: true, - }), - ); - } + await queryRunner.addColumn( + 'draft_donation', + new TableColumn({ + name: 'relevantDonationTxHash', + type: 'varchar', + isNullable: true, + }), + ); } public async down(queryRunner: QueryRunner): Promise { diff --git a/src/repositories/donationRepository.test.ts b/src/repositories/donationRepository.test.ts index 0b864e52b..b3576617e 100644 --- a/src/repositories/donationRepository.test.ts +++ b/src/repositories/donationRepository.test.ts @@ -14,21 +14,29 @@ import { } from '../../test/testUtils'; import { User, UserRole } from '../entities/user'; import { - countUniqueDonorsAndSumDonationValueUsd, + countUniqueDonors, + countUniqueDonorsForRound, createDonation, fillQfRoundDonationsUserScores, findDonationById, findDonationsByTransactionId, + findStableCoinDonationsWithoutPrice, getPendingDonationsIds, isVerifiedDonationExistsInQfRound, - getProjectQfRoundStats, findRelevantDonations, + sumDonationValueUsd, + sumDonationValueUsdForQfRound, } from './donationRepository'; +import { updateOldStableCoinDonationsPrice } from '../services/donationService'; import { Donation, DONATION_STATUS } from '../entities/donation'; import { QfRound } from '../entities/qfRound'; import { Project } from '../entities/project'; -import { refreshProjectEstimatedMatchingView } from '../services/projectViewsService'; +import { + refreshProjectDonationSummaryView, + refreshProjectEstimatedMatchingView, +} from '../services/projectViewsService'; import { calculateEstimateMatchingForProjectById } from '../utils/qfUtils'; +import { NETWORK_IDS } from '../provider'; describe('createDonation test cases', createDonationTestCases); @@ -40,7 +48,10 @@ describe( 'getPendingDonationsIds() test cases', getPendingDonationsIdsTestCases, ); - +describe( + 'findStableCoinDonationsWithoutPrice() test cases', + findStableCoinDonationsWithoutPriceTestCases, +); describe('findDonationById() test cases', findDonationByIdTestCases); describe( 'countUniqueDonorsForActiveQfRound() test cases', @@ -194,6 +205,7 @@ function estimatedMatchingTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); const firstProjectMatch = await calculateEstimateMatchingForProjectById({ projectId: firstProject.id, @@ -253,6 +265,7 @@ function estimatedMatchingTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); const firstProjectMatch = await calculateEstimateMatchingForProjectById({ projectId: firstProject.id, @@ -366,6 +379,93 @@ function findDonationByIdTestCases() { }); } +function findStableCoinDonationsWithoutPriceTestCases() { + it('should just return stable coin donations without price', async () => { + const project = await saveProjectDirectlyToDb(createProjectData()); + const donor = await saveUserDirectlyToDb(generateRandomEtheriumAddress()); + + const donationData1 = { ...createDonationData(), currency: 'USDC' }; + delete donationData1.valueUsd; + + const donationData2 = { ...createDonationData(), currency: 'USDC' }; + + const donationData3 = { ...createDonationData(), currency: 'USDT' }; + delete donationData3.valueUsd; + + const donationData4 = { ...createDonationData(), currency: 'USDT' }; + donationData4.currency = 'USDT'; + + const donationData5 = { + ...createDonationData(), + currency: 'WXDAI', + transactionNetworkId: NETWORK_IDS.XDAI, + }; + delete donationData5.valueUsd; + + const donationData6 = { + ...createDonationData(), + currency: 'WXDAI', + transactionNetworkId: NETWORK_IDS.XDAI, + }; + + const donationData7 = { + ...createDonationData(), + currency: 'WXDAI', + transactionNetworkId: NETWORK_IDS.XDAI, + }; + delete donationData7.valueUsd; + + const donationData8 = { + ...createDonationData(), + currency: 'WXDAI', + transactionNetworkId: NETWORK_IDS.XDAI, + }; + + const donationData9 = createDonationData(); + delete donationData9.valueUsd; + + await saveDonationDirectlyToDb(donationData1, donor.id, project.id); + await saveDonationDirectlyToDb(donationData2, donor.id, project.id); + await saveDonationDirectlyToDb(donationData3, donor.id, project.id); + await saveDonationDirectlyToDb(donationData4, donor.id, project.id); + await saveDonationDirectlyToDb(donationData5, donor.id, project.id); + await saveDonationDirectlyToDb(donationData6, donor.id, project.id); + await saveDonationDirectlyToDb(donationData7, donor.id, project.id); + await saveDonationDirectlyToDb(donationData8, donor.id, project.id); + await saveDonationDirectlyToDb(donationData9, donor.id, project.id); + + const donations = await findStableCoinDonationsWithoutPrice(); + assert.equal(donations.length, 4); + assert.isOk( + donations.find( + donation => donation.transactionId === donationData1.transactionId, + ), + ); + assert.isOk( + donations.find( + donation => donation.transactionId === donationData3.transactionId, + ), + ); + assert.isOk( + donations.find( + donation => donation.transactionId === donationData5.transactionId, + ), + ); + assert.isOk( + donations.find( + donation => donation.transactionId === donationData7.transactionId, + ), + ); + + await updateOldStableCoinDonationsPrice(); + + // Shoud fill valuUsd of all stable coin donations + const stableDonationsWithoutPrice = + await findStableCoinDonationsWithoutPrice(); + assert.isEmpty(stableDonationsWithoutPrice); + }); +} + function createDonationTestCases() { it('should create donation ', async () => { const email = `${new Date().getTime()}@giveth.io`; @@ -476,12 +576,12 @@ function countUniqueDonorsForActiveQfRoundTestCases() { }).save(); project.qfRounds = [qfRound]; await project.save(); - const donorCount = await getProjectQfRoundStats({ + const donorCount = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(donorCount.uniqueDonorsCount, 0); + assert.equal(donorCount, 0); qfRound.isActive = false; await qfRound.save(); @@ -528,12 +628,12 @@ function countUniqueDonorsForActiveQfRoundTestCases() { project.id, ); - const donorCount = await getProjectQfRoundStats({ + const donorCount = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(donorCount.uniqueDonorsCount, 0); + assert.equal(donorCount, 0); qfRound.isActive = false; await qfRound.save(); @@ -570,12 +670,15 @@ function countUniqueDonorsForActiveQfRoundTestCases() { project.id, ); - const donorCount = await getProjectQfRoundStats({ + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + + const donorCount = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(donorCount.uniqueDonorsCount, 0); + assert.equal(donorCount, 0); qfRound.isActive = false; await qfRound.save(); @@ -613,13 +716,14 @@ function countUniqueDonorsForActiveQfRoundTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const donorCount = await getProjectQfRoundStats({ + const donorCount = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(donorCount.uniqueDonorsCount, 1); + assert.equal(donorCount, 1); qfRound.isActive = false; await qfRound.save(); @@ -679,13 +783,14 @@ function countUniqueDonorsForActiveQfRoundTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const donorCount = await getProjectQfRoundStats({ + const donorCount = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(donorCount.uniqueDonorsCount, 2); + assert.equal(donorCount, 2); qfRound.isActive = false; await qfRound.save(); @@ -699,10 +804,10 @@ function countUniqueDonorsTestCases() { title: String(new Date().getTime()), slug: String(new Date().getTime()), }); - const { uniqueDonors } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); - assert.equal(uniqueDonors, 0); + + const donorCount = await countUniqueDonors(project.id); + + assert.equal(donorCount, 0); }); it('should not count unverified donations', async () => { @@ -723,11 +828,9 @@ function countUniqueDonorsTestCases() { project.id, ); - const { uniqueDonors } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); + const donorCount = await countUniqueDonors(project.id); - assert.equal(uniqueDonors, 0); + assert.equal(donorCount, 0); }); it('should return correctly when there is one donation', async () => { const project = await saveProjectDirectlyToDb({ @@ -749,11 +852,10 @@ function countUniqueDonorsTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { uniqueDonors } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); - assert.equal(uniqueDonors, 1); + const donorCount = await countUniqueDonors(project.id); + assert.equal(donorCount, 1); }); it('should return correctly when there is some donations', async () => { // 3 donations with 2 different donor @@ -795,12 +897,11 @@ function countUniqueDonorsTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { uniqueDonors } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); + const donorCount = await countUniqueDonors(project.id); - assert.equal(uniqueDonors, 2); + assert.equal(donorCount, 2); }); } @@ -823,12 +924,12 @@ function sumDonationValueUsdForActiveQfRoundTestCases() { }).save(); project.qfRounds = [qfRound]; await project.save(); - const { sumValueUsd } = await getProjectQfRoundStats({ + const donationSum = await sumDonationValueUsdForQfRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(sumValueUsd, 0); + assert.equal(donationSum, 0); qfRound.isActive = false; await qfRound.save(); @@ -880,12 +981,15 @@ function sumDonationValueUsdForActiveQfRoundTestCases() { project.id, ); - const donationSum = await getProjectQfRoundStats({ + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + + const donationSum = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(donationSum.uniqueDonorsCount, 0); + assert.equal(donationSum, 0); qfRound.isActive = false; await qfRound.save(); @@ -925,18 +1029,19 @@ function sumDonationValueUsdForActiveQfRoundTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { uniqueDonorsCount } = await getProjectQfRoundStats({ + const qfRoundUniqueDonorsCount = await countUniqueDonorsForRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(uniqueDonorsCount, 0); + assert.equal(qfRoundUniqueDonorsCount, 0); qfRound.isActive = false; await qfRound.save(); }); - it('should not count donations usd values when donors have less than minimum passport score', async () => { + it('should not count donations usd values when donors have less than minimum passport score', async () => { const project = await saveProjectDirectlyToDb({ ...createProjectData(), title: String(new Date().getTime()), @@ -968,18 +1073,18 @@ function sumDonationValueUsdForActiveQfRoundTestCases() { donor.id, project.id, ); + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { sumValueUsd } = await getProjectQfRoundStats({ + const donationQfRoundSum = await sumDonationValueUsdForQfRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(sumValueUsd, 0); + assert.equal(donationQfRoundSum, 0); - const { totalDonations } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); - assert.equal(totalDonations, valueUsd); + const donationSum = await sumDonationValueUsd(project.id); + assert.equal(donationSum, valueUsd); qfRound.isActive = false; await qfRound.save(); @@ -1019,12 +1124,13 @@ function sumDonationValueUsdForActiveQfRoundTestCases() { ); await refreshProjectEstimatedMatchingView(); - const { sumValueUsd } = await getProjectQfRoundStats({ + await refreshProjectDonationSummaryView(); + const donationSum = await sumDonationValueUsdForQfRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(sumValueUsd, valueUsd); + assert.equal(donationSum, valueUsd); qfRound.isActive = false; await qfRound.save(); @@ -1091,13 +1197,14 @@ function sumDonationValueUsdForActiveQfRoundTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { sumValueUsd } = await getProjectQfRoundStats({ + const donationSum = await sumDonationValueUsdForQfRound({ projectId: project.id, - qfRound, + qfRoundId: qfRound.id, }); - assert.equal(sumValueUsd, valueUsd1 + valueUsd2 + valueUsd3); + assert.equal(donationSum, valueUsd1 + valueUsd2 + valueUsd3); qfRound.isActive = false; await qfRound.save(); @@ -1112,10 +1219,8 @@ function sumDonationValueUsdTestCases() { slug: String(new Date().getTime()), }); - const { totalDonations } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); - assert.equal(totalDonations, 0); + const donationSum = await sumDonationValueUsd(project.id); + assert.equal(donationSum, 0); }); it('should not count unverified donations', async () => { @@ -1153,12 +1258,11 @@ function sumDonationValueUsdTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { totalDonations } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); + const donationSum = await sumDonationValueUsd(project.id); - assert.equal(totalDonations, valueUsd2); + assert.equal(donationSum, valueUsd2); }); it('should return correctly when there is some verified donation', async () => { @@ -1209,12 +1313,11 @@ function sumDonationValueUsdTestCases() { ); await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); - const { totalDonations } = await countUniqueDonorsAndSumDonationValueUsd( - project.id, - ); + const donationSum = await sumDonationValueUsd(project.id); - assert.equal(totalDonations, valueUsd1 + valueUsd2 + valueUsd3); + assert.equal(donationSum, valueUsd1 + valueUsd2 + valueUsd3); }); } diff --git a/src/repositories/donationRepository.ts b/src/repositories/donationRepository.ts index 0c6190c75..2bf1d68fc 100644 --- a/src/repositories/donationRepository.ts +++ b/src/repositories/donationRepository.ts @@ -3,8 +3,10 @@ import moment from 'moment'; import { Project } from '../entities/project'; import { Donation, DONATION_STATUS } from '../entities/donation'; import { ResourcesTotalPerMonthAndYear } from '../resolvers/donationResolver'; +import { getProjectDonationsSqrtRootSum } from './qfRoundRepository'; import { logger } from '../utils/logger'; -import { QfRound } from '../entities/qfRound'; +import { ProjectDonationSummaryView } from '../views/projectDonationSummaryView'; +import { ProjectEstimatedMatchingView } from '../entities/ProjectEstimatedMatchingView'; export const fillQfRoundDonationsUserScores = async (): Promise => { await Donation.query(` @@ -162,14 +164,14 @@ export const donationsTotalAmountPerDateRange = async ( .andWhere('project.verified = true'); } - const donationsUsdAmount = await query - .cache( - `donationsTotalAmountPerDateRange-${fromDate || ''}-${toDate || ''}-${ - networkId || 'all' - }-${onlyVerified || 'all'}`, - 300000, - ) - .getRawOne(); + const donationsUsdAmount = await query.getRawOne(); + + query.cache( + `donationsTotalAmountPerDateRange-${fromDate || ''}-${toDate || ''}-${ + networkId || 'all' + }-${onlyVerified || 'all'}`, + 300000, + ); return donationsUsdAmount.sum; }; @@ -247,14 +249,14 @@ export const donationsNumberPerDateRange = async ( .andWhere('project.verified = true'); } - const donationsUsdAmount = await query - .cache( - `donationsTotalNumberPerDateRange-${fromDate || ''}-${toDate || ''}--${ - networkId || 'all' - }-${onlyVerified || 'all'}`, - 300000, - ) - .getRawOne(); + const donationsUsdAmount = await query.getRawOne(); + + query.cache( + `donationsTotalNumberPerDateRange-${fromDate || ''}-${toDate || ''}--${ + networkId || 'all' + }-${onlyVerified || 'all'}`, + 300000, + ); return donationsUsdAmount.count; }; @@ -417,8 +419,6 @@ export const findStableCoinDonationsWithoutPrice = async (): Promise< 'token', 'donation.currency = token.symbol AND donation.transactionNetworkId = token.networkId', ) - .leftJoin('donation.project', 'project') - .select('project.adminUserId') .where('token.isStableCoin = true') .andWhere('donation.valueUsd IS NULL') .getMany(); @@ -468,43 +468,67 @@ export const getPendingDonationsIds = (): Promise<{ id: number }[]> => { }); }; -export async function getProjectQfRoundStats(params: { +export async function countUniqueDonorsForRound(params: { + projectId: number; + qfRoundId: number; +}): Promise { + const { projectId, qfRoundId } = params; + return (await getProjectDonationsSqrtRootSum(projectId, qfRoundId)) + .uniqueDonorsCount; +} + +export async function sumDonationValueUsdForQfRound(params: { projectId: number; - qfRound: QfRound; -}): Promise<{ uniqueDonorsCount: number; sumValueUsd: number }> { - const { projectId, qfRound } = params; - const { id: qfRoundId, beginDate, endDate } = qfRound; - const result = await Donation.createQueryBuilder('donation') - .select('COUNT(DISTINCT donation.userId)', 'uniqueDonors') - .addSelect('COALESCE(SUM(donation.valueUsd), 0)', 'totalDonationValueUsd') - .where('donation.qfRoundId = :qfRoundId', { qfRoundId }) - .andWhere('donation.projectId = :projectId', { projectId }) - .andWhere('donation.status = :status', { status: 'verified' }) - .andWhere('donation.createdAt BETWEEN :beginDate AND :endDate', { - beginDate, - endDate, + qfRoundId: number; +}): Promise { + const { projectId, qfRoundId } = params; + const result = await ProjectEstimatedMatchingView.createQueryBuilder( + 'projectEstimatedMatchingView', + ) + .select('projectEstimatedMatchingView.sumValueUsd') + .where('projectEstimatedMatchingView.projectId = :projectId', { + projectId, }) - .getRawOne(); + .andWhere('projectEstimatedMatchingView.qfRoundId = :qfRoundId', { + qfRoundId, + }) + .cache( + `sumDonationValueUsdForQfRound-${projectId}-${qfRoundId}`, + Number(process.env.PROJECT_QFROUND_DONATION_SUMMARY_CACHE_TIME || 60000), + ) + .getOne(); - return { - uniqueDonorsCount: parseInt(result.uniqueDonors, 10) || 0, - sumValueUsd: parseFloat(result.totalDonationValueUsd) || 0, - }; + return result?.sumValueUsd || 0; } -export async function countUniqueDonorsAndSumDonationValueUsd( - projectId: number, -): Promise<{ totalDonations: number; uniqueDonors: number }> { - const result = await Donation.createQueryBuilder('donation') - .select('COALESCE(SUM(donation.valueUsd), 0)', 'totalDonations') - .addSelect('COUNT(DISTINCT donation.userId)', 'uniqueDonors') - .where('donation.projectId = :projectId', { projectId }) - .andWhere('donation.status = :status', { status: 'verified' }) - .getRawOne(); - return { - totalDonations: parseFloat(result.totalDonations) || 0, - uniqueDonors: parseInt(result.uniqueDonors) || 0, - }; +export async function countUniqueDonors(projectId: number): Promise { + const result = await ProjectDonationSummaryView.createQueryBuilder( + 'projectDonationSummaryView', + ) + .select('projectDonationSummaryView.uniqueDonorsCount') + .where('projectDonationSummaryView.projectId = :projectId', { projectId }) + .cache( + `countUniqueDonors-${projectId}`, + Number(process.env.PROJECT_DONATION_SUMMARY_CACHE_TIME || 60000), + ) + .getOne(); + + return result?.uniqueDonorsCount || 0; +} + +export async function sumDonationValueUsd(projectId: number): Promise { + const result = await ProjectDonationSummaryView.createQueryBuilder( + `projectDonationSummaryView`, + ) + .select('projectDonationSummaryView.sumVerifiedDonations') + .where('projectDonationSummaryView.projectId = :projectId', { projectId }) + .cache( + `sumDonationValueUsd-${projectId}`, + Number(process.env.PROJECT_DONATION_SUMMARY_CACHE_TIME || 60000), + ) + .getOne(); + + return result?.sumVerifiedDonations || 0; } export async function isVerifiedDonationExistsInQfRound(params: { diff --git a/src/services/donationService.ts b/src/services/donationService.ts index c32680dba..57db9ec16 100644 --- a/src/services/donationService.ts +++ b/src/services/donationService.ts @@ -18,9 +18,11 @@ import { findProjectIdBySlug, } from '../repositories/projectRepository'; import { convertExponentialNumber } from '../utils/utils'; +import { fetchGivHistoricPrice } from './givPriceService'; import { findDonationById, findRelevantDonations, + findStableCoinDonationsWithoutPrice, } from '../repositories/donationRepository'; import { getChainvineAdapter, @@ -33,7 +35,10 @@ import { updateUserTotalDonated, updateUserTotalReceived, } from './userService'; -import { refreshProjectEstimatedMatchingView } from './projectViewsService'; +import { + refreshProjectDonationSummaryView, + refreshProjectEstimatedMatchingView, +} from './projectViewsService'; import { AppDataSource } from '../orm'; import { getQfRoundHistoriesThatDontHaveRelatedDonations } from '../repositories/qfRoundHistoryRepository'; import { getPowerRound } from '../repositories/powerRoundRepository'; @@ -137,7 +142,7 @@ export const updateDonationByTransakData = async ( donation.fromWalletAddress = transakData.webhookData.fromWalletAddress; if (donation.amount !== transakData.webhookData.cryptoAmount) { // If the transaction amount is different with donation amount - // it proves it might be fraud, so we change the valueEth and valueUsd + // it proves it's might be fraud, so we change the valueEth and valueUsd donation.valueUsd = donation.valueUsd * (transakData.webhookData.cryptoAmount / donation.amount); @@ -175,10 +180,34 @@ export const updateDonationByTransakData = async ( } } await donation.save(); - await updateProjectStatistics(donation.projectId); - await updateUserTotalDonated(donation.userId); - await updateUserTotalReceived(donation.project?.adminUserId); - await refreshProjectEstimatedMatchingView(); + await updateTotalDonationsOfProject(donation.projectId); + + // We dont wait for this to finish + refreshProjectEstimatedMatchingView(); + refreshProjectDonationSummaryView(); +}; + +export const updateTotalDonationsOfProject = async ( + projectId: number, +): Promise => { + try { + await Project.query( + ` + UPDATE "project" + SET "totalDonations" = ( + ( + SELECT COALESCE(SUM(d."valueUsd"),0) + FROM "donation" as d + WHERE d."projectId" = $1 AND d."status" = 'verified' + ) + ) + WHERE "id" = $1 + `, + [projectId], + ); + } catch (e) { + logger.error('updateTotalDonationsOfAProject error', e); + } }; export const isTokenAcceptableForProject = async (inputData: { @@ -211,6 +240,67 @@ export const toFixNumber = (input: number, digits: number): number => { return convertExponentialNumber(Number(input.toFixed(digits))); }; +export const updateOldGivDonationsPrice = async () => { + const donations = await Donation.findXdaiGivDonationsWithoutPrice(); + logger.debug('updateOldGivDonationPrice donations count', donations.length); + for (const donation of donations) { + logger.debug( + 'updateOldGivDonationPrice() updating accurate price, donationId', + donation.id, + ); + try { + const givHistoricPrices = await fetchGivHistoricPrice( + donation.transactionId, + donation.transactionNetworkId, + ); + logger.debug('Update donation usd price ', { + donationId: donation.id, + ...givHistoricPrices, + valueEth: toFixNumber( + donation.amount * givHistoricPrices.givPriceInEth, + 7, + ), + }); + donation.priceEth = toFixNumber(givHistoricPrices.ethPriceInUsd, 7); + donation.priceUsd = toFixNumber(givHistoricPrices.givPriceInUsd, 4); + donation.valueUsd = toFixNumber( + donation.amount * givHistoricPrices.givPriceInUsd, + 4, + ); + donation.valueEth = toFixNumber( + donation.amount * givHistoricPrices.givPriceInEth, + 7, + ); + await donation.save(); + await updateTotalDonationsOfProject(donation.projectId); + } catch (e) { + logger.error('Update GIV donation valueUsd error', e.message); + } + } +}; + +export const updateOldStableCoinDonationsPrice = async () => { + const donations = await findStableCoinDonationsWithoutPrice(); + logger.debug( + 'updateOldStableCoinDonationPrice donations count', + donations.length, + ); + for (const donation of donations) { + logger.debug( + 'updateOldStableCoinDonationPrice() updating accurate price, donationId', + donation.id, + ); + try { + donation.priceUsd = 1; + donation.valueUsd = donation.amount; + await donation.save(); + await updateTotalDonationsOfProject(donation.projectId); + } catch (e) { + logger.error('Update GIV donation valueUsd error', e.message); + } + } +}; + const failedVerifiedDonationErrorMessages = [ errorMessages.TRANSACTION_SMART_CONTRACT_CONFLICTS_WITH_CURRENCY, errorMessages.INVALID_NETWORK_ID, @@ -221,8 +311,6 @@ const failedVerifiedDonationErrorMessages = [ errorMessages.TRANSACTION_NOT_FOUND_AND_NONCE_IS_USED, ]; -const FAILED_VERIFICTION_ALERT_THRESHOLD = 20 * 60 * 1000; // 20 minutes - export const syncDonationStatusWithBlockchainNetwork = async (params: { donationId: number; }): Promise => { @@ -273,18 +361,23 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { await donation.save(); // ONLY verified donations should be accumulated - // After updating, recalculate user and project total donations - await updateProjectStatistics(donation.projectId); + // After updating, recalculate user total donated and owner total received await updateUserTotalDonated(donation.userId); - await updateUserTotalReceived(donation.project.adminUserId); + // After updating price we update the totalDonations + await updateTotalDonationsOfProject(donation.projectId); + const project = await findProjectById(donation.projectId); + await updateUserTotalReceived(project!.adminUser.id); await sendNotificationForDonation({ donation, }); - if (donation.qfRoundId) { - await refreshProjectEstimatedMatchingView(); - } + // Update materialized view for project and qfRound data + await insertDonationsFromQfRoundHistory(); + await refreshProjectEstimatedMatchingView(); + await refreshProjectDonationSummaryView(); + + await updateProjectStatistics(donation.projectId); const donationStats = await getUserDonationStats(donation.userId); const donor = await findUserById(donation.userId); @@ -325,6 +418,7 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { donationId: donation.id, // }); } + logger.debug('donation and transaction', { transaction, donationId: donation.id, @@ -340,25 +434,9 @@ export const syncDonationStatusWithBlockchainNetwork = async (params: { if (failedVerifiedDonationErrorMessages.includes(e.message)) { // if error message is in failedVerifiedDonationi18n.__(translationErrorMessagesKeys.then) we know we should change the status to failed // otherwise we leave it to be checked in next cycle - logger.fatal('donation verification failed', { - error: e, - donationId: donation.id, - txHash: donation.transactionId, - }); donation.verifyErrorMessage = e.message; donation.status = DONATION_STATUS.FAILED; await donation.save(); - } else { - const timeDifference = - new Date().getTime() - donation.createdAt.getTime(); - if (timeDifference > FAILED_VERIFICTION_ALERT_THRESHOLD) { - logger.fatal('donation verification failed', { - error: e, - donationId: donation.id, - txHash: donation.transactionId, - donationAgeInMS: timeDifference, - }); - } } return donation; } @@ -403,55 +481,43 @@ export const sendNotificationForDonation = async (params: { }; export const insertDonationsFromQfRoundHistory = async (): Promise => { - try { - const qfRoundHistories = - await getQfRoundHistoriesThatDontHaveRelatedDonations(); - const donationDotEthAddress = '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; - const powerRound = (await getPowerRound())?.round || 1; - if (qfRoundHistories.length === 0) { - logger.debug( - 'insertDonationsFromQfRoundHistory There is not any qfRoundHistories in DB that doesnt have related donation', - ); - return; - } + const qfRoundHistories = + await getQfRoundHistoriesThatDontHaveRelatedDonations(); + const donationDotEthAddress = '0x6e8873085530406995170Da467010565968C7C62'; // Address behind donation.eth ENS address; + const powerRound = (await getPowerRound())?.round || 1; + if (qfRoundHistories.length === 0) { logger.debug( - `insertDonationsFromQfRoundHistory Filling ${qfRoundHistories.length} qfRoundHistory info ...`, + 'insertDonationsFromQfRoundHistory There is not any qfRoundHistories in DB that doesnt have related donation', ); + return; + } + logger.debug( + `insertDonationsFromQfRoundHistory Filling ${qfRoundHistories.length} qfRoundHistory info ...`, + ); - for (const qfRoundHistory of qfRoundHistories) { - if (qfRoundHistory.distributedFundTxDate) { - continue; - } - // get transaction time from blockchain - try { - const txTimestamp = await getEvmTransactionTimestamp({ - txHash: qfRoundHistory.distributedFundTxHash, - networkId: Number(qfRoundHistory.distributedFundNetwork), - }); - qfRoundHistory.distributedFundTxDate = new Date(txTimestamp); - await qfRoundHistory.save(); - } catch (e) { - logger.error( - 'insertDonationsFromQfRoundHistory-getEvmTransactionTimestamp error', - { - e, - txHash: qfRoundHistory.distributedFundTxHash, - networkId: Number(qfRoundHistory.distributedFundNetwork), - }, - ); - } - } - const matchingFundFromAddress = - (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || - donationDotEthAddress; - const user = await findUserByWalletAddress(matchingFundFromAddress); - if (!user) { - logger.error( - 'insertDonationsFromQfRoundHistory User with walletAddress MATCHING_FUND_DONATIONS_FROM_ADDRESS doesnt exist', - ); - return; + for (const qfRoundHistory of qfRoundHistories) { + if (qfRoundHistory.distributedFundTxDate) { + continue; } - await AppDataSource.getDataSource().query(` + // get transaction time from blockchain + const txTimestamp = await getEvmTransactionTimestamp({ + txHash: qfRoundHistory.distributedFundTxHash, + networkId: Number(qfRoundHistory.distributedFundNetwork), + }); + qfRoundHistory.distributedFundTxDate = new Date(txTimestamp); + await qfRoundHistory.save(); + } + const matchingFundFromAddress = + (process.env.MATCHING_FUND_DONATIONS_FROM_ADDRESS as string) || + donationDotEthAddress; + const user = await findUserByWalletAddress(matchingFundFromAddress); + if (!user) { + logger.error( + 'insertDonationsFromQfRoundHistory User with walletAddress MATCHING_FUND_DONATIONS_FROM_ADDRESS doesnt exist', + ); + return; + } + await AppDataSource.getDataSource().query(` INSERT INTO "donation" ( "transactionId", "transactionNetworkId", @@ -507,17 +573,14 @@ export const insertDonationsFromQfRoundHistory = async (): Promise => { ) `); - for (const qfRoundHistory of qfRoundHistories) { - await updateProjectStatistics(qfRoundHistory.projectId); - const project = await findProjectById(qfRoundHistory.projectId); - if (project) { - await updateUserTotalReceived(project.adminUser.id); - } + for (const qfRoundHistory of qfRoundHistories) { + await updateTotalDonationsOfProject(qfRoundHistory.projectId); + const project = await findProjectById(qfRoundHistory.projectId); + if (project) { + await updateUserTotalReceived(project.adminUser.id); } - await updateUserTotalDonated(user.id); - } catch (e) { - logger.error('insertDonationsFromQfRoundHistory error', e); } + await updateUserTotalDonated(user.id); }; export async function getDonationToGivethWithDonationBoxMetrics(