Skip to content

Commit f9f93d3

Browse files
committed
Allow for setting the payment status via create Winnings for engagement payments that get created as On Hold (Admin)
1 parent 51e60ab commit f9f93d3

12 files changed

Lines changed: 137 additions & 80 deletions

File tree

src/api/admin/admin.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,9 @@ export class AdminService {
385385
// Run all transaction tasks in a single prisma transaction
386386
await this.prisma.$transaction(async (tx) => {
387387
for (let i = 0; i < transactions.length; i++) {
388-
this.logger.log(`Executing transaction ${i + 1}/${transactions.length}`);
388+
this.logger.log(
389+
`Executing transaction ${i + 1}/${transactions.length}`,
390+
);
389391
await transactions[i](tx);
390392
}
391393
});

src/api/challenge-payments/challenge-payments.controller.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
import {
2-
Controller,
3-
Get,
4-
Param,
5-
Query,
6-
Req,
7-
} from '@nestjs/common';
1+
import { Controller, Get, Param, Query, Req } from '@nestjs/common';
82
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
93
import { AllowedM2mScope, Roles, User } from 'src/core/auth/decorators';
104
import { M2mScope, Role } from 'src/core/auth/auth.constants';

src/api/challenges/challenges.controller.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { Controller, Post, Param, HttpCode, HttpStatus, BadRequestException } from '@nestjs/common';
1+
import {
2+
Controller,
3+
Post,
4+
Param,
5+
HttpCode,
6+
HttpStatus,
7+
BadRequestException,
8+
} from '@nestjs/common';
29
import { isUUID } from 'class-validator';
310
import {
411
ApiOperation,
@@ -17,9 +24,7 @@ import { ChallengesService } from './challenges.service';
1724
@Controller('/challenges')
1825
@ApiBearerAuth()
1926
export class ChallengesController {
20-
constructor(
21-
private readonly challengesService: ChallengesService,
22-
) {}
27+
constructor(private readonly challengesService: ChallengesService) {}
2328

2429
@Post('/:challengeId')
2530
@M2M()
@@ -44,7 +49,9 @@ export class ChallengesController {
4449
@User() user: UserInfo,
4550
): Promise<ResponseDto<string>> {
4651
if (!isUUID(challengeId)) {
47-
throw new BadRequestException('Invalid challengeId provided! Uuid expected!');
52+
throw new BadRequestException(
53+
'Invalid challengeId provided! Uuid expected!',
54+
);
4855
}
4956

5057
const result = new ResponseDto<string>();

src/api/challenges/challenges.module.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import { WinningsRepository } from '../repository/winnings.repo';
88
@Module({
99
imports: [TopcoderModule, WinningsModule],
1010
controllers: [ChallengesController],
11-
providers: [
12-
ChallengesService,
13-
WinningsRepository,
14-
],
11+
providers: [ChallengesService, WinningsRepository],
1512
})
1613
export class ChallengesModule {}

src/api/challenges/challenges.service.ts

Lines changed: 53 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import {
77
orderBy,
88
uniqBy,
99
} from 'lodash';
10-
import { BadRequestException, ConflictException, Injectable } from '@nestjs/common';
10+
import {
11+
BadRequestException,
12+
ConflictException,
13+
Injectable,
14+
} from '@nestjs/common';
1115
import { isUUID } from 'class-validator';
1216
import { ENV_CONFIG } from 'src/config';
1317
import { Logger } from 'src/shared/global';
@@ -65,7 +69,9 @@ export class ChallengesService {
6569

6670
async getChallenge(challengeId: string) {
6771
if (!isUUID(challengeId)) {
68-
throw new BadRequestException('Invalid challengeId provided! Uuid expected!');
72+
throw new BadRequestException(
73+
'Invalid challengeId provided! Uuid expected!',
74+
);
6975
}
7076

7177
const requestUrl = `${TC_API_BASE}/challenges/${challengeId}`;
@@ -142,12 +148,13 @@ export class ChallengesService {
142148

143149
return winners.map((winner) => {
144150
const currency = prizes[winner.placement - 1].type;
145-
const winType = currency === PrizeType.USD ? (
146-
type ??
147-
(challenge.task.isTask
148-
? WinningsCategory.TASK_PAYMENT
149-
: WinningsCategory.CONTEST_PAYMENT)
150-
) : WinningsCategory.POINTS_AWARD;
151+
const winType =
152+
currency === PrizeType.USD
153+
? (type ??
154+
(challenge.task.isTask
155+
? WinningsCategory.TASK_PAYMENT
156+
: WinningsCategory.CONTEST_PAYMENT))
157+
: WinningsCategory.POINTS_AWARD;
151158

152159
return {
153160
handle: winner.handle,
@@ -159,7 +166,7 @@ export class ChallengesService {
159166
challenge.type === 'Task'
160167
? challenge.name
161168
: `${challenge.name} - ${type === WinningsCategory.CONTEST_CHECKPOINT_PAYMENT ? 'Checkpoint ' : ''}${placeToOrdinal(winner.placement)} Place`,
162-
}
169+
};
163170
});
164171
}
165172

@@ -239,7 +246,9 @@ export class ChallengesService {
239246

240247
if (placementPrizes[0]?.type !== PrizeType.USD) {
241248
const prizeType = placementPrizes[0].type;
242-
this.logger.log(`Skipping copilot payments generation for challenge ${challenge.id} with "${prizeType}" winning prize!`);
249+
this.logger.log(
250+
`Skipping copilot payments generation for challenge ${challenge.id} with "${prizeType}" winning prize!`,
251+
);
243252
return [];
244253
}
245254

@@ -249,7 +258,10 @@ export class ChallengesService {
249258

250259
const copilotPrize = copilotPrizes[0];
251260
const currency = copilotPrize.type;
252-
const winType = currency === PrizeType.USD ? WinningsCategory.COPILOT_PAYMENT : WinningsCategory.POINTS_AWARD;
261+
const winType =
262+
currency === PrizeType.USD
263+
? WinningsCategory.COPILOT_PAYMENT
264+
: WinningsCategory.POINTS_AWARD;
253265
return copilots.map((copilot) => ({
254266
handle: copilot.memberHandle,
255267
amount: copilotPrizes[0].value,
@@ -272,7 +284,9 @@ export class ChallengesService {
272284

273285
if (placementPrizes[0]?.type !== PrizeType.USD) {
274286
const prizeType = placementPrizes[0].type;
275-
this.logger.log(`Skipping reviewers payments generation for challenge ${challenge.id} with "${prizeType}" winning prize!`);
287+
this.logger.log(
288+
`Skipping reviewers payments generation for challenge ${challenge.id} with "${prizeType}" winning prize!`,
289+
);
276290
return [];
277291
}
278292

@@ -338,10 +352,12 @@ export class ChallengesService {
338352
);
339353
}
340354

341-
342355
const placementPrize = placementPrizes?.[0];
343356
const currency = placementPrize?.type;
344-
const winType = currency === PrizeType.USD ? WinningsCategory.REVIEW_BOARD_PAYMENT : WinningsCategory.POINTS_AWARD;
357+
const winType =
358+
currency === PrizeType.USD
359+
? WinningsCategory.REVIEW_BOARD_PAYMENT
360+
: WinningsCategory.POINTS_AWARD;
345361

346362
return {
347363
handle: reviewer.memberHandle,
@@ -413,13 +429,17 @@ export class ChallengesService {
413429
];
414430

415431
const totalUsdAmount = payments.reduce(
416-
(sum, payment) => sum + (payment.currency === PrizeType.USD ? payment.amount : 0),
432+
(sum, payment) =>
433+
sum + (payment.currency === PrizeType.USD ? payment.amount : 0),
417434
0,
418435
);
419436

420437
return payments.map((payment) => ({
421438
winnerId: payment.userId.toString(),
422-
type: payment.currency === PrizeType.USD ? WinningsType.PAYMENT : WinningsType.POINTS,
439+
type:
440+
payment.currency === PrizeType.USD
441+
? WinningsType.PAYMENT
442+
: WinningsType.POINTS,
423443
origin: 'Topcoder',
424444
category: payment.type,
425445
title: challenge.name,
@@ -485,7 +505,10 @@ export class ChallengesService {
485505
// compute USD totals for BA validation/locking (POINT payments are persisted but not billed)
486506
const totalUsdAmount = payments.reduce(
487507
(sum, payment) =>
488-
sum + (payment.details[0].currency === PrizeType.USD ? payment.details[0].totalAmount : 0),
508+
sum +
509+
(payment.details[0].currency === PrizeType.USD
510+
? payment.details[0].totalAmount
511+
: 0),
489512
0,
490513
);
491514

@@ -527,7 +550,9 @@ export class ChallengesService {
527550
throw new Error('Challenge not found!');
528551
}
529552

530-
this.logger.log(`Challenge ${challenge.id} - "${challenge.name}" with status "${challenge.status}" retrieved`);
553+
this.logger.log(
554+
`Challenge ${challenge.id} - "${challenge.name}" with status "${challenge.status}" retrieved`,
555+
);
531556

532557
const allowedStatuses = [
533558
ChallengeStatuses.Completed.toLowerCase(),
@@ -564,20 +589,26 @@ export class ChallengesService {
564589
}
565590

566591
try {
567-
this.logger.log(`Starting payment creation for challenge ${challenge.id}`);
592+
this.logger.log(
593+
`Starting payment creation for challenge ${challenge.id}`,
594+
);
568595
await this.createPayments(challenge, userId);
569-
this.logger.log(`Payment creation completed for challenge ${challenge.id}`);
596+
this.logger.log(
597+
`Payment creation completed for challenge ${challenge.id}`,
598+
);
570599
} catch (error) {
571600
this.logger.error(
572601
`Error while creating payments for challenge ${challenge.id}`,
573602
error.message ?? error,
574603
);
575604
if (
576605
error &&
577-
(typeof error.message === 'string') &&
606+
typeof error.message === 'string' &&
578607
error.message.includes('Lock already acquired')
579608
) {
580-
this.logger.log(`Conflict detected while creating payments for ${challenge.id}`);
609+
this.logger.log(
610+
`Conflict detected while creating payments for ${challenge.id}`,
611+
);
581612
throw new ConflictException(
582613
'Another payment operation is in progress.',
583614
);

src/api/winnings/winnings.service.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,11 @@ export class WinningsService {
219219
: body.type;
220220

221221
if (isEngagementPayment && body.type !== WinningsType.PAYMENT) {
222-
this.logger.warn(
223-
'Engagement payment type overridden to PAYMENT.',
224-
{
225-
winnerId: body.winnerId,
226-
externalId: body.externalId,
227-
requestedType: body.type,
228-
},
229-
);
222+
this.logger.warn('Engagement payment type overridden to PAYMENT.', {
223+
winnerId: body.winnerId,
224+
externalId: body.externalId,
225+
requestedType: body.type,
226+
});
230227
}
231228

232229
const winningModel = {
@@ -259,6 +256,7 @@ export class WinningsService {
259256

260257
const payrollPayment = (body.attributes || {})['payroll'] === true;
261258
const isPointsAward = body.category === WinningsCategory.POINTS_AWARD;
259+
const requestedStatus = body.status as payment_status | undefined;
262260

263261
const hasActiveTaxForm = await this.taxFormRepo.hasActiveTaxForm(
264262
body.winnerId,
@@ -290,21 +288,36 @@ export class WinningsService {
290288
};
291289

292290
paymentModel.net_amount = Prisma.Decimal(detail.grossAmount);
293-
paymentModel.payment_status =
294-
hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified
295-
? PaymentStatus.OWED
296-
: PaymentStatus.ON_HOLD;
291+
let resolvedStatus: payment_status;
292+
if (requestedStatus) {
293+
resolvedStatus = requestedStatus;
294+
} else {
295+
resolvedStatus =
296+
hasConnectedPaymentMethod && hasActiveTaxForm && isIdentityVerified
297+
? PaymentStatus.OWED
298+
: PaymentStatus.ON_HOLD;
299+
300+
if (payrollPayment) {
301+
this.logger.debug(
302+
`Payroll payment detected. Setting payment status to PAID for user ${body.winnerId}`,
303+
);
304+
resolvedStatus = PaymentStatus.PAID;
305+
} else if (body.category === WinningsCategory.POINTS_AWARD) {
306+
resolvedStatus = payment_status.CREDITED;
307+
}
308+
}
297309

298310
if (payrollPayment) {
299-
this.logger.debug(
300-
`Payroll payment detected. Setting payment status to PAID for user ${body.winnerId}`,
301-
);
302-
paymentModel.payment_status = PaymentStatus.PAID;
311+
if (requestedStatus) {
312+
this.logger.debug(
313+
`Payroll payment detected. Preserving requested payment status ${requestedStatus} for user ${body.winnerId}`,
314+
);
315+
}
303316
await this.setPayrollPaymentMethod(body.winnerId, tx);
304-
} else if (body.category === WinningsCategory.POINTS_AWARD) {
305-
paymentModel.payment_status = payment_status.CREDITED;
306317
}
307318

319+
paymentModel.payment_status = resolvedStatus;
320+
308321
winningModel.payment.create.push(paymentModel);
309322
this.logger.debug('Added payment model to winning model', {
310323
paymentModel,

src/core/auth/guards/roles.guard.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,9 @@ export class RolesGuard implements CanActivate {
6464
this.normalizeRole(role),
6565
);
6666

67-
if (!normalizedRequiredRoles.some((role) => normalizedUserRoles.has(role))) {
67+
if (
68+
!normalizedRequiredRoles.some((role) => normalizedUserRoles.has(role))
69+
) {
6870
return false;
6971
}
7072

src/core/request/requestStore.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { nanoid } from 'nanoid';
66
export class RequestMetadata {
77
requestId: string;
88

9-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-object-type
109
constructor(params: { requestId?: string }) {
1110
this.requestId = params.requestId ?? nanoid(11);
1211
}

src/dto/payment.dto.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { ApiProperty } from '@nestjs/swagger';
2-
import { IsNumber, Min, IsInt, IsString, IsNotEmpty, isEnum, IsEnum } from 'class-validator';
2+
import {
3+
IsNumber,
4+
Min,
5+
IsInt,
6+
IsString,
7+
IsNotEmpty,
8+
isEnum,
9+
IsEnum,
10+
} from 'class-validator';
311
import { PrizeType } from 'src/api/challenges/models';
412

513
export enum PaymentStatus {

src/dto/winning.dto.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,16 @@ export class WinningCreateRequestDto {
257257
@IsObject()
258258
attributes: object;
259259

260+
@ApiProperty({
261+
description: 'Optional payment status to apply to created payments',
262+
enum: PaymentStatus,
263+
example: PaymentStatus.OWED,
264+
required: false,
265+
})
266+
@IsOptional()
267+
@IsEnum(PaymentStatus)
268+
status?: PaymentStatus;
269+
260270
@ApiProperty({
261271
description: 'The payment details',
262272
type: [PaymentCreateRequestDto],

0 commit comments

Comments
 (0)