Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
980598f
Merge pull request #61 from topcoder-platform/develop
kkartunov Jan 13, 2026
23f301b
Merge pull request #66 from topcoder-platform/challenge-winners
jmgasper Jan 30, 2026
f1e5dad
Merge pull request #69 from topcoder-platform/develop
jmgasper Feb 5, 2026
520280d
Add index on challenge term
vas3a Feb 27, 2026
f0e9838
Merge pull request #74 from topcoder-platform/hotfix_add-index-on-cha…
kkartunov Feb 27, 2026
22e95c3
Merge pull request #72 from topcoder-platform/develop
kkartunov Mar 2, 2026
3888786
Merge pull request #77 from topcoder-platform/develop
kkartunov Mar 26, 2026
e65bf83
Merge pull request #88 from topcoder-platform/develop
jmgasper Apr 14, 2026
dd31453
Merge pull request #91 from topcoder-platform/develop
jmgasper Apr 21, 2026
8ac8830
Merge pull request #92 from topcoder-platform/develop
jmgasper Apr 24, 2026
51d81de
Merge pull request #103 from topcoder-platform/develop
kkartunov May 12, 2026
245b040
Merge pull request #104 from topcoder-platform/develop
kkartunov May 12, 2026
930759f
Merge pull request #105 from topcoder-platform/develop
jmgasper May 13, 2026
0f3ab0f
Merge pull request #109 from topcoder-platform/develop
jmgasper May 19, 2026
a8c65db
Merge pull request #113 from topcoder-platform/develop
jmgasper May 27, 2026
b013d4d
Merge pull request #121 from topcoder-platform/develop
kkartunov Jun 16, 2026
c6d8235
Merge pull request #127 from topcoder-platform/develop
jmgasper Jun 23, 2026
3d92051
Merge pull request #131 from topcoder-platform/develop
jmgasper Jul 7, 2026
8ca7e4d
Merge pull request #132 from topcoder-platform/prisma-client
kkartunov Jul 14, 2026
3066e58
Merge pull request #133 from topcoder-platform/develop
jmgasper Jul 15, 2026
f29a911
Merge pull request #137 from topcoder-platform/develop
jmgasper Jul 28, 2026
a1de0f9
Fix: allow closing ai phase when virusscan submission is true
vas3a Jul 31, 2026
96d2316
Merge branch 'develop' of github.com:topcoder-platform/challenge-api-…
vas3a Jul 31, 2026
b7bde89
Merge pull request #143 from topcoder-platform/develop
jmgasper Aug 5, 2026
a6df1dd
Merge branch 'develop' of github.com:topcoder-platform/challenge-api-…
vas3a Aug 6, 2026
666391d
Merge pull request #144 from topcoder-platform/HOTFIX-ensure-phase-cl…
vas3a Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions src/services/ChallengeService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1025,9 +1025,32 @@ async function ensureAIPhaseCanBeClosed(challengeId, phaseName = 'AI Screening')
reviewPrisma.$queryRaw(
Prisma.sql`
SELECT "id", "legacySubmissionId"
FROM ${submissionTable}
WHERE "challengeId" = ${challengeId}
AND "status"::text <> 'DELETED'
FROM (
SELECT
"id",
"legacySubmissionId",
ROW_NUMBER() OVER (
PARTITION BY COALESCE("memberId", "id")
ORDER BY
"submittedDate" DESC NULLS LAST,
"createdAt" DESC NULLS LAST,
"updatedAt" DESC NULLS LAST,
"id" DESC
) AS "rowNumber"
FROM ${submissionTable}
WHERE "challengeId" = ${challengeId}
AND (
"status" IS NULL
OR "status"::text = 'ACTIVE'
OR "status"::text = 'AI_FAILED_REVIEW'
)
AND (
"type" IS NULL
OR UPPER(("type")::text) = 'CONTEST_SUBMISSION'
)
AND ("virusScan" IS NULL OR "virusScan" = TRUE)
) latest
WHERE "rowNumber" = 1
`,
),
reviewPrisma.$queryRaw(
Expand Down
90 changes: 90 additions & 0 deletions test/unit/ChallengeService.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const prisma = getClient();
const reviewSchema = config.get("REVIEW_DB_SCHEMA");
const reviewTableName = `"${reviewSchema}"."review"`;
const submissionTableName = `"${reviewSchema}"."submission"`;
const aiReviewDecisionTableName = `"${reviewSchema}"."aiReviewDecision"`;
const should = chai.should();
let reviewClient;

Expand Down Expand Up @@ -107,7 +108,21 @@ describe("challenge service unit tests", () => {
ALTER TABLE ${submissionTableName}
ADD COLUMN IF NOT EXISTS "isLatest" boolean
`);
await reviewClient.$executeRawUnsafe(`
ALTER TABLE ${submissionTableName}
ADD COLUMN IF NOT EXISTS "virusScan" boolean
`);
await reviewClient.$executeRawUnsafe(`
CREATE TABLE IF NOT EXISTS ${aiReviewDecisionTableName} (
"id" varchar(36) PRIMARY KEY,
"submissionId" varchar(64),
"configId" varchar(255),
"isFinal" boolean
)
`);
await reviewClient.$executeRawUnsafe(`DELETE FROM ${submissionTableName}`);
await reviewClient.$executeRawUnsafe(`DELETE FROM ${aiReviewDecisionTableName}`);
await reviewClient.$executeRawUnsafe(`DELETE FROM ${aiReviewDecisionTableName}`);

testChallengeData = {
typeId: data.challenge.typeId,
Expand Down Expand Up @@ -3234,6 +3249,81 @@ describe("challenge service unit tests", () => {
});
});

describe("AI phase closure tests", () => {
let originalGetAIReviewConfigByChallengeId;

beforeEach(() => {
originalGetAIReviewConfigByChallengeId = helper.getAIReviewConfigByChallengeId;
});

afterEach(async () => {
helper.getAIReviewConfigByChallengeId = originalGetAIReviewConfigByChallengeId;
await reviewClient.$executeRawUnsafe(
`DELETE FROM ${submissionTableName} WHERE "challengeId" = '${data.challenge.id}'`,
);
await reviewClient.$executeRawUnsafe(
`DELETE FROM ${aiReviewDecisionTableName} WHERE "configId" = 'ai-config-test'`,
);
await prisma.challengeReviewer.deleteMany({ where: { challengeId: data.challenge.id } });
});

it("skips latest submissions that failed virus scan when checking AI phase closure readiness", async () => {
helper.getAIReviewConfigByChallengeId = async () => ({ id: "ai-config-test" });
await prisma.challengeReviewer.create({
data: {
id: uuid(),
challengeId: data.challenge.id,
scorecardId: "ai-scorecard-id",
isMemberReview: false,
aiWorkflowId: "workflow-1",
createdBy: "test",
updatedBy: "test",
},
});

await reviewClient.$executeRawUnsafe(`
INSERT INTO ${submissionTableName}
("id", "challengeId", "memberId", "type", "status", "submittedDate", "virusScan")
VALUES
('sub-1', '${data.challenge.id}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-01T00:00:00Z', TRUE),
('sub-2', '${data.challenge.id}', 'member-1', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-02T00:00:00Z', FALSE)
`);

await reviewClient.$executeRawUnsafe(`
INSERT INTO ${aiReviewDecisionTableName}
("id", "submissionId", "configId", "isFinal")
VALUES
('dec-1', 'sub-1', 'ai-config-test', TRUE)
`);

await service.ensureAIPhaseCanBeClosed(data.challenge.id, "AI Screening");
});

it("allows AI phase closure when all latest submissions failed virus scan", async () => {
helper.getAIReviewConfigByChallengeId = async () => ({ id: "ai-config-test" });
await prisma.challengeReviewer.create({
data: {
id: uuid(),
challengeId: data.challenge.id,
scorecardId: "ai-scorecard-id",
isMemberReview: false,
aiWorkflowId: "workflow-2",
createdBy: "test",
updatedBy: "test",
},
});

await reviewClient.$executeRawUnsafe(`
INSERT INTO ${submissionTableName}
("id", "challengeId", "memberId", "type", "status", "submittedDate", "virusScan")
VALUES
('sub-3', '${data.challenge.id}', 'member-2', 'CONTEST_SUBMISSION', 'ACTIVE', '2026-01-03T00:00:00Z', FALSE)
`);

await service.ensureAIPhaseCanBeClosed(data.challenge.id, "AI Screening");
});
});

describe("delete challenge tests", () => {
const challengeIds = [];
let originalEnsureUserCanModifyChallenge;
Expand Down
Loading