From 131cef0899e10ffe94cc012ac251f4f967d1e41e Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Tue, 4 Aug 2026 07:06:38 +1000 Subject: [PATCH] PM-5800: Hide copilot fee for task challenges What was broken Task challenge setup in the Work app displayed a Copilot Fee field, allowing project managers to bundle a copilot payment with the task. Root cause The challenge editor rendered the copilot fee input for every billable challenge without considering the resolved task challenge state. What was changed - Hide the copilot fee input and its layout column for task challenges. - Keep copilot assignment and the rest of task billing unchanged. - Preserve the copilot fee input for non-task challenges and document the task-only restriction. Any added/updated tests - Added form coverage that verifies task challenges hide the fee while retaining copilot assignment and billing. - Added coverage that verifies non-task challenges still show the fee. --- .../challenges/ChallengeEditorPage/README.md | 2 +- .../components/ChallengeEditorForm.spec.tsx | 38 ++++++++++++++++++- .../components/ChallengeEditorForm.tsx | 16 +++++--- 3 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md index 0bef720b9..5a9a842d3 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/README.md @@ -94,7 +94,7 @@ The form uses `challengeBasicInfoSchema` from `src/apps/work/src/lib/schemas/cha - `CheckpointPrizesField`: two-round checkpoint-prize editor that updates only the checkpoint prize set, preserving nested placement-prize edits when the checkpoint amount or submission count changes. - `AssignedMemberField`: task-only assignee selector backed by member ids; persisted through the challenge `Submitter` resource assignment and restored from resources when task payloads omit the legacy field. - `CopilotField`: clearable dropdown populated with Copilot and Full Access member handles from the current project; persisted through the challenge `Copilot` resource assignment and restored from resources when draft payloads omit the legacy field. Full Access members can also use `Assign yourself`, matching the legacy Work Manager behavior. Persisted selections are matched case-insensitively so refreshes still show the saved copilot even when the resource payload and project-member option list disagree on handle casing, and member-id-only copilot resources are normalized back to handles during refresh hydration. Save-time form resets also reload the persisted copilot resource before the editor reopens the saved draft, so sparse challenge responses do not blank the field. When a refreshed draft still carries a legacy member-id-only copilot resource, the next save deletes that stale resource before writing the canonical handle-based assignment so the challenge does not keep duplicate copilot rows. The initial `New` draft-creation step also saves any selected copilot assignment before the editor resets from fetched challenge data, so the basic-information selection survives the transition into the full draft form. A copilot is required whenever the copilot fee is greater than 0, and that rule is enforced by form validation before save or launch actions run. -- `CopilotFeeField`: optional copilot payment input that updates only the underlying copilot prize set, preserving placement prize edits and removing the copilot prize set when cleared so empty fees do not leave hidden validation errors. +- `CopilotFeeField`: optional copilot payment input shown only for non-task challenges. It updates only the underlying copilot prize set, preserving placement prize edits and removing the copilot prize set when cleared so empty fees do not leave hidden validation errors. - `ChallengeFeeField`: derived summary value that uses the challenge billing markup together with the current prize and reviewer estimates so draft saves do not fall back to a stale `challengeFee` snapshot. It uses the same reviewer-cost estimate shown in `Review cost` and always renders two decimal places. For point-based challenges, the derived fee only uses the USD-denominated billable total so point prizes do not inflate the dollar billing summary. When the challenge payload does not yet include billing, or challenge-api returns the draft's billing markup as `0` for the same project billing account, the editor hydrates billing-account id and markup from the parent project billing account so draft pages still show the correct fee. - `ChallengeTotalField`: derived billing summary that always renders a dollar total and adds the current challenge fee on top of the billable subtotal from placement prizes, copilot fee, and estimated review cost. For point-based challenges it matches legacy work-manager behavior by counting only the USD-denominated copilot payment and its derived fee, excluding point prizes from the monetary total. - `Billing Account Id`: read-only `Prizes & Billing` summary value that shows the challenge billing-account id, falling back to the parent project billing account when the saved challenge payload has not populated billing yet. diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx index d0f3c3088..f48741dda 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.spec.tsx @@ -623,7 +623,7 @@ jest.mock('./CopilotField', () => ({ }, })) jest.mock('./CopilotFeeField', () => ({ - CopilotFeeField: () => <>, + CopilotFeeField: () => Copilot Fee Field, })) jest.mock('./DesignWorkTypeField', () => ({ DesignWorkTypeField: function DesignWorkTypeField() { @@ -1265,6 +1265,42 @@ describe('ChallengeEditorForm', () => { .toBeNull() }) + it('hides the copilot fee for task challenges while keeping the copilot assignment', () => { + mockedUseFetchChallengeTypes.mockReturnValue({ + challengeTypes: [{ + abbreviation: 'TSK', + id: 'task-type-id', + isTask: true, + name: 'Task', + }], + isLoading: false, + }) + + render( + + + , + ) + + expect(screen.getByLabelText('Copilot Field')) + .toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Prizes & Billing' })) + .toBeInTheDocument() + expect(screen.queryByText('Copilot Fee Field')) + .toBeNull() + }) + + it('keeps the copilot fee for non-task challenges', () => { + render( + + + , + ) + + expect(screen.getByText('Copilot Fee Field')) + .toBeInTheDocument() + }) + it('hides the editable timeline section for task challenges in read-only view mode', () => { mockedUseFetchChallengeTypes.mockReturnValue({ challengeTypes: [{ diff --git a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx index b2d898f11..3a0abfebb 100644 --- a/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx +++ b/src/apps/work/src/pages/challenges/ChallengeEditorPage/components/ChallengeEditorForm.tsx @@ -4002,12 +4002,16 @@ export const ChallengeEditorForm: FC = ( ) : undefined} -
- -
+ {!isTaskChallenge + ? ( +
+ +
+ ) + : undefined}