Skip to content

Commit efde866

Browse files
committed
Better handling of phase manipulation
1 parent 5e8acd1 commit efde866

File tree

6 files changed

+297
-13
lines changed

6 files changed

+297
-13
lines changed

app.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,19 +37,6 @@ process.on("unhandledRejection", (reason, promise) => {
3737
} catch (_) {}
3838
});
3939

40-
// Allow on-demand diagnostic reports (e.g., docker/ecs kill -s SIGUSR2 <container>)
41-
process.on("SIGUSR2", () => {
42-
try {
43-
if (process.report && typeof process.report.writeReport === "function") {
44-
const reportPath = process.report.writeReport();
45-
if (reportPath) logger.warn(`SIGUSR2 received. Diagnostic report: ${reportPath}`);
46-
} else {
47-
logger.warn("SIGUSR2 received, but process.report is not available.");
48-
}
49-
} catch (err) {
50-
logger.error("Error generating diagnostic report on SIGUSR2:", err);
51-
}
52-
});
5340
const interceptor = require("express-interceptor");
5441
const fileUpload = require("express-fileupload");
5542
const YAML = require("yamljs");

config/default.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ module.exports = {
5353
TERMS_API_URL: process.env.TERMS_API_URL || "http://localhost:4000/v5/terms",
5454
CUSTOMER_PAYMENTS_URL:
5555
process.env.CUSTOMER_PAYMENTS_URL || "https://api.topcoder-dev.com/v5/customer-payments",
56+
FINANCE_API_URL: process.env.FINANCE_API_URL || "http://localhost:8080",
5657
CHALLENGE_MIGRATION_APP_URL:
5758
process.env.CHALLENGE_MIGRATION_APP_URL || "https://api.topcoder.com/v5/challenge-migration",
5859
// copilot resource role ids allowed to upload attachment

src/common/helper.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,41 @@ async function cancelPayment(paymentId) {
461461
}
462462
}
463463

