From f2e2cd03e9b327e79730bbbe34cff79dc411320a Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 11:53:08 +0200 Subject: [PATCH 01/12] fix(preonboarding-requirements) - shouldFreezeEmploymentData makes employment readonly --- src/flows/Onboarding/hooks.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 5bdb94dce..0cbf85e33 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -90,6 +90,7 @@ const getLoadingStates = ({ contractDetailsFields, arePreOnboardingRequirementsFulfilled, isLoadingOnboardingReservesStatus, + shouldFreezeEmploymentData, }: { isLoadingBasicInformationForm: boolean; isLoadingContractDetailsForm: boolean; @@ -109,6 +110,7 @@ const getLoadingStates = ({ contractDetailsFields: JSFFields; arePreOnboardingRequirementsFulfilled: boolean; isLoadingOnboardingReservesStatus: boolean; + shouldFreezeEmploymentData: boolean; }) => { const initialLoading = isLoadingBasicInformationForm || @@ -123,9 +125,13 @@ const getLoadingStates = ({ isLoadingEmploymentAgreementPreview || isLoadingContractDetailsFormV1; + // employment needs to be readonly if its one of the following conditions is met: + // - the employment status is in the review step allowed employment status list + // - the employment data is frozen by a pre-onboarding requirement const isEmploymentReadOnly = - employmentStatus && - reviewStepAllowedEmploymentStatus.includes(employmentStatus); + (employmentStatus && + reviewStepAllowedEmploymentStatus.includes(employmentStatus)) || + shouldFreezeEmploymentData; const canInvite = employmentStatus && @@ -933,6 +939,7 @@ export const useOnboarding = ({ currentStepName: currentStepName, arePreOnboardingRequirementsFulfilled, isLoadingOnboardingReservesStatus, + shouldFreezeEmploymentData, }), [ isLoadingBasicInformationForm, @@ -953,6 +960,7 @@ export const useOnboarding = ({ arePreOnboardingRequirementsFulfilled, isLoadingOnboardingReservesStatus, isLoadingContractDetailsFormV1, + shouldFreezeEmploymentData, ], ); From 863a180d0a3b66982a32c44a4201c0b3c4351ef1 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 11:55:14 +0200 Subject: [PATCH 02/12] cast --- src/flows/Onboarding/hooks.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 0cbf85e33..103bf7042 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -397,6 +397,10 @@ export const useOnboarding = ({ }, }); + const shouldFreezeEmploymentData = Boolean( + requirements?.some((requirement) => requirement.freeze_employment_data), + ); + const arePreOnboardingRequirementsFulfilled = useMemo(() => { // While loading, block the invite if (isLoadingPreOnboardingRequirements) { From f73ffd390589d6a2dcac20555a699a541e3ac7d5 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 12:03:23 +0200 Subject: [PATCH 03/12] fix --- .../Onboarding/tests/OnboardingFlow.test.tsx | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx index 89bf27d35..6550355a5 100644 --- a/src/flows/Onboarding/tests/OnboardingFlow.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingFlow.test.tsx @@ -1198,6 +1198,97 @@ describe('OnboardingFlow', () => { }, ); + it('should automatically navigate to review step when freeze_employment_data is true in pre-onboarding requirement', async () => { + const employmentId = generateUniqueEmploymentId(); + + server.use( + // Mock employment endpoint with 'created' status (editable status normally) + http.get(`*/v1/employments/${employmentId}`, () => { + return HttpResponse.json({ + ...employmentDefaultResponse, + data: { + ...employmentDefaultResponse.data, + employment: { + ...employmentDefaultResponse.data.employment, + id: employmentId, + status: 'created', // Normally editable status + }, + }, + }); + }), + + // Mock pre-onboarding requirements with freeze_employment_data: true + http.get( + `*/v1/onboarding/employments/${employmentId}/pre-onboarding-requirements`, + () => { + return HttpResponse.json({ + data: [ + { + name: 'Individual Labor Agreement', + status: 'awaiting', + type: 'document', + description: 'Individual Labor Agreement required', + slug: '5e39159e-96ef-40ea-82bc-b054917fc82f', + depends_on_requirement: null, + freeze_employment_data: true, + redlining_help_email: null, + supports_redlining: false, + }, + ], + }); + }, + ), + ); + + mockRender.mockImplementation( + ({ onboardingBag, components }: OnboardingRenderProps) => { + const currentStepIndex = onboardingBag.stepState.currentStep.index; + + const steps: Record = { + [0]: 'Basic Information', + [1]: 'Contract Details', + [2]: 'Benefits', + [3]: 'Review', + }; + + return ( + <> +

Step: {steps[currentStepIndex]}

+ + + ); + }, + ); + + render( + , + { + wrapper: TestProviders, + }, + ); + + await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); + + // Should automatically go to review step due to freeze_employment_data: true + await screen.findByText(/Step: Review/i); + + // Verify basic information data is displayed in the Review component + expect(screen.getByText('name: Gabriel')).toBeInTheDocument(); + + // Verify contract details data is displayed in the Review component + expect(screen.getByText('annual_gross_salary: 20000')).toBeInTheDocument(); + }); + it('should not show intermediate steps when automatically navigating to review (no flickering)', async () => { const renderSequence: Array<{ isLoading: boolean; step?: string }> = []; const employmentId = generateUniqueEmploymentId(); // Use a fixed ID for consistency From 3131855fb5a39f4a560271032dc9658bc73bb971 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 12:45:23 +0200 Subject: [PATCH 04/12] fix test --- .../Onboarding/tests/OnboardingInvite.test.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 9f3ecb189..68ec72411 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -17,7 +17,7 @@ import { waitFor, act, } from '@testing-library/react'; -import { http, HttpResponse } from 'msw'; +import { delay, http, HttpResponse } from 'msw'; import { PropsWithChildren } from 'react'; const mockSuccess = vi.fn(); @@ -1319,16 +1319,11 @@ describe('OnboardingInvite', () => { }); it('should keep button disabled during requirements loading to prevent race condition', async () => { - let resolveRequirements: (value: unknown) => void; - const requirementsPromise = new Promise((resolve) => { - resolveRequirements = resolve; - }); - server.use( http.get( - '*/v1/onboarding/employments/:employmentId/pre-onboarding-requirements', + '*/v1/onboarding/employments/*/pre-onboarding-requirements', async () => { - await requirementsPromise; + await delay(100); return HttpResponse.json({ data: [ { @@ -1354,10 +1349,6 @@ describe('OnboardingInvite', () => { const inviteButton = screen.getByTestId('onboarding-invite'); expect(inviteButton).toBeDisabled(); - await act(async () => { - resolveRequirements!({}); - }); - await waitFor(() => { expect(inviteButton).not.toBeDisabled(); }); From ffd16a39f66f24073b1b90404a293015da7d5206 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 12:54:43 +0200 Subject: [PATCH 05/12] fix loadings... --- src/flows/Onboarding/hooks.tsx | 11 ++++++++--- src/flows/Onboarding/tests/OnboardingInvite.test.tsx | 1 - 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/flows/Onboarding/hooks.tsx b/src/flows/Onboarding/hooks.tsx index 103bf7042..940921ab0 100644 --- a/src/flows/Onboarding/hooks.tsx +++ b/src/flows/Onboarding/hooks.tsx @@ -90,6 +90,7 @@ const getLoadingStates = ({ contractDetailsFields, arePreOnboardingRequirementsFulfilled, isLoadingOnboardingReservesStatus, + isLoadingPreOnboardingRequirements, shouldFreezeEmploymentData, }: { isLoadingBasicInformationForm: boolean; @@ -103,6 +104,7 @@ const getLoadingStates = ({ isLoadingCompany: boolean; isLoadingCountries: boolean; isLoadingEmploymentAgreementPreview: boolean; + isLoadingPreOnboardingRequirements: boolean; employmentStatus?: Employment['status']; employmentId?: string; currentStepName: string; @@ -123,7 +125,9 @@ const getLoadingStates = ({ isLoadingCompany || isLoadingCountries || isLoadingEmploymentAgreementPreview || - isLoadingContractDetailsFormV1; + isLoadingContractDetailsFormV1 || + isLoadingPreOnboardingRequirements || + isLoadingOnboardingReservesStatus; // employment needs to be readonly if its one of the following conditions is met: // - the employment status is in the review step allowed employment status list @@ -136,8 +140,7 @@ const getLoadingStates = ({ const canInvite = employmentStatus && !disabledInviteButtonEmploymentStatus.includes(employmentStatus) && - arePreOnboardingRequirementsFulfilled && - !isLoadingOnboardingReservesStatus; + arePreOnboardingRequirementsFulfilled; const shouldHandleReadOnlyEmployment = Boolean( employmentId && isEmploymentReadOnly && currentStepName !== 'review', @@ -933,6 +936,7 @@ export const useOnboarding = ({ isLoadingEmployment, isLoadingBenefitsOffersSchema, isLoadingBenefitOffers, + isLoadingPreOnboardingRequirements, isLoadingCompany, isLoadingCountries, isLoadingEmploymentAgreementPreview, @@ -953,6 +957,7 @@ export const useOnboarding = ({ isLoadingEmployment, isLoadingBenefitsOffersSchema, isLoadingBenefitOffers, + isLoadingPreOnboardingRequirements, isLoadingCompany, isLoadingCountries, isLoadingEmploymentAgreementPreview, diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 68ec72411..4ec76cdf4 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -1347,7 +1347,6 @@ describe('OnboardingInvite', () => { await waitForElementToBeRemoved(() => screen.getByTestId('spinner')); const inviteButton = screen.getByTestId('onboarding-invite'); - expect(inviteButton).toBeDisabled(); await waitFor(() => { expect(inviteButton).not.toBeDisabled(); From 0310adf56da315509fac6078d66b60a74ebcbe98 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 12:56:16 +0200 Subject: [PATCH 06/12] no delay --- src/flows/Onboarding/tests/OnboardingInvite.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 4ec76cdf4..19c851f69 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -1323,7 +1323,6 @@ describe('OnboardingInvite', () => { http.get( '*/v1/onboarding/employments/*/pre-onboarding-requirements', async () => { - await delay(100); return HttpResponse.json({ data: [ { From 839497ed039eedb13901a36d4c34619aa0b24b07 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 12:58:54 +0200 Subject: [PATCH 07/12] fix lint --- src/flows/Onboarding/tests/OnboardingInvite.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx index 19c851f69..9147faac6 100644 --- a/src/flows/Onboarding/tests/OnboardingInvite.test.tsx +++ b/src/flows/Onboarding/tests/OnboardingInvite.test.tsx @@ -17,7 +17,7 @@ import { waitFor, act, } from '@testing-library/react'; -import { delay, http, HttpResponse } from 'msw'; +import { http, HttpResponse } from 'msw'; import { PropsWithChildren } from 'react'; const mockSuccess = vi.fn(); From 69cfea7a868c4878a8e502fa6d17b89fd8f91b92 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 13:15:07 +0200 Subject: [PATCH 08/12] fix conditions --- .../components/OnboardingInvite.tsx | 24 +++++++++---------- src/flows/Onboarding/utils.ts | 12 ++++++++++ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/flows/Onboarding/components/OnboardingInvite.tsx b/src/flows/Onboarding/components/OnboardingInvite.tsx index 0f1d8dacb..9664bcd40 100644 --- a/src/flows/Onboarding/components/OnboardingInvite.tsx +++ b/src/flows/Onboarding/components/OnboardingInvite.tsx @@ -5,6 +5,7 @@ import { FieldError, mutationToPromise } from '@/src/lib/mutations'; import { SuccessResponse } from '@/src/client'; import { useOnboardingContext } from '@/src/flows/Onboarding/context'; import { useFormFields } from '@/src/context'; +import { statusesWithReserveAlreadyHandled } from '@/src/flows/Onboarding/utils'; export type OnboardingInviteProps = Omit< ButtonHTMLAttributes, @@ -56,15 +57,19 @@ export function OnboardingInvite({ onboardingBag.creditRiskStatus === 'deposit_required' || onboardingBag.onboardingReservesStatus === 'deposit_required'; + const shouldCreateReserve = Boolean( + isDepositRequired && + onboardingBag.canInvite && + onboardingBag.employment?.status && + !statusesWithReserveAlreadyHandled.includes( + onboardingBag.employment.status, + ), + ); + const handleSubmit = async () => { try { await onSubmit?.(); - if ( - isDepositRequired && - onboardingBag.employmentId && - onboardingBag.employment?.status && - !onboardingBag.isEmploymentReadOnly - ) { + if (shouldCreateReserve && onboardingBag.employmentId) { const response = await createReserveInvoiceMutationAsync({ employment_slug: onboardingBag.employmentId, }); @@ -121,11 +126,6 @@ export function OnboardingInvite({ } }; - const isReserveFlow = - isDepositRequired && - onboardingBag.employment?.status && - !onboardingBag.isEmploymentReadOnly; - const CustomButton = components?.button; if (!CustomButton) { throw new Error(`Button component not found`); @@ -147,7 +147,7 @@ export function OnboardingInvite({ }} > {render({ - employmentStatus: isReserveFlow + employmentStatus: shouldCreateReserve ? 'created_awaiting_reserve' : 'invited', })} diff --git a/src/flows/Onboarding/utils.ts b/src/flows/Onboarding/utils.ts index 4da1dadb3..3cc0cecae 100644 --- a/src/flows/Onboarding/utils.ts +++ b/src/flows/Onboarding/utils.ts @@ -121,6 +121,18 @@ export const disabledInviteButtonEmploymentStatus: Employment['status'][] = [ 'active', ]; +/** + * Array of employment statuses that have already handled the reserve flow. + * @type {Employment['status'][]} + * @constant + */ +export const statusesWithReserveAlreadyHandled: Employment['status'][] = [ + 'created_awaiting_reserve', + 'created_reserve_paid', + 'invited', + 'active', +]; + export const DEFAULT_VERSION = 1; /** From 20eaf22fcc0ea9ef331e621f0483b6bf02613fd1 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 13:25:06 +0200 Subject: [PATCH 09/12] fix --- src/flows/Onboarding/components/OnboardingInvite.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/flows/Onboarding/components/OnboardingInvite.tsx b/src/flows/Onboarding/components/OnboardingInvite.tsx index 9664bcd40..5b0c166bc 100644 --- a/src/flows/Onboarding/components/OnboardingInvite.tsx +++ b/src/flows/Onboarding/components/OnboardingInvite.tsx @@ -57,15 +57,16 @@ export function OnboardingInvite({ onboardingBag.creditRiskStatus === 'deposit_required' || onboardingBag.onboardingReservesStatus === 'deposit_required'; - const shouldCreateReserve = Boolean( + const isReserveFlow = Boolean( isDepositRequired && - onboardingBag.canInvite && onboardingBag.employment?.status && !statusesWithReserveAlreadyHandled.includes( onboardingBag.employment.status, ), ); + const shouldCreateReserve = Boolean(isReserveFlow && onboardingBag.canInvite); + const handleSubmit = async () => { try { await onSubmit?.(); @@ -147,7 +148,7 @@ export function OnboardingInvite({ }} > {render({ - employmentStatus: shouldCreateReserve + employmentStatus: isReserveFlow ? 'created_awaiting_reserve' : 'invited', })} From 110942d98508d9e8b4d193c31fe4f786dd176c93 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 14:04:23 +0200 Subject: [PATCH 10/12] fix??? --- example/e2e/helpers/general.ts | 20 +++++++++++++------ src/components/form/validationResolver.ts | 2 ++ .../components/BasicInformationStep.tsx | 1 + 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/example/e2e/helpers/general.ts b/example/e2e/helpers/general.ts index 74fcdebf0..a48b328ab 100644 --- a/example/e2e/helpers/general.ts +++ b/example/e2e/helpers/general.ts @@ -160,19 +160,27 @@ export async function fillDatepicker( testId: string, ) { await page.getByTestId(testId).click(); + + // Wait for the calendar popup to be visible + await page + .locator('[role="dialog"]') + .waitFor({ state: 'visible', timeout: 5000 }); + if (value === 'auto') { - await page + const firstAvailableDate = page .locator('button[role="gridcell"]:not([disabled])') - .first() - .click(); + .first(); + await firstAvailableDate.waitFor({ state: 'visible' }); + await firstAvailableDate.click(); } else { - await page + const dateButton = page .getByRole('button', { name: value, exact: true, }) .and(page.locator(':not([disabled])')) - .first() - .click(); + .first(); + await dateButton.waitFor({ state: 'visible' }); + await dateButton.click(); } } diff --git a/src/components/form/validationResolver.ts b/src/components/form/validationResolver.ts index 843bb7ed3..f85ae3fb1 100644 --- a/src/components/form/validationResolver.ts +++ b/src/components/form/validationResolver.ts @@ -73,7 +73,9 @@ export const useJsonSchemasValidationFormResolver = < handleValidation: (data: T) => Promise, ): Resolver => { return async (data: T) => { + console.log('data', data); const result = await handleValidation(data); + console.log('result', result); // Handle null case - return no errors if (!result) { diff --git a/src/flows/Onboarding/components/BasicInformationStep.tsx b/src/flows/Onboarding/components/BasicInformationStep.tsx index 2318d2072..9a22f04ef 100644 --- a/src/flows/Onboarding/components/BasicInformationStep.tsx +++ b/src/flows/Onboarding/components/BasicInformationStep.tsx @@ -49,6 +49,7 @@ export function BasicInformationStep({ return; } if (response?.error) { + console.log('response?.fieldErrors', response?.fieldErrors); const normalizedFieldErrors = normalizeFieldErrors( response?.fieldErrors || [], onboardingBag.meta?.fields?.basic_information, From 0d44c2fd189b78b67d83200d786c0b260e9c284a Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 14:14:27 +0200 Subject: [PATCH 11/12] fix --- src/components/form/validationResolver.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/form/validationResolver.ts b/src/components/form/validationResolver.ts index f85ae3fb1..843bb7ed3 100644 --- a/src/components/form/validationResolver.ts +++ b/src/components/form/validationResolver.ts @@ -73,9 +73,7 @@ export const useJsonSchemasValidationFormResolver = < handleValidation: (data: T) => Promise, ): Resolver => { return async (data: T) => { - console.log('data', data); const result = await handleValidation(data); - console.log('result', result); // Handle null case - return no errors if (!result) { From baa0df5dd4af7ecb74198b09474321e513f44590 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 3 Aug 2026 14:41:45 +0200 Subject: [PATCH 12/12] revert --- src/flows/Onboarding/components/BasicInformationStep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flows/Onboarding/components/BasicInformationStep.tsx b/src/flows/Onboarding/components/BasicInformationStep.tsx index 9a22f04ef..2318d2072 100644 --- a/src/flows/Onboarding/components/BasicInformationStep.tsx +++ b/src/flows/Onboarding/components/BasicInformationStep.tsx @@ -49,7 +49,6 @@ export function BasicInformationStep({ return; } if (response?.error) { - console.log('response?.fieldErrors', response?.fieldErrors); const normalizedFieldErrors = normalizeFieldErrors( response?.fieldErrors || [], onboardingBag.meta?.fields?.basic_information,