Skip to content

Commit b9fe66b

Browse files
committed
PM-4822: add Payment Creator to wallet export CSV
What was broken The wallet-admin payments CSV download still did not include the Payment Creator column after the earlier fix. QA could see the creator in payment details, but the exported report remained unchanged. Root cause The previous fix only populated payment creator data for the payment-details endpoint. The CSV export path in `admin.controller.ts` assembled its own columns and rows and never added the creator field or resolved creator handles for engagement payments. What was changed Updated the winnings export controller to collect engagement payment creator member IDs, resolve their handles, and emit a Payment Creator column before Billing Account. The export leaves that column blank for non-engagement payment types. Any added/updated tests Added `src/api/admin/admin.controller.spec.ts` to verify the CSV header includes Payment Creator and that only engagement payment rows populate it.
1 parent ef5813c commit b9fe66b

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)