Skip to content

Commit ef5813c

Browse files
committed
Additional fields for wallet-admin to show expanded engagement details
1 parent b9610b3 commit ef5813c

5 files changed

Lines changed: 104 additions & 0 deletions

File tree

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ jest.mock('src/shared/access-control/access-control.service', () => ({
3636
AccessControlService: class {},
3737
}));
3838

39+
jest.mock('src/shared/topcoder/members.service', () => ({
40+
TopcoderMembersService: class {},
41+
}));
42+
3943
import { AdminService } from './admin.service';
4044

4145
describe('AdminService', () => {
@@ -54,6 +58,9 @@ describe('AdminService', () => {
5458
getAssignmentContextById: jest.Mock;
5559
getEngagementById: jest.Mock;
5660
};
61+
let tcMembersService: {
62+
getHandlesByUserIds: jest.Mock;
63+
};
5764

5865
beforeEach(() => {
5966
prisma = {
@@ -70,20 +77,27 @@ describe('AdminService', () => {
7077
getAssignmentContextById: jest.fn(),
7178
getEngagementById: jest.fn(),
7279
};
80+
tcMembersService = {
81+
getHandlesByUserIds: jest.fn().mockResolvedValue({
82+
'654321': 'payment-manager',
83+
}),
84+
};
7385

7486
service = new AdminService(
7587
prisma as any,
7688
paymentsService as any,
7789
baService as any,
7890
accessControlService as any,
7991
topcoderEngagementsService as any,
92+
tcMembersService as any,
8093
);
8194
});
8295

8396
it('returns work-log and engagement details for engagement payments', async () => {
8497
prisma.winnings.findFirst.mockResolvedValue({
8598
winning_id: 'winning-1',
8699
category: 'ENGAGEMENT_PAYMENT',
100+
created_by: '654321',
87101
external_id: 'assignment-1',
88102
attributes: {
89103
hoursWorked: 12.5,
@@ -117,6 +131,9 @@ describe('AdminService', () => {
117131
expect(
118132
topcoderEngagementsService.getAssignmentContextById,
119133
).toHaveBeenCalledWith('assignment-1');
134+
expect(tcMembersService.getHandlesByUserIds).toHaveBeenCalledWith([
135+
'654321',
136+
]);
120137
expect(result.data).toEqual({
121138
engagementDetails: {
122139
assignmentId: 'assignment-1',
@@ -130,6 +147,7 @@ describe('AdminService', () => {
130147
ratePerHour: '75.50',
131148
standardHoursPerWeek: 40,
132149
},
150+
paymentCreatorHandle: 'payment-manager',
133151
workLog: {
134152
hoursWorked: 12.5,
135153
remarks: 'Completed sprint support and bug triage.',
@@ -141,6 +159,7 @@ describe('AdminService', () => {
141159
prisma.winnings.findFirst.mockResolvedValue({
142160
winning_id: 'winning-1',
143161
category: 'ENGAGEMENT_PAYMENT',
162+
created_by: '654321',
144163
external_id: 'assignment-1',
145164
attributes: {
146165
hoursWorked: 8,
@@ -158,6 +177,7 @@ describe('AdminService', () => {
158177
);
159178

160179
expect(result.data).toEqual({
180+
paymentCreatorHandle: 'payment-manager',
161181
workLog: {
162182
hoursWorked: 8,
163183
remarks: 'Weekly support work.',
@@ -170,6 +190,7 @@ describe('AdminService', () => {
170190
winning_id: 'winning-1',
171191
winner_id: '123456',
172192
category: 'ENGAGEMENT_PAYMENT',
193+
created_by: '654321',
173194
external_id: 'engagement-1',
174195
attributes: {
175196
hoursWorked: 10,
@@ -222,13 +243,44 @@ describe('AdminService', () => {
222243
ratePerHour: '82.50',
223244
standardHoursPerWeek: 35,
224245
},
246+
paymentCreatorHandle: 'payment-manager',
225247
workLog: {
226248
hoursWorked: 10,
227249
remarks: 'Covered support hours.',
228250
},
229251
});
230252
});
231253

254+
it('falls back to the creator id when the handle lookup misses', async () => {
255+
tcMembersService.getHandlesByUserIds.mockResolvedValue({});
256+
prisma.winnings.findFirst.mockResolvedValue({
257+
winning_id: 'winning-1',
258+
category: 'ENGAGEMENT_PAYMENT',
259+
created_by: '654321',
260+
external_id: 'assignment-1',
261+
attributes: {
262+
hoursWorked: 8,
263+
},
264+
});
265+
topcoderEngagementsService.getAssignmentContextById.mockRejectedValue(
266+
new Error('upstream unavailable'),
267+
);
268+
269+
const result = await service.getWinningPaymentDetails(
270+
'winning-1',
271+
'123456',
272+
['Payment Admin'],
273+
);
274+
275+
expect(result.data).toEqual({
276+
paymentCreatorHandle: '654321',
277+
workLog: {
278+
hoursWorked: 8,
279+
remarks: undefined,
280+
},
281+
});
282+
});
283+
232284
it('prefers a numeric assignmentId stored in winning attributes', async () => {
233285
prisma.winnings.findFirst.mockResolvedValue({
234286
winning_id: 'winning-1',

src/api/admin/admin.service.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
TopcoderEngagementDetails,
2323
TopcoderEngagementsService,
2424
} from 'src/shared/topcoder/engagements.service';
25+
import { TopcoderMembersService } from 'src/shared/topcoder/members.service';
2526
import { WinningPaymentDetailsDto } from './dto/payment-details.dto';
2627

2728
/**
@@ -41,6 +42,7 @@ export class AdminService {
4142
private readonly baService: BillingAccountsService,
4243
private readonly accessControlService: AccessControlService,
4344
private readonly topcoderEngagementsService: TopcoderEngagementsService,
45+
private readonly tcMembersService: TopcoderMembersService,
4446
) {}
4547

4648
async verifyUserAccessToWinning(
@@ -127,6 +129,42 @@ export class AdminService {
127129
return undefined;
128130
}
129131

132+
/**
133+
* Resolves the wallet-admin payment creator into a handle for display.
134+
*
135+
* @param createdBy raw `created_by` value stored on the winnings row.
136+
* @returns The resolved Topcoder handle, or the original identifier when the
137+
* handle lookup fails.
138+
* @throws This helper does not throw.
139+
*/
140+
private async getPaymentCreatorHandle(
141+
createdBy: unknown,
142+
): Promise<string | undefined> {
143+
if (typeof createdBy !== 'string') {
144+
return undefined;
145+
}
146+
147+
const paymentCreatorId = createdBy.trim();
148+
if (!paymentCreatorId) {
149+
return undefined;
150+
}
151+
152+
try {
153+
const handles = await this.tcMembersService.getHandlesByUserIds([
154+
paymentCreatorId,
155+
]);
156+
157+
return handles[paymentCreatorId] ?? paymentCreatorId;
158+
} catch (error) {
159+
this.logger.warn(
160+
`Failed to resolve payment creator handle for winnings creator ${paymentCreatorId}`,
161+
error instanceof Error ? error.message : error,
162+
);
163+
164+
return paymentCreatorId;
165+
}
166+
}
167+
130168
/**
131169
* Finds the engagement assignment that best matches the current winning.
132170
*
@@ -692,8 +730,12 @@ export class AdminService {
692730
hoursWorked: this.getNumericAttribute(winning.attributes, 'hoursWorked'),
693731
remarks: this.getStringAttribute(winning.attributes, 'remarks'),
694732
};
733+
const paymentCreatorHandle = await this.getPaymentCreatorHandle(
734+
winning.created_by,
735+
);
695736

696737
result.data = {
738+
paymentCreatorHandle,
697739
workLog,
698740
};
699741

src/api/admin/dto/payment-details.dto.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ export class PaymentWorkLogDto {
7777
}
7878

7979
export class WinningPaymentDetailsDto {
80+
@ApiPropertyOptional({
81+
description:
82+
'The Topcoder handle of the user who created the payment record',
83+
example: 'pm_admin',
84+
})
85+
paymentCreatorHandle?: string;
86+
8087
@ApiPropertyOptional({
8188
description:
8289
'Engagement and assignment details when the winning is an engagement payment',

src/api/repository/winnings.repo.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,7 @@ export class WinningsRepository {
286286
id: item.winning_id,
287287
type: item.type,
288288
winnerId: item.winner_id,
289+
createdBy: item.payment?.[0]?.created_by ?? item.created_by,
289290
origin: item.origin?.origin_name,
290291
category: (item.category ?? '') as WinningsCategory,
291292
title: item.title as string,
@@ -383,6 +384,7 @@ export class WinningsRepository {
383384
id: item.winning_id,
384385
type: item.type,
385386
winnerId: item.winner_id,
387+
createdBy: item.payment?.[0]?.created_by ?? item.created_by,
386388
origin: item.origin?.origin_name,
387389
category: (item.category ?? '') as WinningsCategory,
388390
title: item.title as string,

src/dto/winning.dto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export class WinningDto {
113113
id: string;
114114
type: string;
115115
winnerId: string;
116+
createdBy?: string;
116117
origin?: string;
117118
category: WinningsCategory;
118119
title: string;

0 commit comments

Comments
 (0)