Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f305a69
Merge pull request #2295 from Giveth/staging
ae2079 Feb 4, 2026
ce7b81f
Merge pull request #2297 from Giveth/staging
ae2079 Feb 4, 2026
a82c1b0
Merge pull request #2298 from Giveth/staging
RamRamez Feb 5, 2026
3ceb0a1
Merge pull request #2301 from Giveth/staging
kkatusic Feb 10, 2026
a7a05b0
fallback for internal txs unavailable
mmahdigh Feb 19, 2026
d163a33
Revert "fallback for internal txs unavailable"
mmahdigh Feb 19, 2026
0371e4e
fallback for internal txs unavailable
mmahdigh Feb 19, 2026
d19adba
Update CORS whitelist to include 'qf.giveth.io'
RamRamez Feb 24, 2026
04ff828
Merge pull request #2302 from Giveth/staging
mmahdigh Feb 27, 2026
42d05e6
Improve GIVbacks allocatedGivTokens value
kkatusic Mar 5, 2026
54ffd01
Fix power round tests
kkatusic Mar 5, 2026
a710c6f
fixing test error
kkatusic Mar 5, 2026
c47e3d0
Merge pull request #2304 from Giveth/fix_allocated_giv_value
kkatusic Mar 5, 2026
11634b7
Update givbacks factor
kkatusic Mar 12, 2026
86a8fa0
fix test
kkatusic Mar 12, 2026
1ba15ae
fix failing test
kkatusic Mar 12, 2026
86e0742
fixing test
kkatusic Mar 12, 2026
4626a38
Merge pull request #2305 from Giveth/fix_allocated_giv_value
kkatusic Mar 12, 2026
52bb672
Merge pull request #2307 from Giveth/release-4-16-2026
kkatusic Apr 16, 2026
1b53b16
Merge pull request #2309 from Giveth/staging
ae2079 Apr 17, 2026
300e394
Merge pull request #2311 from Giveth/staging
ae2079 Apr 17, 2026
f1e6328
Merge pull request #2313 from Giveth/staging
ae2079 Apr 18, 2026
c728ea4
Merge pull request #2314 from Giveth/staging
ae2079 Apr 18, 2026
8d02998
bypass trusted createUserByAddress rate limit
ae2079 Apr 23, 2026
c3ef1dd
skip flaky optimistic rpc assertions in ci
ae2079 Apr 23, 2026
796a5e5
fix trusted createUserByAddress rate limiting
ae2079 Apr 23, 2026
bfab378
Merge pull request #2316 from Giveth/hotfix/trusted-create-user-rate-…
ae2079 Apr 23, 2026
6416aba
add isRecipient to wallet address duplicate check
RamRamez Apr 28, 2026
a4f5726
fix and add test cases
RamRamez Apr 28, 2026
33e1406
Merge pull request #2318 from Giveth/add-isRecipient-to-wallet-addres…
RamRamez Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
247 changes: 127 additions & 120 deletions src/repositories/donationRepository.test.ts

Large diffs are not rendered by default.

126 changes: 87 additions & 39 deletions src/repositories/donationRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { QfRound } from '../entities/qfRound';
import { ChainType } from '../types/network';
import { ORGANIZATION_LABELS } from '../entities/organization';
import { AppDataSource } from '../orm';
import { getPowerRound } from './powerRoundRepository';

