Skip to content

Commit aecb7f6

Browse files
committed
feat(workflows): implement response transformation for plugin workflows
- Create async and sync response transformation functions - Add new rules for KYC workflow decision making - Implement unit tests for KYC_DONE_RULE logic
1 parent 557a716 commit aecb7f6

File tree

5 files changed

+308
-1
lines changed

5 files changed

+308
-1
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { ProcessStatus } from '@ballerine/common';
2+
import { UnifiedApiReasonsString } from './rules';
3+
4+
export const createPluginAsyncResponseTransform = (action: string, metadata?: string) => [
5+
{
6+
transformer: 'jmespath',
7+
mapping: `merge({ name: '${action}', status: contains(${UnifiedApiReasonsString}, reason) && '${
8+
ProcessStatus.CANCELED
9+
}' || error != \`null\` && '${ProcessStatus.ERROR}' || '${ProcessStatus.IN_PROGRESS}' ${
10+
metadata ? `, ${metadata}` : ''
11+
} }, @)`, // jmespath
12+
},
13+
];
14+
15+
export const createPluginSyncResponseTransform = (action: string, metadata?: string) => [
16+
{
17+
transformer: 'jmespath',
18+
mapping: `merge({ name: '${action}', status: contains(${UnifiedApiReasonsString}, reason) && '${
19+
ProcessStatus.CANCELED
20+
}' || error != \`null\` && '${ProcessStatus.ERROR}' || '${ProcessStatus.SUCCESS}' ${
21+
metadata ? `, ${metadata}` : ''
22+
} }, @)`, // jmespath
23+
},
24+
];
25+
26+
export const createPluginSyncOrAsyncResponseTransform = (action: string, isAsync: string) => [
27+
{
28+
transformer: 'jmespath',
29+
mapping: `merge({ name: '${action}', status: contains(${UnifiedApiReasonsString}, reason) && '${ProcessStatus.CANCELED}' || error != \`null\` && '${ProcessStatus.ERROR}' || ${isAsync} && '${ProcessStatus.IN_PROGRESS}' || '${ProcessStatus.SUCCESS}' }, @)`,
30+
},
31+
];

services/workflows-service/src/workflow-defintion/demo-workflow/eu/index.ts

Whitespace-only changes.

