Skip to content

Commit 03694df

Browse files
authored
refactor(mutations): unify error handling to throw errors from promise-based mutations (#708)
* add mutations * fix mutation error handling * fix tests * fix types * fix format * fix handling errors * refactor(contractor-onboarding): extract error handling logic to shared utility function * adapt select country * format
1 parent 5d15ce5 commit 03694df

11 files changed

Lines changed: 301 additions & 143 deletions

File tree

src/flows/ContractorOnboarding/components/BasicInformationStep.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { BasicInformationFormPayload } from '@/src/flows/Onboarding/types';
22
import { EmploymentCreationResponse } from '@/src/client';
33
import { $TSFixMe } from '@/src/types/remoteFlows';
4-
import {
5-
normalizeFieldErrors,
6-
NormalizedFieldError,
7-
} from '@/src/lib/mutations';
4+
import { NormalizedFieldError } from '@/src/lib/mutations';
85
import { useContractorOnboardingContext } from '@/src/flows/ContractorOnboarding/context';
96
import { ContractorOnboardingForm } from '@/src/flows/ContractorOnboarding/components/ContractorOnboardingForm';
7+
import { handleStepError } from '@/src/lib/utils';
108

119
type BasicInformationStepProps = {
1210
/*
@@ -49,24 +47,12 @@ export function BasicInformationStep({
4947
contractorOnboardingBag?.next();
5048
return;
5149
}
52-
if (response?.error) {
53-
const normalizedFieldErrors = normalizeFieldErrors(
54-
response?.fieldErrors || [],
55-
contractorOnboardingBag.meta?.fields?.basic_information,
56-
);
57-
58-
onError?.({
59-
error: response?.error,
60-
rawError: response?.rawError,
61-
fieldErrors: normalizedFieldErrors,
62-
});
63-
}
6450
} catch (error: unknown) {
65-
onError?.({
66-
error: error as Error,
67-
rawError: error as Record<string, unknown>,
68-
fieldErrors: [],
69-
});
51+
const structuredError = handleStepError(
52+
error,
53+
contractorOnboardingBag.meta?.fields?.basic_information,
54+
);
55+
onError?.(structuredError);
7056
}
7157
};
7258

src/flows/ContractorOnboarding/components/ContractDetailsStep.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { $TSFixMe } from '@/src/types/remoteFlows';
2-
import {
3-
normalizeFieldErrors,
4-
NormalizedFieldError,
5-
} from '@/src/lib/mutations';
2+
import { NormalizedFieldError } from '@/src/lib/mutations';
63
import { useContractorOnboardingContext } from '@/src/flows/ContractorOnboarding/context';
74
import { ContractorOnboardingForm } from '@/src/flows/ContractorOnboarding/components/ContractorOnboardingForm';
85
import {
@@ -11,6 +8,7 @@ import {
118
} from '@/src/flows/ContractorOnboarding/types';
129
import { StatementOfWorkDisclaimer } from '@/src/flows/ContractorOnboarding/components/StatementOfWorkDisclaimer';
1310
import { isCMOrCMPlus } from '@/src/flows/ContractorOnboarding/utils';
11+
import { handleStepError } from '@/src/lib/utils';
1412

1513
type ContractDetailsStepProps = {
1614
/*
@@ -57,24 +55,12 @@ export function ContractDetailsStep({
5755
contractorOnboardingBag?.next();
5856
return;
5957
}
60-
if (response?.error) {
61-
const normalizedFieldErrors = normalizeFieldErrors(
62-
response?.fieldErrors || [],
63-
contractorOnboardingBag.meta?.fields?.contract_details,
64-
);
65-
66-
onError?.({
67-
error: response?.error,
68-
rawError: response?.rawError,
69-
fieldErrors: normalizedFieldErrors,
70-
});
71-
}
7258
} catch (error: unknown) {
73-
onError?.({
74-
error: error as Error,
75-
rawError: error as Record<string, unknown>,
76-
fieldErrors: [],
77-
});
59+
const structuredError = handleStepError(
60+
error,
61+
contractorOnboardingBag.meta?.fields?.contract_details,
62+
);
63+
onError?.(structuredError);
7864
}
7965
};
8066

src/flows/ContractorOnboarding/components/ContractPreviewStep.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { useContractorOnboardingContext } from '@/src/flows/ContractorOnboarding/context';
22
import { ContractorOnboardingForm } from '@/src/flows/ContractorOnboarding/components/ContractorOnboardingForm';
3-
import {
4-
NormalizedFieldError,
5-
normalizeFieldErrors,
6-
} from '@/src/lib/mutations';
3+
import { NormalizedFieldError } from '@/src/lib/mutations';
74
import { $TSFixMe } from '@/src/types/remoteFlows';
85
import {
96
ContractPreviewFormPayload,
107
ContractPreviewResponse,
118
} from '@/src/flows/ContractorOnboarding/types';
9+
import { handleStepError } from '@/src/lib/utils';
1210

1311
type ContractPreviewStepProps = {
1412
/*
@@ -51,24 +49,12 @@ export function ContractPreviewStep({
5149
contractorOnboardingBag?.next();
5250
return;
5351
}
54-
if (response?.error) {
55-
const normalizedFieldErrors = normalizeFieldErrors(
56-
response?.fieldErrors || [],
57-
contractorOnboardingBag.meta?.fields?.contract_preview,
58-
);
59-
60-
onError?.({
61-
error: response?.error,
62-
rawError: response?.rawError,
63-
fieldErrors: normalizedFieldErrors,
64-
});
65-
}
6652
} catch (error: unknown) {
67-
onError?.({
68-
error: error as Error,
69-
rawError: error as Record<string, unknown>,
70-
fieldErrors: [],
71-
});
53+
const structuredError = handleStepError(
54+
error,
55+
contractorOnboardingBag.meta?.fields?.contract_preview,
56+
);
57+
onError?.(structuredError);
7258
}
7359
};
7460

src/flows/ContractorOnboarding/components/PricingPlan.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
11
import { $TSFixMe, Components } from '@/src/types/remoteFlows';
2-
import {
3-
normalizeFieldErrors,
4-
NormalizedFieldError,
5-
} from '@/src/lib/mutations';
2+
import { NormalizedFieldError } from '@/src/lib/mutations';
63
import { useContractorOnboardingContext } from '@/src/flows/ContractorOnboarding/context';
74
import { ContractorOnboardingForm } from '@/src/flows/ContractorOnboarding/components/ContractorOnboardingForm';
85
import {
96
PricingPlanFormPayload,
107
PricingPlanResponse,
118
} from '@/src/flows/ContractorOnboarding/types';
9+
import { handleStepError } from '@/src/lib/utils';
1210

1311
type PricingPlanStepProps = {
1412
/**
@@ -56,24 +54,12 @@ export function PricingPlanStep({
5654
contractorOnboardingBag?.next();
5755
return;
5856
}
59-
if (response?.error) {
60-
const normalizedFieldErrors = normalizeFieldErrors(
61-
response?.fieldErrors || [],
62-
contractorOnboardingBag.meta?.fields?.pricing_plan,
63-
);
64-
65-
onError?.({
66-
error: response?.error,
67-
rawError: response?.rawError,
68-
fieldErrors: normalizedFieldErrors,
69-
});
70-
}
7157
} catch (error: unknown) {
72-
onError?.({
73-
error: error as Error,
74-
rawError: error as Record<string, unknown>,
75-
fieldErrors: [],
76-
});
58+
const structuredError = handleStepError(
59+
error,
60+
contractorOnboardingBag.meta?.fields?.pricing_plan,
61+
);
62+
onError?.(structuredError);
7763
}
7864
};
7965

src/flows/ContractorOnboarding/components/SelectCountryStep.tsx

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { NormalizedFieldError } from '@/src/lib/mutations';
77
import { $TSFixMe } from '@/src/types/remoteFlows';
88
import { useContractorOnboardingContext } from '@/src/flows/ContractorOnboarding/context';
99
import { ContractorOnboardingForm } from '@/src/flows/ContractorOnboarding/components/ContractorOnboardingForm';
10+
import { handleStepError } from '@/src/lib/utils';
1011

1112
type SelectCountryStepProps = {
1213
/*
@@ -46,19 +47,13 @@ export function SelectCountryStep({
4647
contractorOnboardingBag?.next();
4748
return;
4849
}
49-
if (response?.error) {
50-
onError?.({
51-
error: response.error,
52-
rawError: response.rawError,
53-
fieldErrors: [],
54-
});
55-
}
5650
} catch (error: unknown) {
57-
onError?.({
58-
error: error as Error,
59-
rawError: error as Record<string, unknown>,
60-
fieldErrors: [],
61-
});
51+
const structuredError = handleStepError(
52+
error,
53+
contractorOnboardingBag.meta?.fields?.select_country,
54+
);
55+
56+
onError?.(structuredError);
6257
}
6358
};
6459

src/flows/ContractorOnboarding/hooks.tsx

Lines changed: 27 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ import {
4141
import { FlowOptions, JSFModify, JSONSchemaFormType } from '@/src/flows/types';
4242
import { Step, useStepState } from '@/src/flows/useStepState';
4343
import { mutationToPromise } from '@/src/lib/mutations';
44-
import { prettifyFormValues } from '@/src/lib/utils';
45-
import { $TSFixMe, JSFFieldset, Meta } from '@/src/types/remoteFlows';
44+
import { createStructuredError, prettifyFormValues } from '@/src/lib/utils';
45+
import { JSFFieldset, Meta } from '@/src/types/remoteFlows';
4646
import {
4747
contractorStandardProductIdentifier,
4848
contractorPlusProductIdentifier,
@@ -152,25 +152,22 @@ export const useContractorOnboarding = ({
152152
);
153153
const createContractorContractDocumentMutation =
154154
useCreateContractorContractDocument();
155-
const { mutateAsync: updateEmploymentMutationAsync } = mutationToPromise(
156-
updateEmploymentMutation,
157-
);
155+
const { mutateAsyncOrThrow: updateEmploymentMutationAsync } =
156+
mutationToPromise(updateEmploymentMutation);
158157
const signContractDocumentMutation = useSignContractDocument();
159158
const manageContractorSubscriptionMutation =
160159
usePostManageContractorSubscriptions();
161160

162-
const { mutateAsync: createEmploymentMutationAsync } = mutationToPromise(
163-
createEmploymentMutation,
164-
);
161+
const { mutateAsyncOrThrow: createEmploymentMutationAsync } =
162+
mutationToPromise(createEmploymentMutation);
165163

166-
const { mutateAsync: createContractorContractDocumentMutationAsync } =
164+
const { mutateAsyncOrThrow: createContractorContractDocumentMutationAsync } =
167165
mutationToPromise(createContractorContractDocumentMutation);
168166

169-
const { mutateAsync: signContractDocumentMutationAsync } = mutationToPromise(
170-
signContractDocumentMutation,
171-
);
167+
const { mutateAsyncOrThrow: signContractDocumentMutationAsync } =
168+
mutationToPromise(signContractDocumentMutation);
172169

173-
const { mutateAsync: manageContractorSubscriptionMutationAsync } =
170+
const { mutateAsyncOrThrow: manageContractorSubscriptionMutationAsync } =
174171
mutationToPromise(manageContractorSubscriptionMutation);
175172

176173
// if the employment is loaded, country code has not been set yet
@@ -666,17 +663,10 @@ export const useContractorOnboarding = ({
666663
country_code: internalCountryCode,
667664
external_id: externalId,
668665
};
669-
try {
670-
const response = await createEmploymentMutationAsync(payload);
671-
// @ts-expect-error the types from the response are not matching
672-
const employmentId = response.data?.data?.employment?.id;
673-
setInternalEmploymentId(employmentId);
674-
675-
return response;
676-
} catch (error) {
677-
console.error('Error creating onboarding:', error);
678-
throw error;
679-
}
666+
const response = await createEmploymentMutationAsync(payload);
667+
const employmentId = response?.data?.employment?.id;
668+
setInternalEmploymentId(employmentId);
669+
return response;
680670
} else if (internalEmploymentId) {
681671
return updateEmploymentMutationAsync({
682672
employmentId: internalEmploymentId,
@@ -690,13 +680,14 @@ export const useContractorOnboarding = ({
690680
const payload: CreateContractDocument = {
691681
contract_document: parsedValues,
692682
};
693-
const response: $TSFixMe =
694-
await createContractorContractDocumentMutationAsync({
695-
employmentId: internalEmploymentId as string,
696-
payload,
697-
});
698-
699-
const contractDocumentId = response.data?.data?.contract_document?.id;
683+
const response = await createContractorContractDocumentMutationAsync({
684+
employmentId: internalEmploymentId as string,
685+
payload,
686+
});
687+
const contractDocumentId = response?.data?.contract_document?.id;
688+
if (!contractDocumentId) {
689+
throw createStructuredError('Contract document ID not found');
690+
}
700691
setInternalContractDocumentId(contractDocumentId);
701692
return response;
702693
}
@@ -726,11 +717,11 @@ export const useContractorOnboarding = ({
726717
},
727718
});
728719
}
729-
return Promise.reject({ error: 'invalid selection' });
720+
throw createStructuredError('invalid selection');
730721
}
731722

732723
default: {
733-
throw new Error('Invalid step state');
724+
throw createStructuredError('Invalid step state');
734725
}
735726
}
736727
}
@@ -896,7 +887,9 @@ export const useContractorOnboarding = ({
896887
isSubmitting:
897888
createEmploymentMutation.isPending ||
898889
createContractorContractDocumentMutation.isPending ||
899-
signContractDocumentMutation.isPending,
890+
signContractDocumentMutation.isPending ||
891+
manageContractorSubscriptionMutation.isPending ||
892+
updateEmploymentMutation.isPending,
900893

901894
/**
902895
* Document preview PDF data

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -552,13 +552,13 @@ describe('ContractorOnboardingFlow', () => {
552552
});
553553
});
554554

555-
it('should NOT call PATCH when resubmitting basic information (contractors cannot be updated)', async () => {
555+
it('should call PATCH when resubmitting basic information', async () => {
556556
const patchSpy = vi.fn();
557557

558558
server.use(
559559
http.patch('*/v1/employments/*', () => {
560560
patchSpy();
561-
return HttpResponse.json({});
561+
return HttpResponse.json(employmentUpdatedResponse);
562562
}),
563563
);
564564

@@ -609,10 +609,11 @@ describe('ContractorOnboardingFlow', () => {
609609
const nextButton = screen.getByText(/Next Step/i);
610610
nextButton.click();
611611

612-
await screen.findByText(/Step: Pricing Plan/i);
612+
await waitFor(() => {
613+
expect(patchSpy).toHaveBeenCalledTimes(1);
614+
});
613615

614-
// Verify PATCH was NOT called (contractors can't be updated)
615-
expect(patchSpy).toHaveBeenCalled();
616+
await screen.findByText(/Step: Pricing Plan/i);
616617
});
617618

618619
it('should create contract document when submitting contract details', async () => {

src/flows/ContractorOnboarding/tests/fixtures.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3693,6 +3693,10 @@ export const mockManageSubscriptionResponse = {
36933693
},
36943694
};
36953695

3696-
export const mockContractDocumentSignedResponse = {};
3696+
export const mockContractDocumentSignedResponse = {
3697+
data: {
3698+
status: 'ok',
3699+
},
3700+
};
36973701

36983702
export const inviteResponse = {};

0 commit comments

Comments
 (0)