Skip to content

Prod - April release#88

Merged
jmgasper merged 31 commits into
masterfrom
develop
Apr 14, 2026
Merged

Prod - April release#88
jmgasper merged 31 commits into
masterfrom
develop

Conversation

@jmgasper
Copy link
Copy Markdown
Contributor

@jmgasper jmgasper commented Apr 13, 2026

jmgasper and others added 30 commits March 30, 2026 22:15
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
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ProjectHelper and 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 .factory validation 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.

Comment on lines +19 to +28
const normalizeAffectedSurfaces = (value) => {
const surfaces = Array.isArray(value) ? value : [];
return Array.from(
new Set(
surfaces
.map((surface) => String(surface || "").trim())
.filter(Boolean)
)
);
};
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +71
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
if (arg === "--") {
continue;
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 === "--".

Copilot uses AI. Check for mistakes.
Comment on lines +169 to +176
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 } : {}),
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment on lines +2555 to +2562
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.",
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
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"));
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@jmgasper jmgasper merged commit e65bf83 into master Apr 14, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants