Skip to content

Commit 6f0c07a

Browse files
Merge pull request #185 from topcoder-platform/PM-4981
Pm 4981 Add payment approver for engagements
2 parents 9d4f7bc + 2570f5d commit 6f0c07a

4 files changed

Lines changed: 96 additions & 27 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ workflows:
7676
- dev
7777
- engagements
7878
- PM-4789_payment-migrations
79+
- PM-4981
7980
- 'build-prod':
8081
context: org-global
8182
filters:

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,48 @@ describe('AdminService', () => {
151151
);
152152
});
153153

154+
it('returns paymentApproverHandle for engagement payments from audit trail', async () => {
155+
prisma.winnings.findFirst.mockResolvedValue({
156+
winning_id: 'winning-1',
157+
category: 'ENGAGEMENT_PAYMENT',
158+
created_by: '654321',
159+
external_id: 'assignment-1',
160+
attributes: {},
161+
});
162+
topcoderEngagementsService.getAssignmentContextById.mockResolvedValue({
163+
assignmentId: 'assignment-1',
164+
engagementId: 'engagement-1',
165+
engagementTitle: 'May 19 pvt eng',
166+
projectId: '100575',
167+
projectName: 'Ai Reviewer Wf testing',
168+
ratePerHour: '2.99',
169+
standardHoursPerWeek: 11,
170+
startDate: '2026-05-23T12:00:00.000Z',
171+
durationMonths: 2,
172+
});
173+
prisma.audit.findMany.mockResolvedValue([
174+
{
175+
id: 'audit-1',
176+
winnings_id: 'winning-1',
177+
user_id: '654321',
178+
action: 'status updated from ON_HOLD_ADMIN to OWED',
179+
note: null,
180+
created_at: new Date(),
181+
},
182+
]);
183+
184+
const result = await service.getWinningPaymentDetails(
185+
'winning-1',
186+
'123456',
187+
['Payment Admin'],
188+
);
189+
190+
expect(result.data?.engagementDetails?.paymentApproverHandle).toBe(
191+
'payment-manager',
192+
);
193+
expect(result.data?.engagementDetails?.assignmentId).toBe('assignment-1');
194+
});
195+
154196
it('returns work-log and engagement details for engagement payments', async () => {
155197
prisma.winnings.findFirst.mockResolvedValue({
156198
winning_id: 'winning-1',

src/api/admin/admin.service.ts

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,6 +1176,38 @@ export class AdminService {
11761176
return result;
11771177
}
11781178

1179+
/**
1180+
* Resolves the payment approver handle from the audit entry that moved the
1181+
* winning from `ON_HOLD_ADMIN` to `OWED`.
1182+
*/
1183+
private async resolvePaymentApproverHandleFromAudit(
1184+
winningsId: string,
1185+
): Promise<string | undefined> {
1186+
try {
1187+
const audits = await this.prisma.audit.findMany({
1188+
where: { winnings_id: winningsId },
1189+
orderBy: { created_at: 'desc' },
1190+
take: 200,
1191+
});
1192+
const approverAudit = audits.find(
1193+
(audit) =>
1194+
typeof audit.action === 'string' &&
1195+
audit.action.includes('ON_HOLD_ADMIN') &&
1196+
audit.action.includes('OWED') &&
1197+
audit.action.indexOf('ON_HOLD_ADMIN') < audit.action.indexOf('OWED'),
1198+
);
1199+
1200+
if (!approverAudit?.user_id) {
1201+
return undefined;
1202+
}
1203+
1204+
return this.getPaymentCreatorHandle(String(approverAudit.user_id));
1205+
} catch {
1206+
// approver is optional — ignore failures
1207+
return undefined;
1208+
}
1209+
}
1210+
11791211
private async buildTaskDetails(
11801212
winningsId: string,
11811213
externalId: string | undefined,
@@ -1209,27 +1241,8 @@ export class AdminService {
12091241
}
12101242
}
12111243

1212-
try {
1213-
const audits = await this.prisma.audit.findMany({
1214-
where: { winnings_id: winningsId },
1215-
orderBy: { created_at: 'desc' },
1216-
take: 200,
1217-
});
1218-
const approverAudit = audits.find(
1219-
(a) =>
1220-
typeof a.action === 'string' &&
1221-
a.action.includes('ON_HOLD_ADMIN') &&
1222-
a.action.includes('OWED') &&
1223-
a.action.indexOf('ON_HOLD_ADMIN') < a.action.indexOf('OWED'),
1224-
);
1225-
if (approverAudit?.user_id) {
1226-
const userId = String(approverAudit.user_id);
1227-
const handle = await this.getPaymentCreatorHandle(userId);
1228-
taskDetails.paymentApproverHandle = handle ?? undefined;
1229-
}
1230-
} catch {
1231-
// approver is optional — ignore failures
1232-
}
1244+
taskDetails.paymentApproverHandle =
1245+
await this.resolvePaymentApproverHandleFromAudit(winningsId);
12331246

12341247
return taskDetails;
12351248
}
@@ -1283,13 +1296,15 @@ export class AdminService {
12831296
return result;
12841297
}
12851298

1299+
const paymentApproverHandle =
1300+
await this.resolvePaymentApproverHandleFromAudit(winningsId);
1301+
12861302
if (assignmentLookupId) {
12871303
try {
12881304
const assignmentContext =
12891305
await this.topcoderEngagementsService.getAssignmentContextById(
12901306
assignmentLookupId,
12911307
);
1292-
12931308
result.data.engagementDetails = {
12941309
assignmentId: assignmentContext.assignmentId,
12951310
engagementId: assignmentContext.engagementId,
@@ -1304,6 +1319,7 @@ export class AdminService {
13041319
standardHoursPerWeek:
13051320
assignmentContext.standardHoursPerWeek ?? undefined,
13061321
otherRemarks: assignmentContext.otherRemarks ?? undefined,
1322+
paymentApproverHandle,
13071323
};
13081324

13091325
return result;
@@ -1328,11 +1344,14 @@ export class AdminService {
13281344
assignmentId,
13291345
);
13301346

1331-
result.data.engagementDetails = this.buildEngagementDetailsFromEngagement(
1332-
engagement,
1333-
assignment,
1334-
assignmentId,
1335-
);
1347+
result.data.engagementDetails = {
1348+
...this.buildEngagementDetailsFromEngagement(
1349+
engagement,
1350+
assignment,
1351+
assignmentId,
1352+
),
1353+
paymentApproverHandle,
1354+
};
13361355
} catch (error) {
13371356
this.logger.warn(
13381357
`Failed to enrich winning ${winningsId} with engagement details`,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ export class PaymentEngagementDetailsDto {
6060
example: 'Complete onboarding within the first week.',
6161
})
6262
otherRemarks?: string;
63+
64+
@ApiPropertyOptional({
65+
description:
66+
'The Topcoder handle of the wallet-admin user who approved this payment',
67+
example: 'payment_approver_handle',
68+
})
69+
paymentApproverHandle?: string;
6370
}
6471

6572
export class PaymentWorkLogDto {

0 commit comments

Comments
 (0)