Skip to content

Commit d8e2264

Browse files
committed
feat: reworked kyc block approval logic
1 parent 4b8d1a2 commit d8e2264

File tree

11 files changed

+228
-70
lines changed

11 files changed

+228
-70
lines changed

apps/backoffice-v2/src/domains/individuals/fetchers.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ export const EntityType = {
1616

1717
export const EndUserVariantSchema = z.enum([EntityType.UBO, EntityType.DIRECTOR]);
1818

19+
export const EndUserApprovalState = {
20+
NEW: 'NEW',
21+
PROCESSING: 'PROCESSING',
22+
APPROVED: 'APPROVED',
23+
REJECTED: 'REJECTED',
24+
} as const;
25+
26+
export const EndUserApprovalStates = [
27+
EndUserApprovalState.NEW,
28+
EndUserApprovalState.PROCESSING,
29+
EndUserApprovalState.APPROVED,
30+
EndUserApprovalState.REJECTED,
31+
] as const;
32+
1933
export const EndUserSchema = z.object({
2034
id: z.string(),
2135
firstName: z.string(),
@@ -28,6 +42,7 @@ export const EndUserSchema = z.object({
2842
phone: z.string().nullable(),
2943
additionalInfo: z.record(z.string(), z.any()).nullable(),
3044
amlHits: z.array(HitSchema.extend({ vendor: z.string().optional() })).optional(),
45+
approvalState: z.enum(EndUserApprovalStates).optional(),
3146
individualVerificationsChecks: z
3247
.object({
3348
status: z.string(),
@@ -50,6 +65,8 @@ export const EndUserSchema = z.object({
5065
createdFrom: z.enum(['user', 'analyst', 'registry']).nullable().optional(),
5166
});
5267

68+
export type TEndUser = z.output<typeof EndUserSchema>;
69+
5370
export const EndUsersSchema = z.array(EndUserSchema);
5471

5572
export const getEndUserById = async ({ id }: { id: string }) => {
@@ -74,3 +91,27 @@ export const getEndUsersByIds = async ({ ids }: { ids: string[] }) => {
7491

7592
return handleZodError(error, endUsers);
7693
};
94+
95+
export type TIndividualDecision =
96+
| `${typeof EndUserApprovalState.APPROVED}`
97+
| `${typeof EndUserApprovalState.REJECTED}`;
98+
99+
export const updateIndividualApprovalDecision = async ({
100+
endUserId,
101+
decision,
102+
}: {
103+
endUserId: string;
104+
decision: TIndividualDecision;
105+
}) => {
106+
const [response, error] = await apiClient({
107+
endpoint: `end-users/${endUserId}/decision`,
108+
method: Method.POST,
109+
schema: EndUserSchema,
110+
body: {
111+
endUserId,
112+
decision,
113+
},
114+
});
115+
116+
return handleZodError(error, response);
117+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useMutation } from '@tanstack/react-query';
2+
import { TIndividualDecision, updateIndividualApprovalDecision } from '../../fetchers';
3+
import { toast } from 'sonner';
4+
import { t } from 'i18next';
5+
import { queryClient } from '@/lib/react-query/query-client';
6+
import { workflowsQueryKeys } from '@/domains/workflows/query-keys';
7+
8+
export const useMutateIndividualApprovalDecision = () => {
9+
return useMutation({
10+
mutationFn: async ({
11+
endUserId,
12+
decision,
13+
}: {
14+
endUserId: string;
15+
decision: TIndividualDecision;
16+
}) => {
17+
return updateIndividualApprovalDecision({
18+
endUserId,
19+
decision,
20+
});
21+
},
22+
onSuccess: () => {
23+
void queryClient.invalidateQueries(workflowsQueryKeys._def);
24+
25+
toast.success(t('toast:approve_case.success'));
26+
},
27+
onError: () => {
28+
toast.error(t('toast:approve_case.error'));
29+
},
30+
});
31+
};

apps/backoffice-v2/src/lib/blocks/components/KycBlock/hooks/useKycBlock/useKycBlock.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ import { SelectItem } from '@/common/components/atoms/Select/Select.Item';
2828
import { SelectTrigger } from '@/common/components/atoms/Select/Select.Trigger';
2929
import { SelectValue } from '@/common/components/atoms/Select/Select.Value';
3030
import { systemCreatedIconCell, userCreatedIconCell } from '@/lib/blocks/utils/constants';
31+
import {
32+
INDIVIDUAL_KYC_CHECK_STATUS_ENUM,
33+
TIndividualKycCheckStatus,
34+
} from '@/lib/blocks/variants/DefaultBlocks/hooks/useCaseBlocksLogic/utils/compute-individual-kyc-check-status';
3135

3236
const motionBadgeProps = {
3337
exit: { opacity: 0, transition: { duration: 0.2 } },
@@ -299,10 +303,8 @@ export const useKycBlock = ({
299303
const isDisabled = isActionsDisabled || noAction || isLoadingApprove || isLoadingReuploadNeeded;
300304
const badgeClassNames = 'text-sm font-bold';
301305

302-
const getDecisionStatusOrAction = (
303-
status: 'revision' | 'approved' | 'rejected' | 'pending' | undefined,
304-
) => {
305-
if (status === 'revision') {
306+
const getDecisionStatusOrAction = (status: TIndividualKycCheckStatus | undefined) => {
307+
if (status === INDIVIDUAL_KYC_CHECK_STATUS_ENUM.revision) {
306308
return createBlocksTyped()
307309
.addBlock()
308310
.addCell({
@@ -317,7 +319,7 @@ export const useKycBlock = ({
317319
.buildFlat();
318320
}
319321

320-
if (status === 'approved') {
322+
if (status === INDIVIDUAL_KYC_CHECK_STATUS_ENUM.approved) {
321323
return createBlocksTyped()
322324
.addBlock()
323325
.addCell({
@@ -332,7 +334,7 @@ export const useKycBlock = ({
332334
.buildFlat();
333335
}
334336

335-
if (status === 'rejected') {
337+
if (status === INDIVIDUAL_KYC_CHECK_STATUS_ENUM.rejected) {
336338
return createBlocksTyped()
337339
.addBlock()
338340
.addCell({
@@ -347,7 +349,7 @@ export const useKycBlock = ({
347349
.buildFlat();
348350
}
349351

350-
if (status === 'pending') {
352+
if (status === INDIVIDUAL_KYC_CHECK_STATUS_ENUM.pending) {
351353
return createBlocksTyped()
352354
.addBlock()
353355
.addCell({
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { EndUserApprovalState, TEndUser } from '@/domains/individuals/fetchers';
2+
import { TWorkflowById } from '@/domains/workflows/fetchers';
3+
import { ObjectValues, StateTag } from '@ballerine/common';
4+
5+
export const INDIVIDUAL_KYC_CHECK_STATUS_ENUM = {
6+
revision: 'revision',
7+
approved: 'approved',
8+
rejected: 'rejected',
9+
pending: 'pending',
10+
} as const;
11+
12+
export type TIndividualKycCheckStatus = ObjectValues<typeof INDIVIDUAL_KYC_CHECK_STATUS_ENUM>;
13+
14+
const getStatusFromTags = (tags: string[]) => {
15+
if (tags?.includes(StateTag.REVISION)) {
16+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.revision;
17+
}
18+
19+
if (tags?.includes(StateTag.APPROVED)) {
20+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.approved;
21+
}
22+
23+
if (tags?.includes(StateTag.REJECTED)) {
24+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.rejected;
25+
}
26+
27+
if (tags?.includes(StateTag.PENDING_PROCESS)) {
28+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.pending;
29+
}
30+
};
31+
32+
export const computeIndividualKycCheckStatus = ({
33+
endUser,
34+
tags,
35+
}: {
36+
endUser?: TEndUser;
37+
tags: TWorkflowById['tags'];
38+
}): TIndividualKycCheckStatus | undefined => {
39+
if (endUser?.individualVerificationsChecks?.status === 'in-progress') {
40+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.pending;
41+
}
42+
43+
if (
44+
!endUser?.approvalState ||
45+
[EndUserApprovalState.NEW, EndUserApprovalState.PROCESSING].includes(endUser?.approvalState)
46+
) {
47+
return getStatusFromTags(tags || []);
48+
}
49+
50+
if (endUser?.approvalState === EndUserApprovalState.APPROVED) {
51+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.approved;
52+
}
53+
54+
if (endUser?.approvalState === EndUserApprovalState.REJECTED) {
55+
return INDIVIDUAL_KYC_CHECK_STATUS_ENUM.rejected;
56+
}
57+
};

0 commit comments

Comments
 (0)