Skip to content

Commit e287058

Browse files
authored
feat(contractor-onboarding) - support skippable errors (#787)
* feat(contractor-onboarding) - support skippable errors * add feedback * format * fix warning * fix review eligibility * add test * add tests * add test * tests * fix css * fix resubmission * simplify * fix types
1 parent 4d9b4be commit e287058

9 files changed

Lines changed: 583 additions & 113 deletions

File tree

example/src/ContractorOnboarding.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,10 +225,37 @@ const MultiStepForm = ({
225225
response: ContractorOnboardingContractDetailsResponse,
226226
) => console.log('response', response)}
227227
onError={({ error, fieldErrors }) => {
228+
console.log('error', error);
228229
setErrors({ apiError: error.message, fieldErrors });
229230
}}
230231
/>
231232
<AlertError errors={errors} />
233+
{contractorOnboardingBag.canSkipAiValidation && (
234+
<div className='bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-4'>
235+
<div className='flex'>
236+
<div className='flex-shrink-0'>
237+
<svg
238+
className='h-5 w-5 text-yellow-400'
239+
viewBox='0 0 20 20'
240+
fill='currentColor'
241+
>
242+
<path
243+
fillRule='evenodd'
244+
d='M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z'
245+
clipRule='evenodd'
246+
/>
247+
</svg>
248+
</div>
249+
<div className='ml-3'>
250+
<p className='text-sm text-yellow-700'>
251+
AI validation detected potential compliance issues. You can
252+
edit the Services and Deliverables field above or continue
253+
at your own risk by clicking Submit again.
254+
</p>
255+
</div>
256+
</div>
257+
</div>
258+
)}
232259
<div className='contractor-onboarding-buttons-container'>
233260
<BackButton
234261
className='back-button'
@@ -240,7 +267,9 @@ const MultiStepForm = ({
240267
className='submit-button'
241268
onClick={() => setErrors({ apiError: '', fieldErrors: [] })}
242269
>
243-
Continue
270+
{contractorOnboardingBag.canSkipAiValidation
271+
? 'Continue Anyway'
272+
: 'Continue'}
244273
</SubmitButton>
245274
</div>
246275
</div>

example/src/ReviewContractorOnboardingStep.tsx

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,20 +90,22 @@ export const ReviewContractorOnboardingStep = ({
9090
Edit Pricing Plan
9191
</button>
9292
{onboardingBag.stepState.values?.pricing_plan?.subscription ===
93-
corProductIdentifier && (
94-
<>
95-
<h2 className='title'>Eligibility Questionnaire</h2>
96-
<ReviewMeta
97-
meta={onboardingBag.meta.fields.eligibility_questionnaire}
98-
/>
99-
<button
100-
className='back-button'
101-
onClick={() => onboardingBag.goTo('eligibility_questionnaire')}
102-
>
103-
Edit Eligibility Questionnaire
104-
</button>
105-
</>
106-
)}
93+
corProductIdentifier &&
94+
Object.keys(onboardingBag.meta.fields.eligibility_questionnaire)
95+
.length > 0 && (
96+
<>
97+
<h2 className='title'>Eligibility Questionnaire</h2>
98+
<ReviewMeta
99+
meta={onboardingBag.meta.fields.eligibility_questionnaire}
100+
/>
101+
<button
102+
className='back-button'
103+
onClick={() => onboardingBag.goTo('eligibility_questionnaire')}
104+
>
105+
Edit Eligibility Questionnaire
106+
</button>
107+
</>
108+
)}
107109
<h2 className='title'>Contract Details</h2>
108110
<ReviewMeta meta={onboardingBag.meta.fields.contract_details} />
109111
<button

example/src/css/contractor-onboarding.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,6 @@
7575
flex-direction: column;
7676
gap: 16px;
7777
flex-direction: column-reverse;
78-
max-width: 95px;
78+
max-width: 140px;
7979
margin: 0 auto;
8080
}

src/flows/ContractorOnboarding/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ export const PRODUCT_IDENTIFIER_MAP: Record<ProductType, string> = {
1818

1919
export const IR35_FILE_SUBTYPE = 'ir_35';
2020

21+
export const REMOTE_AI_ERROR_SOURCE = 'REMOTE_AI';
22+
23+
export const REMOTE_AI_SERVICES_AND_DELIVERABLES_ERROR_MESSAGE =
24+
"You cannot control how a contractor completes the Services and Deliverables. This means, for example, that you can't specify their working hours or use of subcontractors. Please make sure this field includes only project names, descriptions, and deliverables. We do not allow hyperlinks.";
25+
26+
export const REMOTE_AI_SERVICES_AND_DELIVERABLES_COR_ERROR_MESSAGE =
27+
'The content You have entered may not be consistent with the Contractor of Record terms. You are responsible before proceeding for ensuring that the language entered herein accurately reflects those terms and your relationship with Subcontractors.';
28+
2129
const standardOnboardingWorkflow = [
2230
{
2331
title: 'You add a new contractor and their details in Remote.',

src/flows/ContractorOnboarding/hooks.tsx

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,15 @@ import {
5353
contractorPlusProductIdentifier,
5454
corProductIdentifier,
5555
eorProductIdentifier,
56+
REMOTE_AI_ERROR_SOURCE,
5657
} from '@/src/flows/ContractorOnboarding/constants';
5758
import {
5859
buildBasicInformationJsfModify,
5960
buildContractDetailsJsfModify,
6061
buildContractPreviewJsfModify,
6162
} from '@/src/flows/ContractorOnboarding/jsfModify';
63+
import { transformAiErrorResponse } from '@/src/flows/ContractorOnboarding/utils';
64+
import { AiValidationError } from '@/src/flows/ContractorOnboarding/types';
6265
import { useUploadFile } from '@/src/common/api/files';
6366
import { dataURLtoFile } from '@/src/lib/files';
6467
import { useEmploymentQuery } from '@/src/common/api/employment';
@@ -544,6 +547,7 @@ export const useContractorOnboarding = ({
544547
descriptionProvisionalStartDate,
545548
selectedPricingPlan,
546549
fieldValues,
550+
selectedPricingPlan === corProductIdentifier,
547551
),
548552
},
549553
});
@@ -902,6 +906,25 @@ export const useContractorOnboarding = ({
902906
return {};
903907
};
904908

909+
/**
910+
* Extracts AI validation error from the error response
911+
* @param error - The error object from the API call
912+
* @returns The AI validation error if found, null otherwise
913+
*/
914+
const extractAiValidationError = (
915+
error: $TSFixMe,
916+
): AiValidationError | null => {
917+
const errorData = error?.rawError?.error?.errors?.services_and_deliverables;
918+
if (errorData?.source === REMOTE_AI_ERROR_SOURCE) {
919+
return {
920+
error: errorData.error,
921+
source: errorData.source,
922+
skippable: errorData.skippable,
923+
};
924+
}
925+
return null;
926+
};
927+
905928
async function onSubmit(values: FieldValues) {
906929
const currentStepName = stepState.currentStep.name;
907930
if (currentStepName in fieldsMetaRef.current) {
@@ -974,20 +997,41 @@ export const useContractorOnboarding = ({
974997
return;
975998
}
976999
case 'contract_details': {
1000+
console.log('parsedValues', parsedValues);
1001+
const shouldSkipAiChecks =
1002+
fieldValues.services_and_deliverables_error_skippable === true;
9771003
const payload: CreateContractDocument = {
9781004
contract_document: parsedValues,
1005+
skip_ai_checks: shouldSkipAiChecks,
9791006
};
980-
const response = await createContractorContractDocumentMutationAsync({
981-
employmentId: internalEmploymentId as string,
982-
payload,
983-
});
984-
const contractDocumentId = response?.data?.contract_document?.id;
985-
if (!contractDocumentId) {
986-
throw createStructuredError('Contract document ID not found');
987-
}
988-
setInternalContractDocumentId(contractDocumentId);
9891007

990-
return response;
1008+
try {
1009+
const response = await createContractorContractDocumentMutationAsync({
1010+
employmentId: internalEmploymentId as string,
1011+
payload,
1012+
});
1013+
const contractDocumentId = response?.data?.contract_document?.id;
1014+
if (!contractDocumentId) {
1015+
throw createStructuredError('Contract document ID not found');
1016+
}
1017+
setInternalContractDocumentId(contractDocumentId);
1018+
1019+
return response;
1020+
} catch (error) {
1021+
const aiError = extractAiValidationError(error);
1022+
if (aiError) {
1023+
const isContractorOfRecord =
1024+
selectedPricingPlan === corProductIdentifier;
1025+
setFieldValues({
1026+
...values,
1027+
services_and_deliverables_ai_warning:
1028+
transformAiErrorResponse(isContractorOfRecord),
1029+
services_and_deliverables_error_skippable: aiError.skippable,
1030+
});
1031+
}
1032+
1033+
throw error;
1034+
}
9911035
}
9921036

9931037
case 'contract_preview': {
@@ -1199,6 +1243,14 @@ export const useContractorOnboarding = ({
11991243
*/
12001244
parseFormValues,
12011245

1246+
/**
1247+
* Indicates whether AI validation errors can be skipped (user can continue at their own risk).
1248+
* True when there's a skippable AI validation error on services_and_deliverables field.
1249+
* @returns {boolean}
1250+
*/
1251+
canSkipAiValidation:
1252+
fieldValues.services_and_deliverables_error_skippable === true,
1253+
12021254
/**
12031255
* Function to validate form values against the onboarding schema
12041256
* @param values - Form values to validate

src/flows/ContractorOnboarding/jsfModify.tsx

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
1+
import { format } from 'date-fns';
2+
import { FieldValues } from 'react-hook-form';
3+
import { ChangeEvent } from 'react';
4+
15
import { createStatementProperty } from '@/src/components/form/jsf-utils/createFields';
26
import { zendeskArticles } from '@/src/components/shared/zendesk-drawer/utils';
37
import { ZendeskTriggerButton } from '@/src/components/shared/zendesk-drawer/ZendeskTriggerButton';
48
import { ContractPreviewHeader } from '@/src/flows/ContractorOnboarding/components/ContractPreviewHeader';
59
import { ContractPreviewStatement } from '@/src/flows/ContractorOnboarding/components/ContractPreviewStatement';
6-
import { contractorStandardProductIdentifier } from '@/src/flows/ContractorOnboarding/constants';
7-
import { ContractorOnboardingFlowProps } from '@/src/flows/ContractorOnboarding/types';
10+
import {
11+
contractorStandardProductIdentifier,
12+
REMOTE_AI_SERVICES_AND_DELIVERABLES_COR_ERROR_MESSAGE,
13+
REMOTE_AI_SERVICES_AND_DELIVERABLES_ERROR_MESSAGE,
14+
} from '@/src/flows/ContractorOnboarding/constants';
15+
import {
16+
ContractorOnboardingFlowProps,
17+
ContractorOnboardingContractDetailsFormPayload,
18+
} from '@/src/flows/ContractorOnboarding/types';
819
import { isNationalityCountryCode } from '@/src/flows/ContractorOnboarding/utils';
920
import { JSFModify } from '@/src/flows/types';
1021
import { FILE_TYPES, MAX_FILE_SIZE } from '@/src/lib/uploadConfig';
1122
import { JSFCustomComponentProps } from '@/src/types/remoteFlows';
12-
import { format } from 'date-fns';
13-
import { FieldValues } from 'react-hook-form';
1423

1524
const isStandardPricingPlan = (pricingPlan: string | undefined) => {
1625
return pricingPlan === contractorStandardProductIdentifier;
@@ -36,6 +45,31 @@ const showBackDateWarning = (
3645
return undefined;
3746
};
3847

48+
/**
49+
* Handles changes to the services_and_deliverables field to clear AI warning state
50+
*/
51+
function onServicesAndDeliverablesChange(
52+
_event: ChangeEvent<HTMLTextAreaElement>,
53+
values: ContractorOnboardingContractDetailsFormPayload & {
54+
services_and_deliverables_ai_warning: string;
55+
services_and_deliverables_error_skippable: boolean;
56+
},
57+
setValues: (
58+
formValues: Partial<
59+
ContractorOnboardingContractDetailsFormPayload & {
60+
services_and_deliverables_ai_warning: string;
61+
services_and_deliverables_error_skippable: boolean;
62+
}
63+
>,
64+
) => void,
65+
) {
66+
setValues({
67+
...values,
68+
services_and_deliverables_ai_warning: '',
69+
services_and_deliverables_error_skippable: false,
70+
});
71+
}
72+
3973
/**
4074
* Merges internal jsfModify modifications with user-provided options for contract_details step
4175
* This abstracts the logic of applying internal field modifications (like dynamic descriptions)
@@ -46,6 +80,7 @@ export const buildContractDetailsJsfModify = (
4680
provisionalStartDateDescription: string | undefined,
4781
selectedPricingPlan: string | undefined,
4882
fieldValues: FieldValues,
83+
isContractorOfRecord: boolean,
4984
): JSFModify => {
5085
const isStandardPricingPlanSelected =
5186
isStandardPricingPlan(selectedPricingPlan);
@@ -55,8 +90,30 @@ export const buildContractDetailsJsfModify = (
5590
isStandardPricingPlanSelected,
5691
provisionalStartDate,
5792
);
93+
const AiStatementWarning = createStatementProperty({
94+
severity: 'warning',
95+
title: 'Possible misclassification risk',
96+
description: isContractorOfRecord
97+
? REMOTE_AI_SERVICES_AND_DELIVERABLES_COR_ERROR_MESSAGE
98+
: REMOTE_AI_SERVICES_AND_DELIVERABLES_ERROR_MESSAGE,
99+
});
58100
return {
59101
...userJsfModify,
102+
create: {
103+
...userJsfModify?.create,
104+
services_and_deliverables_ai_warning: {
105+
type: 'string',
106+
'x-jsf-presentation': {
107+
inputType: 'hidden',
108+
},
109+
},
110+
services_and_deliverables_error_skippable: {
111+
type: 'boolean',
112+
'x-jsf-presentation': {
113+
inputType: 'hidden',
114+
},
115+
},
116+
},
60117
fields: {
61118
...userJsfModify?.fields,
62119
...{
@@ -69,6 +126,16 @@ export const buildContractDetailsJsfModify = (
69126
...statement,
70127
},
71128
},
129+
services_and_deliverables: {
130+
onChange: onServicesAndDeliverablesChange,
131+
'x-jsf-presentation': {
132+
calculateDynamicProperties: (formValues: FieldValues) => ({
133+
statement: formValues.services_and_deliverables_ai_warning
134+
? AiStatementWarning.statement
135+
: undefined,
136+
}),
137+
},
138+
},
72139
},
73140
},
74141
};

0 commit comments

Comments
 (0)