diff --git a/src/resolvers/donationResolver.test.ts b/src/resolvers/donationResolver.test.ts index 939a7edec..8d7503e11 100644 --- a/src/resolvers/donationResolver.test.ts +++ b/src/resolvers/donationResolver.test.ts @@ -63,6 +63,7 @@ import { addOrUpdatePowerSnapshotBalances } from '../repositories/powerBalanceSn import { findPowerSnapshots } from '../repositories/powerSnapshotRepository'; import { ChainType } from '../types/network'; import { getDefaultSolanaChainId } from '../services/chains'; +import { calculateGivbackFactorByRank } from '../services/givbackService'; import { DRAFT_DONATION_STATUS, DraftDonation, @@ -2247,13 +2248,15 @@ function createDonationTestCases() { where: { id: saveDonationResponse.data.data.createDonation }, }); - // because this project is rank1 - assert.equal( - donation?.givbackFactor, - Number(process.env.GIVBACK_MAX_FACTOR), - ); + const expectedGivbackFactor = calculateGivbackFactorByRank({ + projectRank: donation?.projectRank, + bottomRank: donation?.bottomRankInRound || 1, + minGivFactor: Number(process.env.GIVBACK_MIN_FACTOR), + maxGivFactor: Number(process.env.GIVBACK_MAX_FACTOR), + }); + assert.equal(donation?.givbackFactor, expectedGivbackFactor); assert.equal(donation?.powerRound, roundNumber); - assert.equal(donation?.projectRank, 1); + assert.isAtLeast(donation?.projectRank || 0, 1); }); it('should create GIV donation for giveth project on mainnet successfully', async () => { const project = await saveProjectDirectlyToDb(createProjectData()); diff --git a/src/services/givbackService.ts b/src/services/givbackService.ts index 5871535cc..81e43021c 100644 --- a/src/services/givbackService.ts +++ b/src/services/givbackService.ts @@ -4,6 +4,41 @@ import { getBottomGivbackRank, } from '../repositories/projectGivbackViewRepository'; +export const calculateGivbackFactorByRank = (params: { + projectRank?: number; + bottomRank: number; + minGivFactor: number; + maxGivFactor: number; +}): number => { + const { projectRank, bottomRank, minGivFactor, maxGivFactor } = params; + + // Keep configured bounds stable even if env values are swapped. + const minFactor = Math.min(minGivFactor, maxGivFactor); + const maxFactor = Math.max(minGivFactor, maxGivFactor); + + const parsedBottomRank = Number(bottomRank); + const parsedProjectRank = Number(projectRank); + + // With no ranking spread (or invalid bottom rank), avoid division by zero. + // If project has a rank (rank 1 in this case), keep top project on max factor. + if (!Number.isFinite(parsedBottomRank) || parsedBottomRank <= 1) { + return Number.isFinite(parsedProjectRank) && parsedProjectRank > 0 + ? maxFactor + : minFactor; + } + + // When rank is missing/invalid, default to bottom rank -> minimum factor. + const normalizedRank = + Number.isFinite(parsedProjectRank) && parsedProjectRank > 0 + ? parsedProjectRank + : parsedBottomRank; + + const step = (maxFactor - minFactor) / (parsedBottomRank - 1); + const rawFactor = maxFactor - (normalizedRank - 1) * step; + const boundedFactor = Math.max(minFactor, Math.min(maxFactor, rawFactor)); + return Number.isFinite(boundedFactor) ? boundedFactor : minFactor; +}; + export const calculateGivbackFactor = async ( projectId: number, ): Promise<{ @@ -12,24 +47,31 @@ export const calculateGivbackFactor = async ( projectRank?: number; powerRound: number; }> => { - const minGivFactor = Number(process.env.GIVBACK_MIN_FACTOR); - const maxGivFactor = Number(process.env.GIVBACK_MAX_FACTOR); + const minGivFactorRaw = Number(process.env.GIVBACK_MIN_FACTOR); + const maxGivFactorRaw = Number(process.env.GIVBACK_MAX_FACTOR); + const minGivFactor = Number.isFinite(minGivFactorRaw) ? minGivFactorRaw : 0; + const maxGivFactor = Number.isFinite(maxGivFactorRaw) + ? maxGivFactorRaw + : minGivFactor; + const [projectGivbackRankView, bottomRank, powerRound] = await Promise.all([ findProjectGivbackRankViewByProjectId(projectId), getBottomGivbackRank(), getPowerRound(), ]); - const eachRoundImpact = (maxGivFactor - minGivFactor) / (bottomRank - 1); - const givbackFactor = projectGivbackRankView?.powerRank - ? minGivFactor + - eachRoundImpact * (bottomRank - projectGivbackRankView?.powerRank) - : minGivFactor; + const givbackFactor = calculateGivbackFactorByRank({ + projectRank: projectGivbackRankView?.powerRank, + bottomRank, + minGivFactor, + maxGivFactor, + }); return { - givbackFactor: givbackFactor || 0, + givbackFactor, projectRank: projectGivbackRankView?.powerRank, - bottomRankInRound: bottomRank, + bottomRankInRound: + Number.isFinite(bottomRank) && bottomRank > 0 ? bottomRank : 1, powerRound: powerRound?.round as number, }; };