Skip to content

Commit 49c4dbe

Browse files
committed
Fix challenge payments calculations
1 parent cc7edf3 commit 49c4dbe

File tree

1 file changed

+108
-51
lines changed

1 file changed

+108
-51
lines changed

src/api/challenges/challenges.service.ts

Lines changed: 108 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import { includes, isEmpty, sortBy, find, camelCase, groupBy } from 'lodash';
1+
import {
2+
includes,
3+
isEmpty,
4+
sortBy,
5+
find,
6+
camelCase,
7+
groupBy,
8+
orderBy,
9+
} from 'lodash';
210
import { Injectable } from '@nestjs/common';
311
import { ENV_CONFIG } from 'src/config';
412
import { Logger } from 'src/shared/global';
@@ -11,18 +19,14 @@ import { WinningsCategory, WinningsType } from 'src/dto/winning.dto';
1119
import { WinningsRepository } from '../repository/winnings.repo';
1220

1321
const placeToOrdinal = (place: number) => {
14-
if (place === 1) return "1st";
15-
if (place === 2) return "2nd";
16-
if (place === 3) return "3rd";
22+
if (place === 1) return '1st';
23+
if (place === 2) return '2nd';
24+
if (place === 3) return '3rd';
1725

1826
return `${place}th`;
19-
}
20-
21-
const {
22-
TOPCODER_API_V6_BASE_URL,
23-
TGBillingAccounts,
24-
} = ENV_CONFIG;
27+
};
2528

29+
const { TOPCODER_API_V6_BASE_URL, TGBillingAccounts } = ENV_CONFIG;
2630

2731
@Injectable()
2832
export class ChallengesService {
@@ -42,30 +46,45 @@ export class ChallengesService {
4246
const challenge = await this.m2MService.m2mFetch<Challenge>(requestUrl);
4347
this.logger.log(JSON.stringify(challenge, null, 2));
4448
return challenge;
45-
} catch(e) {
46-
this.logger.error(`Challenge ${challengeId} details couldn't be fetched!`, e);
49+
} catch (e) {
50+
this.logger.error(
51+
`Challenge ${challengeId} details couldn't be fetched!`,
52+
e,
53+
);
4754
}
4855
}
4956

5057
async getChallengeResources(challengeId: string) {
5158
try {
52-
const resources = await this.m2MService.m2mFetch<ChallengeResource[]>(`${TOPCODER_API_V6_BASE_URL}/resources?challengeId=${challengeId}`);
53-
const resourceRoles = await this.m2MService.m2mFetch<ResourceRole[]>(`${TOPCODER_API_V6_BASE_URL}/resource-roles`);
54-
55-
const rolesMap = resourceRoles.reduce((map, role) => {
56-
map[role.id] = camelCase(role.name);
57-
return map;
58-
}, {} as {[key: string]: string});
59-
60-
return groupBy(resources, (r) => rolesMap[r.roleId]) as {[role: string]: ChallengeResource[]};
61-
} catch(e) {
62-
this.logger.error(`Challenge resources for challenge ${challengeId} couldn\'t be fetched!`, e);
59+
const resources = await this.m2MService.m2mFetch<ChallengeResource[]>(
60+
`${TOPCODER_API_V6_BASE_URL}/resources?challengeId=${challengeId}`,
61+
);
62+
const resourceRoles = await this.m2MService.m2mFetch<ResourceRole[]>(
63+
`${TOPCODER_API_V6_BASE_URL}/resource-roles`,
64+
);
65+
66+
const rolesMap = resourceRoles.reduce(
67+
(map, role) => {
68+
map[role.id] = camelCase(role.name);
69+
return map;
70+
},
71+
{} as { [key: string]: string },
72+
);
73+
74+
return groupBy(resources, (r) => rolesMap[r.roleId]) as {
75+
[role: string]: ChallengeResource[];
76+
};
77+
} catch (e) {
78+
this.logger.error(
79+
`Challenge resources for challenge ${challengeId} couldn\'t be fetched!`,
80+
e,
81+
);
6382
}
6483
}
6584

6685
async getChallengePayments(challenge: Challenge) {
6786
this.logger.log(
68-
`Generating payments for challenge ${challenge.name} (${challenge.id}).`
87+
`Generating payments for challenge ${challenge.name} (${challenge.id}).`,
6988
);
7089
const challengeResources = await this.getChallengeResources(challenge.id);
7190

@@ -87,25 +106,37 @@ export class ChallengesService {
87106
ChallengeStatuses.CancelledFailedReview.toLowerCase();
88107

89108
// generate placement payments
90-
const placementPrizes = sortBy(find(prizeSets, {type: 'PLACEMENT'})?.prizes, 'value');
109+
const placementPrizes = orderBy(
110+
find(prizeSets, { type: 'PLACEMENT' })?.prizes,
111+
'value',
112+
'desc',
113+
);
114+
91115
if (!isCancelledFailedReview) {
92116
if (placementPrizes.length < winners.length) {
93-
throw new Error('Task has incorrect number of placement prizes! There are more winners than prizes!');
117+
throw new Error(
118+
'Task has incorrect number of placement prizes! There are more winners than prizes!',
119+
);
94120
}
95121

96122
winners.forEach((winner) => {
97123
payments.push({
98124
handle: winner.handle,
99125
amount: placementPrizes[winner.placement - 1].value,
100126
userId: winner.userId.toString(),
101-
type: challenge.task.isTask ? WinningsCategory.TASK_PAYMENT : WinningsCategory.CONTEST_PAYMENT,
102-
description: challenge.type === 'Task' ? challenge.name : `${challenge.name} - ${placeToOrdinal(winner.placement)} Place`,
127+
type: challenge.task.isTask
128+
? WinningsCategory.TASK_PAYMENT
129+
: WinningsCategory.CONTEST_PAYMENT,
130+
description:
131+
challenge.type === 'Task'
132+
? challenge.name
133+
: `${challenge.name} - ${placeToOrdinal(winner.placement)} Place`,
103134
});
104135
});
105136
}
106137

107138
// generate copilot payments
108-
const copilotPrizes = find(prizeSets, {type: 'COPILOT'})?.prizes ?? [];
139+
const copilotPrizes = find(prizeSets, { type: 'COPILOT' })?.prizes ?? [];
109140
if (copilotPrizes.length && !isCancelledFailedReview) {
110141
const copilots = challengeResources.copilot;
111142

@@ -119,8 +150,8 @@ export class ChallengesService {
119150
amount: copilotPrizes[0].value,
120151
userId: copilot.memberId.toString(),
121152
type: WinningsCategory.COPILOT_PAYMENT,
122-
})
123-
})
153+
});
154+
});
124155
}
125156