export const exportClusterMatchingDonationsFormat = async (
qfRoundId: number,
Expand Down Expand Up @@ -584,45 +583,94 @@ export const getRecentDonations = async (take: number): Promise<Donation[]> => {
.getMany();
};

export const getSumOfGivbackEligibleDonationsForSpecificRound = async (params: {
powerRound?: number;
}): Promise<number> => {
// This function calculates the total USD value of all donations that are eligible for Givback in a specific PowerRound

try {
// If no powerRound is passed, get the latest one from the PowerRound table
const powerRound = params.powerRound || (await getPowerRound())?.round;
if (!powerRound) {
throw new Error('No powerRound found in the database.');
export const getSumOfGivbackEligibleDonationsForSpecificRound =
async (_params: { powerRound?: number }): Promise<number> => {
// This function calculates the total USD value of all donations eligible for GIVback
// Uses the operational window (16:00 UTC offset) to match givbacks service
// Groups recurring donations and applies minimum thresholds

try {
// Calculate date range for current month with 16:00 UTC offset (matches givbacks service)
const now = new Date();
const year = now.getUTCFullYear();
const month = now.getUTCMonth();

// Start: 1st of current month at 16:00 UTC
const startDate = new Date(Date.UTC(year, month, 1, 16, 0, 0));
// End: Last day of current month at 15:59:59 UTC (or 1st of next month at 15:59:59)
const endDate = new Date(Date.UTC(year, month + 1, 0, 15, 59, 59));

const startDateStr = startDate
.toISOString()
.replace('T', ' ')
.replace('Z', '');
const endDateStr = endDate
.toISOString()
.replace('T', ' ')
.replace('Z', '');

// Minimum threshold: $4 for most projects, $0.05 for community project
const minEligibleValueUsd = 4;
const communityMinValueUsd = 0.05;
const communityProjectSlug = 'the-giveth-community-of-makers';

// Execute query with:
// 1. Date range (operational window)
// 2. Group recurring donations by parent ID
// 3. Apply minimum threshold on grouped totals
// 4. Purple list exclusion
const result = await AppDataSource.getDataSource().query(
`
WITH grouped_donations AS (
SELECT
COALESCE("recurringDonationId"::text, "id"::text) AS group_id,
SUM("valueUsd") AS total_usd,
SUM("valueUsd" * "givbackFactor") AS total_usd_with_factor,
MAX("fromWalletAddress") AS from_address,
MAX("projectId") AS project_id
FROM "donation"
WHERE "status" = 'verified'
AND "isProjectGivbackEligible" = true
AND "createdAt" > $1
AND "createdAt" < $2
Comment on lines +634 to +635

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Boundary conditions may exclude donations at exact window edges.

The SQL uses strict inequality (> and <) which excludes donations at exactly 16:00:00.000 on the 1st and at 15:59:59.xxx on the last day. If the operational window should be inclusive of these boundaries (as the comments suggest "at 16:00 UTC"), consider using >= for the start boundary.

Suggested fix for inclusive boundaries
-          AND "createdAt" > $1
-          AND "createdAt" < $2
+          AND "createdAt" >= $1
+          AND "createdAt" < $2

And adjust endDate to be 16:00:00 on the 1st of the next month for cleaner boundary:

-      // End: Last day of current month at 15:59:59 UTC (or 1st of next month at 15:59:59)
-      const endDate = new Date(Date.UTC(year, month + 1, 0, 15, 59, 59));
+      // End: 1st of next month at 16:00 UTC (exclusive)
+      const endDate = new Date(Date.UTC(year, month + 1, 1, 16, 0, 0));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
AND "createdAt" > $1
AND "createdAt" < $2
AND "createdAt" >= $1
AND "createdAt" < $2
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repositories/donationRepository.ts` around lines 634 - 635, The SQL date
filter in donationRepository.ts currently uses strict inequalities (">" and "<")
which excludes donations that fall exactly on the window edges; update the WHERE
clause to use inclusive comparisons (">=" for the start boundary and "<" or
preferably "<=" for the end boundary depending on desired inclusivity) and, if
following the suggested approach, compute endDate as 16:00:00 on the first day
of the next month so the range is [startDate >= 16:00:00 on 1st, endDate < next
month 16:00:00) or use startDate >= and endDate <= if you want both endpoints
included; modify the SQL fragment and the code that builds startDate/endDate
accordingly to ensure donations exactly at 16:00:00 are captured.

GROUP BY COALESCE("recurringDonationId"::text, "id"::text)
)
SELECT COALESCE(SUM(gd.total_usd_with_factor), 0) AS "totalUsdWithGivbackFactor"
FROM grouped_donations gd
JOIN "project" p ON p.id = gd.project_id
WHERE
(
(p.slug = $3 AND gd.total_usd > $4)
OR (p.slug != $3 AND gd.total_usd >= $5)
)
AND NOT EXISTS (
SELECT 1
FROM "project_address" pa
INNER JOIN "project" proj ON proj.id = pa."projectId"
WHERE LOWER(pa.address) = LOWER(gd.from_address)
AND pa."isRecipient" = true
AND proj.verified = true
AND proj."statusId" = 5
);
`,
[
startDateStr,
endDateStr,
communityProjectSlug,
communityMinValueUsd,
minEligibleValueUsd,
],
);

return result?.[0]?.totalUsdWithGivbackFactor ?? 0;
} catch (e) {
logger.error(
'getSumOfGivbackEligibleDonationsForSpecificRound() error',
e,
);
return 0;
}

// Execute the main raw SQL query with the powerRound
const result = await AppDataSource.getDataSource().query(
`
SELECT
SUM("donation"."valueUsd" * "donation"."givbackFactor") AS "totalUsdWithGivbackFactor"
FROM "donation"
WHERE "donation"."status" = 'verified'
AND "donation"."isProjectGivbackEligible" = true
AND "donation"."powerRound" = $1
AND NOT EXISTS (
SELECT 1
FROM "project_address" "pa"
INNER JOIN "project" "p" ON "p"."id" = "pa"."projectId"
WHERE "pa"."address" = "donation"."fromWalletAddress"
AND "pa"."isRecipient" = true
AND "p"."verified" = true
);
`,
[powerRound],
);

return result?.[0]?.totalUsdWithGivbackFactor ?? 0;
} catch (e) {
logger.error('getSumOfGivbackEligibleDonationsForSpecificRound() error', e);
return 0;
}
};
};

export const getPendingDonationsIds = (): Promise<{ id: number }[]> => {
const date = moment()
Expand Down
60 changes: 31 additions & 29 deletions src/repositories/projectAddressRepository.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { assert } from 'chai';
import {
createProjectData,
generateRandomEtheriumAddress,
generateRandomSolanaAddress,
saveProjectDirectlyToDb,
saveUserDirectlyToDb,
} from '../../test/testUtils';
import { ProjStatus } from '../entities/project';
import { ProjectStatus } from '../entities/projectStatus';
import { NETWORK_IDS } from '../provider';
import { ChainType } from '../types/network';
import {
addBulkNewProjectAddress,
addNewProjectAddress,
findAllRelatedAddressByWalletAddress,
findProjectRecipientAddressByNetworkId,
findProjectRecipientAddressByProjectId,
findRelatedAddressByWalletAddress,
getPurpleListAddresses,
isWalletAddressInPurpleList,
removeRecipientAddressOfProject,
} from './projectAddressRepository';
import {
createProjectData,
generateRandomEtheriumAddress,
generateRandomSolanaAddress,
saveProjectDirectlyToDb,
saveUserDirectlyToDb,
} from '../../test/testUtils';
import { NETWORK_IDS } from '../provider';
import { ProjectStatus } from '../entities/projectStatus';
import { ProjStatus } from '../entities/project';
import { ChainType } from '../types/network';

describe('getPurpleListAddresses test cases', getPurpleListAddressesTestCases);
describe(
Expand Down Expand Up @@ -211,11 +210,12 @@ function addBulkNewProjectAddressTestCases() {
user,
},
]);
const newRelatedAddress =
await findRelatedAddressByWalletAddress(newAddress);
assert.isOk(newRelatedAddress);
assert.equal(newRelatedAddress?.address, newAddress);
assert.isFalse(newRelatedAddress?.isRecipient);
const newRelatedAddresses =
await findAllRelatedAddressByWalletAddress(newAddress);
assert.equal(newRelatedAddresses.length, 1);
const newRelatedAddress = newRelatedAddresses[0];
assert.equal(newRelatedAddress.address, newAddress);
assert.isFalse(newRelatedAddress.isRecipient);
});
it('should add two related address ', async () => {
const walletAddress = generateRandomEtheriumAddress();
Expand Down Expand Up @@ -252,18 +252,20 @@ function addBulkNewProjectAddressTestCases() {
user,
},
]);
const newRelatedAddress1 =
await findRelatedAddressByWalletAddress(newAddress1);
assert.isOk(newRelatedAddress1);
assert.equal(newRelatedAddress1?.address, newAddress1);
assert.equal(newRelatedAddress1?.project?.id, project.id);
assert.isFalse(newRelatedAddress1?.isRecipient);
const newRelatedAddress2 =
await findRelatedAddressByWalletAddress(newAddress2);
assert.isOk(newRelatedAddress2);
assert.equal(newRelatedAddress2?.address, newAddress2);
assert.equal(newRelatedAddress2?.project?.id, project.id);
assert.isFalse(newRelatedAddress2?.isRecipient);
const newRelatedAddresses1 =
await findAllRelatedAddressByWalletAddress(newAddress1);
assert.equal(newRelatedAddresses1.length, 1);
const newRelatedAddress1 = newRelatedAddresses1[0];
assert.equal(newRelatedAddress1.address, newAddress1);
assert.equal(newRelatedAddress1.project?.id, project.id);
assert.isFalse(newRelatedAddress1.isRecipient);
const newRelatedAddresses2 =
await findAllRelatedAddressByWalletAddress(newAddress2);
assert.equal(newRelatedAddresses2.length, 1);
const newRelatedAddress2 = newRelatedAddresses2[0];
assert.equal(newRelatedAddress2.address, newAddress2);
assert.equal(newRelatedAddress2.project?.id, project.id);
assert.isFalse(newRelatedAddress2.isRecipient);
});
}
function findProjectRecipientAddressByNetworkIdTestCases() {
Expand Down
9 changes: 6 additions & 3 deletions src/repositories/projectAddressRepository.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ProjectAddress } from '../entities/projectAddress';
import { Project } from '../entities/project';
import { ProjectAddress } from '../entities/projectAddress';
import { User } from '../entities/user';
import SentryLogger from '../sentryLogger';
import { ChainType } from '../types/network';
import { logger } from '../utils/logger';
import SentryLogger from '../sentryLogger';

export const getPurpleListAddresses = async (): Promise<
{ projectAddress: string }[]
Expand Down Expand Up @@ -77,7 +77,10 @@ export const findRelatedAddressByWalletAddress = async (
});
break;
}
return query.leftJoinAndSelect('projectAddress.project', 'project').getOne();
return query
.andWhere(`"isRecipient" = true`)
.leftJoinAndSelect('projectAddress.project', 'project')
.getOne();
};
export const findAllRelatedAddressByWalletAddress = async (
walletAddress: string,
Expand Down
Loading
Loading