464+
/**
465+
* Generate payments for challenge resources via finance API
466+
* @param {String|Number} challengeId the challenge id
467+
* @returns {Boolean} true if the request succeeds, false otherwise
468+
*/
469+
async function generateChallengePayments(challengeId) {
470+
if (!config.FINANCE_API_URL) {
471+
logger.warn("helper.generateChallengePayments: FINANCE_API_URL not configured");
472+
return false;
473+
}
474+
475+
const token = await m2mHelper.getM2MToken();
476+
const url = `${config.FINANCE_API_URL}/challenges/${challengeId}`;
477+
logger.debug(`helper.generateChallengePayments: POST ${url}`);
478+
479+
try {
480+
const res = await axios.post(
481+
url,
482+
{},
483+
{
484+
headers: { Authorization: `Bearer ${token}` },
485+
}
486+
);
487+
logger.debug(`helper.generateChallengePayments: response status ${res.status}`);
488+
return res.status >= 200 && res.status < 300;
489+
} catch (err) {
490+
logger.debug(
491+
`helper.generateChallengePayments: error for challenge ${challengeId} - status ${
492+
_.get(err, "response.status", "n/a")
493+
}: ${err.message}`
494+
);
495+
return false;
496+
}
497+
}
498+
464499
/**
465500
* Cancel project
466501
* @param {String} projectId the project id
@@ -1496,6 +1531,7 @@ module.exports = {
14961531
getProjectPayment,
14971532
capturePayment,
14981533
cancelPayment,
1534+
generateChallengePayments,
14991535
sendSelfServiceNotification,
15001536
getMemberByHandle,
15011537
getMembersByHandles,

src/services/ChallengePhaseService.js

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,62 @@
44
const _ = require("lodash");
55
const Joi = require("joi");
66
const moment = require("moment");
7+
const { Prisma } = require("@prisma/client");
8+
const config = require("config");
79
const helper = require("../common/helper");
810
const logger = require("../common/logger");
911
const errors = require("../common/errors");
1012
const constants = require("../../app-constants");
13+
const { getReviewClient } = require("../common/review-prisma");
1114

1215
const { getClient } = require("../common/prisma");
1316
const prisma = getClient();
17+
const PENDING_REVIEW_STATUSES = Object.freeze(["PENDING", "IN_PROGRESS", "DRAFT", "SUBMITTED"]);
18+
19+
async function hasPendingScorecardsForPhase(challengePhaseId) {
20+
if (!config.REVIEW_DB_URL) {
21+
logger.debug(
22+
`Skipping pending scorecard check for phase ${challengePhaseId} because REVIEW_DB_URL is not configured`
23+
);
24+
return false;
25+
}
26+
27+
const reviewPrisma = getReviewClient();
28+
const reviewSchema = config.REVIEW_DB_SCHEMA;
29+
const reviewTable = Prisma.raw(`"${reviewSchema}"."review"`);
30+
const statusText = Prisma.raw(`"status"::text`);
31+
32+
const pendingStatusClause =
33+
PENDING_REVIEW_STATUSES.length > 0
34+
? Prisma.sql`${statusText} IN (${Prisma.join(
35+
PENDING_REVIEW_STATUSES.map((status) => Prisma.sql`${status}`)
36+
)})`
37+
: Prisma.sql`FALSE`;
38+
39+
let rows;
40+
try {
41+
rows = await reviewPrisma.$queryRaw(
42+
Prisma.sql`
43+
SELECT COUNT(*)::int AS count
44+
FROM ${reviewTable}
45+
WHERE "phaseId" = ${challengePhaseId}
46+
AND (
47+
"status" IS NULL
48+
OR ${pendingStatusClause}
49+
)
50+
`
51+
);
52+
} catch (err) {
53+
logger.error(
54+
`Failed to check pending scorecards for phase ${challengePhaseId}: ${err.message}`,
55+
err
56+
);
57+
throw err;
58+
}
59+
60+
const [{ count = 0 } = {}] = rows || [];
61+
return Number(count) > 0;
62+
}
1463

1564
async function checkChallengeExists(challengeId) {
1665
const challenge = await prisma.challenge.findUnique({ where: { id: challengeId } });
@@ -147,8 +196,41 @@ async function partiallyUpdateChallengePhase(currentUser, challengeId, id, data)
147196
}
148197
}
149198

199+
const isClosingPhase =
200+
"isOpen" in data && data["isOpen"] === false && Boolean(challengePhase.isOpen);
201+
const isReopeningPhase =
202+
"isOpen" in data && data["isOpen"] === true && !challengePhase.isOpen;
203+
if (isClosingPhase) {
204+
const pendingScorecards = await hasPendingScorecardsForPhase(challengePhase.id);
205+
if (pendingScorecards) {
206+
const phaseName = challengePhase.name || "phase";
207+
throw new errors.ForbiddenError(
208+
`Cannot close ${phaseName} because there are still pending scorecards`
209+
);
210+
}
211+
}
212+
if (isReopeningPhase) {
213+
data.actualEndDate = null;
214+
}
215+
150216
// Update ChallengePhase
151217
data.updatedBy = String(currentUser.userId);
218+
if (!_.isNil(data.duration)) {
219+
const startInput = !_.isNil(data.scheduledStartDate)
220+
? data.scheduledStartDate
221+
: !_.isNil(challengePhase.scheduledStartDate)
222+
? challengePhase.scheduledStartDate
223+
: null;
224+
if (startInput) {
225+
const startDate = new Date(startInput);
226+
if (!Number.isNaN(startDate.getTime())) {
227+
const recalculatedScheduledEndDate = new Date(
228+
startDate.getTime() + Number(data.duration) * 1000
229+
);
230+
data.scheduledEndDate = recalculatedScheduledEndDate;
231+
}
232+
}
233+
}
152234
const dataToUpdate = _.omit(data, "constraints");
153235
const result = await prisma.$transaction(async (tx) => {
154236
const result = await tx.challengePhase.update({

src/services/ChallengeService.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1918,6 +1918,63 @@ function validateTask(currentUser, challenge, data, challengeResources) {
19181918
}
19191919
}
19201920

1921+
function prepareTaskCompletionData(challenge, challengeResources, data) {
1922+
const isTask = _.get(challenge, "legacy.pureV5Task");
1923+
const isCompleteTask =
1924+
data.status === ChallengeStatusEnum.COMPLETED &&
1925+
challenge.status !== ChallengeStatusEnum.COMPLETED;
1926+
1927+
if (!isTask || !isCompleteTask) {
1928+
return null;
1929+
}
1930+
1931+
const submitters = _.filter(
1932+
challengeResources,
1933+
(resource) => resource.roleId === config.SUBMITTER_ROLE_ID
1934+
);
1935+
1936+
if (!submitters || submitters.length === 0) {
1937+
throw new errors.BadRequestError("Task has no submitter resource");
1938+
}
1939+
1940+
if (!data.winners || data.winners.length === 0) {
1941+
const submitter = submitters[0];
1942+
data.winners = [
1943+
{
1944+
userId: parseInt(submitter.memberId, 10),
1945+
handle: submitter.memberHandle,
1946+
placement: 1,
1947+
type: PrizeSetTypeEnum.PLACEMENT,
1948+
},
1949+
];
1950+
}
1951+
1952+
const completionTime = new Date().toISOString();
1953+
const startTime = challenge.startDate || completionTime;
1954+
1955+
const updatedPhases = _.map(challenge.phases || [], (phase) => ({
1956+
id: phase.id,
1957+
phaseId: phase.phaseId,
1958+
name: phase.name,
1959+
description: phase.description,
1960+
duration: phase.duration,
1961+
scheduledStartDate: phase.scheduledStartDate,
1962+
scheduledEndDate: phase.scheduledEndDate,
1963+
predecessor: phase.predecessor,
1964+
constraints: _.cloneDeep(phase.constraints),
1965+
actualStartDate: startTime,
1966+
actualEndDate: completionTime,
1967+
isOpen: false,
1968+
}));
1969+
1970+
data.phases = updatedPhases;
1971+
1972+
return {
1973+
shouldTriggerPayments: true,
1974+
completionTime,
1975+
};
1976+
}
1977+
19211978
/**
19221979
* Update challenge.
19231980
* @param {Object} currentUser the user who perform operation
@@ -2000,6 +2057,7 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) {
20002057
);
20012058
logger.debug(`updateChallenge(${challengeId}): payload validation complete`);
20022059
validateTask(currentUser, challenge, data, challengeResources);
2060+
const taskCompletionInfo = prepareTaskCompletionData(challenge, challengeResources, data);
20032061

20042062
let sendActivationEmail = false;
20052063
let sendSubmittedEmail = false;
@@ -2512,6 +2570,19 @@ async function updateChallenge(currentUser, challengeId, data, options = {}) {
25122570
include: includeReturnFields,
25132571
});
25142572
});
2573+
if (taskCompletionInfo && taskCompletionInfo.shouldTriggerPayments) {
2574+
logger.info(`Triggering payment generation for Task challenge ${challengeId}`);
2575+
try {
2576+
const paymentSuccess = await helper.generateChallengePayments(challengeId);
2577+
if (!paymentSuccess) {
2578+
logger.warn(`Failed to generate payments for Task challenge ${challengeId}`);
2579+
}
2580+
} catch (err) {
2581+
logger.error(
2582+
`Error generating payments for Task challenge ${challengeId}: ${err.message}`
2583+
);
2584+
}
2585+
}
25152586
// Re-fetch the challenge outside the transaction to ensure we publish
25162587
// only after the commit succeeds and using the committed snapshot.
25172588
if (emitEvent) {

0 commit comments

Comments
 (0)