Skip to content

Commit

Permalink
Workflow endpoints models (#2716)
Browse files Browse the repository at this point in the history
* chore(wf): workflows endpoint models update

* chore(versions): bump

* chore(versions): bump

* chore(versions): bump
  • Loading branch information
alonp99 committed Sep 18, 2024
1 parent c13e7be commit 7b97b88
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 27 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"apps/backoffice-v2",
"apps/workflows-dashboard",
"packages/workflow-core",
"services/workflows-service"
"services/workflows-service",
"packages/common"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import { BusinessInformationPluginSchema } from '@/schemas/documents/schemas/bus
export const defaultInputContextSchema = Type.Object({
customData: Type.Optional(Type.Object({}, { additionalProperties: true })),
entity: Type.Union([
Type.Composite([EntitySchema, Type.Object({ id: Type.String() })]),
Type.Composite([EntitySchema, Type.Object({ ballerineEntityId: Type.String() })]),
Type.Composite([EntitySchema, Type.Object({ id: Type.String() })]),
]),
documents: DocumentsSchema,
});
Expand Down
69 changes: 61 additions & 8 deletions packages/common/src/schemas/documents/schemas/entity-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,67 @@ import { Type } from '@sinclair/typebox';
export const EntitySchema = Type.Object(
{
type: Type.String({ enum: ['individual', 'business'] }),
data: Type.Optional(
Type.Object(
{
additionalInfo: Type.Optional(Type.Object({})),
},
{ additionalProperties: true },
),
),
data: Type.Union([
Type.Object({
isContactPerson: Type.Optional(Type.Boolean()),
correlationId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
endUserType: Type.Optional(Type.Union([Type.String(), Type.Null()])),
firstName: Type.String(),
lastName: Type.String(),
email: Type.Optional(Type.Union([Type.String(), Type.Null()])),
phone: Type.Optional(Type.Union([Type.String(), Type.Null()])),
country: Type.Optional(Type.Union([Type.String(), Type.Null()])),
dateOfBirth: Type.Optional(
Type.Union([Type.String({ format: 'date' }), Type.String(), Type.Null()]),
),
avatarUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
nationalId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
additionalInfo: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
}),
Type.Object({
correlationId: Type.Optional(Type.Union([Type.String(), Type.Null()])),
businessType: Type.Optional(Type.Union([Type.String(), Type.Null()])),
companyName: Type.String(),
registrationNumber: Type.Optional(Type.Union([Type.String(), Type.Null()])),
legalForm: Type.Optional(Type.Union([Type.String(), Type.Null()])),
country: Type.Optional(Type.Union([Type.String(), Type.Null()])),
countryOfIncorporation: Type.Optional(Type.Union([Type.String(), Type.Null()])),
dateOfIncorporation: Type.Optional(
Type.Union([Type.String({ format: 'date' }), Type.String(), Type.Null()]),
),
address: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
phoneNumber: Type.Optional(Type.Union([Type.String(), Type.Null()])),
email: Type.Optional(Type.Union([Type.String(), Type.Null()])),
website: Type.Optional(Type.Union([Type.String(), Type.Null()])),
industry: Type.Optional(Type.Union([Type.String(), Type.Null()])),
taxIdentificationNumber: Type.Optional(Type.Union([Type.String(), Type.Null()])),
vatNumber: Type.Optional(Type.Union([Type.String(), Type.Null()])),
shareholderStructure: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
numberOfEmployees: Type.Optional(Type.Number()),
businessPurpose: Type.Optional(Type.Union([Type.String(), Type.Null()])),
avatarUrl: Type.Optional(Type.Union([Type.String(), Type.Null()])),
additionalInfo: Type.Optional(
Type.Union([
Type.Object(
{
mainRepresentative: Type.Optional(
Type.Object({
email: Type.Optional(Type.String()),
lastName: Type.Optional(Type.String()),
firstName: Type.Optional(Type.String()),
}),
),
},
{ additionalProperties: true },
),
Type.Null(),
]),
),
bankInformation: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
mccCode: Type.Optional(Type.Number()),
metadata: Type.Optional(Type.Union([Type.Object({}), Type.Null()])),
}),
]),
},
{ additionalProperties: false },
);
78 changes: 78 additions & 0 deletions packages/common/src/schemas/documents/workflow/config-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Static, Type } from '@sinclair/typebox';

