Skip to content
Merged
10 changes: 8 additions & 2 deletions docs/reference/Translations/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"` |
| <a id="property-contractoraddressw9editwarning"></a> `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"` |
| <a id="property-contractoraddresszip"></a> `zip` | `"Zip"` |

***
Expand Down Expand Up @@ -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…"` |
| <a id="property-contractorprofilefields"></a> `fields` | |
| `fields.businessName` | |
| `fields.businessName.label` | `"Business Name"` |
Expand Down Expand Up @@ -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"` |
| <a id="property-contractorprofilew9editwarning"></a> `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"` |

***

Expand Down
2 changes: 1 addition & 1 deletion e2e/tests/contractor/03-onboarding-edit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
})
})
42 changes: 42 additions & 0 deletions src/components/Contractor/Address/Address.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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(<Address contractorId="contractor_id" onEvent={() => {}} />)

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(<Address contractorId="contractor_id" onEvent={() => {}} />)

await screen.findByText('Home address')
expect(screen.queryByText('Changes will require an updated Form W-9')).not.toBeInTheDocument()
})
})
})
15 changes: 15 additions & 0 deletions src/components/Contractor/Address/Address.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<Components.Alert status="warning" disableScrollIntoView label={t('w9EditWarning.label')}>
<Components.Text>{t('w9EditWarning.body')}</Components.Text>
</Components.Alert>
)
}

function AddressRoot({
contractorId,
defaultValues,
Expand Down Expand Up @@ -117,6 +130,8 @@ function AddressRoot({
<BaseLayout error={contractorAddress.errorHandling.errors}>
<SDKFormProvider formHookResult={contractorAddress}>
<Form onSubmit={() => void handleSubmit()}>
<ContractorAddressW9Warning contractorId={contractorId} />

<Flex flexDirection="column" gap={32} alignItems="stretch">
<header>
<Flex flexDirection="column" gap={4}>
Expand Down
89 changes: 87 additions & 2 deletions src/components/Contractor/Profile/ContractorProfile.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -427,14 +431,15 @@ describe('Contractor profile component behavior', () => {
onboarding_status: postSaveStatus,
}),
),
handleGetContractorDocuments(() => HttpResponse.json(buildContractorDocumentsList())),
)

renderWithProviders(
<ContractorProfile companyId={companyId} contractorId="contractor_id" onEvent={onEvent} />,
)

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(
Expand Down Expand Up @@ -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(
<ContractorProfile
companyId="company-123"
contractorId="contractor_id"
onEvent={vi.fn()}
/>,
)

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(
<ContractorProfile
companyId="company-123"
contractorId="contractor_id"
onEvent={vi.fn()}
/>,
)

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<HttpResponseResolver>(() =>
HttpResponse.json(buildContractorDocumentsList()),
)
server.use(handleGetContractorDocuments(documentsResolver))

renderWithProviders(<ContractorProfile companyId="company-123" onEvent={vi.fn()} />)

await screen.findByText('Contractor profile')
expect(screen.queryByText('Changes will require an updated Form W-9')).not.toBeInTheDocument()
expect(documentsResolver).not.toHaveBeenCalled()
})
})
})
16 changes: 16 additions & 0 deletions src/components/Contractor/Profile/ContractorProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<Components.Alert status="warning" disableScrollIntoView label={t('w9EditWarning.label')}>
<Components.Text>{t('w9EditWarning.body')}</Components.Text>
</Components.Alert>
)
}

function ContractorProfileRoot({
companyId,
contractorId,
Expand Down Expand Up @@ -262,6 +275,9 @@ function ContractorProfileReady({
<SDKFormProvider formHookResult={contractor}>
<Form onSubmit={() => void handleSubmit()}>
<Flex flexDirection="column" gap={20} alignItems="stretch">
{mode === 'update' && contractor.data.contractor?.uuid && (
<ContractorProfileW9Warning contractorId={contractor.data.contractor.uuid} />
)}
<header>
<Flex flexDirection="column" gap={4}>
<Components.Heading as="h2">{t('title')}</Components.Heading>
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/en/Contractor.Address.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 6 additions & 2 deletions src/i18n/en/Contractor.Profile.json
Original file line number Diff line number Diff line change
@@ -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.",
Expand Down Expand Up @@ -66,8 +70,8 @@
"buttons": {
"cancel": "Cancel",
"create": "Create Contractor",
"update": "Update Contractor",
"update": "Continue",
"creating": "Creating…",
"updating": "Updating…"
"updating": "Saving…"
}
}
16 changes: 14 additions & 2 deletions src/i18n/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"` */
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Expand Down
Loading