Skip to content

Commit 020fdc2

Browse files
authored
Merge pull request #158 from topcoder-platform/PM-4822
PM-4822: add Payment Creator to wallet export CSV
2 parents ef5813c + b9fe66b commit 020fdc2

2 files changed

Lines changed: 219 additions & 4 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
jest.mock('./admin.service', () => ({
2+
AdminService: class {},
3+
}));
4+
5+
jest.mock('../repository/winnings.repo', () => ({
6+
WinningsRepository: class {},
7+
}));
8+
9+
jest.mock('src/shared/topcoder/members.service', () => ({
10+
TopcoderMembersService: class {},
11+
}));
12+
13+
jest.mock('src/shared/access-control', () => ({
14+
AccessControlService: class {},
15+
}));
16+
17+
import { AdminController } from './admin.controller';
18+
import { PaymentStatus } from 'src/dto/payment.dto';
19+
import {
20+
WinningRequestDto,
21+
WinningsCategory,
22+
WinningsType,
23+
} from 'src/dto/winning.dto';
24+
25+
describe('AdminController', () => {
26+
let controller: AdminController;
27+
let winningsRepo: {
28+
searchWinnings: jest.Mock;
29+
};
30+
let tcMembersService: {
31+
getHandlesByUserIds: jest.Mock;
32+
};
33+
let accessControlService: {
34+
applyFilters: jest.Mock;
35+
};
36+
37+
beforeEach(() => {
38+
winningsRepo = {
39+
searchWinnings: jest.fn(),
40+
};
41+
tcMembersService = {
42+
getHandlesByUserIds: jest.fn(),
43+
};
44+
accessControlService = {
45+
applyFilters: jest.fn(),
46+
};
47+
48+
controller = new AdminController(
49+
{} as any,
50+
winningsRepo as any,
51+
tcMembersService as any,
52+
accessControlService as any,
53+
);
54+
});
55+
56+
it('includes the payment creator column for engagement payment exports only', async () => {
57+
accessControlService.applyFilters.mockResolvedValue({
58+
type: WinningsType.PAYMENT,
59+
} satisfies WinningRequestDto);
60+
winningsRepo.searchWinnings
61+
.mockResolvedValueOnce({
62+
data: {
63+
winnings: [
64+
{
65+
id: 'winning-1',
66+
winnerId: '1001',
67+
createdBy: '9001',
68+
origin: 'Topcoder',
69+
category: WinningsCategory.ENGAGEMENT_PAYMENT,
70+
title: 'Engagement payment',
71+
description: 'Monthly engagement payment',
72+
externalId: 'assignment-1',
73+
details: [
74+
{
75+
status: PaymentStatus.OWED,
76+
totalAmount: 1200,
77+
datePaid: new Date('2026-04-09T01:02:03.000Z'),
78+
billingAccount: '80001063',
79+
},
80+
],
81+
paymentStatus: {} as any,
82+
attributes: {},
83+
createdAt: new Date('2026-04-09T01:02:03.000Z'),
84+
updatedAt: new Date('2026-04-09T04:05:06.000Z'),
85+
releaseDate: new Date('2026-04-24T07:08:09.000Z'),
86+
},
87+
{
88+
id: 'winning-2',
89+
winnerId: '1002',
90+
createdBy: '9002',
91+
origin: 'Topcoder',
92+
category: WinningsCategory.ONE_OFF_PAYMENT,
93+
title: 'One-off payment',
94+
description: 'Non-engagement payment',
95+
externalId: 'task-1',
96+
details: [
97+
{
98+
status: PaymentStatus.PAID,
99+
totalAmount: 500,
100+
billingAccount: '80001064',
101+
},
102+
],
103+
paymentStatus: {} as any,
104+
attributes: {},
105+
createdAt: new Date('2026-04-08T01:02:03.000Z'),
106+
updatedAt: new Date('2026-04-08T04:05:06.000Z'),
107+
releaseDate: new Date('2026-04-23T07:08:09.000Z'),
108+
},
109+
],
110+
pagination: {
111+
totalItems: 2,
112+
totalPages: 1,
113+
pageSize: 1000,
114+
currentPage: 1,
115+
},
116+
},
117+
})
118+
.mockResolvedValueOnce({
119+
data: {
120+
winnings: [],
121+
pagination: {
122+
totalItems: 0,
123+
totalPages: 0,
124+
pageSize: 1000,
125+
currentPage: 2,
126+
},
127+
},
128+
});
129+
tcMembersService.getHandlesByUserIds.mockResolvedValue({
130+
'1001': 'winner-one',
131+
'1002': 'winner-two',
132+
'9001': 'creator-handle',
133+
});
134+
135+
const output = await controller.exportWinnings(
136+
{ type: WinningsType.PAYMENT } as WinningRequestDto,
137+
{ id: 'admin-user', roles: ['Payment Admin'] },
138+
);
139+
140+
expect(accessControlService.applyFilters).toHaveBeenCalledWith(
141+
'admin-user',
142+
['Payment Admin'],
143+
{
144+
type: WinningsType.PAYMENT,
145+
limit: undefined,
146+
offset: undefined,
147+
},
148+
);
149+
expect(winningsRepo.searchWinnings).toHaveBeenNthCalledWith(
150+
1,
151+
{
152+
type: WinningsType.PAYMENT,
153+
limit: 1000,
154+
offset: 0,
155+
},
156+
{
157+
includeCount: false,
158+
includePayoutStatus: false,
159+
latestPaymentOnly: true,
160+
},
161+
);
162+
expect(tcMembersService.getHandlesByUserIds).toHaveBeenCalledWith([
163+
'1001',
164+
'1002',
165+
'9001',
166+
]);
167+
168+
const [headerRow, engagementRow, nonEngagementRow] = output
169+
.trim()
170+
.split(/\r?\n/)
171+
.map((line) => line.split(','));
172+
173+
expect(headerRow).toEqual([
174+
'Winnings ID',
175+
'Winner ID',
176+
'Handle',
177+
'Origin',
178+
'Category',
179+
'Title',
180+
'Description',
181+
'External ID',
182+
'Status',
183+
'Total Amount',
184+
'Date Paid',
185+
'Created At',
186+
'Updated At',
187+
'Release Date',
188+
'Payment Creator',
189+
'Billing Account',
190+
]);
191+
expect(engagementRow[14]).toBe('creator-handle');
192+
expect(nonEngagementRow[14]).toBe('');
193+
});
194+
});

