Skip to content

Commit 3e1bf7a

Browse files
authored
Merge pull request #147 from topcoder-platform/dev
[PROD RELEASE] - Update & fixes
2 parents bda7ea2 + 258d602 commit 3e1bf7a

14 files changed

Lines changed: 314 additions & 18 deletions

File tree

.circleci/config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ workflows:
7575
only:
7676
- dev
7777
- engagements
78+
- PM-4165_allow-editing-credited-payments
7879
- 'build-prod':
7980
context: org-global
8081
filters:

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@
8888
"**/*.(t|j)s"
8989
],
9090
"coverageDirectory": "../coverage",
91-
"testEnvironment": "node"
91+
"testEnvironment": "node",
92+
"moduleNameMapper": {
93+
"^src/(.*)$": "<rootDir>/$1"
94+
}
9295
},
9396
"prisma": {
9497
"seed": "ts-node prisma/seed.ts"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
CREATE INDEX idx_winnings_type_created_at ON winnings USING btree (type, created_at DESC);

prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ model winnings {
147147
148148
@@index([category, created_at(sort: Desc)], map: "idx_winnings_category_created_at")
149149
@@index([created_at(sort: Desc)], map: "idx_winnings_created_at")
150+
@@index([type, created_at(sort: Desc)], map: "idx_winnings_type_created_at")
150151
@@index([winner_id, created_at(sort: Desc)], map: "idx_winnings_winner_created_at")
151152
@@index([winner_id, winning_id], map: "idx_winnings_winner_id_only")
152153
}

src/api/admin/admin.controller.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ export class AdminController {
140140
{
141141
includeCount: false,
142142
includePayoutStatus: false,
143+
latestPaymentOnly: true,
143144
},
144145
);
145146

src/api/admin/admin.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,8 @@ export class AdminService {
368368
// Update payment amount if requested
369369
if (
370370
body.paymentAmount !== undefined &&
371-
(payment.payment_status === PaymentStatus.OWED ||
371+
(payment.payment_status === PaymentStatus.CREDITED ||
372+
payment.payment_status === PaymentStatus.OWED ||
372373
payment.payment_status === PaymentStatus.ON_HOLD ||
373374
payment.payment_status === PaymentStatus.ON_HOLD_ADMIN)
374375
) {
@@ -646,6 +647,7 @@ export class AdminService {
646647
version: currentVersion,
647648
payment_status: {
648649
in: [
650+
PaymentStatus.CREDITED,
649651
PaymentStatus.OWED,
650652
PaymentStatus.ON_HOLD,
651653
PaymentStatus.ON_HOLD_ADMIN,
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
jest.mock('src/shared/global', () => ({
2+
Logger: class {
3+
debug = jest.fn();
4+
5+
error = jest.fn();
6+
7+
info = jest.fn();
8+
9+
log = jest.fn();
10+
11+
warn = jest.fn();
12+
},
13+
}));
14+
15+
jest.mock('src/config', () => ({
16+
ENV_CONFIG: {
17+
TOPCODER_API_V6_BASE_URL: 'https://api.topcoder-dev.com/v6',
18+
},
19+
}));
20+
21+
import { ChallengePaymentsService } from './challenge-payments.service';
22+
23+
describe('ChallengePaymentsService', () => {
24+
let findManyMock: jest.Mock;
25+
let m2mFetchMock: jest.Mock;
26+
let service: ChallengePaymentsService;
27+
28+
beforeEach(() => {
29+
findManyMock = jest.fn().mockResolvedValue([]);
30+
m2mFetchMock = jest.fn();
31+
32+
service = new ChallengePaymentsService(
33+
{
34+
winnings: {
35+
findMany: findManyMock,
36+
},
37+
} as any,
38+
{
39+
m2mFetch: m2mFetchMock,
40+
} as any,
41+
);
42+
});
43+
44+
it('returns all challenge payments for managers', async () => {
45+
m2mFetchMock
46+
.mockResolvedValueOnce([
47+
{
48+
memberHandle: 'manager-user',
49+
memberId: '123',
50+
roleId: 'manager-role-id',
51+
},
52+
])
53+
.mockResolvedValueOnce([
54+
{
55+
fullWriteAccess: false,
56+
id: 'manager-role-id',
57+
name: 'Manager',
58+
},
59+
]);
60+
61+
await service.listChallengePayments({
62+
auth0User: { roles: ['Topcoder User'] },
63+
challengeId: 'challenge-id',
64+
isMachineToken: false,
65+
requestUserId: '123',
66+
winnerOnly: false,
67+
});
68+
69+
const where = findManyMock.mock.calls[0][0].where;
70+
expect(where.winner_id).toBeUndefined();
71+
});
72+
73+
it('keeps winner filtering for users without challenge-wide access', async () => {
74+
m2mFetchMock
75+
.mockResolvedValueOnce([
76+
{
77+
memberHandle: 'reviewer-user',
78+
memberId: '456',
79+
roleId: 'reviewer-role-id',
80+
},
81+
])
82+
.mockResolvedValueOnce([
83+
{
84+
fullWriteAccess: false,
85+
id: 'reviewer-role-id',
86+
name: 'Reviewer',
87+
},
88+
]);
89+
90+
await service.listChallengePayments({
91+
auth0User: { roles: ['Topcoder User'] },
92+
challengeId: 'challenge-id',
93+
isMachineToken: false,
94+
requestUserId: '456',
95+
winnerOnly: false,
96+
});
97+
98+
const where = findManyMock.mock.calls[0][0].where;
99+
expect(where.winner_id).toBe('456');
100+
});
101+
});

src/api/challenge-payments/challenge-payments.service.ts

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,16 +101,17 @@ export class ChallengePaymentsService {
101101
}
102102

103103
try {
104-
const isCopilot = await this.isCopilotForChallenge(
105-
challengeId,
106-
requestUserId,
107-
);
108-
if (isCopilot) {
104+
const hasChallengeWideAccess =
105+
await this.hasChallengeWideAccessForChallenge(
106+
challengeId,
107+
requestUserId,
108+
);
109+
if (hasChallengeWideAccess) {
109110
allowAllForChallenge = true;
110111
}
111112
} catch (error) {
112113
this.logger.warn(
113-
`Failed to verify copilot status for user ${requestUserId} on challenge ${challengeId}`,
114+
`Failed to verify challenge-wide payment access for user ${requestUserId} on challenge ${challengeId}`,
114115
error instanceof Error ? error.message : error,
115116
);
116117
}
@@ -150,7 +151,11 @@ export class ChallengePaymentsService {
150151
roles.push(String(role));
151152
}
152153
});
153-
} else if (value) {
154+
} else if (
155+
typeof value === 'string' ||
156+
typeof value === 'number' ||
157+
typeof value === 'boolean'
158+
) {
154159
roles.push(String(value));
155160
}
156161
});
@@ -165,7 +170,7 @@ export class ChallengePaymentsService {
165170
return roles;
166171
}
167172

168-
private async isCopilotForChallenge(
173+
private async hasChallengeWideAccessForChallenge(
169174
challengeId: string,
170175
userId: string,
171176
): Promise<boolean> {
@@ -181,19 +186,26 @@ export class ChallengePaymentsService {
181186
return false;
182187
}
183188

184-
const copilotRoleIds = new Set(
189+
const challengeWideRoleIds = new Set(
185190
resourceRoles
186-
?.filter((role) =>
187-
role?.name ? role.name.toLowerCase().includes('copilot') : false,
191+
?.filter(
192+
(role) =>
193+
role?.fullWriteAccess === true ||
194+
(role?.name
195+
? role.name.toLowerCase().includes('copilot') ||
196+
role.name.toLowerCase().includes('manager')
197+
: false),
188198
)
189199
.map((role) => role.id),
190200
);
191201

192-
if (copilotRoleIds.size === 0) {
202+
if (challengeWideRoleIds.size === 0) {
193203
return false;
194204
}
195205

196-
return resources.some((resource) => copilotRoleIds.has(resource.roleId));
206+
return resources.some((resource) =>
207+
challengeWideRoleIds.has(resource.roleId),
208+
);
197209
}
198210

199211
private async fetchWinnings(
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { ChallengeStatuses } from '../../dto/challenge.dto';
2+
3+
jest.mock('src/config', () => ({
4+
ENV_CONFIG: {
5+
TGBillingAccounts: [],
6+
TOPCODER_API_V6_BASE_URL: 'https://api.topcoder-dev.com/v6',
7+
},
8+
}));
9+
10+
jest.mock('src/shared/global', () => ({
11+
Logger: class {
12+
debug = jest.fn();
13+
14+
error = jest.fn();
15+
16+
info = jest.fn();
17+
18+
log = jest.fn();
19+
20+
warn = jest.fn();
21+
},
22+
}));
23+
24+
import { ChallengesService } from './challenges.service';
25+
26+
describe('ChallengesService', () => {
27+
it('skips creating payments for fun challenges', async () => {
28+
const prisma = {
29+
challenge_lock: {
30+
create: jest.fn(),
31+
deleteMany: jest.fn(),
32+
},
33+
};
34+
const service = new ChallengesService(
35+
prisma as any,
36+
{} as any,
37+
{} as any,
38+
{} as any,
39+
{} as any,
40+
);
41+
const challenge = {
42+
funChallenge: true,
43+
id: '11111111-1111-1111-1111-111111111111',
44+
name: 'MM 163',
45+
status: ChallengeStatuses.Completed,
46+
};
47+
const createPaymentsSpy = jest
48+
.spyOn(service as any, 'createPayments')
49+
.mockResolvedValue(undefined);
50+
51+
jest.spyOn(service, 'getChallenge').mockResolvedValue(challenge as any);
52+
53+
await service.generateChallengePayments(
54+
'11111111-1111-1111-1111-111111111111',
55+
'test-user',
56+
);
57+
58+
expect(createPaymentsSpy).not.toHaveBeenCalled();
59+
expect(prisma.challenge_lock.create).not.toHaveBeenCalled();
60+
});
61+
});

src/api/challenges/challenges.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ interface PaymentPayload {
4141
amount: number;
4242
userId: string;
4343
type: WinningsCategory;
44-
currency: string;
44+
currency: PrizeType;
4545
description?: string;
4646
}
4747

@@ -566,6 +566,13 @@ export class ChallengesService {
566566
throw new Error("Challenge isn't in a payable status!");
567567
}
568568

569+
if (challenge.funChallenge === true) {
570+
this.logger.log(
571+
`Skipping payment generation for fun challenge ${challenge.id} (${challenge.name}).`,
572+
);
573+
return;
574+
}
575+
569576
// need to read for update (LOCK the rows)
570577
this.logger.log(`Attempting to acquire lock for challenge ${challenge.id}`);
571578
try {

0 commit comments

Comments
 (0)