Conversation
What was broken Payments were not generated when tasks created from the new work manager were completed because the task completion path did not recognize them as task challenges. Root cause The completion and payment logic only checked legacy.pureV5Task, while new work manager tasks are stored through canonical task metadata (task.isTask/taskIsTask) without that legacy flag. What was changed Updated ChallengeService task handling to treat either canonical task metadata or legacy.pureV5Task as a task challenge during validation, completion preparation, winner normalization, and task.memberId synchronization. Any added/updated tests Added a ChallengeService regression test covering task completion/payment generation when legacy.pureV5Task is absent.
What was broken:\nChallenge editor GET responses stripped billing for all interactive users, so the draft page challenge fee fell back to $0.\n\nRoot cause:\nChallengeService.getChallenge unconditionally removed billing for non-machine, non-admin callers even when they already had project write access.\n\nWhat was changed:\nPreserved challenge billing details in getChallenge for project write users while continuing to hide billing for users without that access.\nAdded regression coverage for both the allowed and denied billing-response paths.\n\nTests:\nAdded getChallenge billing visibility tests in test/unit/ChallengeService.test.js.\nRan npm run lint.\nRan npm test, which is blocked in this workspace because Prisma has no DATABASE_URL configured.\nRan npm run build, which fails because challenge-api-v6 does not define a build script.
What was broken Updating a challenge deleted existing attachments, so saving a draft challenge cleared uploaded files. Root cause The challenge update transaction deleted attachments whenever the patch payload omitted attachment data because it checked updateData.attachment, which is not populated in the normal save flow. What was changed Removed the attachment deletion from the general challenge update path so attachments are only managed by the dedicated attachment endpoints. Added a unit test that updates a challenge without an attachments field and verifies the existing attachment remains in both the response and the database. Any added/updated tests Added a unit test in test/unit/ChallengeService.test.js covering attachment persistence when the update payload omits attachments.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…al MM importer Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…onvergence Dry-run now derives and reports explicit MM/Data Science challenge shape plus Registration/Submission/Review phase plans before returning create, and unresolveds when that plan cannot be derived so apply-time reconciliation stays consistent. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Retry submitter resource creation with a temporary challenge status transition when the Resource API blocks writes on COMPLETED rounds, then always restore the original status before returning. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Cast target-member lookup placeholders to bigint to avoid postgres type-mismatch failures, and add regression coverage so dry-run can classify missing-member partitions from live MEMBER_DB data. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
…rting Commit the resumed importer planning/reporting implementation and mission artifacts, including fail-closed prerequisite handling, stable missing-member/explicit-skip partitions, and deterministic skipped-file output coverage. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
What was broken Challenges tied to projects could still be activated when the linked billing account was inactive, expired, or had no remaining budget. Root cause Activation validation only checked whether a project billing account id existed. It did not verify billing-account lifecycle state or remaining funds against the billing account data. What was changed Added billing-account detail lookup support in the project helper, including normalized active, end-date, status, and remaining-budget fields. Updated challenge activation validation to block project-backed launches when the billing account is missing, inactive, expired, not found, or has no remaining funds. Wired the billing account validation into challenge status updates while preserving the existing project billing-account persistence flow. Any added/updated tests Added focused unit coverage for the new activation billing-account validation paths. Updated challenge service tests to cover inactive, expired, insufficient-funds, and valid billing-account activation scenarios.
What was broken challenge-api-v6 still allowed Registration to be reopened whenever a Submission phase was open, even when that Submission phase depended on Checkpoint Review instead of Registration. That kept surfacing the wrong Reopen action after the earlier platform-ui fix. Root cause ChallengePhaseService had a special-case exemption that bypassed explicit dependency checks for Registration whenever any submission variant was open. What was changed Removed the submission-based Registration reopen exemption so reopening now requires a real open dependent phase. Added a regression test for the Checkpoint Review to Submission path. Stabilized ChallengePhaseService unit setup so the seeded phase records are reset between tests and challenge update publishing is stubbed for this suite. Any added/updated tests Added a regression test that forbids reopening Registration when the open Submission phase depends on Checkpoint Review. Updated ChallengePhaseService unit setup to run the full file reliably in isolation.
Attach one provisional review summation to each imported non-example submission keyed by legacySubmissionId, and preserve deterministic missing-member skip behavior across apply reruns. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
PM-4686: block challenge activation for invalid billing accounts
PM-3167: tighten registration reopen dependency checks
Discover existing resource/submission/review counts from live APIs when available so reuse planning and rerun no-op classification reflect actual linked state instead of snapshot hints.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
What was broken Challenge listings only matched challenge names when the query casing matched the stored challenge name exactly. The project challenge page used the `name` filter and the common challenge listing used the shared `search` filter, so lowercase queries could miss mixed-case challenge names. Root cause The Prisma `contains` filters for challenge name matching were case-sensitive in both the dedicated `name` branch and the shared `search` branch of `searchChallenges`. What was changed Updated challenge name matching in `searchChallenges` to use Prisma's case-insensitive mode for both `criteria.name` and the challenge-name portion of `criteria.search`. Added focused unit coverage for both the project-page `name` filter path and the common-listing `search` filter path. Any added/updated tests Updated `test/unit/ChallengeService.test.js` with regression tests covering case-insensitive matching for both `name` and `search` challenge queries.
PM-4837: Make challenge listing name search case-insensitive
There was a problem hiding this comment.
Pull request overview
This release bundle introduces billing-account launch validation for challenges and lands a substantial set of data-migration (historical Marathon Match importer) utilities, tests, and operational docs/artifacts.
Changes:
- Add Billing Accounts API integration in
ProjectHelperand unit tests validating challenge activation billing-account rules. - Add/extend historical Marathon Match importer scripts (planning/apply helpers, deterministic skipped-member artifacts, linked-count discovery) with comprehensive unit tests.
- Add operational documentation and
.factoryvalidation artifacts/scripts to support repeatable importer validation.
Reviewed changes
Copilot reviewed 77 out of 78 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit/challenge-activation-billing.test.js | Unit tests for activation billing validation behavior |
| src/common/project-helper.js | Add Billing Accounts API lookup + normalization helpers |
| data-migration/test/importMarathonMatchWinners.test.js | Tests for placement normalization / duplicate handling |
| data-migration/test/importHistoricalMarathonMatches.targetMemberResolution.test.js | Tests for member presence resolution via Prisma |
| data-migration/test/importHistoricalMarathonMatches.submissionHistory.test.js | Tests for submission history loading/reconciliation |
| data-migration/test/importHistoricalMarathonMatches.resourceApi.test.js | Tests for Auth0 token provider URL handling |
| data-migration/test/importHistoricalMarathonMatches.provisionalScores.test.js | Tests for provisional score import/reconciliation |
| data-migration/test/importHistoricalMarathonMatches.planningPrerequisites.test.js | Tests for prerequisite-driven planning outcomes |
| data-migration/test/importHistoricalMarathonMatches.plan.test.js | CLI planning behavior tests (parseability/determinism/errors) |
| data-migration/test/importHistoricalMarathonMatches.participants.test.js | Tests for participant identity normalization |
| data-migration/test/importHistoricalMarathonMatches.missingMemberPlanning.test.js | Tests for missing-member planning + skipped artifact writing |
| data-migration/test/importHistoricalMarathonMatches.linkedCounts.test.js | Tests for linked record count discovery (resource/review) |
| data-migration/test/importHistoricalMarathonMatches.existingState.test.js | Tests for authoritative existing-state discovery/merging |
| data-migration/src/scripts/importMarathonMatchWinners.js | Add placement normalization + export testables |
| data-migration/src/scripts/importHistoricalMarathonMatches/targetMemberResolution.js | Add member presence resolver (schema-safe, batched) |
| data-migration/src/scripts/importHistoricalMarathonMatches/skippedArtifact.js | Add deterministic skipped-member artifact builder/writer |
| data-migration/src/scripts/importHistoricalMarathonMatches/resourceApi.js | Add Auth0 token provider + Resource API client |
| data-migration/src/scripts/importHistoricalMarathonMatches/reporting.js | Emit structured plan/apply records to stdout |
| data-migration/src/scripts/importHistoricalMarathonMatches/participants.js | Legacy user/handle identity normalization helpers |
| data-migration/src/scripts/importHistoricalMarathonMatches/linkedCounts.js | Best-effort linked count introspection and resolution |
| data-migration/src/scripts/importHistoricalMarathonMatches/legacyDataReader.js | Legacy JSON file listing + streaming reader utilities |
| data-migration/src/scripts/importHistoricalMarathonMatches/existingState.js | Existing v6 state discovery + snapshot hint parsing |
| data-migration/src/scripts/importHistoricalMarathonMatches/argParser.js | Importer CLI argument parsing + usage text |
| data-migration/src/scripts/importHistoricalMarathonMatches.js | Importer CLI entrypoint wiring (planning/apply orchestration) |
| data-migration/src/migrators/_baseMigrator.js | Regex fix + remove unused catch variables |
| data-migration/src/index.js | Remove unused DB connectivity helper/import noise |
| data-migration/MARATHON_README.md | Operator-facing guide for historical MM importer usage |
| config/default.js | Add BILLING_ACCOUNTS_API_URL configuration |
| .factory/validation/planning-challenge/user-testing/synthesis.round2.json | Validation artifact update |
| .factory/validation/planning-challenge/user-testing/synthesis.round1.json | Validation artifact update |
| .factory/validation/planning-challenge/user-testing/synthesis.json | Validation artifact update |
| .factory/validation/planning-challenge/user-testing/flows/plan-cli-core.json | Validation flow artifact |
| .factory/validation/planning-challenge/user-testing/flows/dry-run-deltas.json | Validation flow artifact |
| .factory/validation/planning-challenge/user-testing/flows/create-path-plan-check.json | Validation flow artifact |
| .factory/validation/planning-challenge/user-testing/flows/create-and-rerun-10815.json | Validation flow artifact |
| .factory/validation/planning-challenge/user-testing/flows/challenge-reuse-apply.json | Validation flow artifact |
| .factory/validation/planning-challenge/scrutiny/synthesis.round1.json | Scrutiny artifact update |
| .factory/validation/planning-challenge/scrutiny/synthesis.json | Scrutiny artifact update |
| .factory/validation/planning-challenge/scrutiny/reviews/mm-importer-reuse-challenge-safety.json | Scrutiny review artifact |
| .factory/validation/planning-challenge/scrutiny/reviews/mm-importer-planning-prerequisite-convergence-fix.json | Scrutiny review artifact |
| .factory/validation/planning-challenge/scrutiny/reviews/mm-importer-plan-cli.json | Scrutiny review artifact |
| .factory/validation/planning-challenge/scrutiny/reviews/mm-importer-create-historical-challenge-phases.json | Scrutiny review artifact |
| .factory/validation/planning-challenge/scrutiny/reviews/mm-importer-challenge-phase-plan-and-rerun-convergence-fix.json | Scrutiny review artifact |
| .factory/validation/planning-challenge/scrutiny/reviews/mm-importer-authoritative-round-selection-and-reuse-fix.json | Scrutiny review artifact |
| .factory/validation/participant-scores/user-testing/synthesis.json | Participant-scores validation synthesis artifact |
| .factory/validation/participant-scores/user-testing/flows/round-17948-finals.json | Participant-scores validation flow artifact |
| .factory/validation/participant-scores/user-testing/flows/round-10815-participants.json | Participant-scores validation flow artifact |
| .factory/validation/participant-scores/user-testing/flows/plan-dry-run.json | Participant-scores validation flow artifact |
| .factory/validation/participant-scores/user-testing/flows/existing-v6-reruns.json | Participant-scores validation flow artifact |
| .factory/validation/participant-scores/scrutiny/synthesis.json | Participant-scores scrutiny synthesis artifact |
| .factory/validation/participant-scores/scrutiny/reviews/mm-importer-submitter-resources.json | Scrutiny review artifact |
| .factory/validation/participant-scores/scrutiny/reviews/mm-importer-submission-history.json | Scrutiny review artifact |
| .factory/validation/participant-scores/scrutiny/reviews/mm-importer-provisional-scores.json | Scrutiny review artifact |
| .factory/validation/participant-scores/scrutiny/reviews/mm-importer-missing-member-planning-and-reporting.json | Scrutiny review artifact |
| .factory/validation/participant-scores/scrutiny/reviews/mm-importer-final-scores.json | Scrutiny review artifact |
| .factory/validation/participant-scores/scrutiny/reviews/mm-importer-end-to-end-orchestration.json | Scrutiny review artifact |
| .factory/skills/migration-worker/SKILL.md | Update migration-worker skill procedure |
| .factory/services.yaml | Define install/test/lint commands for validation |
| .factory/library/user-testing.md | Update validation guidance + fixture round notes |
| .factory/library/legacy-data.md | Document legacy source rules + fixture round facts |
| .factory/library/environment.md | Document required env + Node version constraints |
| .factory/library/architecture.md | Document importer architecture/invariants |
| .factory/init.sh | Initialize dependencies + warn on missing env |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const normalizeAffectedSurfaces = (value) => { | ||
| const surfaces = Array.isArray(value) ? value : []; | ||
| return Array.from( | ||
| new Set( | ||
| surfaces | ||
| .map((surface) => String(surface || "").trim()) | ||
| .filter(Boolean) | ||
| ) | ||
| ); | ||
| }; |
There was a problem hiding this comment.
normalizeAffectedSurfaces deduplicates but does not sort, so the same logical record can serialize with different affectedSurfaces ordering depending on input order. Because this module is used to write a deterministic skipped-members artifact and the ordering participates in compareSkipRecords (via .join("|")), consider sorting the normalized surfaces array (e.g., locale/numeric) to guarantee stable output across reruns regardless of upstream array ordering.
| for (let index = 0; index < argv.length; index += 1) { | ||
| const arg = argv[index]; | ||
| if (arg === "--") { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
parseArgs treats the -- end-of-options marker as a no-op (continue). Standard CLI behavior is to stop option parsing at -- and treat the rest of argv as positional args; the current behavior will still interpret subsequent tokens as flags and can throw Unknown option unexpectedly. Consider breaking out of the loop (or collecting remaining args separately) when arg === "--".
| const active = normalizeOptionalBoolean(_.get(res, "data.active", null)); | ||
| const endDate = normalizeOptionalString(_.get(res, "data.endDate", null)); | ||
|
|
||
| return { | ||
| billingAccountId: _.get(res, "data.tcBillingAccountId", null), | ||
| markup: normalizeBillingMarkup(_.get(res, "data.markup", null)), | ||
| ...(active !== null ? { active } : {}), | ||
| ...(endDate ? { endDate } : {}), |
There was a problem hiding this comment.
getProjectBillingInformation normalizes active and endDate, but returns billingAccountId directly from data.tcBillingAccountId without trimming/empty-string normalization. Elsewhere (e.g., validateChallengeActivationBillingAccount) billing account ids are normalized via normalizeOptionalString, so returning ""/whitespace here could lead to inconsistent hasAccount behavior and downstream validation. Consider returning billingAccountId: normalizeOptionalString(_.get(res, "data.tcBillingAccountId", null)) for consistency.
| const resolvedActive = resolveBillingAccountActive(billingAccountDetails); | ||
| const resolvedEndDate = normalizeOptionalString(_.get(billingAccountDetails, "endDate")); | ||
|
|
||
| if (!billingAccountDetails) { | ||
| throw new errors.BadRequestError( | ||
| "Cannot activate challenge because the project billing account could not be found.", | ||
| ); | ||
| } |
There was a problem hiding this comment.
🟡 Null check for billingAccountDetails occurs after the value is already consumed
resolveBillingAccountActive(billingAccountDetails) on line 2555 and _.get(billingAccountDetails, "endDate") on line 2556 are called before the null guard on line 2558. When getBillingAccountDetails returns null (e.g. billing account not found), resolvedActive and resolvedEndDate are derived from a null object, producing undefined silently, and only then does the null check throw the expected error. While lodash's _.get(null, ...) happens to return undefined without crashing today, the ordering is logically incorrect: the null guard should precede any consumption of the value. A future refactoring that replaces the lodash-wrapped access with direct property access (e.g., billingAccountDetails.endDate) would turn this into a TypeError crash.
| const resolvedActive = resolveBillingAccountActive(billingAccountDetails); | |
| const resolvedEndDate = normalizeOptionalString(_.get(billingAccountDetails, "endDate")); | |
| if (!billingAccountDetails) { | |
| throw new errors.BadRequestError( | |
| "Cannot activate challenge because the project billing account could not be found.", | |
| ); | |
| } | |
| if (!billingAccountDetails) { | |
| throw new errors.BadRequestError( | |
| "Cannot activate challenge because the project billing account could not be found.", | |
| ); | |
| } | |
| const resolvedActive = resolveBillingAccountActive(billingAccountDetails); | |
| const resolvedEndDate = normalizeOptionalString(_.get(billingAccountDetails, "endDate")); |
Was this helpful? React with 👍 or 👎 to provide feedback.
https://topcoder.atlassian.net/browse/PM-4545
https://topcoder.atlassian.net/browse/PM-4528
https://topcoder.atlassian.net/browse/PM-4618
https://topcoder.atlassian.net/browse/PM-4686
https://topcoder.atlassian.net/browse/PM-3167