Skip to content

Commit 3066e58

Browse files
authored
Merge pull request #133 from topcoder-platform/develop
Prod - July release 2
2 parents 8ca7e4d + 2c3c6df commit 3066e58

4 files changed

Lines changed: 88 additions & 53 deletions

File tree

src/common/project-helper.js

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@ const errors = require("./errors");
99
const logger = require("./logger");
1010

1111
/**
12-
* Normalizes billing-account markup to the decimal format persisted on
12+
* Normalizes billing-account markup to the multiplier format persisted on
1313
* challenges.
1414
*
15-
* Legacy project-service responses can return whole percentage points (for
16-
* example `50` for 50%), while newer billing-account records use decimal
17-
* fractions (for example `0.58` for 58%). This helper preserves modern values
18-
* and converts only legacy percentages.
15+
* Billing-account records use the direct multiplier consumed by challenge,
16+
* finance, and billing-account ledger math. Values greater than `1` are valid
17+
* and must not be converted to percentage form.
1918
*
2019
* @param {unknown} rawMarkup Markup value returned by Projects API.
21-
* @returns {number|null} Decimal markup or `null` when the input is empty or invalid.
20+
* @returns {number|null} Billing markup multiplier or `null` when the input is empty or invalid.
2221
*/
2322
function normalizeBillingMarkup(rawMarkup) {
2423
if (_.isNil(rawMarkup) || rawMarkup === "") {
@@ -30,7 +29,7 @@ function normalizeBillingMarkup(rawMarkup) {
3029
return null;
3130
}
3231

33-
return markup > 1 ? markup / 100 : markup;
32+
return markup;
3433
}
3534

3635
/**
@@ -261,7 +260,7 @@ class ProjectHelper {
261260
* @param {string|number} params.billingAccountId Billing-account identifier.
262261
* @param {string} params.challengeId Challenge id to use as the external reference.
263262
* @param {number|string} params.memberPaymentAmount Challenge member-payment amount before markup.
264-
* @param {number|string|null|undefined} params.markup Billing-account markup as a decimal or percentage.
263+
* @param {number|string|null|undefined} params.markup Billing-account markup multiplier.
265264
* @returns {Promise<object>} Billing Accounts API lock response.
266265
* @throws {errors.BadRequestError} When required values are missing or invalid.
267266
* @throws {Error} When the Billing Accounts API rejects the lock request.

src/services/ChallengeService.js

Lines changed: 9 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1956,7 +1956,6 @@ async function searchChallenges(currentUser, criteria) {
19561956
const requestedMemberId = !_.isNil(criteria.memberId) ? _.toString(criteria.memberId) : null;
19571957
const currentUserMemberId =
19581958
currentUser && !_hasAdminRole && !_isMachineToken ? _.toString(currentUser.userId) : null;
1959-
const memberIdForTaskFilter = requestedMemberId || currentUserMemberId;
19601959
const isSelfMemberSearch = Boolean(
19611960
requestedMemberId && currentUserMemberId && requestedMemberId === currentUserMemberId,
19621961
);
@@ -2078,27 +2077,11 @@ async function searchChallenges(currentUser, criteria) {
20782077
});
20792078
}
20802079

2081-
// FIXME: Tech Debt
2082-
let excludeTasks = true;
2083-
if (requestedMemberId) {
2084-
// When we already restrict the result set to a specific member,
2085-
// rerunning the generic task visibility filter is redundant.
2086-
excludeTasks = false;
2087-
} else if (
2088-
currentUser &&
2089-
(_hasAdminRole || _isMachineToken || hasProjectManagerAccessForSearch)
2090-
) {
2091-
// if you're an admin or m2m, security rules wont be applied
2092-
excludeTasks = false;
2093-
}
2094-
20952080
/**
2096-
* For non-authenticated users:
2097-
* - Only unassigned tasks will be returned
2098-
* For authenticated users (non-admin):
2099-
* - Only unassigned tasks and tasks assigned to the logged in user will be returned
2100-
* For admins/m2m:
2101-
* - All tasks will be returned
2081+
* Task challenges are visible to ordinary authenticated users only when they
2082+
* have a resource on the task. Anonymous users never receive tasks. Admin,
2083+
* machine-token and project-manager searches retain their operational access.
2084+
* A requested memberId narrows results but never grants the caller task access.
21022085
*/
21032086
if (currentUser && (_hasAdminRole || _isMachineToken)) {
21042087
// For admins/m2m, allow filtering based on task properties
@@ -2117,28 +2100,16 @@ async function searchChallenges(currentUser, criteria) {
21172100
taskMemberId: criteria.taskMemberId,
21182101
});
21192102
}
2120-
} else if (excludeTasks) {
2121-
const taskFilter = [];
2122-
if (memberIdForTaskFilter) {
2103+
} else if (!hasProjectManagerAccessForSearch) {
2104+
const taskFilter = [{ taskIsTask: false }];
2105+
if (currentUserMemberId) {
21232106
taskFilter.push({
2107+
taskIsTask: true,
21242108
memberAccesses: {
2125-
some: { memberId: memberIdForTaskFilter },
2109+
some: { memberId: currentUserMemberId },
21262110
},
21272111
});
21282112
}
2129-
taskFilter.push({
2130-
taskIsTask: false,
2131-
});
2132-
taskFilter.push({
2133-
taskIsTask: true,
2134-
taskIsAssigned: false,
2135-
taskMemberId: null,
2136-
});
2137-
if (currentUser && !_hasAdminRole && !_isMachineToken) {
2138-
taskFilter.push({
2139-
taskMemberId: currentUser.userId,
2140-
});
2141-
}
21422113
prismaFilter.where.AND.push({
21432114
OR: taskFilter,
21442115
});

test/unit/ChallengeService.test.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,6 +1006,71 @@ describe("challenge service unit tests", () => {
10061006
should.equal(result.numOfRegistrants, 0);
10071007
});
10081008

1009+
it("search challenges hides unassigned tasks from anonymous users", async () => {
1010+
await prisma.challenge.update({
1011+
where: { id: data.taskChallenge.id },
1012+
data: {
1013+
taskIsAssigned: false,
1014+
taskMemberId: null,
1015+
},
1016+
});
1017+
1018+
try {
1019+
const result = await service.searchChallenges(undefined, {
1020+
id: data.taskChallenge.id,
1021+
});
1022+
1023+
should.equal(result.total, 0);
1024+
should.equal(result.result.length, 0);
1025+
} finally {
1026+
await prisma.challenge.update({
1027+
where: { id: data.taskChallenge.id },
1028+
data: { taskIsAssigned: true },
1029+
});
1030+
}
1031+
});
1032+
1033+
it("search challenges uses the caller resource association for task visibility", async () => {
1034+
const originalGetCompleteUserGroupTreeIds = helper.getCompleteUserGroupTreeIds;
1035+
const originalMemberChallengeAccessFindMany = prisma.memberChallengeAccess.findMany;
1036+
const originalChallengeFindMany = prisma.challenge.findMany;
1037+
let capturedWhere;
1038+
1039+
helper.getCompleteUserGroupTreeIds = async () => [];
1040+
prisma.memberChallengeAccess.findMany = async () => [{ challengeId: data.taskChallenge.id }];
1041+
prisma.challenge.findMany = async (query) => {
1042+
capturedWhere = query.where;
1043+
return [];
1044+
};
1045+
1046+
try {
1047+
const result = await service.searchChallenges(
1048+
{ roles: ["Topcoder User"], userId: "caller-resource-id" },
1049+
{ memberId: "different-member-id" },
1050+
);
1051+
1052+
should.equal(result.total, 0);
1053+
const taskVisibilityFilter = capturedWhere.AND.find(
1054+
(filter) => filter.OR && filter.OR.some((condition) => condition.taskIsTask === false),
1055+
);
1056+
taskVisibilityFilter.should.deep.equal({
1057+
OR: [
1058+
{ taskIsTask: false },
1059+
{
1060+
taskIsTask: true,
1061+
memberAccesses: {
1062+
some: { memberId: "caller-resource-id" },
1063+
},
1064+
},
1065+
],
1066+
});
1067+
} finally {
1068+
helper.getCompleteUserGroupTreeIds = originalGetCompleteUserGroupTreeIds;
1069+
prisma.memberChallengeAccess.findMany = originalMemberChallengeAccessFindMany;
1070+
prisma.challenge.findMany = originalChallengeFindMany;
1071+
}
1072+
});
1073+
10091074
it("search challenges sorts status alphabetically for member and non-member searches", async () => {
10101075
const statusChallenges = [
10111076
{

test/unit/project-helper.test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,21 +41,21 @@ describe('project helper unit tests', () => {
4141
})
4242
})
4343

44-
it('converts legacy whole-percentage billing markup to decimal format', async () => {
44+
it('preserves billing markup multipliers greater than one', async () => {
4545
m2mHelper.getM2MToken = async () => 'test-token'
4646
axios.get = async () => ({
4747
status: 200,
4848
data: {
4949
tcBillingAccountId: '80004217',
50-
markup: 58
50+
markup: 1.2229
5151
}
5252
})
5353

5454
const result = await projectHelper.getProjectBillingInformation(123)
5555

5656
result.should.deep.equal({
5757
billingAccountId: '80004217',
58-
markup: 0.58
58+
markup: 1.2229
5959
})
6060
})
6161

@@ -81,21 +81,21 @@ describe('project helper unit tests', () => {
8181
const result = await projectHelper.lockChallengeBillingAccountAmount({
8282
billingAccountId: '80001012',
8383
challengeId: 'challenge-id',
84-
memberPaymentAmount: 1000,
85-
markup: 0.1
84+
memberPaymentAmount: 394,
85+
markup: 1.2229
8686
})
8787

8888
patchUrl.should.equal('http://localhost:4000/v6/billing-accounts/80001012/lock-amount')
8989
patchBody.should.deep.equal({
90-
amount: 1100,
90+
amount: 875.8226,
9191
challengeId: 'challenge-id',
9292
externalId: 'challenge-id',
9393
externalType: 'CHALLENGE'
9494
})
9595
patchHeaders.should.deep.equal({ Authorization: 'Bearer test-token' })
9696
result.should.deep.equal({
9797
externalId: 'challenge-id',
98-
amount: 1100
98+
amount: 875.8226
9999
})
100100
})
101101
})

0 commit comments

Comments
 (0)