Skip to content

Commit 11ace12

Browse files
authored
People of interest AML on registry information from vendor (#3400)
* feat(workflows-service): added end-user createdFrom column * feat(*): added registry provided individuals table based on createdFrom column * feat(workflows-service): added handler for aml for people of interest * feat(*): completed end to end people of interest data to ui * chore(*): pr comments * fix(workflows-service): added missing test service * fixed test issues * fixed test issues * fixed test issues * fixed test issues * fixed format * version bump
1 parent 3b245c5 commit 11ace12

File tree

43 files changed

+18982
-50095
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+18982
-50095
lines changed

apps/backoffice-v2/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# @ballerine/backoffice-v2
22

3+
## 0.7.165
4+
5+
### Patch Changes
6+
7+
- Updated dependencies
8+
- @ballerine/common@0.9.115
9+
- @ballerine/workflow-browser-sdk@0.6.139
10+
- @ballerine/workflow-node-sdk@0.6.139
11+
312
## 0.7.164
413

514
### Patch Changes

apps/backoffice-v2/package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ballerine/backoffice-v2",
3-
"version": "0.7.164",
3+
"version": "0.7.165",
44
"description": "Ballerine - Backoffice",
55
"homepage": "https://github.com/ballerine-io/ballerine",
66
"type": "module",
@@ -55,9 +55,9 @@
5555
"@ballerine/react-pdf-toolkit": "^1.2.127",
5656
"@ballerine/ui": "0.7.164",
5757
"@ballerine/blocks": "0.2.47",
58-
"@ballerine/common": "0.9.113",
59-
"@ballerine/workflow-browser-sdk": "0.6.137",
60-
"@ballerine/workflow-node-sdk": "0.6.137",
58+
"@ballerine/common": "0.9.115",
59+
"@ballerine/workflow-browser-sdk": "0.6.139",
60+
"@ballerine/workflow-node-sdk": "0.6.139",
6161
"@fontsource/inter": "^4.5.15",
6262
"@formkit/auto-animate": "0.8.2",
6363
"@hookform/resolvers": "^3.1.0",

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const EndUserSchema = z.object({
1212
id: z.string(),
1313
firstName: z.string(),
1414
lastName: z.string(),
15-
email: z.string().optional(),
15+
email: z.string().nullable().optional(),
1616
gender: z.string().nullable(),
1717
nationality: z.string().nullable(),
1818
address: z.string().nullable(),
@@ -38,6 +38,7 @@ export const EndUserSchema = z.object({
3838
}),
3939
})
4040
.optional(),
41+
createdFrom: z.enum(['user', 'analyst', 'registry']).nullable().optional(),
4142
});
4243

