Skip to content

Commit 9d4f7bc

Browse files
authored
Merge pull request #183 from topcoder-platform/PM-5049_engagements-release-window
PM-5049 - make release window configurable for engagement payments
2 parents 237922f + 4707d9e commit 9d4f7bc

3 files changed

Lines changed: 98 additions & 0 deletions

File tree

src/api/winnings/winnings.service.spec.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
jest.mock('src/config', () => ({
22
ENV_CONFIG: {
3+
ENGAGEMENT_PAYMENT_RELEASE_WINDOW_DAYS: 5,
34
SENDGRID_TEMPLATE_ID_PAYMENT_SETUP_NOTIFICATION: 'template-id',
45
TGBillingAccounts: [80000062, 80002800],
56
TOPCODER_WALLET_URL: 'https://wallet.topcoder.com',
@@ -256,6 +257,73 @@ describe('WinningsService', () => {
256257
});
257258
});
258259

260+
it('applies a short release window to engagement payments', async () => {
261+
const beforeCreate = Date.now();
262+
263+
await service.createWinningWithPayments(
264+
{
265+
winnerId: 'user-1',
266+
type: WinningsType.PAYMENT,
267+
origin: 'Topcoder',
268+
category: WinningsCategory.ENGAGEMENT_PAYMENT,
269+
title: 'Engagement work',
270+
description: 'Engagement payment',
271+
externalId: 'assignment-1',
272+
details: [
273+
{
274+
totalAmount: 100,
275+
grossAmount: 100,
276+
installmentNumber: 1,
277+
currency: PrizeType.USD,
278+
billingAccount: '123456',
279+
},
280+
],
281+
} as any,
282+
'creator-1',
283+
);
284+
285+
const persistedPayment =
286+
tx.winnings.create.mock.calls[0][0].data.payment.create[0];
287+
288+
const releaseDate = new Date(persistedPayment.release_date).getTime();
289+
const daysUntilRelease = (releaseDate - beforeCreate) / (24 * 60 * 60 * 1000);
290+
291+
expect(daysUntilRelease).toBeGreaterThanOrEqual(4.99);
292+
expect(daysUntilRelease).toBeLessThanOrEqual(5.01);
293+
});
294+
295+
it('keeps the default release window for non-engagement payments', async () => {
296+
await service.createWinningWithPayments(
297+
{
298+
winnerId: 'user-1',
299+
type: WinningsType.PAYMENT,
300+
origin: 'Topcoder',
301+
category: WinningsCategory.CONTEST_PAYMENT,
302+
title: 'Contest payout',
303+
description: 'Contest payment',
304+
externalId: 'challenge-1',
305+
details: [
306+
{
307+
totalAmount: 100,
308+
grossAmount: 100,
309+
installmentNumber: 1,
310+
currency: PrizeType.USD,
311+
billingAccount: '80001012',
312+
},
313+
],
314+
} as any,
315+
'creator-1',
316+
);
317+
318+
const persistedPayment =
319+
tx.winnings.create.mock.calls[0][0].data.payment.create[0];
320+
321+
expect(persistedPayment.release_date).toBeUndefined();
322+
expect(
323+
Object.prototype.hasOwnProperty.call(persistedPayment, 'release_date'),
324+
).toBe(false);
325+
});
326+
259327
it('rejects engagement payment details that do not match the assignment billing account', async () => {
260328
await expect(
261329
service.createWinningWithPayments(

src/api/winnings/winnings.service.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,18 @@ interface ChallengeBillingAccountSyncPlan {
6969
status: string;
7070
}
7171

72+
const DAY_IN_MS = 24 * 60 * 60 * 1000;
73+
7274
/**
7375
* The winning service.
7476
*/
7577
@Injectable()
7678
export class WinningsService {
7779
private readonly logger = new Logger(WinningsService.name);
7880

81+
private readonly engagementPaymentReleaseWindowDays =
82+
ENV_CONFIG.ENGAGEMENT_PAYMENT_RELEASE_WINDOW_DAYS;
83+
7984
/**
8085
* Constructs the admin winning service with the given dependencies.
8186
* @param prisma the prisma service.
@@ -276,6 +281,18 @@ export class WinningsService {
276281
return attributes?.[CHALLENGE_BUDGET_SYNC_SKIP_ATTRIBUTE] === true;
277282
}
278283

284+
/**
285+
* Calculates the default release date for engagement payments.
286+
*
287+
* @returns A Date offset from now by the configured engagement release
288+
* window.
289+
*/
290+
private buildEngagementReleaseDate(): Date {
291+
return new Date(
292+
Date.now() + this.engagementPaymentReleaseWindowDays * DAY_IN_MS,
293+
);
294+
}
295+
279296
/**
280297
* Normalizes the billing-account id from a challenge payment detail.
281298
*
@@ -1242,6 +1259,7 @@ export class WinningsService {
12421259
| 'billing_account'
12431260
| 'challenge_markup'
12441261
| 'challenge_fee'
1262+
| 'release_date'
12451263
>[],
12461264
},
12471265
};
@@ -1271,6 +1289,9 @@ export class WinningsService {
12711289
tx,
12721290
);
12731291

1292+
const engagementReleaseDate = isEngagementPayment
1293+
? this.buildEngagementReleaseDate()
1294+
: undefined;
12741295
for (const [detailIndex, detail] of (body.details || []).entries()) {
12751296
const challengeFee = engagementConsumePlan
12761297
? this.calculateEngagementChallengeFee(
@@ -1294,6 +1315,9 @@ export class WinningsService {
12941315
? Prisma.Decimal(engagementConsumePlan.challengeMarkup)
12951316
: null,
12961317
challenge_fee: Prisma.Decimal(challengeFee),
1318+
...(engagementReleaseDate !== undefined
1319+
? { release_date: engagementReleaseDate }
1320+
: {}),
12971321
};
12981322

12991323
paymentModel.net_amount = Prisma.Decimal(detail.grossAmount);

src/config/config.env.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ export class ConfigEnv {
9696
@IsString()
9797
TOPCODER_WALLET_URL = 'https://wallet.topcoder.com';
9898

99+
@IsInt()
100+
@Min(3)
101+
@Max(5)
102+
@IsOptional()
103+
ENGAGEMENT_PAYMENT_RELEASE_WINDOW_DAYS: number = 5;
104+
99105
@IsInt()
100106
@Min(0)
101107
@Max(99)

0 commit comments

Comments
 (0)