From 5bf6894d551ccd998b7c1d146a72a795ebc5cd91 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 12 Oct 2026 09:27:29 +0200 Subject: [PATCH 1/2] feat(onboarding): show job title eligibility statements for maybe and not eligible roles When the job_title_eligibility check returns needs_review (maybe) or not_eligible (no), render an info or error statement under role_description on contract details, mirroring Dragon's ineligible role message. The statement follows re-checks and is removed once the role is eligible. Submit is not blocked; the backend still decides. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/flows/Onboarding/hooks.tsx | 33 +++++++ ...OnboardingFlowJobTitleEligibility.test.tsx | 85 +++++++++++++++++++ src/flows/Onboarding/utils.ts | 28 ++++++ 3 files changed, 146 insertions(+) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index a588e364c..5f2e0993f 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -15,6 +15,8 @@ import { buildSteps, StepKeys, usesJsfV1ContractDetails, + getJobTitleEligibilityStatement, + JOB_TITLE_ELIGIBILITY_RESULT_FIELD, } from '@/src/flows/Onboarding/utils'; import { prettifyFormValues } from '@/src/lib/utils'; import { @@ -596,6 +598,9 @@ export const useOnboarding = ({ options?.features?.includes('daily_schedule'), ); + const roleDescriptionField = + options?.jsfModify?.contract_details?.fields?.role_description; + const contractDetailsCustomFields = useMemo( () => ({ fields: { @@ -644,6 +649,31 @@ export const useOnboarding = ({ }, }, }, + ...(isJobTitleEligibilityEnabled + ? { + role_description: { + ...(roleDescriptionField as $TSFixMe), + presentation: { + ...(roleDescriptionField as $TSFixMe)?.presentation, + calculateDynamicProperties: ( + values: FieldValues, + field: JSFField, + ) => ({ + ...( + roleDescriptionField as $TSFixMe + )?.presentation?.calculateDynamicProperties?.( + values, + field, + ), + ...getJobTitleEligibilityStatement( + values[JOB_TITLE_ELIGIBILITY_RESULT_FIELD], + employment?.country?.name, + ), + }), + }, + }, + } + : {}), ...(isDailyScheduleEnabled ? { daily_schedule: { @@ -684,6 +714,9 @@ export const useOnboarding = ({ useSplitSalaryDescription, isDailyScheduleEnabled, dailyScheduleField, + isJobTitleEligibilityEnabled, + roleDescriptionField, + employment?.country?.name, ], ); diff --git a/src/flows/Onboarding/tests/OnboardingFlowJobTitleEligibility.test.tsx b/src/flows/Onboarding/tests/OnboardingFlowJobTitleEligibility.test.tsx index d9266b45d..57b0810e9 100644 --- a/src/flows/Onboarding/tests/OnboardingFlowJobTitleEligibility.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlowJobTitleEligibility.test.tsx @@ -637,6 +637,91 @@ describe.each(schemaPaths)( }); }); + it('shows an info statement when the check needs review', async () => { + mockCheckResponse([{ check_id: 'check-id', verdict: 'needs_review' }]); + + await renderContractDetailsStep(); + await fillRoleFields(); + + expect( + await screen.findByText( + `AI isn't sure if we can hire this role in ${countryName}`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + "Complete the add employee process and we'll let you know as soon as we can if we can hire this role.", + ), + ).toBeInTheDocument(); + }); + + it('shows an error statement when the role is not eligible', async () => { + mockCheckResponse([{ check_id: 'check-id', verdict: 'not_eligible' }]); + + await renderContractDetailsStep(); + await fillRoleFields(); + + expect( + await screen.findByText( + `AI determined we can't hire this role in ${countryName}`, + ), + ).toBeInTheDocument(); + expect( + screen.getByText( + "Based on the job title and role description entered, we can't hire this role. Update the role details to check again.", + ), + ).toBeInTheDocument(); + }); + + it('does not show a statement when the role is eligible', async () => { + mockCheckResponse([{ check_id: 'check-id', verdict: 'eligible' }]); + + await renderContractDetailsStep(); + await fillRoleFields(); + await waitFor(() => + expect(checkResponsesSent).toHaveBeenCalledTimes(1), + ); + + expect(screen.queryByText(/^AI /)).not.toBeInTheDocument(); + }); + + it('switches the statement when a new check changes the verdict and removes it once eligible', async () => { + mockCheckResponse([ + { check_id: 'not-eligible-check-id', verdict: 'not_eligible' }, + { check_id: 'needs-review-check-id', verdict: 'needs_review' }, + { check_id: 'eligible-check-id', verdict: 'eligible' }, + ]); + + await renderContractDetailsStep(); + await fillRoleFields(); + await screen.findByText( + `AI determined we can't hire this role in ${countryName}`, + ); + + const user = userEvent.setup(); + const changeRoleDescription = async (text: string) => { + const roleDescription = screen.getByLabelText(/Role description/i); + await user.clear(roleDescription); + await user.type(roleDescription, text); + await user.tab(); + }; + + await changeRoleDescription('Frontend engineer responsibilities'); + await screen.findByText( + `AI isn't sure if we can hire this role in ${countryName}`, + ); + expect( + screen.queryByText( + `AI determined we can't hire this role in ${countryName}`, + ), + ).not.toBeInTheDocument(); + + await changeRoleDescription('Product engineer responsibilities'); + await waitFor(() => + expect(screen.queryByText(/^AI /)).not.toBeInTheDocument(), + ); + }); + it('hides the risk acknowledgement again once the role changes and the new check is eligible', async () => { mockCheckResponse([ { diff --git a/src/flows/Onboarding/utils.ts b/src/flows/Onboarding/utils.ts index 1e0098a6c..a3f866878 100644 --- a/src/flows/Onboarding/utils.ts +++ b/src/flows/Onboarding/utils.ts @@ -5,6 +5,7 @@ import { import { Employment, OnboardingFlowProps } from '@/src/flows/Onboarding/types'; import { Step } from '@/src/flows/useStepState'; import { JSFField, JSFFields } from '@/src/types/remoteFlows'; +import { createStatementProperty } from '@/src/components/form/jsf-utils/createFields'; export type StepKeys = | 'select_country' @@ -353,3 +354,30 @@ export const getJobTitleEligibilityValues = ( [JOB_TITLE_ELIGIBILITY_RESULT_FIELD]: JOB_TITLE_ELIGIBILITY_VERDICT_RESULTS[check.verdict] ?? null, }); + +export const getJobTitleEligibilityStatement = ( + result: unknown, + countryName?: string, +) => { + const inCountry = countryName ? `in ${countryName}` : 'in this country'; + + if (result === 'maybe') { + return createStatementProperty({ + title: `AI isn't sure if we can hire this role ${inCountry}`, + description: + "Complete the add employee process and we'll let you know as soon as we can if we can hire this role.", + severity: 'info', + }); + } + + if (result === 'no') { + return createStatementProperty({ + title: `AI determined we can't hire this role ${inCountry}`, + description: + "Based on the job title and role description entered, we can't hire this role. Update the role details to check again.", + severity: 'error', + }); + } + + return null; +}; From 1ddb34e7fec727ffa3d96639dd53e18e70b53d93 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 12 Oct 2026 09:54:13 +0200 Subject: [PATCH 2/2] refactor(onboarding): spell out how role_description dynamic props are combined Co-Authored-By: Claude Opus 5.5 (1M context) --- src/flows/Onboarding/hooks.tsx | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 5f2e0993f..f963d112a 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -658,18 +658,20 @@ export const useOnboarding = ({ calculateDynamicProperties: ( values: FieldValues, field: JSFField, - ) => ({ - ...( - roleDescriptionField as $TSFixMe - )?.presentation?.calculateDynamicProperties?.( - values, - field, - ), - ...getJobTitleEligibilityStatement( + ) => { + const partnerFn = (roleDescriptionField as $TSFixMe) + ?.presentation?.calculateDynamicProperties; + const partnerProps = partnerFn + ? partnerFn(values, field) + : {}; + + const ourProps = getJobTitleEligibilityStatement( values[JOB_TITLE_ELIGIBILITY_RESULT_FIELD], employment?.country?.name, - ), - }), + ); + + return { ...partnerProps, ...ourProps }; + }, }, }, }