Skip to content

Commit ad04560

Browse files
authored
fix(contractor-onboarding)- avoid signing the document if it was already signed (#960)
* fix signature problem when going back * add test
1 parent 539c6b8 commit ad04560

3 files changed

Lines changed: 177 additions & 9 deletions

File tree

src/flows/ContractorOnboarding/api.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,35 @@ export const useGetShowContractDocument = ({
177177
});
178178
};
179179

180+
export const useHasCompanySignedContract = ({
181+
employmentId,
182+
contractDocumentId,
183+
options,
184+
}: {
185+
employmentId: string;
186+
contractDocumentId: string;
187+
options?: { queryOptions?: { enabled?: boolean } };
188+
}) => {
189+
const { data: documentPreviewPdf } = useGetShowContractDocument({
190+
employmentId,
191+
contractDocumentId,
192+
options: {
193+
queryOptions: {
194+
enabled: options?.queryOptions?.enabled,
195+
},
196+
},
197+
});
198+
199+
const hasCompanySignedContract =
200+
documentPreviewPdf?.contract_document?.signatories?.some(
201+
(signatory) =>
202+
signatory.type === 'company' && signatory.status === 'signed',
203+
);
204+
205+
return {
206+
hasCompanySignedContract,
207+
};
208+
};
180209
/**
181210
* Get the contractor subscriptions for the given employment id
182211
* @param employmentId - The employment ID

src/flows/ContractorOnboarding/hooks.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
useCountriesSchemaField,
3030
useContractorOnboardingDetailsSchemaWithCurrencies,
3131
CONTRACT_PRODUCT_TITLES,
32+
useHasCompanySignedContract,
3233
} from '@/src/flows/ContractorOnboarding/api';
3334
import {
3435
ContractorOnboardingFlowProps,
@@ -581,16 +582,29 @@ export const useContractorOnboarding = ({
581582
},
582583
});
583584

584-
const { data: documentPreviewPdf, isLoading: isLoadingDocumentPreviewForm } =
585-
useGetShowContractDocument({
586-
employmentId: internalEmploymentId as string,
587-
contractDocumentId: internalContractDocumentId as string,
588-
options: {
589-
queryOptions: {
590-
enabled: Boolean(internalContractDocumentId),
591-
},
585+
const {
586+
data: documentPreviewPdf,
587+
isLoading: isLoadingDocumentPreviewForm,
588+
refetch: refetchDocumentPreviewForm,
589+
} = useGetShowContractDocument({
590+
employmentId: internalEmploymentId as string,
591+
contractDocumentId: internalContractDocumentId as string,
592+
options: {
593+
queryOptions: {
594+
enabled: Boolean(internalContractDocumentId),
592595
},
593-
});
596+
},
597+
});
598+
599+
const { hasCompanySignedContract } = useHasCompanySignedContract({
600+
employmentId: internalEmploymentId as string,
601+
contractDocumentId: internalContractDocumentId as string,
602+
options: {
603+
queryOptions: {
604+
enabled: Boolean(internalContractDocumentId),
605+
},
606+
},
607+
});
594608

595609
const stepFields: Record<StepKeys, JSFFields> = useMemo(
596610
() => ({
@@ -1046,13 +1060,23 @@ export const useContractorOnboarding = ({
10461060
}
10471061

10481062
case 'contract_preview': {
1063+
if (hasCompanySignedContract) {
1064+
return Promise.resolve({
1065+
data: {
1066+
contract_document: {
1067+
id: internalContractDocumentId,
1068+
},
1069+
},
1070+
});
1071+
}
10491072
const response = await signContractDocumentMutationAsync({
10501073
employmentId: internalEmploymentId as string,
10511074
contractDocumentId: internalContractDocumentId as string,
10521075
payload: {
10531076
signature: parsedValues.signature,
10541077
},
10551078
});
1079+
await refetchDocumentPreviewForm();
10561080
return response;
10571081
}
10581082
case 'pricing_plan': {

src/flows/ContractorOnboarding/tests/ContractorOnboarding.test.tsx

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3071,5 +3071,120 @@ describe('ContractorOnboardingFlow', () => {
30713071
expect(screen.getByLabelText(/Enter full name/i)).toBeInTheDocument();
30723072
expect(signatureField).toHaveValue('John Doe');
30733073
});
3074+
3075+
it('should refetch contract document after signing and allow navigation back without errors', async () => {
3076+
const employmentId = generateUniqueEmploymentId();
3077+
const contractDocumentId = 'f4f32dbf-4d15-42ef-a960-fea60ab3b68c';
3078+
let getContractDocumentCallCount = 0;
3079+
const signContractSpy = vi.fn();
3080+
3081+
server.use(
3082+
http.post(
3083+
'*/v1/contractors/employments/*/contract-documents',
3084+
async () => {
3085+
return HttpResponse.json({
3086+
data: {
3087+
contract_document: {
3088+
id: contractDocumentId,
3089+
},
3090+
},
3091+
});
3092+
},
3093+
),
3094+
http.get(
3095+
'*/v1/contractors/employments/*/contract-documents/*',
3096+
async () => {
3097+
getContractDocumentCallCount++;
3098+
return HttpResponse.json({
3099+
data: {
3100+
contract_document: {
3101+
name: '2025-10-23_TestContract.pdf',
3102+
content: 'data:application/pdf;base64,JVBERi0xLjQ=',
3103+
signatories: [
3104+
{
3105+
type: 'company',
3106+
status:
3107+
getContractDocumentCallCount > 2 ? 'signed' : 'pending',
3108+
},
3109+
],
3110+
},
3111+
},
3112+
});
3113+
},
3114+
),
3115+
http.post(
3116+
'*/v1/contractors/employments/*/contract-documents/*/sign',
3117+
async ({ request }) => {
3118+
const requestBody = await request.json();
3119+
signContractSpy(requestBody);
3120+
return HttpResponse.json(mockContractDocumentSignedResponse);
3121+
},
3122+
),
3123+
);
3124+
3125+
mockRender.mockImplementation(
3126+
createMockRenderImplementation(MultiStepFormWithoutCountry),
3127+
);
3128+
3129+
render(
3130+
<ContractorOnboardingFlow
3131+
employmentId={employmentId}
3132+
countryCode='PRT'
3133+
skipSteps={['select_country']}
3134+
{...defaultProps}
3135+
/>,
3136+
{ wrapper: TestProviders },
3137+
);
3138+
3139+
await screen.findByText(/Step: Basic Information/i);
3140+
await waitForElementToBeRemoved(() => screen.getByTestId('spinner'));
3141+
3142+
await fillBasicInformation();
3143+
let nextButton = screen.getByText(/Next Step/i);
3144+
nextButton.click();
3145+
3146+
await screen.findByText(/Step: Pricing Plan/i);
3147+
3148+
await fillContractorSubscription();
3149+
nextButton = screen.getByText(/Next Step/i);
3150+
nextButton.click();
3151+
3152+
await screen.findByText(/Step: Contract Details/i);
3153+
await fillContractDetails();
3154+
3155+
nextButton = screen.getByText(/Next Step/i);
3156+
nextButton.click();
3157+
3158+
await screen.findByText(/Step: Contract Preview/i);
3159+
await waitForElementToBeRemoved(() => screen.getByTestId('spinner'));
3160+
const countBeforeSigning = getContractDocumentCallCount;
3161+
3162+
await fillSignature();
3163+
3164+
nextButton = screen.getByText(/Continue/i);
3165+
nextButton.click();
3166+
3167+
// Wait for navigation to review step, which means signing AND refetching completed
3168+
await screen.findByText(/Step: Review/i);
3169+
3170+
// Verify signing happened
3171+
expect(signContractSpy).toHaveBeenCalledTimes(1);
3172+
3173+
// After signing, the refetch should have happened
3174+
expect(getContractDocumentCallCount).toBe(countBeforeSigning + 1);
3175+
3176+
// Now navigate back to contract preview
3177+
const backButton = screen.getByText(/Back/i);
3178+
backButton.click();
3179+
3180+
await screen.findByText(/Step: Contract Preview/i);
3181+
3182+
// Verify no errors occurred during navigation back
3183+
expect(mockOnError).not.toHaveBeenCalled();
3184+
3185+
// Count might be 3 now if going back triggers another fetch,
3186+
// or still 2 if it uses cached data
3187+
expect(getContractDocumentCallCount).toBeGreaterThanOrEqual(2);
3188+
});
30743189
});
30753190
});

0 commit comments

Comments
 (0)