services/workflows-service/src/workflow-defintion/demo-workflow/eu/kyb-workflow-definition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
UBO_DONE_OR_ERRORED,
99
WEBSITE_ANALYSIS_DONE,
1010
} from 'prisma/data-migrations/templates/utils/rules';
11-
import { createPluginSyncResponseTransform } from 'prisma/data-migrations/utils/create-plugin-response-mapping';
11+
import { createPluginSyncResponseTransform } from './create-plugin-response-mapping';
1212
import { demoInputSchema } from './demo.idle.schema';
1313
import { generateKycDefinition } from './kyc-workflow-definition';
1414

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { KYC_DONE_RULE } from './index';
2+
import { search } from 'jmespath';
3+
4+
describe('KYC_DONE_RULE #rule #unit', () => {
5+
describe('when there is no data', () => {
6+
it('returns false', () => {
7+
// Arrange
8+
const context = {};
9+
10+
// Act
11+
const result = search(context, KYC_DONE_RULE());
12+
13+
// Assert
14+
expect(result).toEqual(false);
15+
});
16+
});
17+
18+
describe('when there are no child workflows', () => {
19+
it('returns false', () => {
20+
// Arrange
21+
const context = {
22+
childWorkflows: {},
23+
};
24+
25+
// Act
26+
const result = search(context, KYC_DONE_RULE());
27+
28+
// Assert
29+
expect(result).toEqual(false);
30+
});
31+
});
32+
33+
describe('when there are no child workflows with an id of "kyc_email_session_example"', () => {
34+
it('returns false', () => {
35+
// Arrange
36+
const childWorkflows = {
37+
childWorkflows: {
38+
kyb_phone_session_example: {},
39+
},
40+
};
41+
42+
// Act
43+
const result = search(childWorkflows, KYC_DONE_RULE());
44+
45+
// Assert
46+
expect(result).toEqual(false);
47+
});
48+
});
49+
50+
describe('when there are child workflows with an id of "kyc_email_session_example" but no results', () => {
51+
it.each([
52+
{
53+
context: {
54+
childWorkflows: {
55+
kyc_email_session_example: {
56+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9b': {},
57+
},
58+
},
59+
},
60+
expected: false,
61+
},
62+
{
63+
context: {
64+
childWorkflows: {
65+
kyc_email_session_example: {
66+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9b': {
67+
result: {},
68+
},
69+
},
70+
},
71+
},
72+
expected: false,
73+
},
74+
{
75+
context: {
76+
childWorkflows: {
77+
kyc_email_session_example: {
78+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9b': {
79+
result: {
80+
vendorResult: {},
81+
},
82+
},
83+
},
84+
},
85+
},
86+
expected: false,
87+
},
88+
])('$context returns false', ({ context, expected }) => {
89+
// Act
90+
const result = search(context, KYC_DONE_RULE());
91+
92+
// Assert
93+
expect(result).toEqual(expected);
94+
});
95+
});
96+
97+
describe('when not all child workflows have a decision', () => {
98+
it('returns false', () => {
99+
// Arrange
100+
const context = {
101+
childWorkflows: {
102+
kyc_email_session_example: {
103+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9b': {
104+
result: {
105+
vendorResult: {
106+
decision: 'approved',
107+
},
108+
},
109+
},
110+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9c': {
111+
result: {
112+
vendorResult: {},
113+
},
114+
},
115+
},
116+
},
117+
};
118+
119+
// Act
120+
const result = search(context, KYC_DONE_RULE());
121+
122+
// Assert
123+
expect(result).toEqual(false);
124+
});
125+
});
126+
127+
describe('when all child workflows have a decision and no revision', () => {
128+
it('returns true', () => {
129+
// Arrange
130+
const context = {
131+
childWorkflows: {
132+
kyc_email_session_example: {
133+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9b': {
134+
result: {
135+
vendorResult: {
136+
decision: 'approved',
137+
},
138+
},
139+
},
140+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9c': {
141+
result: {
142+
vendorResult: {
143+
decision: 'rejected',
144+
},
145+
},
146+
},
147+
},
148+
},
149+
};
150+
151+
// Act
152+
const result = search(context, KYC_DONE_RULE());
153+
154+
// Assert
155+
expect(result).toEqual(true);
156+
});
157+
});
158+
159+
describe('when all child workflows have a decision and some are in revision', () => {
160+
it('returns false', () => {
161+
// Arrange
162+
const context = {
163+
childWorkflows: {
164+
kyc_email_session_example: {
165+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9b': {
166+
result: {
167+
vendorResult: {
168+
decision: 'approved',
169+
},
170+
},
171+
},
172+
'd4e9c9b7-0f6b-4c2b-8b4e-8e3e3c3a0b9c': {
173+
result: {
174+
vendorResult: {
175+
decision: 'rejected',
176+
},
177+
},
178+
state: 'revision',
179+
},
180+
},
181+
},
182+
};
183+
184+
// Act
185+
const result = search(context, KYC_DONE_RULE());
186+
187+
// Assert
188+
expect(result).toEqual(false);
189+
});
190+
});
191+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { ProcessStatus, UnifiedApiReasons } from '@ballerine/common';
2+
3+
export const KYC_DONE_RULE = (definitionId = 'kyc_email_session_example') => {
4+
return `(childWorkflows.${definitionId}.*.[result.vendorResult.decision] != null && length(childWorkflows.${definitionId}.*.[result.vendorResult.decision][]) == length(childWorkflows.${definitionId}.*[]) && length(childWorkflows.${definitionId}.* | [?state == 'revision']) == \`0\`)`;
5+
};
6+
7+
export const CHILD_KYB_DONE_RULE = (definitionId: string): string => {
8+
return `(childWorkflows.${definitionId} == null || length(childWorkflows.${definitionId}) == \`0\` || length(childWorkflows.${definitionId}.*.state[?@ == 'idle' || @ == 'manual_review']) == length(childWorkflows.${definitionId}.*))`;
9+
};
10+
11+
export const UnifiedApiReasonsString = `[${UnifiedApiReasons.map(reason => `'${reason}'`).join(
12+
', ',
13+
)}]`;
14+
15+
export const BUSINESS_INFORMATION_DONE = `(
16+
pluginsOutput.businessInformation.data ||
17+
contains(${UnifiedApiReasonsString}, pluginsOutput.businessInformation.reason)
18+
)`;
19+
20+
export const UBO_DONE = `(
21+
pluginsOutput.ubo.data ||
22+
contains(${UnifiedApiReasonsString}, pluginsOutput.ubo.reason)
23+
)`;
24+
25+
const UnifiedApiStatuses = [ProcessStatus.SUCCESS, ProcessStatus.CANCELED, ProcessStatus.ERROR];
26+
27+
const UnifiedApiStatusesString = `[${UnifiedApiStatuses.map(status => `'${status}'`).join(', ')}]`;
28+
29+
export const BANK_ACCOUNT_VERIFICATION_DONE = `contains(${UnifiedApiStatusesString}, pluginsOutput.bankAccountVerification.status)`;
30+
31+
export const COMMERCIAL_CREDIT_CHECK_DONE = `contains(${UnifiedApiStatusesString}, pluginsOutput.commercialCreditCheck.status)`;
32+
33+
export const SANCTIONS_DONE = `pluginsOutput.companySanctions.data != null`;
34+
35+
export const BUSINESS_UBO_AND_SANCTIONS_DONE = `
36+
${BUSINESS_INFORMATION_DONE} &&
37+
${UBO_DONE} &&
38+
${SANCTIONS_DONE}
39+
`;
40+
41+
export const BUSINESS_INFORMATION_DONE_OR_ERRORED = `
42+
(
43+
(
44+
pluginsOutput.businessInformation.data ||
45+
pluginsOutput.businessInformation.error != null
46+
) ||
47+
contains(${UnifiedApiReasonsString}, pluginsOutput.businessInformation.reason)
48+
)`;
49+
50+
export const UBO_DONE_OR_ERRORED = `
51+
(
52+
(
53+
pluginsOutput.ubo.data ||
54+
pluginsOutput.ubo.error != null
55+
) ||
56+
contains(${UnifiedApiReasonsString}, pluginsOutput.ubo.reason)
57+
)`;
58+
59+
export const BUSINESS_UBO_AND_SANCTIONS_DONE_OR_ERRORED = `
60+
${BUSINESS_INFORMATION_DONE_OR_ERRORED} &&
61+
${UBO_DONE_OR_ERRORED} &&
62+
${SANCTIONS_DONE}
63+
`;
64+
65+
export const MERCHANT_SCREENING_DONE_OR_ERRORED = `
66+
(
67+
(
68+
pluginsOutput.merchantScreening.raw != null && pluginsOutput.merchantScreening.processed != null ||
69+
pluginsOutput.merchantScreening.error != null
70+
) ||
71+
contains(${UnifiedApiReasonsString}, pluginsOutput.merchantScreening.reason)
72+
)
73+
`;
74+
75+
export const WEBSITE_ANALYSIS_DONE = `pluginsOutput.merchantMonitoring.data != null`;
76+
77+
export const kycAndVendorDone = {
78+
target: 'manual_review',
79+
cond: {
80+
type: 'jmespath',
81+
options: {
82+
rule: `${KYC_DONE_RULE()} && ${BUSINESS_UBO_AND_SANCTIONS_DONE_OR_ERRORED} && ${WEBSITE_ANALYSIS_DONE}`,
83+
},
84+
},
85+
};

0 commit comments

Comments
 (0)