src/api/admin/admin.controller.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ import { AdminService } from './admin.service';
2929
import { ResponseDto, ResponseStatusType } from 'src/dto/api-response.dto';
3030
import { WinningAuditDto, AuditPayoutDto } from './dto/audit.dto';
3131

32-
import { WinningRequestDto, SearchWinningResult } from 'src/dto/winning.dto';
32+
import {
33+
WinningRequestDto,
34+
SearchWinningResult,
35+
WinningsCategory,
36+
} from 'src/dto/winning.dto';
3337
import { WinningsRepository } from '../repository/winnings.repo';
3438
import { WinningUpdateRequestDto } from './dto/winnings.dto';
3539
import { WinningPaymentDetailsDto } from './dto/payment-details.dto';
@@ -149,7 +153,7 @@ export class AdminController {
149153
@ApiOperation({
150154
summary: 'Export search winnings result in csv file format',
151155
description:
152-
'Roles: Payment Admin, Payment BA Admin, Engagement Payment Approver, Wipro TaaS Admin, Payment Editor, Payment Viewer',
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.',
153157
})
154158
@ApiBody({
155159
description: 'Winning request body',
@@ -207,10 +211,20 @@ export class AdminController {
207211
offset += AdminController.EXPORT_BATCH_SIZE;
208212
}
209213

210-
const handles = await this.tcMembersService.getHandlesByUserIds(
211-
winnings.map((d) => d.winnerId),
214+
const memberIds = Array.from(
215+
new Set([
216+
...winnings.map((item) => item.winnerId),
217+
...winnings
218+
.filter(
219+
(item) => item.category === WinningsCategory.ENGAGEMENT_PAYMENT,
220+
)
221+
.map((item) => item.createdBy?.trim())
222+
.filter((item): item is string => !!item),
223+
]),
212224
);
213225

226+
const handles = await this.tcMembersService.getHandlesByUserIds(memberIds);
227+
214228
const csvRes = winnings.map((item) => {
215229
const payment =
216230
item.details && item.details.length > 0 ? item.details[0] : null;
@@ -230,6 +244,12 @@ export class AdminController {
230244
createdAt: item.createdAt.toISOString(),
231245
updatedAt: item.updatedAt?.toISOString() ?? '',
232246
releaseDate: item.releaseDate?.toISOString() ?? '',
247+
paymentCreator:
248+
item.category === WinningsCategory.ENGAGEMENT_PAYMENT
249+
? (handles[item.createdBy?.trim() ?? ''] ??
250+
item.createdBy?.trim() ??
251+
'')
252+
: '',
233253
billingAccount: payment?.billingAccount,
234254
};
235255
});
@@ -251,6 +271,7 @@ export class AdminController {
251271
{ key: 'createdAt', header: 'Created At' },
252272
{ key: 'updatedAt', header: 'Updated At' },
253273
{ key: 'releaseDate', header: 'Release Date' },
274+
{ key: 'paymentCreator', header: 'Payment Creator' },
254275
{ key: 'billingAccount', header: 'Billing Account' },
255276
],
256277
});

0 commit comments

Comments
 (0)