Skip to content

Commit fab1e16

Browse files
committed
test(analytics): add coverage for funding_gap metric
- Add getCampaignAnalytics() function to campaignStore.ts - Implement funding_gap calculation (target - pledged) - Add comprehensive unit tests covering: - Campaign with partial funding - Fully funded campaign (gap = 0) - Empty campaign (gap = target) - Non-existent campaign (returns undefined) This is a ~10% contribution toward the larger analytics feature, implementing the simplest metric that reuses existing campaign data without additional database queries.
1 parent 10f827c commit fab1e16

2 files changed

Lines changed: 84 additions & 0 deletions

File tree

backend/src/services/campaignStore.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ let getPledges: CampaignStoreModule['getPledges'];
2222
let getDb: DbModule['getDb'];
2323
let getCampaignHistory: EventHistoryModule['getCampaignHistory'];
2424
let addPledge: CampaignStoreModule['addPledge'];
25+
let getCampaignAnalytics: CampaignStoreModule['getCampaignAnalytics'];
2526

2627
const CREATOR = `G${'A'.repeat(55)}`;
2728
const CONTRIBUTOR = `G${'B'.repeat(55)}`;
@@ -41,6 +42,7 @@ beforeAll(async () => {
4142
getCampaign,
4243
getPledges,
4344
addPledge,
45+
getCampaignAnalytics,
4446
} = await import('./campaignStore'));
4547
({ getDb } = await import('./db'));
4648
({ getCampaignHistory } = await import('./eventHistory'));
@@ -208,3 +210,62 @@ describe('campaign pledge pagination', () => {
208210
expect(page2.pledges[0].amount).toBe(50);
209211
});
210212
});
213+
214+
describe('campaign analytics', () => {
215+
it('returns correct funding_gap for campaign with pledges', () => {
216+
const futureDeadline = Math.floor(Date.now() / 1000) + 86400;
217+
const campaign = createCampaign({
218+
creator: CREATOR,
219+
title: 'Analytics Test Campaign',
220+
description: 'Campaign to test analytics metrics',
221+
assetCode: 'USDC',
222+
targetAmount: 1000,
223+
deadline: futureDeadline,
224+
});
225+
226+
addPledge(campaign.id, { contributor: CONTRIBUTOR, amount: 250 });
227+
228+
const analytics = getCampaignAnalytics(campaign.id);
229+
expect(analytics).toBeDefined();
230+
expect(analytics?.fundingGap).toBe(750); // 1000 - 250 = 750
231+
});
232+
233+
it('returns zero funding_gap when campaign is fully funded', () => {
234+
const futureDeadline = Math.floor(Date.now() / 1000) + 86400;
235+
const campaign = createCampaign({
236+
creator: CREATOR,
237+
title: 'Fully Funded Campaign',
238+
description: 'Campaign to test funding_gap when fully funded',
239+
assetCode: 'XLM',
240+
targetAmount: 500,
241+
deadline: futureDeadline,
242+
});
243+
244+
addPledge(campaign.id, { contributor: CONTRIBUTOR, amount: 500 });
245+
246+
const analytics = getCampaignAnalytics(campaign.id);
247+
expect(analytics).toBeDefined();
248+
expect(analytics?.fundingGap).toBe(0); // 500 - 500 = 0
249+
});
250+
251+
it('returns funding_gap equal to target for campaign with no pledges', () => {
252+
const futureDeadline = Math.floor(Date.now() / 1000) + 86400;
253+
const campaign = createCampaign({
254+
creator: CREATOR,
255+
title: 'Empty Campaign',
256+
description: 'Campaign with no pledges to test analytics',
257+
assetCode: 'USDC',
258+
targetAmount: 2000,
259+
deadline: futureDeadline,
260+
});
261+
262+
const analytics = getCampaignAnalytics(campaign.id);
263+
expect(analytics).toBeDefined();
264+
expect(analytics?.fundingGap).toBe(2000); // 2000 - 0 = 2000
265+
});
266+
267+
it('returns undefined for non-existent campaign', () => {
268+
const analytics = getCampaignAnalytics('99999');
269+
expect(analytics).toBeUndefined();
270+
});
271+
});

backend/src/services/campaignStore.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1262,3 +1262,26 @@ export function getTopContributors(limit: number = 10): LeaderboardEntry[] {
12621262
averagePledgeAmount: round(row.avg_pledge),
12631263
}));
12641264
}
1265+
1266+
export interface CampaignAnalytics {
1267+
fundingGap: number;
1268+
}
1269+
1270+
/**
1271+
* Computes basic analytics for a campaign.
1272+
*
1273+
* @param campaignId - The unique campaign identifier.
1274+
* @returns A {@link CampaignAnalytics} object with computed metrics, or undefined if campaign doesn't exist.
1275+
*/
1276+
export function getCampaignAnalytics(campaignId: string): CampaignAnalytics | undefined {
1277+
const campaign = getCampaign(campaignId);
1278+
if (!campaign) {
1279+
return undefined;
1280+
}
1281+
1282+
const fundingGap = round(Math.max(0, campaign.targetAmount - campaign.pledgedAmount));
1283+
1284+
return {
1285+
fundingGap,
1286+
};
1287+
}

0 commit comments

Comments
 (0)