126157
// generate reviewer payments
@@ -132,32 +163,45 @@ export class ChallengesService {
132163
payments.push({
133164
handle: reviewer.memberHandle,
134165
userId: reviewer.memberId.toString(),
135-
amount: Math.round((challengeReviewer.basePayment ?? 0) + ((challengeReviewer.incrementalPayment ?? 0) * challenge.numOfSubmissions) * firstPlacePrize),
166+
amount: Math.round(
167+
(challengeReviewer.basePayment ?? 0) +
168+
((challengeReviewer.incrementalPayment ?? 0) / 100) *
169+
challenge.numOfSubmissions *
170+
firstPlacePrize,
171+
),
136172
type: WinningsCategory.REVIEW_BOARD_PAYMENT,
137-
})
173+
});
138174
});
139175
}
140176

141-
const totalAmount = payments.reduce((sum, payment) => sum + payment.amount, 0);
177+
const totalAmount = payments.reduce(
178+
(sum, payment) => sum + payment.amount,
179+
0,
180+
);
142181
return payments.map((payment) => ({
143182
winnerId: payment.userId.toString(),
144183
type: WinningsType.PAYMENT,
145-
origin: "Topcoder",
184+
origin: 'Topcoder',
146185
category: payment.type,
147186
title: challenge.name,
148187
description: payment.description || challenge.name,
149188
externalId: challenge.id,
150-
details: [{
151-
totalAmount: payment.amount,
152-
grossAmount: payment.amount,
153-
installmentNumber: 1,
154-
currency: "USD",
155-
billingAccount: `${challenge.billing.billingAccountId}`,
156-
challengeFee: totalAmount * challenge.billing.markup,
157-
}],
189+
details: [
190+
{
191+
totalAmount: payment.amount,
192+
grossAmount: payment.amount,
193+
installmentNumber: 1,
194+
currency: 'USD',
195+
billingAccount: `${challenge.billing.billingAccountId}`,
196+
challengeFee: totalAmount * challenge.billing.markup,
197+
},
198+
],
158199
attributes: {
159200
billingAccountId: challenge.billing.billingAccountId,
160-
payroll: includes(TGBillingAccounts, challenge.billing.billingAccountId),
201+
payroll: includes(
202+
TGBillingAccounts,
203+
challenge.billing.billingAccountId,
204+
),
161205
},
162206
}));
163207
}
@@ -175,17 +219,26 @@ export class ChallengesService {
175219
];
176220

177221
if (!allowedStatuses.includes(challenge.status.toLowerCase())) {
178-
throw new Error('Challenge isn\'t in a payable status!');
222+
throw new Error("Challenge isn't in a payable status!");
179223
}
180224

181-
const existingPayments = (await this.winningsRepo.searchWinnings({ externalIds: [challengeId] }))?.data?.winnings;
225+
const existingPayments = (
226+
await this.winningsRepo.searchWinnings({ externalIds: [challengeId] })
227+
)?.data?.winnings;
182228
if (existingPayments?.length > 0) {
183-
this.logger.log(`Payments already exist for challenge ${challengeId}, skipping payment generation`);
184-
throw new Error(`Payments already exist for challenge ${challengeId}, skipping payment generation`);
229+
this.logger.log(
230+
`Payments already exist for challenge ${challengeId}, skipping payment generation`,
231+
);
232+
throw new Error(
233+
`Payments already exist for challenge ${challengeId}, skipping payment generation`,
234+
);
185235
}
186236

187237
const payments = await this.getChallengePayments(challenge);
188-
const totalAmount = payments.reduce((sum, payment) => sum + payment.details[0].totalAmount, 0);
238+
const totalAmount = payments.reduce(
239+
(sum, payment) => sum + payment.details[0].totalAmount,
240+
0,
241+
);
189242

190243
const baValidation = {
191244
challengeId: challenge.id,
@@ -199,9 +252,13 @@ export class ChallengesService {
199252
baValidation.markup = challenge.billing.clientBillingRate;
200253
}
201254

202-
await Promise.all(payments.map(p => this.winningsService.createWinningWithPayments(p,userId)));
255+
await Promise.all(
256+
payments.map((p) =>
257+
this.winningsService.createWinningWithPayments(p, userId),
258+
),
259+
);
203260

204-
this.logger.log("Task Completed. locking consumed budget", baValidation);
261+
this.logger.log('Task Completed. locking consumed budget', baValidation);
205262
await this.baService.lockConsumeAmount(baValidation);
206263
}
207264
}

0 commit comments

Comments
 (0)