Skip to content

Commit 95b85a3

Browse files
authored
Merge pull request #177 from topcoder-platform/dev
PROD - Task payment approver changes, BA check and updates for engagements / challenges
2 parents b339c49 + d86f537 commit 95b85a3

22 files changed

Lines changed: 2624 additions & 181 deletions

src/api/admin/admin.controller.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,15 @@ export class AdminController {
5656
@Roles(
5757
Role.PaymentAdmin,
5858
Role.PaymentBaAdmin,
59-
Role.EngagementPaymentApprover,
59+
Role.PaymentApprover,
6060
Role.WiproTaasAdmin,
6161
Role.PaymentEditor,
6262
Role.PaymentViewer,
6363
)
6464
@ApiOperation({
6565
summary: 'Search winnings with parameters',
6666
description:
67-
'Roles: Payment Admin, Payment BA Admin, Engagement Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
67+
'Roles: Payment Admin, Payment BA Admin, Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
6868
})
6969
@ApiBody({
7070
description: 'Winning request body',
@@ -102,7 +102,7 @@ export class AdminController {
102102
@Roles(
103103
Role.PaymentAdmin,
104104
Role.PaymentBaAdmin,
105-
Role.EngagementPaymentApprover,
105+
Role.PaymentApprover,
106106
Role.WiproTaasAdmin,
107107
Role.PaymentEditor,
108108
Role.PaymentViewer,
@@ -145,15 +145,15 @@ export class AdminController {
145145
@Roles(
146146
Role.PaymentAdmin,
147147
Role.PaymentBaAdmin,
148-
Role.EngagementPaymentApprover,
148+
Role.PaymentApprover,
149149
Role.WiproTaasAdmin,
150150
Role.PaymentEditor,
151151
Role.PaymentViewer,
152152
)
153153
@ApiOperation({
154154
summary: 'Export search winnings result in csv file format',
155155
description:
156-
'Roles: Payment Admin, Payment BA Admin, Engagement Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer. Engagement payment exports include the Payment Creator column.',
156+
'Roles: Payment Admin, Payment BA Admin, Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer. Engagement payment exports include the Payment Creator column.',
157157
})
158158
@ApiBody({
159159
description: 'Winning request body',
@@ -283,14 +283,14 @@ export class AdminController {
283283
@Roles(
284284
Role.PaymentAdmin,
285285
Role.PaymentBaAdmin,
286-
Role.EngagementPaymentApprover,
286+
Role.PaymentApprover,
287287
Role.WiproTaasAdmin,
288288
Role.PaymentEditor,
289289
)
290290
@ApiOperation({
291291
summary: 'Update winnings with given parameter',
292292
description:
293-
'Roles: Payment Admin, Payment BA Admin, Engagement Payment Approver, Wipro TaaS Admin, Payment Editor. paymentStatus, releaseDate and paymentAmount cannot be null at the same time.',
293+
'Roles: Payment Admin, Payment BA Admin, Payment Approver, Wipro TaaS Admin, Payment Editor. paymentStatus, releaseDate and paymentAmount cannot be null at the same time.',
294294
})
295295
@ApiResponse({
296296
status: 200,
@@ -330,15 +330,15 @@ export class AdminController {
330330
@Roles(
331331
Role.PaymentAdmin,
332332
Role.PaymentBaAdmin,
333-
Role.EngagementPaymentApprover,
333+
Role.PaymentApprover,
334334
Role.WiproTaasAdmin,
335335
Role.PaymentEditor,
336336
Role.PaymentViewer,
337337
)
338338
@ApiOperation({
339339
summary: 'List winning audit logs with given winning id',
340340
description:
341-
'Roles: Payment Admin, Payment BA Admin, Engagement Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
341+
'Roles: Payment Admin, Payment BA Admin, Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
342342
})
343343
@ApiParam({
344344
name: 'winningID',
@@ -374,15 +374,15 @@ export class AdminController {
374374
@Roles(
375375
Role.PaymentAdmin,
376376
Role.PaymentBaAdmin,
377-
Role.EngagementPaymentApprover,
377+
Role.PaymentApprover,
378378
Role.WiproTaasAdmin,
379379
Role.PaymentEditor,
380380
Role.PaymentViewer,
381381
)
382382
@ApiOperation({
383383
summary: 'Fetch winnings payout audit logs with given winning id.',
384384
description:
385-
'Roles: Payment Admin, Payment BA Admin, Engagement Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
385+
'Roles: Payment Admin, Payment BA Admin, Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
386386
})
387387
@ApiParam({
388388
name: 'winningID',

src/api/admin/admin.service.spec.ts

Lines changed: 241 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,42 @@ jest.mock('src/shared/topcoder/members.service', () => ({
4040
TopcoderMembersService: class {},
4141
}));
4242

43+
jest.mock('src/shared/topcoder/challenges.service', () => ({
44+
TopcoderChallengesService: class {},
45+
}));
46+
4347
import { AdminService } from './admin.service';
48+
import { PaymentStatus } from 'src/dto/payment.dto';
4449

4550
describe('AdminService', () => {
4651
let service: AdminService;
4752
let prisma: {
53+
$transaction: jest.Mock;
54+
audit: {
55+
create: jest.Mock;
56+
findMany: jest.Mock;
57+
};
58+
payment: {
59+
findMany: jest.Mock;
60+
update: jest.Mock;
61+
};
62+
payment_releases: {
63+
findFirst: jest.Mock;
64+
updateMany: jest.Mock;
65+
};
4866
winnings: {
4967
findFirst: jest.Mock;
5068
};
5169
};
52-
let paymentsService: object;
53-
let baService: object;
70+
let paymentsService: {
71+
reconcileUserPayments: jest.Mock;
72+
};
73+
let baService: {
74+
getBillingAccountById: jest.Mock;
75+
getBillingAccountsForUser: jest.Mock;
76+
lockConsumeAmount: jest.Mock;
77+
syncEngagementConsumeAmounts: jest.Mock;
78+
};
5479
let accessControlService: {
5580
verifyAccess: jest.Mock;
5681
};
@@ -61,15 +86,43 @@ describe('AdminService', () => {
6186
let tcMembersService: {
6287
getHandlesByUserIds: jest.Mock;
6388
};
89+
let topcoderChallengesService: {
90+
getChallengeById: jest.Mock;
91+
getProjectById: jest.Mock;
92+
};
6493

6594
beforeEach(() => {
6695
prisma = {
96+
$transaction: jest.fn((callback: (tx: unknown) => unknown) =>
97+
Promise.resolve(callback(prisma)),
98+
),
99+
audit: {
100+
create: jest.fn().mockResolvedValue({}),
101+
findMany: jest.fn().mockResolvedValue([]),
102+
},
103+
payment: {
104+
findMany: jest.fn().mockResolvedValue([]),
105+
update: jest.fn().mockResolvedValue({}),
106+
},
107+
payment_releases: {
108+
findFirst: jest.fn().mockResolvedValue(null),
109+
updateMany: jest.fn().mockResolvedValue({ count: 1 }),
110+
},
67111
winnings: {
68112
findFirst: jest.fn(),
69113
},
70114
};
71-
paymentsService = {};
72-
baService = {};
115+
paymentsService = {
116+
reconcileUserPayments: jest.fn().mockResolvedValue(undefined),
117+
};
118+
baService = {
119+
getBillingAccountById: jest
120+
.fn()
121+
.mockResolvedValue({ id: 80001012, markup: 0.1 }),
122+
getBillingAccountsForUser: jest.fn().mockResolvedValue(['80001012']),
123+
lockConsumeAmount: jest.fn().mockResolvedValue(undefined),
124+
syncEngagementConsumeAmounts: jest.fn().mockResolvedValue(undefined),
125+
};
73126
accessControlService = {
74127
verifyAccess: jest.fn().mockResolvedValue(undefined),
75128
};
@@ -82,6 +135,10 @@ describe('AdminService', () => {
82135
'654321': 'payment-manager',
83136
}),
84137
};
138+
topcoderChallengesService = {
139+
getChallengeById: jest.fn().mockResolvedValue(undefined),
140+
getProjectById: jest.fn().mockResolvedValue(undefined),
141+
};
85142

86143
service = new AdminService(
87144
prisma as any,
@@ -90,6 +147,7 @@ describe('AdminService', () => {
90147
accessControlService as any,
91148
topcoderEngagementsService as any,
92149
tcMembersService as any,
150+
topcoderChallengesService as any,
93151
);
94152
});
95153

@@ -323,4 +381,183 @@ describe('AdminService', () => {
323381
service.getWinningPaymentDetails('missing-winning', '123456', []),
324382
).rejects.toBeInstanceOf(NotFoundException);
325383
});
384+
385+
it('recalculates the challenge billing-account line item when a challenge payment is cancelled', async () => {
386+
prisma.payment.findMany
387+
.mockResolvedValueOnce([
388+
{
389+
billing_account: '80001012',
390+
currency: 'USD',
391+
installment_number: 1,
392+
payment_id: 'payment-1',
393+
payment_status: PaymentStatus.OWED,
394+
release_date: new Date('2026-04-27T00:00:00.000Z'),
395+
version: 1,
396+
winnings: {
397+
category: 'CONTEST_PAYMENT',
398+
external_id: 'challenge-1',
399+
type: 'PAYMENT',
400+
},
401+
},
402+
])
403+
.mockResolvedValueOnce([
404+
{ total_amount: '12.00' },
405+
{ total_amount: '27.50' },
406+
]);
407+
topcoderChallengesService.getChallengeById.mockResolvedValue({
408+
billing: {
409+
billingAccountId: '80001012',
410+
markup: 0.1,
411+
},
412+
id: 'challenge-1',
413+
status: 'COMPLETED',
414+
});
415+
416+
const result = await service.updateWinnings(
417+
{
418+
paymentId: 'payment-1',
419+
paymentStatus: PaymentStatus.CANCELLED,
420+
winningsId: 'winning-1',
421+
} as any,
422+
'admin-1',
423+
['Payment Admin'],
424+
);
425+
426+
expect(result.data).toBe('Successfully updated winnings');
427+
expect(prisma.payment.update).toHaveBeenCalledWith({
428+
where: {
429+
payment_id: 'payment-1',
430+
winnings_id: 'winning-1',
431+
version: 1,
432+
},
433+
data: {
434+
date_paid: undefined,
435+
payment_status: PaymentStatus.CANCELLED,
436+
updated_at: expect.any(Date),
437+
updated_by: 'admin-1',
438+
version: 2,
439+
},
440+
});
441+
expect(prisma.payment.findMany).toHaveBeenNthCalledWith(2, {
442+
select: { total_amount: true },
443+
where: {
444+
billing_account: '80001012',
445+
currency: 'USD',
446+
payment_status: { not: PaymentStatus.CANCELLED },
447+
winnings: {
448+
external_id: 'challenge-1',
449+
type: 'PAYMENT',
450+
},
451+
},
452+
});
453+
expect(baService.lockConsumeAmount).toHaveBeenCalledWith({
454+
billingAccountId: 80001012,
455+
challengeId: 'challenge-1',
456+
markup: 0.1,
457+
status: 'COMPLETED',
458+
totalPrizesInCents: 3950,
459+
});
460+
});
461+
462+
it('releases engagement billing-account rows when an engagement payment is cancelled', async () => {
463+
prisma.payment.findMany
464+
.mockResolvedValueOnce([
465+
{
466+
billing_account: '80001012',
467+
currency: 'USD',
468+
installment_number: 1,
469+
payment_id: 'payment-1',
470+
payment_status: PaymentStatus.OWED,
471+
release_date: new Date('2026-04-28T00:00:00.000Z'),
472+
version: 1,
473+
winnings: {
474+
category: 'ENGAGEMENT_PAYMENT',
475+
external_id: 'assignment-1',
476+
type: 'PAYMENT',
477+
},
478+
},
479+
])
480+
.mockResolvedValueOnce([]);
481+
482+
const result = await service.updateWinnings(
483+
{
484+
paymentId: 'payment-1',
485+
paymentStatus: PaymentStatus.CANCELLED,
486+
winningsId: 'winning-1',
487+
} as any,
488+
'admin-1',
489+
['Payment Admin'],
490+
);
491+
492+
expect(result.data).toBe('Successfully updated winnings');
493+
expect(prisma.payment.findMany).toHaveBeenNthCalledWith(2, {
494+
select: {
495+
challenge_fee: true,
496+
total_amount: true,
497+
},
498+
where: {
499+
billing_account: '80001012',
500+
currency: 'USD',
501+
payment_status: { not: PaymentStatus.CANCELLED },
502+
winnings: {
503+
category: 'ENGAGEMENT_PAYMENT',
504+
external_id: 'assignment-1',
505+
type: 'PAYMENT',
506+
},
507+
},
508+
orderBy: [{ created_at: 'asc' }, { payment_id: 'asc' }],
509+
});
510+
expect(baService.syncEngagementConsumeAmounts).toHaveBeenCalledWith({
511+
amounts: [],
512+
billingAccountId: 80001012,
513+
externalId: 'assignment-1',
514+
});
515+
expect(baService.lockConsumeAmount).not.toHaveBeenCalled();
516+
});
517+
518+
it('returns task details for task payment with projectId and approver', async () => {
519+
prisma.winnings.findFirst.mockResolvedValue({
520+
winning_id: 'winning-task',
521+
category: 'TASK_PAYMENT',
522+
created_by: '654321',
523+
winner_id: '123456',
524+
external_id: 'challenge-uuid-1',
525+
attributes: {},
526+
});
527+
topcoderChallengesService.getChallengeById.mockResolvedValue({
528+
id: 'challenge-uuid-1',
529+
name: 'Build a widget',
530+
projectId: 42,
531+
createdBy: 'challenge-creator',
532+
});
533+
topcoderChallengesService.getProjectById.mockResolvedValue({
534+
id: 42,
535+
name: 'My Project',
536+
});
537+
prisma.audit.findMany.mockResolvedValue([
538+
{
539+
id: 'audit-1',
540+
winnings_id: 'winning-task',
541+
user_id: '654321',
542+
action: 'status updated from ON_HOLD_ADMIN to OWED',
543+
note: null,
544+
created_at: new Date(),
545+
},
546+
]);
547+
548+
const result = await service.getWinningPaymentDetails(
549+
'winning-task',
550+
'123456',
551+
['Payment Admin'],
552+
);
553+
554+
expect(result.data?.taskDetails?.projectId).toBe('42');
555+
expect(result.data?.taskDetails?.projectName).toBe('My Project');
556+
expect(result.data?.taskDetails?.paymentCreatorHandle).toBe(
557+
'challenge-creator',
558+
);
559+
expect(result.data?.taskDetails?.paymentApproverHandle).toBe(
560+
'payment-manager',
561+
);
562+
});
326563
});

0 commit comments

Comments
 (0)