const SubscriptionSchema = Type.Object({
type: Type.String(),
url: Type.String(),
events: Type.Array(Type.String()),
});

const CallbackResultSchema = Type.Object({
transformers: Type.Array(Type.Any()),
action: Type.Optional(Type.String()),
deliverEvent: Type.Optional(Type.String()),
persistenceStates: Type.Optional(Type.Array(Type.String())),
});

const ChildCallbackResultSchema = Type.Object({
definitionId: Type.String(),
transformers: Type.Array(Type.Any()),
action: Type.Optional(Type.String()),
deliverEvent: Type.Optional(Type.String()),
});

const MainRepresentativeSchema = Type.Object({
fullName: Type.String(),
email: Type.String(),
});

const AvailableDocumentSchema = Type.Object({
category: Type.String(),
type: Type.String(),
});

const language = Type.Optional(Type.String());
const initialEvent = Type.Optional(Type.String());
const subscriptions = Type.Optional(Type.Array(SubscriptionSchema));

export const WorkflowRuntimeConfigSchema = Type.Object({
language,
initialEvent,
subscriptions,
});

export type TWorkflowRuntimeConfig = Static<typeof WorkflowRuntimeConfigSchema>;

export const WorkflowConfigSchema = Type.Object({
language,
initialEvent,
subscriptions,
isAssociatedCompanyKybEnabled: Type.Optional(Type.Boolean()),
isCaseOverviewEnabled: Type.Optional(Type.Boolean()),
isCaseRiskOverviewEnabled: Type.Optional(Type.Boolean()),
isLegacyReject: Type.Optional(Type.Boolean()),
isLockedDocumentCategoryAndType: Type.Optional(Type.Boolean()),
isManualCreation: Type.Optional(Type.Boolean()),
isDemo: Type.Optional(Type.Boolean()),
isExample: Type.Optional(Type.Boolean()),
supportedLanguages: Type.Optional(Type.Array(Type.String())),
completedWhenTasksResolved: Type.Optional(Type.Boolean()),
workflowLevelResolution: Type.Optional(Type.Boolean()),
allowMultipleActiveWorkflows: Type.Optional(Type.Boolean()),
availableDocuments: Type.Optional(Type.Array(AvailableDocumentSchema)),
callbackResult: Type.Optional(CallbackResultSchema),
childCallbackResults: Type.Optional(Type.Array(ChildCallbackResultSchema)),
createCollectionFlowToken: Type.Optional(Type.Boolean()),
mainRepresentative: Type.Optional(MainRepresentativeSchema),
customerName: Type.Optional(Type.String()),
enableManualCreation: Type.Optional(Type.Boolean()),
kybOnExitAction: Type.Optional(
Type.Union([Type.Literal('send-event'), Type.Literal('redirect-to-customer-portal')]),
),
reportConfig: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
hasUboOngoingMonitoring: Type.Optional(Type.Boolean()),
maxBusinessReports: Type.Optional(Type.Number()),
isMerchantMonitoringEnabled: Type.Optional(Type.Boolean()),
isChatbotEnabled: Type.Optional(Type.Boolean()),
});

