diff --git a/docs/reference/Translations/index.md b/docs/reference/Translations/index.md index 6de1bb77b..8c39f7d54 100644 --- a/docs/reference/Translations/index.md +++ b/docs/reference/Translations/index.md @@ -1249,6 +1249,9 @@ Translation keys for the `Contractor.Address` i18n namespace. | `validations.street1` | `"Street address is required"` | | `validations.zip` | `"Please provide valid zip code"` | | `validations.zipInvalid` | `"Please enter a valid ZIP code"` | +| `w9EditWarning` | | +| `w9EditWarning.body` | `"This contractor has already signed a form W-9. If you are making corrections, you’re also required to update and retain a new signed version of Form W-9 reflecting the corrected information."` | +| `w9EditWarning.label` | `"Changes will require an updated Form W-9"` | | `zip` | `"Zip"` | *** @@ -1697,8 +1700,8 @@ Translation keys for the `Contractor.Profile` i18n namespace. | `buttons.cancel` | `"Cancel"` | | `buttons.create` | `"Create Contractor"` | | `buttons.creating` | `"Creating…"` | -| `buttons.update` | `"Update Contractor"` | -| `buttons.updating` | `"Updating…"` | +| `buttons.update` | `"Continue"` | +| `buttons.updating` | `"Saving…"` | | `fields` | | | `fields.businessName` | | | `fields.businessName.label` | `"Business Name"` | @@ -1748,6 +1751,9 @@ Translation keys for the `Contractor.Profile` i18n namespace. | `validations.ssn` | `"SSN is required for individual contractors"` | | `validations.ssnFormat` | `"SSN must be valid format"` | | `validations.startDate` | `"Start date is required"` | +| `w9EditWarning` | | +| `w9EditWarning.body` | `"This contractor has already signed a form W-9. If you are making corrections, you’re also required to update and retain a new signed version of Form W-9 reflecting the corrected information."` | +| `w9EditWarning.label` | `"Changes will require an updated Form W-9"` | *** diff --git a/e2e/tests/contractor/03-onboarding-edit.spec.ts b/e2e/tests/contractor/03-onboarding-edit.spec.ts index 6f6f0fd91..a6f823fda 100644 --- a/e2e/tests/contractor/03-onboarding-edit.spec.ts +++ b/e2e/tests/contractor/03-onboarding-edit.spec.ts @@ -45,6 +45,6 @@ test.describe('ContractorOnboardingFlow - edit re-entry from list lifecycle', () await expect(page.getByRole('radio', { name: /^business$/i })).toBeChecked() await expect(page.getByLabel(/business name/i)).toHaveValue(/Acme Consulting/i) - await expect(page.getByRole('button', { name: /update contractor/i })).toBeVisible() + await expect(page.getByRole('button', { name: /^continue$/i })).toBeVisible() }) }) diff --git a/src/components/Contractor/Address/Address.test.tsx b/src/components/Contractor/Address/Address.test.tsx index 445b2af32..d23ce555c 100644 --- a/src/components/Contractor/Address/Address.test.tsx +++ b/src/components/Contractor/Address/Address.test.tsx @@ -9,6 +9,10 @@ import { handleGetContractorAddress, handleUpdateContractorAddress, } from '@/test/mocks/apis/contractor_address' +import { + buildContractorDocumentsList, + handleGetContractorDocuments, +} from '@/test/mocks/apis/contractor_documents' import { setupApiTestMocks } from '@/test/mocks/apiServer' import { contractorEvents } from '@/shared/constants' import { renderWithProviders } from '@/test-utils/renderWithProviders' @@ -431,4 +435,42 @@ describe('Contractor/Address', () => { ).toBeInTheDocument() }) }) + + describe('W-9 edit warning', () => { + beforeEach(() => { + setupApiTestMocks() + server.use(handleGetContractorAddress(() => HttpResponse.json(emptyAddressResponse))) + }) + + it('renders the warning when the contractor has a signed W-9 on file', async () => { + server.use( + handleGetContractorDocuments(() => + HttpResponse.json( + buildContractorDocumentsList([ + { + uuid: 'signed-w9-uuid', + title: 'W-9', + name: 'taxpayer_identification_form_w_9', + requires_signing: true, + signed_at: '2025-01-01T00:00:00Z', + }, + ]), + ), + ), + ) + + renderWithProviders(
{}} />) + + expect( + await screen.findByText('Changes will require an updated Form W-9'), + ).toBeInTheDocument() + }) + + it('does not render the warning when the W-9 is unsigned', async () => { + renderWithProviders(
{}} />) + + await screen.findByText('Home address') + expect(screen.queryByText('Changes will require an updated Form W-9')).not.toBeInTheDocument() + }) + }) }) diff --git a/src/components/Contractor/Address/Address.tsx b/src/components/Contractor/Address/Address.tsx index baf54795d..1472061c2 100644 --- a/src/components/Contractor/Address/Address.tsx +++ b/src/components/Contractor/Address/Address.tsx @@ -15,6 +15,7 @@ import { useI18n, useComponentDictionary } from '@/i18n' import type { ResourceDictionary } from '@/types/Helpers' import { contractorEvents, type EventType } from '@/shared/constants' import type { OnEventType } from '@/components/Base/useBase' +import { useContractorHasSignedW9 } from '@/components/Contractor/shared/useContractorHasSignedW9' // The hook defaults to the API contract, which treats every address field as // optional. The SDK's address form has always required a complete mailing @@ -78,6 +79,18 @@ export function Address({ onEvent, FallbackComponent, ...rootProps }: AddressPro ) } +function ContractorAddressW9Warning({ contractorId }: { contractorId: string }) { + const hasSignedW9 = useContractorHasSignedW9(contractorId) + const { t } = useTranslation('Contractor.Address') + const Components = useComponentContext() + if (!hasSignedW9) return null + return ( + + {t('w9EditWarning.body')} + + ) +} + function AddressRoot({ contractorId, defaultValues, @@ -117,6 +130,8 @@ function AddressRoot({
void handleSubmit()}> + +
diff --git a/src/components/Contractor/Profile/ContractorProfile.test.tsx b/src/components/Contractor/Profile/ContractorProfile.test.tsx index f1531e2f9..f17cb076f 100644 --- a/src/components/Contractor/Profile/ContractorProfile.test.tsx +++ b/src/components/Contractor/Profile/ContractorProfile.test.tsx @@ -9,6 +9,10 @@ import { handleGetContractor, handleUpdateContractor, } from '@/test/mocks/apis/contractors' +import { + buildContractorDocumentsList, + handleGetContractorDocuments, +} from '@/test/mocks/apis/contractor_documents' import { setupApiTestMocks } from '@/test/mocks/apiServer' import { contractorEvents } from '@/shared/constants' import { renderWithProviders } from '@/test-utils/renderWithProviders' @@ -372,7 +376,7 @@ describe('Contractor profile component behavior', () => { await screen.findByText('Contractor profile') expect(screen.getByLabelText('First Name')).toHaveValue('John') - await user.click(screen.getByRole('button', { name: 'Update Contractor' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) await waitFor(() => { expect(updateResolver).toHaveBeenCalledTimes(1) @@ -427,6 +431,7 @@ describe('Contractor profile component behavior', () => { onboarding_status: postSaveStatus, }), ), + handleGetContractorDocuments(() => HttpResponse.json(buildContractorDocumentsList())), ) renderWithProviders( @@ -434,7 +439,7 @@ describe('Contractor profile component behavior', () => { ) await screen.findByText('Contractor profile') - await user.click(screen.getByRole('button', { name: 'Update Contractor' })) + await user.click(screen.getByRole('button', { name: 'Continue' })) await waitFor(() => { expect(onEvent).toHaveBeenCalledWith( @@ -625,4 +630,84 @@ describe('Contractor profile component behavior', () => { }) }) }) + + describe('W-9 edit warning', () => { + beforeEach(() => { + setupApiTestMocks() + server.use( + handleGetContractor(() => + HttpResponse.json({ + uuid: 'contractor_id', + version: 'version-1', + type: 'Individual', + wage_type: 'Fixed', + start_date: '2024-01-01', + first_name: 'John', + last_name: 'Doe', + has_ssn: true, + has_ein: false, + is_active: true, + file_new_hire_report: false, + onboarding_status: 'self_onboarding_review', + }), + ), + ) + }) + + it('renders the warning when the contractor has a signed W-9 on file', async () => { + server.use( + handleGetContractorDocuments(() => + HttpResponse.json( + buildContractorDocumentsList([ + { + uuid: 'signed-w9-uuid', + title: 'W-9', + name: 'taxpayer_identification_form_w_9', + requires_signing: true, + signed_at: '2025-01-01T00:00:00Z', + }, + ]), + ), + ), + ) + + renderWithProviders( + , + ) + + expect( + await screen.findByText('Changes will require an updated Form W-9'), + ).toBeInTheDocument() + }) + + it('does not render the warning when the W-9 is unsigned', async () => { + renderWithProviders( + , + ) + + await screen.findByText('Contractor profile') + expect(screen.queryByText('Changes will require an updated Form W-9')).not.toBeInTheDocument() + }) + + it('does not fetch documents or render the warning in create mode', async () => { + const documentsResolver = vi.fn(() => + HttpResponse.json(buildContractorDocumentsList()), + ) + server.use(handleGetContractorDocuments(documentsResolver)) + + renderWithProviders() + + await screen.findByText('Contractor profile') + expect(screen.queryByText('Changes will require an updated Form W-9')).not.toBeInTheDocument() + expect(documentsResolver).not.toHaveBeenCalled() + }) + }) }) diff --git a/src/components/Contractor/Profile/ContractorProfile.tsx b/src/components/Contractor/Profile/ContractorProfile.tsx index b660baef6..98b6d67c8 100644 --- a/src/components/Contractor/Profile/ContractorProfile.tsx +++ b/src/components/Contractor/Profile/ContractorProfile.tsx @@ -21,6 +21,7 @@ import { useComponentContext } from '@/contexts/ComponentAdapter/useComponentCon import { useI18n } from '@/i18n' import { useComponentDictionary } from '@/i18n/I18n' import { componentEvents, ContractorOnboardingStatus } from '@/shared/constants' +import { useContractorHasSignedW9 } from '@/components/Contractor/shared/useContractorHasSignedW9' // Once the contractor has finished self-onboarding (or the record is otherwise // under admin review / completed), the admin needs to view and edit SSN/EIN even @@ -150,6 +151,18 @@ export function ContractorProfile(props: ContractorProfileProps) { ) } +function ContractorProfileW9Warning({ contractorId }: { contractorId: string }) { + const hasSignedW9 = useContractorHasSignedW9(contractorId) + const { t } = useTranslation('Contractor.Profile') + const Components = useComponentContext() + if (!hasSignedW9) return null + return ( + + {t('w9EditWarning.body')} + + ) +} + function ContractorProfileRoot({ companyId, contractorId, @@ -262,6 +275,9 @@ function ContractorProfileReady({ void handleSubmit()}> + {mode === 'update' && contractor.data.contractor?.uuid && ( + + )}
{t('title')} diff --git a/src/i18n/en/Contractor.Address.json b/src/i18n/en/Contractor.Address.json index bf8a20c71..43ee149e8 100644 --- a/src/i18n/en/Contractor.Address.json +++ b/src/i18n/en/Contractor.Address.json @@ -3,6 +3,10 @@ "businessAddressDescription": "Contractor's business address, within the United States.", "homeAddressTitle": "Home address", "homeAddressDescription": "Contractor's home mailing address, within the United States.", + "w9EditWarning": { + "label": "Changes will require an updated Form W-9", + "body": "This contractor has already signed a form W-9. If you are making corrections, you’re also required to update and retain a new signed version of Form W-9 reflecting the corrected information." + }, "street1": "Street 1", "street2": "Street 2", "city": "City", diff --git a/src/i18n/en/Contractor.Profile.json b/src/i18n/en/Contractor.Profile.json index e7622edb6..3618c3771 100644 --- a/src/i18n/en/Contractor.Profile.json +++ b/src/i18n/en/Contractor.Profile.json @@ -1,6 +1,10 @@ { "title": "Contractor profile", "subtitle": "This information will be used for payments and on tax documents, so double-check that it's accurate.", + "w9EditWarning": { + "label": "Changes will require an updated Form W-9", + "body": "This contractor has already signed a form W-9. If you are making corrections, you’re also required to update and retain a new signed version of Form W-9 reflecting the corrected information." + }, "selfOnboarding": { "title": "Complete your profile", "individualDescription": "Please verify your name and provide your Social Security Number.", @@ -66,8 +70,8 @@ "buttons": { "cancel": "Cancel", "create": "Create Contractor", - "update": "Update Contractor", + "update": "Continue", "creating": "Creating…", - "updating": "Updating…" + "updating": "Saving…" } } diff --git a/src/i18n/types.d.ts b/src/i18n/types.d.ts index 33d08638e..08208504b 100644 --- a/src/i18n/types.d.ts +++ b/src/i18n/types.d.ts @@ -1710,6 +1710,12 @@ export namespace Translations { homeAddressTitle: string /** @defaultValue `"Contractor's home mailing address, within the United States."` */ homeAddressDescription: string + w9EditWarning: { + /** @defaultValue `"Changes will require an updated Form W-9"` */ + label: string + /** @defaultValue `"This contractor has already signed a form W-9. If you are making corrections, you’re also required to update and retain a new signed version of Form W-9 reflecting the corrected information."` */ + body: string + } /** @defaultValue `"Street 1"` */ street1: string /** @defaultValue `"Street 2"` */ @@ -2352,6 +2358,12 @@ export namespace Translations { title: string /** @defaultValue `"This information will be used for payments and on tax documents, so double-check that it's accurate."` */ subtitle: string + w9EditWarning: { + /** @defaultValue `"Changes will require an updated Form W-9"` */ + label: string + /** @defaultValue `"This contractor has already signed a form W-9. If you are making corrections, you’re also required to update and retain a new signed version of Form W-9 reflecting the corrected information."` */ + body: string + } selfOnboarding: { /** @defaultValue `"Complete your profile"` */ title: string @@ -2451,11 +2463,11 @@ export namespace Translations { cancel: string /** @defaultValue `"Create Contractor"` */ create: string - /** @defaultValue `"Update Contractor"` */ + /** @defaultValue `"Continue"` */ update: string /** @defaultValue `"Creating…"` */ creating: string - /** @defaultValue `"Updating…"` */ + /** @defaultValue `"Saving…"` */ updating: string } }