4344
export const EndUsersSchema = z.array(EndUserSchema);
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed';
2+
import { Button, ctw, TextWithNAFallback } from '@ballerine/ui';
3+
import { createColumnHelper } from '@tanstack/react-table';
4+
import { useMemo } from 'react';
5+
import { ChevronDown } from 'lucide-react';
6+
import { ReadOnlyDetailsCell } from '../../components/ReadOnlyDetailsCell/ReadOnlyDetailsCell';
7+
import { ExtendedJson } from '@/common/types';
8+
import { titleCase } from 'string-ts';
9+
import { systemCreatedIconCell } from '@/lib/blocks/utils/constants';
10+
11+
export const useIndividualsRegistryProvidedBlock = (
12+
individualsRegistryProvided: Array<{
13+
name: string;
14+
role: string;
15+
percentageOfOwnership: number;
16+
collapsibleData: Record<string, unknown>;
17+
}>,
18+
) => {
19+
const columnHelper = createColumnHelper<(typeof individualsRegistryProvided)[number]>();
20+
const columns = [
21+
columnHelper.display({
22+
id: 'collapsible',
23+
cell: ({ row }) => (
24+
<Button
25+
onClick={() => row.toggleExpanded()}
26+
disabled={row.getCanExpand()}
27+
variant="ghost"
28+
size="icon"
29+
className={`p-[7px]`}
30+
>
31+
<ChevronDown
32+
className={ctw('d-4', {
33+
'rotate-180': row.getIsExpanded(),
34+
})}
35+
/>
36+
</Button>
37+
),
38+
}),
39+
columnHelper.accessor('name', {
40+
header: 'Name',
41+
}),
42+
columnHelper.accessor('role', {
43+
header: 'Role',
44+
cell: ({ getValue }) => {
45+
const value = getValue();
46+
47+
return <TextWithNAFallback>{titleCase(value ?? '')}</TextWithNAFallback>;
48+
},
49+
}),
50+
columnHelper.accessor('percentageOfOwnership', {
51+
header: '% of Ownership',
52+
cell: ({ getValue }) => {
53+
const value = getValue();
54+
55+
return (
56+
<TextWithNAFallback>{value || value === 0 ? `${value}%` : value}</TextWithNAFallback>
57+
);
58+
},
59+
}),
60+
];
61+
62+
return useMemo(() => {
63+
if (Object.keys(individualsRegistryProvided ?? {}).length === 0) {
64+
return [];
65+
}
66+
67+
return createBlocksTyped()
68+
.addBlock()
69+
.addCell({
70+
type: 'block',
71+
value: createBlocksTyped()
72+
.addBlock()
73+
.addCell({
74+
type: 'container',
75+
value: createBlocksTyped()
76+
.addBlock()
77+
.addCell(systemCreatedIconCell)
78+
.addCell({
79+
type: 'container',
80+
value: createBlocksTyped()
81+
.addBlock()
82+
.addCell({
83+
type: 'heading',
84+
value: 'Individuals',
85+
props: { className: 'mt-0' },
86+
})
87+
.addCell({
88+
type: 'subheading',
89+
value: 'Registry-Provided Data',
90+
})
91+
.buildFlat(),
92+
})
93+
.buildFlat(),
94+
props: {
95+
className: 'flex space-x-1 items-center mt-4',
96+
},
97+
})
98+
.addCell({
99+
type: 'dataTable',
100+
value: {
101+
columns,
102+
props: {
103+
scroll: {
104+
className: ctw('[&>div]:max-h-[50vh]', {
105+
'h-[100px]': individualsRegistryProvided.length === 0,
106+
}),
107+
},
108+
cell: { className: '!p-0' },
109+
},
110+
data: individualsRegistryProvided,
111+
options: {
112+
enableSorting: false,
113+
},
114+
CollapsibleContent: ({ row: individualData }) => {
115+
const { collapsibleData } = individualData ?? {};
116+
117+
return (
118+
<ReadOnlyDetailsCell
119+
value={Object.entries(collapsibleData).map(([key, value]) => ({
120+
label: key,
121+
value: value as ExtendedJson,
122+
}))}
123+
props={{
124+
config: {
125+
parse: {
126+
boolean: true,
127+
date: true,
128+
datetime: true,
129+
isoDate: true,
130+
nullish: true,
131+
url: true,
132+
},
133+
},
134+
}}
135+
/>
136+
);
137+
},
138+
},
139+
})
140+
.build()
141+
.flat(1),
142+
})
143+
.build();
144+
}, [individualsRegistryProvided]);
145+
};

apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useCaseBlocksLogic/utils/useTabsToBlocksMap.tsx

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ export const useTabsToBlocksMap = ({
100100
kybRegistryInfoBlock,
101101
companySanctionsBlock,
102102
individualsUserProvidedBlock,
103+
individualsRegistryProvidedBlock,
103104
ubosRegistryProvidedBlock,
104105
storeInfoBlock,
105106
websiteBasicRequirementBlock,
@@ -450,9 +451,106 @@ export const useTabsToBlocksMap = ({
450451
[workflow, endUsers, directorToIndividualAdapter],
451452
);
452453

454+
const personOfInterestToIndividualAdapter = useCallback(
455+
({
456+
ballerineEntityId,
457+
role,
458+
}: NonNullable<
459+
TWorkflowById['context']['entity']['data']['additionalInfo']['peopleOfInterest']
460+
>[number]) => {
461+
const {
462+
id: _id,
463+
amlHits,
464+
individualVerificationsChecks,
465+
...personOfInterestEndUser
466+
} = endUsers?.find(endUser => endUser.id === ballerineEntityId) ?? {};
467+
const status = getStatusFromCheckStatus(individualVerificationsChecks?.status);
468+
const kycSession = omitPropsFromObject(
469+
individualVerificationsChecks?.data ?? {},
470+
'invokedAt',
471+
'error',
472+
'name',
473+
'status',
474+
'isRequestTimedOut',
475+
);
476+
477+
return {
478+
status,
479+
documents: [],
480+
kycSession,
481+
aml: {
482+
vendor: amlHits?.find(aml => !!aml.vendor)?.vendor,
483+
hits: amlHits,
484+
},
485+
entityData: {
486+
...personOfInterestEndUser,
487+
role,
488+
},
489+
isActionsDisabled: true,
490+
isLoadingReuploadNeeded: false,
491+
isLoadingApprove: false,
492+
onInitiateKyc: () => {
493+
if (!workflow?.id) {
494+
console.error('No workflow id found');
495+
toast.error('Something went wrong. Please try again later.');
496+
497+
return;
498+
}
499+
500+
return mutateInitiateIndividualVerificationAndSendEmail({
501+
endUserId: ballerineEntityId,
502+
ongoingMonitoring: false,
503+
withAml: true,
504+
workflowRuntimeDataId: workflow?.id,
505+
vendor: 'veriff',
506+
language: workflow?.workflowDefinition?.config?.language ?? 'en',
507+
});
508+
},
509+
onInitiateSanctionsScreening: () => {},
510+
onApprove:
511+
({ ids }: { ids: string[] }) =>
512+
() => {},
513+
onReuploadNeeded:
514+
({ reason, ids }: { reason: string; ids: string[] }) =>
515+
() => {},
516+
onEdit: onEditCollectionFlow({ steps: ['company_ownership'] }),
517+
reasons: [],
518+
isReuploadNeededDisabled: true,
519+
isApproveDisabled: true,
520+
isInitiateKycDisabled: [
521+
!workflow?.id,
522+
!caseState.actionButtonsEnabled,
523+
!workflow?.workflowDefinition?.config?.isInitiateKycEnabled,
524+
].some(Boolean),
525+
isInitiateSanctionsScreeningDisabled: true,
526+
isEditDisabled: [
527+
!caseState.actionButtonsEnabled,
528+
!workflow?.workflowDefinition?.config?.isKycEndUserEditEnabled,
529+
].some(Boolean),
530+
} satisfies Parameters<typeof createKycBlocks>[0][number];
531+
},
532+
[
533+
workflow?.id,
534+
caseState.actionButtonsEnabled,
535+
workflow?.workflowDefinition?.config?.isInitiateKycEnabled,
536+
workflow?.workflowDefinition?.config?.isKycEndUserEditEnabled,
537+
],
538+
);
539+
540+
const peopleOfInterest = useMemo(
541+
() =>
542+
workflow?.context?.entity?.data?.additionalInfo?.peopleOfInterest?.map(
543+
personOfInterestToIndividualAdapter,
544+
) ?? [],
545+
[
546+
workflow?.context?.entity?.data?.additionalInfo?.peopleOfInterest,
547+
personOfInterestToIndividualAdapter,
548+
],
549+
);
550+
453551
const individuals = useMemo(
454-
() => [...childWorkflows, ...deDupedDirectors],
455-
[childWorkflows, deDupedDirectors],
552+
() => [...childWorkflows, ...deDupedDirectors, ...peopleOfInterest],
553+
[childWorkflows, deDupedDirectors, peopleOfInterest],
456554
);
457555

458556
const kycBlocks = useKYCBlocks(individuals);
@@ -490,6 +588,7 @@ export const useTabsToBlocksMap = ({
490588
[Tab.DOCUMENTS]: [...businessDocumentBlocks],
491589
[Tab.INDIVIDUALS]: [
492590
...individualsUserProvidedBlock,
591+
...individualsRegistryProvidedBlock,
493592
...amlWithContainerBlock,
494593
...mainRepresentativeBlock,
495594
...uboDocumentBlocks,

apps/backoffice-v2/src/lib/blocks/variants/DefaultBlocks/hooks/useDefaultBlocksLogic/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const ALL_BLOCKS = [
77
'kybRegistryInfoBlock',
88
'companySanctionsBlock',
99
'individualsUserProvidedBlock',
10+
'individualsRegistryProvidedBlock',
1011
'ubosRegistryProvidedBlock',
1112
'storeInfoBlock',
1213
'websiteBasicRequirementBlock',

0 commit comments

Comments
 (0)