export type TWorkflowConfig = Static<typeof WorkflowConfigSchema>;
5 changes: 5 additions & 0 deletions packages/common/src/schemas/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export { type TDefaultSchemaDocumentPage } from './documents/default-context-page-schema';
export {
defaultContextSchema,
defaultInputContextSchema,
type DefaultContextSchema,
} from './documents/default-context-schema';
export { getGhanaDocuments } from './documents/workflow/documents/schemas/GH';
Expand All @@ -14,3 +15,7 @@ export { type TAvailableDocuments, type TDocument } from './documents/workflow/d
export { WorkflowDefinitionConfigThemeSchema } from './workflow/workflow-config-theme';
export * from './workflow/end-user.schema';
export { DocumentsSchema, DocumentInsertSchema } from './documents/schemas/documents-schema';
export {
WorkflowRuntimeConfigSchema,
type TWorkflowRuntimeConfig,
} from './documents/workflow/config-schema';
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export class WebhooksService {
context: {
aml: data,
entity: {
// @ts-expect-error -- prisma date not compatible with typebox
data: {
...rest,
additionalInfo: rest.additionalInfo ?? {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ describe('OngoingMonitoringCron', () => {
return [
{
id: 'business1',
companyName: 'Test Business 1',
metadata: {
featureConfig: {
[FEATURE_LIST.ONGOING_MERCHANT_REPORT_T1]: {
Expand All @@ -236,9 +237,11 @@ describe('OngoingMonitoringCron', () => {
},
{
id: 'business2',
companyName: 'Test Business 2',
},
{
id: 'business3',
companyName: 'Test Business 3',
metadata: {
featureConfig: {
[FEATURE_LIST.ONGOING_MERCHANT_REPORT_T1]: {
Expand All @@ -257,6 +260,7 @@ describe('OngoingMonitoringCron', () => {
},
{
id: 'business4',
companyName: 'Test Business 4',
metadata: {
featureConfig: {
[FEATURE_LIST.ONGOING_MERCHANT_REPORT_T1]: {
Expand Down
10 changes: 10 additions & 0 deletions services/workflows-service/src/workflow/schemas/workflow-run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defaultInputContextSchema, WorkflowRuntimeConfigSchema } from '@ballerine/common';
import { Type } from '@sinclair/typebox';

export const WorkflowRunSchema = Type.Object({
workflowId: Type.String(),
context: defaultInputContextSchema,
config: Type.Optional(WorkflowRuntimeConfigSchema),
salesforceObjectName: Type.Optional(Type.String()),
salesforceRecordId: Type.Optional(Type.String()),
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { UseCustomerAuthGuard } from '@/common/decorators/use-customer-auth-guar
import { VerifyUnifiedApiSignatureDecorator } from '@/common/decorators/verify-unified-api-signature.decorator';
import { env } from '@/env';
import { PrismaService } from '@/prisma/prisma.service';
import type { InputJsonValue, TProjectId, TProjectIds } from '@/types';
import type { AnyRecord, InputJsonValue, TProjectId, TProjectIds } from '@/types';
import { WORKFLOW_DEFINITION_TAG } from '@/workflow-defintion/workflow-definition.controller';
import { WorkflowDefinitionService } from '@/workflow-defintion/workflow-definition.service';
import { CreateCollectionFlowUrlDto } from '@/workflow/dtos/create-collection-flow-url';
Expand All @@ -42,6 +42,8 @@ import { WorkflowService } from './workflow.service';
import { Validate } from 'ballerine-nestjs-typebox';
import { PutWorkflowExtensionSchema, WorkflowExtensionSchema } from './schemas/extensions.schemas';
import { type Static, Type } from '@sinclair/typebox';
import { defaultContextSchema } from '@ballerine/common';
import { WorkflowRunSchema } from './schemas/workflow-run';

export const WORKFLOW_TAG = 'Workflows';
@swagger.ApiBearerAuth()
Expand Down Expand Up @@ -230,15 +232,37 @@ export class WorkflowControllerExternal {
}

@common.Post('/run')
@swagger.ApiOkResponse()
@swagger.ApiOkResponse({
description: 'Workflow run initiated successfully',
schema: {
type: 'object',
properties: {
workflowDefinitionId: { type: 'string' },
workflowRuntimeId: { type: 'string' },
ballerineEntityId: { type: 'string' },
entity: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['individual', 'business'],
},
id: { type: 'string' },
},
required: ['type', 'id'],
},
},
},
})
@swagger.ApiOperation({
summary: `The /run endpoint initiates and executes various workflows based on initial data and configurations. Supported workflows include KYB, KYC, KYB with UBOs, KYB with Associated Companies, Ongoing Sanctions, and Merchant Monitoring. To start a workflow, provide workflowId, context (with entity and documents), and config (with checks) in the request body. Customization is possible through the config object. The response includes workflowDefinitionId, workflowRuntimeId, ballerineEntityId, and entities. Workflow execution is asynchronous, with progress tracked via webhook notifications.`,
})
@UseCustomerAuthGuard()
@common.HttpCode(200)
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
@swagger.ApiBody({
type: WorkflowRunDto,
// @ts-expect-error -- Something with swagger package
schema: WorkflowRunSchema,
description: 'Workflow run data.',
examples: {
KYB: {
Expand Down Expand Up @@ -320,13 +344,13 @@ export class WorkflowControllerExternal {
@Res() res: Response,
@ProjectIds() projectIds: TProjectIds,
@CurrentProject() currentProjectId: TProjectId,
): Promise<any> {
): Promise<unknown> {
const { workflowId, context, config } = body;
const { entity } = context;

// @ts-ignore
if (!entity.id && !entity.ballerineEntityId)
if (!('id' in entity) && !('ballerineEntityId' in entity)) {
throw new common.BadRequestException('Entity id is required');
}

const hasSalesforceRecord =
Boolean(body.salesforceObjectName) && Boolean(body.salesforceRecordId);
Expand All @@ -349,10 +373,10 @@ export class WorkflowControllerExternal {
});

return res.json({
workflowDefinitionId: actionResult[0]!.workflowDefinition.id,
workflowRuntimeId: actionResult[0]!.workflowRuntimeData.id,
ballerineEntityId: actionResult[0]!.ballerineEntityId,
entities: actionResult[0]!.entities,
workflowDefinitionId: actionResult[0]?.workflowDefinition.id,
workflowRuntimeId: actionResult[0]?.workflowRuntimeData.id,
ballerineEntityId: actionResult[0]?.ballerineEntityId,
entities: actionResult[0]?.entities,
});
}

Expand Down Expand Up @@ -395,7 +419,6 @@ export class WorkflowControllerExternal {
};
}

/// POST /event
@common.Post('/:id/event')
@swagger.ApiOkResponse()
@common.HttpCode(200)
Expand All @@ -417,7 +440,6 @@ export class WorkflowControllerExternal {
);
}

// POST /event
@common.Post('/:id/send-event')
@swagger.ApiOkResponse()
@UseCustomerAuthGuard()
Expand All @@ -440,10 +462,19 @@ export class WorkflowControllerExternal {
);
}

// curl -X GET -H "Content-Type: application/json" http://localhost:3000/api/v1/external/workflows/:id/context
@common.Get('/:id/context')
@UseCustomerAuthGuard()
@swagger.ApiOkResponse()
@swagger.ApiOkResponse({
schema: {
type: 'object',
properties: {
context: {
type: 'object',
properties: defaultContextSchema,
},
},
},
})
@common.HttpCode(200)
@swagger.ApiForbiddenResponse({ type: errors.ForbiddenException })
async getWorkflowRuntimeDataContext(
Expand Down Expand Up @@ -472,7 +503,7 @@ export class WorkflowControllerExternal {
async hook(
@common.Param() params: WorkflowIdWithEventInput,
@common.Query() query: WorkflowHookQuery,
@common.Body() hookResponse: any,
@common.Body() hookResponse: unknown,
): Promise<void> {
try {
await this.prismaService.$transaction(async transaction => {
Expand All @@ -482,8 +513,8 @@ export class WorkflowControllerExternal {
});

const context = await this.normalizeService.handleHookResponse({
workflowRuntime: workflowRuntime,
data: hookResponse,
workflowRuntime,
data: hookResponse as AnyRecord,
resultDestinationPath: query.resultDestination || 'hookResponse',
processName: query.processName,
projectIds: [workflowRuntime.projectId],
Expand Down

0 comments on commit 7b97b88

Please sign in to comment.