diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index b594ed33c4..77cb79e579 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -33,6 +33,7 @@ export type { TDefaultSchemaDocumentPage, TDocument, } from './schemas'; + export type { AnyRecord, LoggerInterface, Serializable, SortDirection } from './types'; export { diff --git a/packages/common/src/rule-engine/index.ts b/packages/common/src/rule-engine/index.ts index 18f719f5ff..89734d0211 100644 --- a/packages/common/src/rule-engine/index.ts +++ b/packages/common/src/rule-engine/index.ts @@ -9,6 +9,7 @@ export type { FailedRuleResult, PassedRuleResult, RuleResult, + RuleSetWithChildren, TFindAllRulesOptions, } from './types'; diff --git a/packages/common/src/rule-engine/types.ts b/packages/common/src/rule-engine/types.ts index 4d578f8c53..36cccb33a1 100644 --- a/packages/common/src/rule-engine/types.ts +++ b/packages/common/src/rule-engine/types.ts @@ -1,5 +1,6 @@ import { Rule, RuleSet } from './rules/types'; import { EngineErrors } from './errors'; +import { TOperator } from '@/rule-engine/operators/types'; export type PassedRuleResult = { status: 'PASSED' | 'SKIPPED'; @@ -8,6 +9,12 @@ export type PassedRuleResult = { rules?: Rule | RuleSet; // 'rules' should be present }; +export type RuleSetWithChildren = { + rules: Rule[]; + operator: TOperator; + childRuleSet: RuleSetWithChildren[]; +}; + export type FailedRuleResult = { status: 'FAILED'; message?: string; @@ -18,10 +25,17 @@ export type RuleResult = PassedRuleResult | FailedRuleResult; export type RuleResultSet = RuleResult[]; -export interface TFindAllRulesOptions { +export type TNotionRulesOptions = { databaseId: string; source: 'notion'; -} +}; + +export type TDatabaseRulesOptions = { + policyId: string; + source: 'database'; +}; + +export type TFindAllRulesOptions = TNotionRulesOptions | TDatabaseRulesOptions; export * from './operators/types'; diff --git a/packages/workflow-core/src/lib/plugins/common-plugin/risk-rules-plugin.ts b/packages/workflow-core/src/lib/plugins/common-plugin/risk-rules-plugin.ts index 16b5ab356b..efd5cbb110 100644 --- a/packages/workflow-core/src/lib/plugins/common-plugin/risk-rules-plugin.ts +++ b/packages/workflow-core/src/lib/plugins/common-plugin/risk-rules-plugin.ts @@ -63,7 +63,10 @@ export class RiskRulePlugin { logger.error(`Risk Rules Plugin - Failed`, { context, name: this.name, - databaseId: this.rulesSource.databaseId, + databaseId: + this.rulesSource.source === 'notion' + ? this.rulesSource.databaseId + : this.rulesSource.policyId, source: this.rulesSource.source, }); diff --git a/packages/workflow-core/src/lib/plugins/common-plugin/types.ts b/packages/workflow-core/src/lib/plugins/common-plugin/types.ts index a570497bd7..8e19d5e978 100644 --- a/packages/workflow-core/src/lib/plugins/common-plugin/types.ts +++ b/packages/workflow-core/src/lib/plugins/common-plugin/types.ts @@ -60,10 +60,7 @@ export interface IterativePluginParams { export interface RiskRulesPluginParams { name: string; - rulesSource: { - source: 'notion'; - databaseId: string; - }; + rulesSource: TFindAllRulesOptions; stateNames: string[]; successAction?: string; errorAction?: string; diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 24b2334f4b..971b8944f4 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 24b2334f4b50086ff80a7590d0ebdd332ce0d0be +Subproject commit 971b8944f4da5e6c0e7df6674c5ab7596ae3f1e3 diff --git a/services/workflows-service/prisma/migrations/20240813104622_create_risk_policy_and_rules/migration.sql b/services/workflows-service/prisma/migrations/20240813104622_create_risk_policy_and_rules/migration.sql new file mode 100644 index 0000000000..da0059c3f0 --- /dev/null +++ b/services/workflows-service/prisma/migrations/20240813104622_create_risk_policy_and_rules/migration.sql @@ -0,0 +1,185 @@ +-- CreateEnum +CREATE TYPE "IndicatorRiskLevel" AS ENUM ('positive', 'moderate', 'critical'); + +-- CreateEnum +CREATE TYPE "RuleEngine" AS ENUM ('Ballerine', 'JsonLogic'); + +-- CreateEnum +CREATE TYPE "RulesetOperator" AS ENUM ('and', 'or'); + +-- CreateTable +CREATE TABLE "RiskRulesPolicy" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RiskRulesPolicy_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RiskRule" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "riskRulePolicyId" TEXT NOT NULL, + "operator" "RulesetOperator" NOT NULL, + "projectId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "domain" TEXT NOT NULL, + "indicator" TEXT NOT NULL, + "riskLevel" "IndicatorRiskLevel" NOT NULL, + "baseRiskScore" INTEGER NOT NULL, + "additionalRiskScore" INTEGER NOT NULL, + "minRiskScore" INTEGER, + "maxRiskScore" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RiskRule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RiskRuleRuleSet" ( + "id" TEXT NOT NULL, + "riskRuleId" TEXT NOT NULL, + "ruleSetId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RiskRuleRuleSet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RuleSet" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "projectId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "operator" "RulesetOperator" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RuleSet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RuleSetToRuleSet" ( + "id" TEXT NOT NULL, + "parentId" TEXT NOT NULL, + "childId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RuleSetToRuleSet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RuleSetRule" ( + "id" TEXT NOT NULL, + "ruleId" TEXT NOT NULL, + "ruleSetId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RuleSetRule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Rule" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "projectId" TEXT, + "isPublic" BOOLEAN NOT NULL DEFAULT false, + "key" TEXT NOT NULL, + "operation" TEXT NOT NULL, + "value" JSONB NOT NULL, + "comparisonValue" JSONB NOT NULL, + "engine" "RuleEngine" NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Rule_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_RiskRuleSetToRuleSet" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE INDEX "RiskRulesPolicy_projectId_idx" ON "RiskRulesPolicy"("projectId"); + +-- CreateIndex +CREATE INDEX "RiskRule_riskRulePolicyId_idx" ON "RiskRule"("riskRulePolicyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "RiskRule_id_riskRulePolicyId_key" ON "RiskRule"("id", "riskRulePolicyId"); + +-- CreateIndex +CREATE INDEX "RiskRuleRuleSet_riskRuleId_idx" ON "RiskRuleRuleSet"("riskRuleId"); + +-- CreateIndex +CREATE INDEX "RiskRuleRuleSet_ruleSetId_idx" ON "RiskRuleRuleSet"("ruleSetId"); + +-- CreateIndex +CREATE UNIQUE INDEX "RiskRuleRuleSet_riskRuleId_ruleSetId_key" ON "RiskRuleRuleSet"("riskRuleId", "ruleSetId"); + +-- CreateIndex +CREATE INDEX "RuleSetToRuleSet_parentId_idx" ON "RuleSetToRuleSet"("parentId"); + +-- CreateIndex +CREATE INDEX "RuleSetToRuleSet_childId_idx" ON "RuleSetToRuleSet"("childId"); + +-- CreateIndex +CREATE UNIQUE INDEX "RuleSetToRuleSet_parentId_childId_key" ON "RuleSetToRuleSet"("parentId", "childId"); + +-- CreateIndex +CREATE INDEX "RuleSetRule_ruleId_idx" ON "RuleSetRule"("ruleId"); + +-- CreateIndex +CREATE INDEX "RuleSetRule_ruleSetId_idx" ON "RuleSetRule"("ruleSetId"); + +-- CreateIndex +CREATE UNIQUE INDEX "RuleSetRule_ruleId_ruleSetId_key" ON "RuleSetRule"("ruleId", "ruleSetId"); + +-- CreateIndex +CREATE UNIQUE INDEX "_RiskRuleSetToRuleSet_AB_unique" ON "_RiskRuleSetToRuleSet"("A", "B"); + +-- CreateIndex +CREATE INDEX "_RiskRuleSetToRuleSet_B_index" ON "_RiskRuleSetToRuleSet"("B"); + +-- AddForeignKey +ALTER TABLE "RiskRulesPolicy" ADD CONSTRAINT "RiskRulesPolicy_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RiskRule" ADD CONSTRAINT "RiskRule_riskRulePolicyId_fkey" FOREIGN KEY ("riskRulePolicyId") REFERENCES "RiskRulesPolicy"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RiskRuleRuleSet" ADD CONSTRAINT "RiskRuleRuleSet_riskRuleId_fkey" FOREIGN KEY ("riskRuleId") REFERENCES "RiskRule"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RiskRuleRuleSet" ADD CONSTRAINT "RiskRuleRuleSet_ruleSetId_fkey" FOREIGN KEY ("ruleSetId") REFERENCES "RuleSet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RuleSetToRuleSet" ADD CONSTRAINT "RuleSetToRuleSet_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "RuleSet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RuleSetToRuleSet" ADD CONSTRAINT "RuleSetToRuleSet_childId_fkey" FOREIGN KEY ("childId") REFERENCES "RuleSet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RuleSetRule" ADD CONSTRAINT "RuleSetRule_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "Rule"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RuleSetRule" ADD CONSTRAINT "RuleSetRule_ruleSetId_fkey" FOREIGN KEY ("ruleSetId") REFERENCES "RuleSet"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Rule" ADD CONSTRAINT "Rule_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_RiskRuleSetToRuleSet" ADD CONSTRAINT "_RiskRuleSetToRuleSet_A_fkey" FOREIGN KEY ("A") REFERENCES "RiskRule"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_RiskRuleSetToRuleSet" ADD CONSTRAINT "_RiskRuleSetToRuleSet_B_fkey" FOREIGN KEY ("B") REFERENCES "RuleSet"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/services/workflows-service/prisma/schema.prisma b/services/workflows-service/prisma/schema.prisma index 0ab5ed5e62..8157fc5c98 100644 --- a/services/workflows-service/prisma/schema.prisma +++ b/services/workflows-service/prisma/schema.prisma @@ -373,12 +373,14 @@ model Project { SalesforceIntegration SalesforceIntegration? workflowDefinitions WorkflowDefinition[] uiDefinitions UiDefinition[] + riskRulesPolicies RiskRulesPolicy[] WorkflowRuntimeDataToken WorkflowRuntimeDataToken[] TransactionRecord TransactionRecord[] AlertDefinition AlertDefinition[] Alert Alert[] Counterparty Counterparty[] BusinessReport BusinessReport[] + rules Rule[] @@unique([name, customerId]) @@index(name) @@ -815,6 +817,12 @@ enum RiskCategory { high } +enum IndicatorRiskLevel { + positive + moderate + critical +} + enum ComplianceStatus { compliant non_compliant @@ -864,6 +872,16 @@ enum BusinessReportStatus { completed } +enum RuleEngine { + Ballerine + JsonLogic +} + +enum RulesetOperator { + and + or +} + model BusinessReport { id String @id @default(cuid()) type BusinessReportType @@ -887,3 +905,145 @@ model BusinessReport { @@index([riskScore]) @@index([type]) } + +// model WorkflowDefinitionRiskRulePolicy { +// id String @id @default(cuid()) +// workflowDefinitionId String +// riskRulesPolicyId String +// riskRulesPolicy RiskRulesPolicy @relation(fields: [riskRulesPolicyId], references: [id]) +// workflowDefinition WorkflowDefinition @relation(fields: [workflowDefinitionId], references: [id]) +// +// createdAt DateTime @default(now()) +// updatedAt DateTime @updatedAt +// +// @@unique([workflowDefinitionId, riskRulesPolicyId]) +// @@index([workflowDefinitionId]) +// @@index([riskRulesPolicyId]) +// } + +model RiskRulesPolicy { + id String @id @default(cuid()) + name String + projectId String + isPublic Boolean @default(false) + + riskRules RiskRule[] + project Project @relation(fields: [projectId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@index([projectId]) +} + +model RiskRule { + id String @id @default(cuid()) + name String + riskRulePolicyId String + operator RulesetOperator + projectId String? + isPublic Boolean @default(false) + + domain String + indicator String + riskLevel IndicatorRiskLevel + + baseRiskScore Int + additionalRiskScore Int + minRiskScore Int? + maxRiskScore Int? + + riskRulePolicy RiskRulesPolicy @relation(fields: [riskRulePolicyId], references: [id]) + riskRuleRuleSets RiskRuleRuleSet[] + ruleSets RuleSet[] @relation("RiskRuleSetToRuleSet") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([id, riskRulePolicyId]) + @@index([riskRulePolicyId]) +} + +model RiskRuleRuleSet { + id String @id @default(cuid()) + riskRuleId String + ruleSetId String + + riskRule RiskRule @relation(fields: riskRuleId, references: [id]) + ruleSet RuleSet @relation(fields: [ruleSetId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([riskRuleId, ruleSetId]) + @@index([riskRuleId]) + @@index([ruleSetId]) +} + +model RuleSet { + id String @id @default(cuid()) + name String + projectId String? + isPublic Boolean @default(false) + operator RulesetOperator + + rulesetRules RuleSetRule[] + riskRuleRuleSets RiskRuleRuleSet[] + riskRuleSets RiskRule[] @relation("RiskRuleSetToRuleSet") + + parentRuleSets RuleSetToRuleSet[] @relation("ParentRuleSets") + childRuleSets RuleSetToRuleSet[] @relation("ChildRuleSets") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model RuleSetToRuleSet { + id String @id @default(cuid()) + parentId String + childId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + parent RuleSet @relation("ParentRuleSets", fields: [parentId], references: [id]) + child RuleSet @relation("ChildRuleSets", fields: [childId], references: [id]) + + @@unique([parentId, childId]) + @@index([parentId]) + @@index([childId]) +} + +model RuleSetRule { + id String @id @default(cuid()) + ruleId String + ruleSetId String + + rule Rule @relation("RuleToRuleSetRule", fields: [ruleId], references: [id], onDelete: Cascade) + ruleSet RuleSet @relation(fields: [ruleSetId], references: [id]) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([ruleId, ruleSetId]) + @@index([ruleId]) + @@index([ruleSetId]) +} + +model Rule { + id String @id @default(cuid()) + name String + projectId String? + isPublic Boolean @default(false) + + key String + operation String + value Json + comparisonValue Json + engine RuleEngine + + project Project? @relation(fields: [projectId], references: [id]) + rulesetRules RuleSetRule[] @relation("RuleToRuleSetRule") + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/services/workflows-service/src/app.module.ts b/services/workflows-service/src/app.module.ts index 95a6d860b7..10d4fa6d8a 100644 --- a/services/workflows-service/src/app.module.ts +++ b/services/workflows-service/src/app.module.ts @@ -47,7 +47,11 @@ import z from 'zod'; import { hashKey } from './customer/api-key/utils'; import { RuleEngineModule } from './rule-engine/rule-engine.module'; import { NotionModule } from '@/notion/notion.module'; +import { RiskRulePolicyModule } from '@/risk-rules/risk-rule-policy/risk-rule-policy.module'; +import { RuleModule } from '@/risk-rules/rule/rule.module'; import { SecretsManagerModule } from '@/secrets-manager/secrets-manager.module'; +import { RiskRuleModule } from '@/risk-rules/risk-rule/risk-rule.module'; +import { RuleSetModule } from '@/risk-rules/rule-set/rule-set.module'; export const validate = async (config: Record) => { const zodEnvSchema = z @@ -126,6 +130,11 @@ export const validate = async (config: Record) => { RuleEngineModule, NotionModule, SecretsManagerModule, + // Risk rules Modules + RiskRulePolicyModule, + RiskRuleModule, + RuleSetModule, + RuleModule, ], providers: [ { diff --git a/services/workflows-service/src/helpers/type-box/type-string-enum.ts b/services/workflows-service/src/helpers/type-box/type-string-enum.ts new file mode 100644 index 0000000000..4c0b3812bc --- /dev/null +++ b/services/workflows-service/src/helpers/type-box/type-string-enum.ts @@ -0,0 +1,13 @@ +import { Type } from '@sinclair/typebox'; + +export const TypeStringEnum = ( + values: [...T], + description?: string, + examples?: string[], +) => + Type.Unsafe({ + type: 'string', + enum: values, + description, + examples, + }); diff --git a/services/workflows-service/src/project/project-scope.service.ts b/services/workflows-service/src/project/project-scope.service.ts index e058edad69..2336d1a0cc 100644 --- a/services/workflows-service/src/project/project-scope.service.ts +++ b/services/workflows-service/src/project/project-scope.service.ts @@ -43,6 +43,31 @@ export class ProjectScopeService { return args!; } + scopeFindManyOrPublic( + args?: Prisma.SelectSubset, + projectIds?: TProjectIds, + ): T { + // @ts-expect-error - dynamically typed for all queries + args ||= {}; + // @ts-expect-error - dynamically typed for all queries + args!.where = { + // @ts-expect-error - dynamically typed for all queries + ...args?.where, + OR: [ + { + project: { + id: { in: projectIds }, + }, + }, + { + isPublic: true, + }, + ], + }; + + return args!; + } + scopeFindOne( args: Prisma.SelectSubset, projectIds: TProjectIds, @@ -59,6 +84,29 @@ export class ProjectScopeService { return args as T; } + scopeFindOneOrPublic( + args: Prisma.SelectSubset, + projectIds: TProjectIds, + ): T { + // @ts-expect-error + args.where = { + // @ts-expect-error + ...args.where, + OR: [ + { + project: { + id: { in: projectIds }, + }, + }, + { + isPublic: true, + }, + ], + }; + + return args as T; + } + scopeUpdateMany( args: Prisma.SelectSubset, projectIds: TProjectIds, diff --git a/services/workflows-service/src/risk-rules/consts/rule-set-depth-of-3-with-rules.ts b/services/workflows-service/src/risk-rules/consts/rule-set-depth-of-3-with-rules.ts new file mode 100644 index 0000000000..97d91dca8f --- /dev/null +++ b/services/workflows-service/src/risk-rules/consts/rule-set-depth-of-3-with-rules.ts @@ -0,0 +1,48 @@ +import { Prisma } from '@prisma/client'; + +export const RULESET_DEPTH_OF_3_WITH_RULES = { + rulesetRules: { + include: { + rule: true, + }, + }, + childRuleSets: { + include: { + child: { + include: { + rulesetRules: { + include: { + rule: true, + }, + }, + childRuleSets: { + include: { + child: { + include: { + rulesetRules: { + include: { + rule: true, + }, + }, + childRuleSets: { + include: { + child: { + include: { + rulesetRules: { + include: { + rule: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +} satisfies Prisma.RuleSetInclude; diff --git a/services/workflows-service/src/risk-rules/consts/rule-set-parent-depth-3-with-policies.ts b/services/workflows-service/src/risk-rules/consts/rule-set-parent-depth-3-with-policies.ts new file mode 100644 index 0000000000..eeb65fdb61 --- /dev/null +++ b/services/workflows-service/src/risk-rules/consts/rule-set-parent-depth-3-with-policies.ts @@ -0,0 +1,64 @@ +import { Prisma } from '@prisma/client'; + +export const RULESET_PARENT_DEPTH_3_WITH_POLICIES = { + riskRuleRuleSets: { + include: { + riskRule: { + include: { + riskRulePolicy: true, + }, + }, + }, + }, + parentRuleSets: { + include: { + parent: { + include: { + riskRuleRuleSets: { + include: { + riskRule: { + include: { + riskRulePolicy: true, + }, + }, + }, + }, + parentRuleSets: { + include: { + parent: { + include: { + riskRuleRuleSets: { + include: { + riskRule: { + include: { + riskRulePolicy: true, + }, + }, + }, + }, + parentRuleSets: { + include: { + parent: { + include: { + riskRuleRuleSets: { + include: { + riskRule: { + include: { + riskRulePolicy: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, +} satisfies Prisma.RuleSetInclude; diff --git a/services/workflows-service/src/risk-rules/helpers/rule-set-search-and-flatten-values.ts b/services/workflows-service/src/risk-rules/helpers/rule-set-search-and-flatten-values.ts new file mode 100644 index 0000000000..1addd3feba --- /dev/null +++ b/services/workflows-service/src/risk-rules/helpers/rule-set-search-and-flatten-values.ts @@ -0,0 +1,25 @@ +import { uniq } from 'lodash'; +import { RuleSetWithParent } from '@/risk-rules/types/types'; + +export const extractRiskRulePolicy = (ruleSet: RuleSetWithParent) => { + const traverse = (currentRuleSet: RuleSetWithParent, riskRulesPoliciesId: string[]) => { + if (currentRuleSet?.riskRuleRuleSets.length > 0) { + riskRulesPoliciesId = uniq([ + ...riskRulesPoliciesId, + ...currentRuleSet.riskRuleRuleSets.flatMap(({ riskRule }) => riskRule.riskRulePolicyId), + ]); + } + + if (currentRuleSet.parentRuleSets?.length === 0) { + currentRuleSet.parentRuleSets.flatMap(parentRuleSet => { + if (parentRuleSet.parent) { + return traverse(parentRuleSet.parent as RuleSetWithParent, riskRulesPoliciesId); + } + }); + } + + return riskRulesPoliciesId; + }; + + return traverse(ruleSet, []); +}; diff --git a/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.controller.ts b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.controller.ts new file mode 100644 index 0000000000..c1d639e003 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.controller.ts @@ -0,0 +1,107 @@ +import * as swagger from '@nestjs/swagger'; +import * as common from '@nestjs/common'; +import { RiskRulePolicyService } from './risk-rule-policy.service'; +import { Type } from '@sinclair/typebox'; +import { Validate } from 'ballerine-nestjs-typebox'; +import { ProjectIds } from '@/common/decorators/project-ids.decorator'; +import type { TProjectId, TProjectIds } from '@/types'; +import { CurrentProject } from '@/common/decorators/current-project.decorator'; +import { + CreateRiskRulePolicySchema, + type TCreateRiskRulePolicy, +} from './schemas/create-risk-rule-policy-schema'; +import { + type TUpdateRiskRulePolicy, + UpdateRiskRulePolicySchema, +} from './schemas/update-risk-rule-policy-schema'; + +@swagger.ApiTags('Risk Rule Policies') +@common.Controller('external/risk-rule-policies') +export class RiskRulePolicyController { + constructor(protected readonly riskRulePolicyService: RiskRulePolicyService) {} + + @common.Post() + @Validate({ + request: [ + { + type: 'body', + schema: CreateRiskRulePolicySchema, + }, + ], + response: Type.Any(), + }) + async createRiskRulePolicy( + @common.Body() data: TCreateRiskRulePolicy, + @CurrentProject() currentProjectId: TProjectId, + ) { + return this.riskRulePolicyService.createRiskRulePolicy({ + ...data, + projectId: currentProjectId, + isPublic: false, + }); + } + + @common.Get(':id') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + ], + response: Type.Any(), + }) + async getRiskRulePolicy(@common.Param('id') id: string, @ProjectIds() projectIds: TProjectIds) { + return this.riskRulePolicyService.findById(id, projectIds); + } + + @common.Get() + @Validate({ + response: Type.Array(Type.Any()), + }) + async listRiskRulePolicies(@ProjectIds() projectIds: TProjectIds) { + return this.riskRulePolicyService.findMany({}, projectIds); + } + + @common.Patch(':id') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + { + type: 'body', + schema: UpdateRiskRulePolicySchema, + }, + ], + response: Type.Any(), + }) + async updateRiskRulePolicy( + @common.Param('id') id: string, + @common.Body() updateData: TUpdateRiskRulePolicy, + @CurrentProject() projectId: TProjectId, + ) { + return this.riskRulePolicyService.updateRiskRulePolicy(id, updateData, projectId); + } + + @common.Delete(':id') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + ], + response: Type.Any(), + }) + async deleteRiskRulePolicy( + @common.Param('id') id: string, + @ProjectIds() projectIds: TProjectIds, + ) { + return this.riskRulePolicyService.deleteRiskRulePolicy(id, projectIds); + } +} diff --git a/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.module.ts b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.module.ts new file mode 100644 index 0000000000..cfc37abda9 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { AppLoggerModule } from '@/common/app-logger/app-logger.module'; +import { RiskRulePolicyService } from '@/risk-rules/risk-rule-policy/risk-rule-policy.service'; +import { RiskRulePolicyRepository } from '@/risk-rules/risk-rule-policy/risk-rule-policy.repository'; +import { PrismaModule } from '@/prisma/prisma.module'; +import { ProjectModule } from '@/project/project.module'; +import { RiskRulePolicyController } from '@/risk-rules/risk-rule-policy/risk-rule-policy.controller'; + +@Module({ + controllers: [RiskRulePolicyController], + imports: [AppLoggerModule, PrismaModule, ProjectModule], + providers: [RiskRulePolicyService, RiskRulePolicyRepository], + exports: [RiskRulePolicyService], +}) +export class RiskRulePolicyModule {} diff --git a/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.repository.ts b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.repository.ts new file mode 100644 index 0000000000..76c0b0cbc8 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.repository.ts @@ -0,0 +1,94 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { PrismaService } from '@/prisma/prisma.service'; +import { Prisma, RiskRulesPolicy } from '@prisma/client'; +import { ProjectScopeService } from '@/project/project-scope.service'; +import { TProjectId, TProjectIds } from '@/types'; +import { RULESET_DEPTH_OF_3_WITH_RULES } from '@/risk-rules/consts/rule-set-depth-of-3-with-rules'; + +@Injectable() +export class RiskRulePolicyRepository { + constructor( + private readonly prisma: PrismaService, + private readonly scopeService: ProjectScopeService, + ) {} + + async create(data: Prisma.RiskRulesPolicyUncheckedCreateInput) { + return this.prisma.riskRulesPolicy.create({ data }); + } + + async findById(id: string, projectIds: TProjectIds) { + return this.prisma.riskRulesPolicy.findFirstOrThrow( + this.scopeService.scopeFindOneOrPublic( + { + where: { id }, + include: { + riskRules: { + include: { + riskRuleRuleSets: { + include: { + ruleSet: { + include: { + ...RULESET_DEPTH_OF_3_WITH_RULES, + }, + }, + }, + }, + }, + }, + }, + }, + projectIds, + ), + ); + } + + async findMany(args: Prisma.RiskRulesPolicyFindManyArgs, projectIds: TProjectIds) { + return this.prisma.riskRulesPolicy.findMany( + this.scopeService.scopeFindManyOrPublic({ ...args }, projectIds), + ); + } + + async update( + id: string, + data: Prisma.RiskRulesPolicyUncheckedUpdateInput, + projectId: TProjectId, + ) { + const policy = await this.findById(id, [projectId]); + + if (policy.isPublic) { + throw new BadRequestException('Cannot add risk rule to public policy'); + } + + return this.prisma.riskRulesPolicy.update({ + where: { id }, + data: data, + }); + } + + async delete(id: string, projectIds: TProjectIds) { + return this.prisma.riskRulesPolicy.delete( + this.scopeService.scopeDelete({ where: { id } }, projectIds), + ); + } + + async addRiskRule( + policyId: string, + riskRuleId: string, + projectIds: TProjectIds, + ): Promise { + const policy = await this.findById(policyId, projectIds); + + if (policy.isPublic) { + throw new BadRequestException('Cannot add risk rule to public policy'); + } + + return this.prisma.riskRulesPolicy.update({ + where: { id: policyId }, + data: { + riskRules: { + connect: { id: riskRuleId }, + }, + }, + }); + } +} diff --git a/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.service.ts b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.service.ts new file mode 100644 index 0000000000..1552faec3f --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule-policy/risk-rule-policy.service.ts @@ -0,0 +1,104 @@ +import { Injectable } from '@nestjs/common'; +import { RiskRulePolicyRepository } from './risk-rule-policy.repository'; +import { TProjectId, TProjectIds } from '@/types'; +import { Prisma } from '@prisma/client'; +import { RuleSetWithChildrenAndRules } from '@/risk-rules/types/types'; +import { Rule, RuleSchema, RuleSet, RuleSetWithChildren, Serializable } from '@ballerine/common'; + +@Injectable() +export class RiskRulePolicyService { + constructor(private readonly riskRulePolicyRepository: RiskRulePolicyRepository) {} + + async createRiskRulePolicy(data: Prisma.RiskRulesPolicyUncheckedCreateInput) { + return this.riskRulePolicyRepository.create(data); + } + + async findById(id: string, projectIds: TProjectIds) { + const policy = await this.riskRulePolicyRepository.findById(id, projectIds); + + return policy; + } + + private extractRulesFromRuleSet(rulesSet: RuleSetWithChildrenAndRules): RuleSet { + const rules = rulesSet.rulesetRules.map(rulesetRule => { + const { key, value, operation, comparisonValue, engine } = rulesetRule.rule; + + const parseResult = RuleSchema.parse({ + key, + value, + operator: operation, + }); + + return { + ...parseResult, + comparisonValue: comparisonValue as NonNullable, + engine, + } satisfies Rule & { + comparisonValue: NonNullable; + engine: string; + }; + }) satisfies Rule[]; + + const childRuleSets = rulesSet.childRuleSets.map(childRuleSet => + this.extractRulesFromRuleSet(childRuleSet.child), + ); + + return { + rules: [...rules, ...childRuleSets], + operator: rulesSet.operator, + }; + } + + async formatRiskRuleWithRules(id: string, projectIds: TProjectIds) { + const policyWithRiskRules = await this.findById(id, projectIds); + + return policyWithRiskRules.riskRules.map(riskRule => { + const correlatedRuleSet = riskRule.riskRuleRuleSets[0]?.ruleSet; + + if (correlatedRuleSet) { + const ruleSet = this.extractRulesFromRuleSet(correlatedRuleSet); + + return { + operator: riskRule.operator, + domain: riskRule.domain, + indicator: riskRule.indicator, + baseRiskScore: riskRule.baseRiskScore, + additionalRiskScore: riskRule.additionalRiskScore, + minRiskScore: riskRule.minRiskScore, + maxRiskScore: riskRule.maxRiskScore, + ruleSet: ruleSet, + }; + } + + return null; + }); + } + + async findMany(args: Prisma.RiskRulesPolicyFindManyArgs, projectIds: TProjectIds) { + return this.riskRulePolicyRepository.findMany(args, projectIds); + } + + async updateRiskRulePolicy( + id: string, + data: Prisma.RiskRulesPolicyUncheckedUpdateInput, + projectId: TProjectId, + ) { + return this.riskRulePolicyRepository.update( + id, + { + ...data, + projectId, + isPublic: false, + }, + projectId, + ); + } + + async deleteRiskRulePolicy(id: string, projectIds: TProjectIds) { + return this.riskRulePolicyRepository.delete(id, projectIds); + } + + async addRiskRuleToPolicy(policyId: string, riskRuleId: string, projectIds: TProjectIds) { + return await this.riskRulePolicyRepository.addRiskRule(policyId, riskRuleId, projectIds); + } +} diff --git a/services/workflows-service/src/risk-rules/risk-rule-policy/schemas/create-risk-rule-policy-schema.ts b/services/workflows-service/src/risk-rules/risk-rule-policy/schemas/create-risk-rule-policy-schema.ts new file mode 100644 index 0000000000..50828d7fd8 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule-policy/schemas/create-risk-rule-policy-schema.ts @@ -0,0 +1,10 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const CreateRiskRulePolicySchema = Type.Object({ + name: Type.String({ + description: 'Name of the risk rule policy', + examples: ['High Risk Transaction Policy'], + }), +}); + +export type TCreateRiskRulePolicy = Static; diff --git a/services/workflows-service/src/risk-rules/risk-rule-policy/schemas/update-risk-rule-policy-schema.ts b/services/workflows-service/src/risk-rules/risk-rule-policy/schemas/update-risk-rule-policy-schema.ts new file mode 100644 index 0000000000..5d1a051a69 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule-policy/schemas/update-risk-rule-policy-schema.ts @@ -0,0 +1,12 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const UpdateRiskRulePolicySchema = Type.Partial( + Type.Object({ + name: Type.String({ + description: 'Name of the risk rule policy', + examples: ['Updated High Risk Transaction Policy'], + }), + }), +); + +export type TUpdateRiskRulePolicy = Static; diff --git a/services/workflows-service/src/risk-rules/risk-rule/risk-rule.controller.ts b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.controller.ts new file mode 100644 index 0000000000..8e682c2a0f --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.controller.ts @@ -0,0 +1,183 @@ +import * as swagger from '@nestjs/swagger'; +import * as common from '@nestjs/common'; +import { RiskRuleService } from './risk-rule.service'; +import { Type } from '@sinclair/typebox'; +import { Validate } from 'ballerine-nestjs-typebox'; +import { ProjectIds } from '@/common/decorators/project-ids.decorator'; +import type { TProjectId, TProjectIds } from '@/types'; +import { CurrentProject } from '@/common/decorators/current-project.decorator'; +import { CreateRiskRuleSchema, type TCreateRiskRule } from './schemas/create-risk-rule.schema'; +import { UpdateRiskRuleSchema, type TUpdateRiskRule } from './schemas/update-risk-rule.schema'; +import { DisconnectRiskRuleToRulesetSchema } from '@/risk-rules/risk-rule/schemas/disconnect-risk-rule-to-ruleset.schema'; +import { + ConnectRiskRuleToRulesetSchema, + type TConnectRiskRuleToRuleset, +} from '@/risk-rules/risk-rule/schemas/connect-risk-rule-to-ruleset.schema'; +import { + CopyRiskRuleSchema, + type TCopyRiskRule, +} from '@/risk-rules/risk-rule/schemas/copy-risk-rule.schema'; + +@swagger.ApiTags('Risk Rules') +@common.Controller('external/risk-rules') +export class RiskRuleController { + constructor(protected readonly riskRuleService: RiskRuleService) {} + + @common.Post() + @Validate({ + request: [ + { + type: 'body', + schema: CreateRiskRuleSchema, + }, + ], + response: Type.Any(), + }) + async createRiskRule( + @common.Body() data: TCreateRiskRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + return this.riskRuleService.createNewRiskRule({ + riskRuleData: data, + projectId: currentProjectId, + ruleSetId: data.ruleSetId, + }); + } + + @common.Get(':id') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + ], + response: Type.Any(), + }) + async getRiskRule(@common.Param('id') id: string, @ProjectIds() projectIds: TProjectIds) { + return this.riskRuleService.findById(id, projectIds); + } + + @common.Get() + @Validate({ + response: Type.Array(Type.Any()), + }) + async listRiskRules(@ProjectIds() projectIds: TProjectIds) { + return this.riskRuleService.findMany({}, projectIds); + } + + @common.Patch(':id') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + { + type: 'body', + schema: UpdateRiskRuleSchema, + }, + ], + response: Type.Any(), + }) + async updateRiskRule( + @common.Param('id') id: string, + @common.Body() updateData: TUpdateRiskRule, + @CurrentProject() projectId: TProjectId, + ) { + return this.riskRuleService.updateRiskRule({ id, updateData, projectId }); + } + + @common.Delete(':id') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + ], + response: Type.Any(), + }) + async deleteRiskRule(@common.Param('id') id: string, @ProjectIds() projectIds: TProjectIds) { + return this.riskRuleService.deleteRiskRule(id, projectIds); + } + + @common.Post(':id/copy') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + { + type: 'body', + schema: CopyRiskRuleSchema, + }, + ], + response: Type.Any(), + }) + async copyRiskRule( + @common.Param('id') id: string, + @common.Body() { newName }: TCopyRiskRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + return this.riskRuleService.createCopyOfRiskRule({ + originalId: id, + newName, + projectId: currentProjectId, + projectIds, + }); + } + @common.Post(':id/connect-ruleset') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + { + type: 'body', + schema: ConnectRiskRuleToRulesetSchema, + }, + ], + response: Type.Object({ message: Type.String() }), + }) + async connectRiskRuleToRuleset( + @common.Param('id') id: string, + @common.Body() body: TConnectRiskRuleToRuleset, + ) { + await this.riskRuleService.connectRiskRuleToRuleset(id, body.ruleSetId); + + return { message: 'RiskRule successfully connected to RuleSet' }; + } + + @common.Post(':id/disconnect-ruleset') + @Validate({ + request: [ + { + type: 'param', + name: 'id', + schema: Type.String(), + }, + { + type: 'body', + schema: DisconnectRiskRuleToRulesetSchema, + }, + ], + response: Type.Object({ message: Type.String() }), + }) + async disconnectRiskRuleFromRuleset( + @common.Param('id') id: string, + @common.Body() body: { ruleSetId: string }, + ) { + await this.riskRuleService.disconnectRiskRuleFromRuleset(id, body.ruleSetId); + return { message: 'RiskRule successfully disconnected from RuleSet' }; + } +} diff --git a/services/workflows-service/src/risk-rules/risk-rule/risk-rule.module.ts b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.module.ts new file mode 100644 index 0000000000..cb9a499077 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { AppLoggerModule } from '@/common/app-logger/app-logger.module'; +import { PrismaModule } from '@/prisma/prisma.module'; +import { ProjectModule } from '@/project/project.module'; +import { RiskRuleService } from '@/risk-rules/risk-rule/risk-rule.service'; +import { RiskRuleRepository } from '@/risk-rules/risk-rule/risk-rule.repository'; +import { RiskRuleController } from '@/risk-rules/risk-rule/risk-rule.controller'; + +@Module({ + controllers: [RiskRuleController], + imports: [AppLoggerModule, PrismaModule, ProjectModule], + providers: [RiskRuleService, RiskRuleService], + exports: [RiskRuleRepository, RiskRuleRepository], +}) +export class RiskRuleModule {} diff --git a/services/workflows-service/src/risk-rules/risk-rule/risk-rule.repository.ts b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.repository.ts new file mode 100644 index 0000000000..7b4105ef86 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.repository.ts @@ -0,0 +1,115 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@/prisma/prisma.service'; +import { Prisma, RiskRule } from '@prisma/client'; +import { ProjectScopeService } from '@/project/project-scope.service'; +import { TProjectId, TProjectIds } from '@/types'; + +@Injectable() +export class RiskRuleRepository { + constructor( + private readonly prisma: PrismaService, + private readonly scopeService: ProjectScopeService, + ) {} + + async create({ + createArgs, + projectId, + }: { + createArgs: Omit; + projectId: string; + }) { + return this.prisma.riskRule.create({ + data: { + ...createArgs, + projectId, + isPublic: false, + }, + }); + } + + async findById(id: string, projectIds: TProjectIds) { + return this.prisma.riskRule.findFirstOrThrow( + this.scopeService.scopeFindOneOrPublic( + { + where: { id }, + }, + projectIds, + ), + ); + } + + async findMany(args: Prisma.RiskRuleFindManyArgs, projectIds: TProjectIds) { + return this.prisma.riskRule.findMany( + this.scopeService.scopeFindManyOrPublic( + { + ...args, + where: { ...args.where }, + }, + projectIds, + ), + ); + } + + async update(id: string, updateArgs: Prisma.RiskRuleUpdateInput, projectId: TProjectId) { + return this.prisma.riskRule.update({ + where: { id }, + data: { + ...updateArgs, + projectId, + }, + }); + } + + async delete(id: string, projectIds: TProjectIds) { + return this.prisma.riskRule.delete( + this.scopeService.scopeDelete( + { + where: { id }, + }, + projectIds, + ), + ); + } + + async createCopy( + originalId: string, + newName: string, + projectId: string, + projectIds: TProjectIds, + ): Promise { + const original = await this.findById(originalId, projectIds); + if (!original) { + throw new Error('Original RiskRule not found'); + } + + const { id, name, createdAt, updatedAt, ...copyData } = original; + + return this.create({ + createArgs: { + ...copyData, + name: newName, + }, + projectId, + }); + } + + async connectToRuleset(riskRuleId: string, ruleSetId: string) { + return await this.prisma.riskRuleRuleSet.create({ + data: { + riskRule: { connect: { id: riskRuleId } }, + ruleSet: { connect: { id: ruleSetId } }, + }, + }); + } + + async disconnectFromRuleset(riskRuleId: string, ruleSetId: string) { + return await this.prisma.riskRuleRuleSet.delete({ + where: { + riskRuleId_ruleSetId: { + riskRuleId, + ruleSetId, + }, + }, + }); + } +} diff --git a/services/workflows-service/src/risk-rules/risk-rule/risk-rule.service.ts b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.service.ts new file mode 100644 index 0000000000..7508cfc04d --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/risk-rule.service.ts @@ -0,0 +1,85 @@ +import { Injectable, NotFoundException } from '@nestjs/common'; +import { RiskRuleRepository } from './risk-rule.repository'; +import { TProjectId, TProjectIds } from '@/types'; +import { Prisma, RiskRule } from '@prisma/client'; + +@Injectable() +export class RiskRuleService { + constructor(private readonly riskRuleRepository: RiskRuleRepository) {} + + async createNewRiskRule({ + riskRuleData, + projectId, + ruleSetId, + }: { + riskRuleData: Omit; + projectId: TProjectId; + ruleSetId?: string; + }): Promise { + const riskRule = await this.riskRuleRepository.create({ + createArgs: riskRuleData, + projectId, + }); + + if (ruleSetId) { + await this.connectRiskRuleToRuleset(riskRule.id, ruleSetId); + } + + return riskRule; + } + + async findById(id: string, projectIds: TProjectIds) { + const riskRule = await this.riskRuleRepository.findById(id, projectIds); + + return riskRule; + } + + async findMany(args: Prisma.RiskRuleFindManyArgs, projectIds: TProjectIds) { + return this.riskRuleRepository.findMany(args, projectIds); + } + + async updateRiskRule({ + id, + updateData, + projectId, + }: { + id: string; + updateData: Prisma.RiskRuleUpdateInput; + projectId: TProjectId; + }) { + return this.riskRuleRepository.update( + id, + { + ...updateData, + isPublic: false, + }, + projectId, + ); + } + + async deleteRiskRule(id: string, projectIds: TProjectIds): Promise { + return this.riskRuleRepository.delete(id, projectIds); + } + + async createCopyOfRiskRule({ + originalId, + newName, + projectId, + projectIds, + }: { + originalId: string; + newName: string; + projectId: TProjectId; + projectIds: TProjectIds; + }) { + return this.riskRuleRepository.createCopy(originalId, newName, projectId, projectIds); + } + + async connectRiskRuleToRuleset(riskRuleId: string, ruleSetId: string) { + await this.riskRuleRepository.connectToRuleset(riskRuleId, ruleSetId); + } + + async disconnectRiskRuleFromRuleset(riskRuleId: string, ruleSetId: string) { + await this.riskRuleRepository.disconnectFromRuleset(riskRuleId, ruleSetId); + } +} diff --git a/services/workflows-service/src/risk-rules/risk-rule/schemas/connect-risk-rule-to-ruleset.schema.ts b/services/workflows-service/src/risk-rules/risk-rule/schemas/connect-risk-rule-to-ruleset.schema.ts new file mode 100644 index 0000000000..07db4a0707 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/schemas/connect-risk-rule-to-ruleset.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const ConnectRiskRuleToRulesetSchema = Type.Object({ + ruleSetId: Type.String({ + description: 'The ID of the ruleset to connect the risk rule to', + }), +}); + +export type TConnectRiskRuleToRuleset = Static; diff --git a/services/workflows-service/src/risk-rules/risk-rule/schemas/copy-risk-rule.schema.ts b/services/workflows-service/src/risk-rules/risk-rule/schemas/copy-risk-rule.schema.ts new file mode 100644 index 0000000000..d244a600ae --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/schemas/copy-risk-rule.schema.ts @@ -0,0 +1,10 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const CopyRiskRuleSchema = Type.Object({ + newName: Type.String({ + description: 'The name for the new copy of the risk rule', + examples: ['Copy of High Transaction Amount Risk'], + }), +}); + +export type TCopyRiskRule = Static; diff --git a/services/workflows-service/src/risk-rules/risk-rule/schemas/create-risk-rule.schema.ts b/services/workflows-service/src/risk-rules/risk-rule/schemas/create-risk-rule.schema.ts new file mode 100644 index 0000000000..8a21d65f52 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/schemas/create-risk-rule.schema.ts @@ -0,0 +1,56 @@ +import { Static, Type } from '@sinclair/typebox'; +import { TypeStringEnum } from '@/helpers/type-box/type-string-enum'; +import { RulesetOperator, IndicatorRiskLevel } from '@prisma/client'; + +export const CreateRiskRuleSchema = Type.Object({ + name: Type.String({ + description: 'Name of the risk rule', + examples: ['High Transaction Amount Risk'], + }), + riskRulePolicyId: Type.String({ + description: 'The ID of the associated risk rule policy', + }), + operator: TypeStringEnum(Object.values(RulesetOperator), 'The operator for the risk rule'), + domain: Type.String({ + description: 'The domain of the risk rule', + examples: ['transaction'], + }), + indicator: Type.String({ + description: 'The indicator for the risk rule', + examples: ['Amount'], + }), + riskLevel: TypeStringEnum(Object.values(IndicatorRiskLevel), 'The risk level for the rule'), + baseRiskScore: Type.Number({ + minimum: 0, + maximum: 100, + description: 'The base risk score for the rule', + }), + additionalRiskScore: Type.Number({ + minimum: 0, + maximum: 100, + description: 'The additional risk score for the rule', + }), + minRiskScore: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'The minimum risk score (optional)', + }), + ), + maxRiskScore: Type.Optional( + Type.Number({ + minimum: 0, + maximum: 100, + description: 'The maximum risk score (optional)', + }), + ), + ruleSetId: Type.Optional( + Type.String({ + minimum: 0, + maximum: 100, + description: 'The ID of the ruleset to connect the risk rule to (optional)', + }), + ), +}); + +export type TCreateRiskRule = Static; diff --git a/services/workflows-service/src/risk-rules/risk-rule/schemas/disconnect-risk-rule-to-ruleset.schema.ts b/services/workflows-service/src/risk-rules/risk-rule/schemas/disconnect-risk-rule-to-ruleset.schema.ts new file mode 100644 index 0000000000..581e4f4baa --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/schemas/disconnect-risk-rule-to-ruleset.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const DisconnectRiskRuleToRulesetSchema = Type.Object({ + ruleSetId: Type.String({ + description: 'The ID of the ruleset to disconnect the from the risk rule', + }), +}); + +export type TDisconnectRiskRuleToRuleset = Static; diff --git a/services/workflows-service/src/risk-rules/risk-rule/schemas/update-risk-rule.schema.ts b/services/workflows-service/src/risk-rules/risk-rule/schemas/update-risk-rule.schema.ts new file mode 100644 index 0000000000..54f2b7dda8 --- /dev/null +++ b/services/workflows-service/src/risk-rules/risk-rule/schemas/update-risk-rule.schema.ts @@ -0,0 +1,6 @@ +import { Static, Type } from '@sinclair/typebox'; +import { CreateRiskRuleSchema } from '@/risk-rules/risk-rule/schemas/create-risk-rule.schema'; + +export const UpdateRiskRuleSchema = Type.Partial(CreateRiskRuleSchema); + +export type TUpdateRiskRule = Static; diff --git a/services/workflows-service/src/risk-rules/rule-set/rule-set.controller.ts b/services/workflows-service/src/risk-rules/rule-set/rule-set.controller.ts new file mode 100644 index 0000000000..bd2f22133d --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/rule-set.controller.ts @@ -0,0 +1,149 @@ +import * as swagger from '@nestjs/swagger'; +import * as common from '@nestjs/common'; +import { Delete } from '@nestjs/common'; +import { Type } from '@sinclair/typebox'; +import { Validate } from 'ballerine-nestjs-typebox'; +import { ProjectIds } from '@/common/decorators/project-ids.decorator'; +import type { TProjectId, TProjectIds } from '@/types'; +import { CurrentProject } from '@/common/decorators/current-project.decorator'; +import { type TUpdateRule, UpdateRuleSchema } from '@/risk-rules/rule/schemas/update-rule.schema'; +import { RuleSetService } from '@/risk-rules/rule-set/rule-set.service'; +import { + CreateRulesetSchema, + type TCreatedRuleset, +} from '@/risk-rules/rule-set/schemas/create-rule-set.schema'; +import { + AssignRuleSetToParentRuleSet, + type TassignToParentRuleSet, +} from '@/risk-rules/rule-set/schemas/assign-rule-set.schema'; +import { + type TUnassignRulesetFromSchema, + UnassignRulesetFromParentSchema, +} from '@/risk-rules/rule-set/schemas/unassign-ruleset-from-parent.schema'; + +@swagger.ApiTags('Rules') +@common.Controller('external/rule-set') +export class RuleSetController { + constructor(protected readonly ruleSetService: RuleSetService) {} + + @common.Post('') + @Validate({ + request: [ + { + type: 'body', + schema: CreateRulesetSchema, + }, + ], + response: Type.Any(), + }) + async createRule( + @common.Body() data: TCreatedRuleset, + @CurrentProject() currentProjectId: TProjectId, + ) { + const { parentRuleSetId, ...rulesetCreationData } = data; + + return await this.ruleSetService.createRuleSet({ + ruleSetData: rulesetCreationData, + projectId: currentProjectId, + parentRuleSetId: parentRuleSetId, + }); + } + + @common.Put('/:ruleset/assign') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + { + type: 'body', + schema: AssignRuleSetToParentRuleSet, + }, + ], + response: Type.Any(), + }) + async assignRuleToParentRuleSet( + @common.Query() ruleId: string, + @common.Body() assignRuleDate: TassignToParentRuleSet, + ) { + const ruleSetAssociation = await this.ruleSetService.assignRuleSetToParentRuleset( + ruleId, + assignRuleDate.parentRuleSetId, + ); + + return ruleSetAssociation; + } + + @common.Put('/:ruleSetId/unassign') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleSetId', + schema: Type.String(), + }, + { + type: 'body', + schema: UnassignRulesetFromParentSchema, + }, + ], + response: Type.Any(), + }) + async unassignRule( + @common.Query() ruleSetId: string, + @common.Body() parentRuleSet: TUnassignRulesetFromSchema, + ) { + return await this.ruleSetService.unassignRuleFromRuleset( + ruleSetId, + parentRuleSet.parentRuleSetId, + ); + } + + @common.Patch('/:ruleId') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + { + type: 'body', + schema: UpdateRuleSchema, + }, + ], + response: UpdateRuleSchema, + }) + async updateRule( + @common.Query() ruleId: string, + @common.Body() updateRuleSchema: TUpdateRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + const rule = await this.ruleSetService.updateRuleSet({ + ruleId, + ruleData: updateRuleSchema, + projectId: currentProjectId, + projectIds, + }); + + return rule as TUpdateRule; + } + + @Delete('/:ruleId') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + ], + response: Type.Any(), + }) + async deleteRule(@common.Query() ruleId: string, @ProjectIds() projectIds: TProjectIds) { + await this.ruleSetService.deleteRule({ ruleId, projectIds }); + } +} diff --git a/services/workflows-service/src/risk-rules/rule-set/rule-set.module.ts b/services/workflows-service/src/risk-rules/rule-set/rule-set.module.ts new file mode 100644 index 0000000000..1d17f2aa18 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/rule-set.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { AppLoggerModule } from '@/common/app-logger/app-logger.module'; +import { PrismaModule } from '@/prisma/prisma.module'; +import { ProjectModule } from '@/project/project.module'; +import { RuleSetController } from '@/risk-rules/rule-set/rule-set.controller'; +import { RuleSetService } from '@/risk-rules/rule-set/rule-set.service'; +import { RuleSetRepository } from '@/risk-rules/rule-set/rule-set.repository'; + +@Module({ + controllers: [RuleSetController], + imports: [AppLoggerModule, PrismaModule, ProjectModule], + providers: [RuleSetService, RuleSetRepository], + exports: [RuleSetService], +}) +export class RuleSetModule {} diff --git a/services/workflows-service/src/risk-rules/rule-set/rule-set.repository.ts b/services/workflows-service/src/risk-rules/rule-set/rule-set.repository.ts new file mode 100644 index 0000000000..f49e79bd1f --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/rule-set.repository.ts @@ -0,0 +1,193 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@/prisma/prisma.service'; +import { Prisma } from '@prisma/client'; +import { ProjectScopeService } from '@/project/project-scope.service'; +import { TProjectIds } from '@/types'; +import { RULESET_DEPTH_OF_3_WITH_RULES } from '@/risk-rules/consts/rule-set-depth-of-3-with-rules'; +import { RULESET_PARENT_DEPTH_3_WITH_POLICIES } from '@/risk-rules/consts/rule-set-parent-depth-3-with-policies'; + +@Injectable() +export class RuleSetRepository { + constructor( + private readonly prisma: PrismaService, + private readonly scopeService: ProjectScopeService, + ) {} + + async create({ + createArgs, + projectId, + riskRuleId, + parentId, + }: { + createArgs: Omit; + projectId: string; + riskRuleId?: string; + parentId?: string; + }) { + return this.prisma.ruleSet.create({ + data: { + ...createArgs, + projectId, + isPublic: false, + ...(riskRuleId + ? { + riskRuleRuleSets: { + create: { + riskRuleId: riskRuleId, + }, + }, + } + : {}), + ...(parentId + ? { + childRuleSets: { + create: { + parentId: parentId, + }, + }, + } + : {}), + }, + }); + } + + async findManyByRiskRule(riskRuleId: string, projectIds: TProjectIds) { + return this.prisma.ruleSet.findFirstOrThrow( + this.scopeService.scopeFindOneOrPublic( + { + where: { + riskRuleRuleSets: { + some: { + riskRuleId: riskRuleId, + }, + }, + }, + include: RULESET_DEPTH_OF_3_WITH_RULES, + }, + projectIds, + ), + ); + } + + async findById(id: string, projectIds: TProjectIds, args?: Prisma.RuleSetFindFirstOrThrowArgs) { + return this.prisma.ruleSet.findFirstOrThrow( + this.scopeService.scopeFindOneOrPublic( + { + ...(args ? args : {}), + where: { + ...(args?.where ? { ...args?.where } : {}), + id: id, + }, + include: RULESET_DEPTH_OF_3_WITH_RULES, + }, + projectIds, + ), + ); + } + + async findMany(args: Prisma.RuleSetFindManyArgs, projectIds: TProjectIds) { + return this.prisma.ruleSet.findMany( + this.scopeService.scopeFindOneOrPublic( + { + ...(args ? args : {}), + where: { + ...(args?.where ? { ...args?.where } : {}), + }, + include: RULESET_DEPTH_OF_3_WITH_RULES, + }, + projectIds, + ), + ); + } + + async updateById( + id: string, + projectId: string, + dataArgs: Omit, + ) { + return this.prisma.ruleSet.update({ + data: { + ...dataArgs, + projectId, + isPublic: false, + }, + where: { + id: id, + }, + }); + } + + // eslint-disable-next-line ballerine/verify-repository-project-scoped + async connectToRuleset(childRulesetId: string, parentRuleSetId: string) { + return this.prisma.ruleSetToRuleSet.create({ + data: { + parentId: parentRuleSetId, + childId: childRulesetId, + }, + }); + } + + // eslint-disable-next-line ballerine/verify-repository-project-scoped + async disconnectFromRuleset(childRuleSetId: string, parentRuleSetId: string) { + return this.prisma.ruleSetToRuleSet.delete({ + where: { + parentId_childId: { + parentId: parentRuleSetId, + childId: childRuleSetId, + }, + }, + }); + } + + async deleteById(id: string, projectIds: TProjectIds) { + return this.prisma.ruleSet.delete( + this.scopeService.scopeDelete( + { + where: { + id, + }, + }, + projectIds, + ), + ); + } + + async findAllAssociatedPolicies(childRulesetId: string, projectIds: TProjectIds) { + return this.prisma.ruleSet.findFirst({ + where: { + id: childRulesetId, + }, + include: RULESET_PARENT_DEPTH_3_WITH_POLICIES, + }); + } + + async findAssociatedRulesetsAndDefinitions(id: string, projectIds: TProjectIds) { + return this.prisma.ruleSet.findFirst({ + where: { + id, + }, + include: { + rulesetRules: { + include: { + ruleSet: { + include: { + riskRuleRuleSets: { + include: { + riskRule: true, + }, + }, + }, + }, + }, + where: { + ruleSet: { + projectId: { + in: projectIds, + }, + }, + }, + }, + }, + }); + } +} diff --git a/services/workflows-service/src/risk-rules/rule-set/rule-set.service.ts b/services/workflows-service/src/risk-rules/rule-set/rule-set.service.ts new file mode 100644 index 0000000000..d0dd0dc221 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/rule-set.service.ts @@ -0,0 +1,96 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { TProjectId, TProjectIds } from '@/types'; +import { Prisma } from '@prisma/client'; +import { RuleSetRepository } from '@/risk-rules/rule-set/rule-set.repository'; +import { extractRiskRulePolicy } from '@/risk-rules/helpers/rule-set-search-and-flatten-values'; +import { RuleSetWithParent } from '../types/types'; + +@Injectable() +export class RuleSetService { + constructor(private readonly ruleSetRepository: RuleSetRepository) {} + + async findById(id: string, projectIds: TProjectIds) { + return await this.ruleSetRepository.findById(id, projectIds); + } + + async findManyByRuleset(rulesetId: string, projectIds: TProjectIds) { + return await this.ruleSetRepository.findMany( + { + where: { + id: rulesetId, + }, + }, + projectIds, + ); + } + + async findMany(projectIds: TProjectIds, args?: Prisma.RuleSetFindManyArgs) { + return await this.ruleSetRepository.findMany(args || {}, projectIds); + } + + async findAssociatedRiskPolcies(rulesetId: string, projectIds: TProjectIds) { + const ruleSetWithParents = (await this.ruleSetRepository.findAllAssociatedPolicies( + rulesetId, + projectIds, + )) as RuleSetWithParent; + const riskRulePolicy = extractRiskRulePolicy(ruleSetWithParents); + + return riskRulePolicy; + } + + async assignRuleSetToParentRuleset(childRulesetId: string, parentRulesetId: string) { + return await this.ruleSetRepository.connectToRuleset(childRulesetId, parentRulesetId); + } + + async unassignRuleFromRuleset(ruleSetId: string, parentRuleSetId: string) { + return await this.ruleSetRepository.disconnectFromRuleset(ruleSetId, parentRuleSetId); + } + + async createRuleSet({ + ruleSetData, + parentRuleSetId, + projectId, + }: { + ruleSetData: Omit; + parentRuleSetId?: string; + projectId: TProjectId; + }) { + const ruleSet = await this.ruleSetRepository.create({ + createArgs: ruleSetData, + parentId: parentRuleSetId, + projectId, + }); + + return ruleSet; + } + + async updateRuleSet({ + ruleId, + ruleData, + projectId, + projectIds, + }: { + ruleId: string; + ruleData: Partial>; + projectId: TProjectId; + projectIds: TProjectIds; + }) { + const rule = await this.findById(ruleId, projectIds); + + if (rule.isPublic) { + throw new BadRequestException('Cannot delete public rule'); + } + + return await this.ruleSetRepository.updateById(ruleId, projectId, ruleData); + } + + async deleteRule({ ruleId, projectIds }: { ruleId: string; projectIds: TProjectIds }) { + const rule = await this.findById(ruleId, projectIds); + + if (rule.isPublic) { + throw new BadRequestException('Cannot delete public rule'); + } + + return await this.ruleSetRepository.deleteById(ruleId, projectIds); + } +} diff --git a/services/workflows-service/src/risk-rules/rule-set/schemas/assign-rule-set.schema.ts b/services/workflows-service/src/risk-rules/rule-set/schemas/assign-rule-set.schema.ts new file mode 100644 index 0000000000..56d4e9f6b7 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/schemas/assign-rule-set.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const AssignRuleSetToParentRuleSet = Type.Object({ + parentRuleSetId: Type.String({ + description: 'The risk rule set id to assign the rule to', + }), +}); + +export type TassignToParentRuleSet = Static; diff --git a/services/workflows-service/src/risk-rules/rule-set/schemas/create-rule-set.schema.ts b/services/workflows-service/src/risk-rules/rule-set/schemas/create-rule-set.schema.ts new file mode 100644 index 0000000000..86a7fcab31 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/schemas/create-rule-set.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; +import { RulesetOperator } from '@prisma/client'; + +export const CreateRulesetSchema = Type.Object({ + name: Type.String(), + operator: Type.Enum(RulesetOperator), + parentRuleSetId: Type.Optional(Type.String()), +}); +export type TCreatedRuleset = Static; diff --git a/services/workflows-service/src/risk-rules/rule-set/schemas/unassign-ruleset-from-parent.schema.ts b/services/workflows-service/src/risk-rules/rule-set/schemas/unassign-ruleset-from-parent.schema.ts new file mode 100644 index 0000000000..77bb6b7f5c --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/schemas/unassign-ruleset-from-parent.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const UnassignRulesetFromParentSchema = Type.Object({ + parentRuleSetId: Type.String({ + description: 'The risk rule set id to unassign from', + }), +}); + +export type TUnassignRulesetFromSchema = Static; diff --git a/services/workflows-service/src/risk-rules/rule-set/schemas/update-rule-set.schema.ts b/services/workflows-service/src/risk-rules/rule-set/schemas/update-rule-set.schema.ts new file mode 100644 index 0000000000..90fb8b137c --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule-set/schemas/update-rule-set.schema.ts @@ -0,0 +1,32 @@ +import { Static, Type } from '@sinclair/typebox'; +import { TypeStringEnum } from '@/helpers/type-box/type-string-enum'; +import { OPERATION } from '@ballerine/common'; + +export const UpdateRuleSetSchema = Type.Object({ + name: Type.Optional( + Type.String({ + description: 'Name of the rule', + examples: ['Transaction Amount Check'], + }), + ), + key: Type.Optional( + Type.String({ + description: 'The unique key of the parameter for the rule', + examples: ['entity.transaction.amount'], + }), + ), + operation: Type.Optional( + TypeStringEnum(Object.values(OPERATION), 'The operation to perform', [OPERATION.GT]), + ), + comparisonValue: Type.Optional( + Type.Any({ + description: 'The value to compare against using the operation', + examples: [100], + }), + ), + engine: Type.Optional( + TypeStringEnum(['Ballerine', 'JsonLogic'], 'The rule engine to use', ['1000']), + ), +}); + +export type TUpdateRule = Static; diff --git a/services/workflows-service/src/risk-rules/rule/rule.controller.ts b/services/workflows-service/src/risk-rules/rule/rule.controller.ts new file mode 100644 index 0000000000..a2288f3518 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/rule.controller.ts @@ -0,0 +1,161 @@ +import * as swagger from '@nestjs/swagger'; +import * as common from '@nestjs/common'; +import { RuleService } from '@/risk-rules/rule/rule.service'; +import { Type } from '@sinclair/typebox'; +import { Validate } from 'ballerine-nestjs-typebox'; +import { ProjectIds } from '@/common/decorators/project-ids.decorator'; +import type { TProjectId, TProjectIds } from '@/types'; +import { CurrentProject } from '@/common/decorators/current-project.decorator'; +import { CreateRuleSchema, type TCreateRule } from '@/risk-rules/rule/schemas/create-rule.schema'; +import { AssignRuleSchema, type TAssignRule } from '@/risk-rules/rule/schemas/assign-rule.schema'; +import { type TUpdateRule, UpdateRuleSchema } from '@/risk-rules/rule/schemas/update-rule.schema'; +import { Delete } from '@nestjs/common'; +import { + type TUnassignRule, + UnassignRuleFromRuleSetSchema, +} from '@/risk-rules/rule/schemas/unassign-rule-from-rule-set.schema'; + +@swagger.ApiTags('Rules') +@common.Controller('external/rules') +export class RuleController { + constructor(protected readonly ruleService: RuleService) {} + + @common.Post('') + @Validate({ + request: [ + { + type: 'body', + schema: CreateRuleSchema, + }, + ], + response: Type.Composite([CreateRuleSchema, Type.Object({ id: Type.String() })]), + }) + async createRule( + @common.Body() data: TCreateRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + return (await this.ruleService.createNewRule({ + ruleData: data, + projectId: currentProjectId, + rulesetId: data.ruleSetId, + })) as TCreateRule & { id: string }; + } + + @common.Put('/:ruleId/assign') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + { + type: 'body', + schema: AssignRuleSchema, + }, + ], + response: Type.Composite([AssignRuleSchema, Type.Object({ ruleId: Type.String() })]), + }) + async assignRuleToRuleset( + @common.Query() ruleId: string, + @common.Body() assignRuleDate: TAssignRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + const rule = await this.ruleService.assignRuleToRuleset( + ruleId, + assignRuleDate.ruleSetId, + currentProjectId, + projectIds, + ); + + return { + ruleId: rule.id, + ...assignRuleDate, + }; + } + + @common.Put('/:ruleId/unassign') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + { + type: 'body', + schema: UnassignRuleFromRuleSetSchema, + }, + ], + response: Type.Composite([ + UnassignRuleFromRuleSetSchema, + Type.Object({ ruleId: Type.String() }), + ]), + }) + async unassignRule( + @common.Query() ruleId: string, + @common.Body() unassignRule: TUnassignRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + const rule = await this.ruleService.unassignRuleFromRuleset( + ruleId, + unassignRule.ruleSetId, + currentProjectId, + projectIds, + ); + + return { + ruleId: rule.id, + ...unassignRule, + }; + } + + @common.Patch('/:ruleId') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + { + type: 'body', + schema: UpdateRuleSchema, + }, + ], + response: Type.Any(), + }) + async updateRule( + @common.Query() ruleId: string, + @common.Body() updateRuleSchema: TUpdateRule, + @ProjectIds() projectIds: TProjectIds, + @CurrentProject() currentProjectId: TProjectId, + ) { + const rule = await this.ruleService.updateRule({ + ruleId, + ruleData: updateRuleSchema, + projectId: currentProjectId, + projectIds, + }); + + return rule as TUpdateRule; + } + + @Delete('/:ruleId') + @Validate({ + request: [ + { + type: 'param', + name: 'ruleId', + schema: Type.String(), + }, + ], + response: Type.Any(), + }) + async deleteRule(@common.Query() ruleId: string, @ProjectIds() projectIds: TProjectIds) { + await this.ruleService.deleteRule({ ruleId, projectIds }); + } +} diff --git a/services/workflows-service/src/risk-rules/rule/rule.module.ts b/services/workflows-service/src/risk-rules/rule/rule.module.ts new file mode 100644 index 0000000000..e8ee7ea465 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/rule.module.ts @@ -0,0 +1,15 @@ +import { Module } from '@nestjs/common'; +import { AppLoggerModule } from '@/common/app-logger/app-logger.module'; +import { PrismaModule } from '@/prisma/prisma.module'; +import { RuleService } from '@/risk-rules/rule/rule.service'; +import { RuleRepository } from '@/risk-rules/rule/rule.repository'; +import { RuleController } from '@/risk-rules/rule/rule.controller'; +import { ProjectModule } from '@/project/project.module'; + +@Module({ + controllers: [RuleController], + imports: [AppLoggerModule, PrismaModule, ProjectModule], + providers: [RuleService, RuleRepository], + exports: [RuleService], +}) +export class RuleModule {} diff --git a/services/workflows-service/src/risk-rules/rule/rule.repository.ts b/services/workflows-service/src/risk-rules/rule/rule.repository.ts new file mode 100644 index 0000000000..daef96e036 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/rule.repository.ts @@ -0,0 +1,156 @@ +import { Injectable } from '@nestjs/common'; +import { PrismaService } from '@/prisma/prisma.service'; +import { Prisma } from '@prisma/client'; +import { ProjectScopeService } from '@/project/project-scope.service'; +import { TProjectIds } from '@/types'; + +@Injectable() +export class RuleRepository { + constructor( + private readonly prisma: PrismaService, + private readonly scopeService: ProjectScopeService, + ) {} + + async create({ + createArgs, + projectId, + ruleSetId, + }: { + createArgs: Omit; + projectId: string; + ruleSetId?: string; + }) { + return this.prisma.rule.create({ + data: { + ...createArgs, + projectId, + isPublic: false, + ...(ruleSetId + ? { + rulesetRules: { + create: { + ruleSetId: ruleSetId, + }, + }, + } + : {}), + }, + }); + } + + async findMany(args: Prisma.RuleFindManyArgs, projectIds: TProjectIds) { + return this.prisma.rule.findMany( + this.scopeService.scopeFindManyOrPublic( + { + ...args, + where: { ...args.where }, + }, + projectIds, + ), + ); + } + + async findById(id: string, projectIds: TProjectIds, args?: Prisma.RuleFindFirstOrThrowArgs) { + return this.prisma.rule.findFirstOrThrow( + this.scopeService.scopeFindOneOrPublic( + { + ...(args ? args : {}), + where: { ...(args?.where ? { ...args?.where } : {}), id: id }, + }, + projectIds, + ), + ); + } + + async updateById( + id: string, + projectId: string, + dataArgs: Omit, + ) { + return this.prisma.rule.update({ + data: { + ...dataArgs, + projectId, + isPublic: false, + }, + where: { + id: id, + }, + }); + } + + // eslint-disable-next-line ballerine/verify-repository-project-scoped + async connectToRuleset(id: string, ruleSetId: string) { + return this.prisma.ruleSetRule.upsert({ + create: { + ruleSetId, + ruleId: id, + }, + update: { + ruleSetId, + }, + where: { + ruleId_ruleSetId: { + ruleId: id, + ruleSetId: ruleSetId, + }, + }, + }); + } + + // eslint-disable-next-line ballerine/verify-repository-project-scoped + async disconnectFromRuleset(id: string, ruleSetId: string) { + return this.prisma.ruleSetRule.delete({ + where: { + ruleId_ruleSetId: { + ruleId: id, + ruleSetId: ruleSetId, + }, + }, + }); + } + + async deleteById(id: string, projectIds: TProjectIds) { + return this.prisma.rule.delete( + this.scopeService.scopeDelete( + { + where: { + id, + }, + }, + projectIds, + ), + ); + } + + async findAssociatedRulesetsAndDefinitions(id: string, projectIds: TProjectIds) { + // TODO query result for each definition uses this policies + return this.prisma.rule.findFirst({ + where: { + id, + }, + include: { + rulesetRules: { + include: { + ruleSet: { + include: { + riskRuleRuleSets: { + include: { + riskRule: true, + }, + }, + }, + }, + }, + where: { + ruleSet: { + projectId: { + in: projectIds, + }, + }, + }, + }, + }, + }); + } +} diff --git a/services/workflows-service/src/risk-rules/rule/rule.service.ts b/services/workflows-service/src/risk-rules/rule/rule.service.ts new file mode 100644 index 0000000000..3474b292ee --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/rule.service.ts @@ -0,0 +1,126 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { RuleRepository } from './rule.repository'; +import { TProjectId, TProjectIds } from '@/types'; +import { Prisma } from '@prisma/client'; + +@Injectable() +export class RuleService { + constructor(private readonly ruleRepository: RuleRepository) {} + + async findById(id: string, projectIds: TProjectIds) { + return await this.ruleRepository.findById(id, projectIds); + } + + async findManyByRuleset(rulesetId: string, projectIds: TProjectIds) { + return this.ruleRepository.findMany( + { + where: { + rulesetRules: { + some: { + ruleSetId: rulesetId, + }, + }, + }, + }, + projectIds, + ); + } + + async findMany(projectIds: TProjectIds, args?: Prisma.RuleFindManyArgs) { + return await this.ruleRepository.findMany(args || {}, projectIds); + } + + async assignRuleToRuleset( + ruleId: string, + rulesetId: string, + projectId: TProjectId, + projectIds: TProjectIds, + ) { + const { + id, + projectId: ruleProjectId, + isPublic: ruleIsPublic, + ...resetRule + } = await this.ruleRepository.findById(ruleId, projectIds); + + if (ruleProjectId) { + return await this.ruleRepository.connectToRuleset(id, rulesetId); + } + + return await this.ruleRepository.create({ + createArgs: { + ...resetRule, + value: resetRule.value as Prisma.RuleUncheckedCreateInput['value'], + comparisonValue: + resetRule.comparisonValue as Prisma.RuleUncheckedCreateInput['comparisonValue'], + }, + projectId, + ruleSetId: rulesetId, + }); + } + + async unassignRuleFromRuleset( + ruleId: string, + rulesetId: string, + projectId: TProjectId, + projectIds: TProjectIds, + ) { + const { id } = await this.ruleRepository.findById(ruleId, projectIds); + + if (!projectId) { + throw new BadRequestException('Cannot unassign rule from ruleset without project id'); + } + + return await this.ruleRepository.disconnectFromRuleset(id, rulesetId); + } + + async createNewRule({ + ruleData, + rulesetId, + projectId, + }: { + ruleData: Omit; + rulesetId?: string; + projectId: TProjectId; + }) { + const rule = await this.ruleRepository.create({ + createArgs: ruleData, + ruleSetId: rulesetId, + projectId, + }); + + return rule; + } + + async updateRule({ + ruleId, + ruleData, + projectId, + projectIds, + }: { + ruleId: string; + ruleData: Partial< + Omit + >; + projectId: TProjectId; + projectIds: TProjectIds; + }) { + const rule = await this.findById(ruleId, projectIds); + + if (rule.isPublic) { + throw new BadRequestException('Cannot delete public rule'); + } + + return await this.ruleRepository.updateById(ruleId, projectId, ruleData); + } + + async deleteRule({ ruleId, projectIds }: { ruleId: string; projectIds: TProjectIds }) { + const rule = await this.findById(ruleId, projectIds); + + if (rule.isPublic) { + throw new BadRequestException('Cannot delete public rule'); + } + + return await this.ruleRepository.deleteById(ruleId, projectIds); + } +} diff --git a/services/workflows-service/src/risk-rules/rule/schemas/assign-rule.schema.ts b/services/workflows-service/src/risk-rules/rule/schemas/assign-rule.schema.ts new file mode 100644 index 0000000000..92cc2e334c --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/schemas/assign-rule.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const AssignRuleSchema = Type.Object({ + ruleSetId: Type.String({ + description: 'The risk rule set id to assign the rule to', + }), +}); + +export type TAssignRule = Static; diff --git a/services/workflows-service/src/risk-rules/rule/schemas/create-rule.schema.ts b/services/workflows-service/src/risk-rules/rule/schemas/create-rule.schema.ts new file mode 100644 index 0000000000..c50a47d318 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/schemas/create-rule.schema.ts @@ -0,0 +1,31 @@ +import { Static, Type } from '@sinclair/typebox'; +import { TypeStringEnum } from '@/helpers/type-box/type-string-enum'; +import { OPERATION } from '@ballerine/common'; + +export const CreateRuleSchema = Type.Object({ + name: Type.String({ + description: 'Name of the rule', + examples: ['Transaction Amount Check'], + }), + key: Type.String({ + description: 'The unique key of the parameter for the rule', + examples: ['entity.transaction.amount'], + }), + value: Type.Record(Type.String(), Type.String(), { + description: 'The unique key of the parameter for the rule', + examples: ['entity.transaction.amount'], + }), + operation: TypeStringEnum(Object.values(OPERATION), 'The operation to perform', [OPERATION.GT]), + comparisonValue: Type.Any({ + description: 'The value to compare against using the operation', + examples: [100], + }), + engine: TypeStringEnum(['Ballerine', 'JsonLogic'], 'The rule engine to use', ['1000']), + ruleSetId: Type.Optional( + Type.String({ + description: 'The risk rule set id to assign the rule to', + }), + ), +}); + +export type TCreateRule = Static; diff --git a/services/workflows-service/src/risk-rules/rule/schemas/unassign-rule-from-rule-set.schema.ts b/services/workflows-service/src/risk-rules/rule/schemas/unassign-rule-from-rule-set.schema.ts new file mode 100644 index 0000000000..d91a81e1c3 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/schemas/unassign-rule-from-rule-set.schema.ts @@ -0,0 +1,9 @@ +import { Static, Type } from '@sinclair/typebox'; + +export const UnassignRuleFromRuleSetSchema = Type.Object({ + ruleSetId: Type.String({ + description: 'The risk rule set id to unassign from', + }), +}); + +export type TUnassignRule = Static; diff --git a/services/workflows-service/src/risk-rules/rule/schemas/update-rule.schema.ts b/services/workflows-service/src/risk-rules/rule/schemas/update-rule.schema.ts new file mode 100644 index 0000000000..0760a666a3 --- /dev/null +++ b/services/workflows-service/src/risk-rules/rule/schemas/update-rule.schema.ts @@ -0,0 +1,32 @@ +import { Static, Type } from '@sinclair/typebox'; +import { TypeStringEnum } from '@/helpers/type-box/type-string-enum'; +import { OPERATION } from '@ballerine/common'; + +export const UpdateRuleSchema = Type.Object({ + name: Type.Optional( + Type.String({ + description: 'Name of the rule', + examples: ['Transaction Amount Check'], + }), + ), + key: Type.Optional( + Type.String({ + description: 'The unique key of the parameter for the rule', + examples: ['entity.transaction.amount'], + }), + ), + operation: Type.Optional( + TypeStringEnum(Object.values(OPERATION), 'The operation to perform', [OPERATION.GT]), + ), + comparisonValue: Type.Optional( + Type.Any({ + description: 'The value to compare against using the operation', + examples: [100], + }), + ), + engine: Type.Optional( + TypeStringEnum(['Ballerine', 'JsonLogic'], 'The rule engine to use', ['1000']), + ), +}); + +export type TUpdateRule = Static; diff --git a/services/workflows-service/src/risk-rules/schemas/create-risk-rule.schema.ts b/services/workflows-service/src/risk-rules/schemas/create-risk-rule.schema.ts new file mode 100644 index 0000000000..dee121ec79 --- /dev/null +++ b/services/workflows-service/src/risk-rules/schemas/create-risk-rule.schema.ts @@ -0,0 +1,38 @@ +import { Type } from '@sinclair/typebox'; +import { CreateRuleSetSchema } from '@/risk-rules/schemas/create-rule-set.schema'; +import { IndicatorRiskLevel } from '@prisma/client'; +import { TypeStringEnum } from '@/helpers/type-box/type-string-enum'; + +export const CreateRiskRuleSchema = Type.Object({ + ruleSet: CreateRuleSetSchema, + domain: Type.String({ + description: 'The domain of the rule', + }), + indicator: Type.String({ + description: 'The indicator name of the rule', + }), + riskLevel: TypeStringEnum( + [IndicatorRiskLevel.positive, IndicatorRiskLevel.moderate, IndicatorRiskLevel.critical], + 'The risk level of the rule', + ), + baseRiskScore: Type.Number({ + description: 'The base risk score, as the highest base of all rulesets', + minimum: 0, + maximum: 100, + }), + additionalRiskScore: Type.Number({ + description: 'The additional risk score, as the highest additional of all rulesets', + minimum: 0, + maximum: 100, + }), + minRiskScore: Type.Number({ + description: 'The minimum risk score, the minimum total risk score if the rule is met', + minimum: 0, + maximum: 100, + }), + maxRiskScore: Type.Number({ + description: 'The maximum risk score in which this rule is relevant', + minimum: 0, + maximum: 100, + }), +}); diff --git a/services/workflows-service/src/risk-rules/schemas/create-rule-policy.schema.ts b/services/workflows-service/src/risk-rules/schemas/create-rule-policy.schema.ts new file mode 100644 index 0000000000..ca1e064140 --- /dev/null +++ b/services/workflows-service/src/risk-rules/schemas/create-rule-policy.schema.ts @@ -0,0 +1,10 @@ +import { Type } from '@sinclair/typebox'; +// eslint-disable-next-line import/namespace +import { CreateRiskRuleSchema } from './create-risk-rule.schema'; + +export const CreateRuleSetSchema = Type.Object({ + workflowDefinitionId: Type.String({ + description: 'The unique key of the parameter for the rule', + }), + riskRules: Type.Array(CreateRiskRuleSchema, { description: 'The Rules to create the policy' }), +}); diff --git a/services/workflows-service/src/risk-rules/schemas/create-rule-set.schema.ts b/services/workflows-service/src/risk-rules/schemas/create-rule-set.schema.ts new file mode 100644 index 0000000000..e6ca42b45e --- /dev/null +++ b/services/workflows-service/src/risk-rules/schemas/create-rule-set.schema.ts @@ -0,0 +1,15 @@ +import { Type } from '@sinclair/typebox'; +import { TypeStringEnum } from '@/helpers/type-box/type-string-enum'; +import { OPERATOR } from '@ballerine/common'; +import { CreateRuleSchema } from '@/risk-rules/rule/schemas/create-rule.schema'; + +export const CreateRuleSetSchema = Type.Object( + { + operator: TypeStringEnum( + Object.values(OPERATOR), + 'The operator to apply to aggregate the rules', + ), + rules: Type.Array(CreateRuleSchema), + }, + { description: 'The rule set to apply for the rule' }, +); diff --git a/services/workflows-service/src/risk-rules/types/types.ts b/services/workflows-service/src/risk-rules/types/types.ts new file mode 100644 index 0000000000..0c1c4a500d --- /dev/null +++ b/services/workflows-service/src/risk-rules/types/types.ts @@ -0,0 +1,52 @@ +import { + RiskRule, + RiskRuleRuleSet, + RiskRulesPolicy, + Rule, + RuleSet, + RuleSetRule, + RuleSetToRuleSet, +} from '@prisma/client'; + +type RiskRuleWithPolicy = RiskRule & { + riskRulePolicy: RiskRulesPolicy | null; +}; + +type RiskRuleRuleSetWithRule = RiskRuleRuleSet & { + riskRule: RiskRuleWithPolicy; +}; + +type RuleSetWithRiskRules = RuleSet & { + riskRuleRuleSets: RiskRuleRuleSetWithRule[]; +}; + +export type RiskRuleWithRuleSet = RiskRule & { + riskRuleRuleSets: RiskRuleRuleSet[]; +}; + +export type RuleSetWithChildrenAndRules = RuleSet & { + childRuleSets: RulesetToRuleWithChild[]; + rulesetRules: RuleSetRuleWithRule[]; +}; + +export type RulesetToRuleWithChild = RuleSetToRuleSet & { + child: RuleSetWithChildrenAndRules; +}; + +export type RuleSetRuleWithRule = RuleSetRule & { + rule: Rule; +}; + +export type RuleSetWithParent = RuleSetWithRiskRules & { + parentRuleSets: Array<{ + parent: RuleSetWithRiskRules & { + parentRuleSets: Array<{ + parent: RuleSetWithRiskRules & { + parentRuleSets: Array<{ + parent: RuleSetWithRiskRules; + }>; + }; + }>; + }; + }>; +}; diff --git a/services/workflows-service/src/rule-engine/core/rule-engine.ts b/services/workflows-service/src/rule-engine/core/rule-engine.ts index 510745351f..3b4157104a 100644 --- a/services/workflows-service/src/rule-engine/core/rule-engine.ts +++ b/services/workflows-service/src/rule-engine/core/rule-engine.ts @@ -8,8 +8,13 @@ import { OPERATOR, RuleSchema, ValidationFailedError, + RuleSetWithChildren, } from '@ballerine/common'; +const isRulesetWithChildren = (ruleset: unknown): ruleset is RuleSetWithChildren => { + return Object.prototype.hasOwnProperty.call(ruleset, 'childRuleSet'); +}; + export const validateRule = (rule: Rule, data: any): RuleResult => { const result = RuleSchema.safeParse(rule); @@ -39,16 +44,18 @@ export const validateRule = (rule: Rule, data: any): RuleResult => { } }; -export const runRuleSet = (ruleSet: RuleSet, data: any): RuleResultSet => { +export const runRuleSet = (ruleSet: RuleSet, data: any) => { return ruleSet.rules.map(rule => { + const nestedResults: RuleResultSet = []; + if ('rules' in rule) { // RuleSet - const nestedResults = runRuleSet(rule, data); + nestedResults.push(...(runRuleSet(rule, data) as RuleResultSet)); const passed = rule.operator === OPERATOR.AND - ? nestedResults.every(r => r.status === 'PASSED') - : nestedResults.some(r => r.status === 'PASSED'); + ? nestedResults.every((r: RuleResult) => r.status === 'PASSED') + : nestedResults.some((r: RuleResult) => r.status === 'PASSED'); const status = passed ? 'PASSED' : 'SKIPPED'; @@ -77,7 +84,10 @@ export const runRuleSet = (ruleSet: RuleSet, data: any): RuleResultSet => { }); }; -export const RuleEngine = (ruleSets: RuleSet, helpers?: typeof OperationHelpers) => { +export const RuleEngine = ( + ruleSets: RuleSet | RuleSetWithChildren, + helpers?: typeof OperationHelpers, +) => { // TODO: inject helpers const allHelpers = { ...(helpers || {}), ...OperationHelpers }; diff --git a/services/workflows-service/src/rule-engine/risk-rule.service.intg.test.ts b/services/workflows-service/src/rule-engine/risk-rule.service.intg.test.ts index 09abed522f..b4727d2cc4 100644 --- a/services/workflows-service/src/rule-engine/risk-rule.service.intg.test.ts +++ b/services/workflows-service/src/rule-engine/risk-rule.service.intg.test.ts @@ -17,6 +17,7 @@ describe.skip('#RiskRuleService', () => { { databaseId: '', source: 'notion' }, { shouldThrowOnValidation: true, + projectIds: [], }, ); diff --git a/services/workflows-service/src/rule-engine/risk-rule.service.ts b/services/workflows-service/src/rule-engine/risk-rule.service.ts index 83e6bfc1b0..34be6585bd 100644 --- a/services/workflows-service/src/rule-engine/risk-rule.service.ts +++ b/services/workflows-service/src/rule-engine/risk-rule.service.ts @@ -4,6 +4,7 @@ import { NotionService } from '@/notion/notion.service'; import z from 'zod'; import { AppLoggerService } from '@/common/app-logger/app-logger.service'; import { RuleSetSchema } from '@ballerine/common'; +import { RiskRulePolicyService } from '@/risk-rules/risk-rule-policy/risk-rule-policy.service'; const isJsonString = (value: string) => { try { @@ -42,22 +43,32 @@ const NotionRiskRuleRecordSchema = z maxRiskScore: value['Max risk score'], })); -export interface TFindAllRulesOptions { - databaseId: string; - source: 'notion'; -} +export type TFindAllRulesOptions = + | { + databaseId: string; + source: 'notion'; + } + | { + databaseId: string; + source: 'database'; + }; @Injectable() export class RiskRuleService { constructor( private readonly notionService: NotionService, + private readonly riskRulePolicyService: RiskRulePolicyService, private readonly logger: AppLoggerService, ) {} public async findAll( { databaseId, source }: TFindAllRulesOptions, - options: { shouldThrowOnValidation: boolean } = { + options: { + shouldThrowOnValidation: boolean; + projectIds: string[]; + } = { shouldThrowOnValidation: false, + projectIds: [], }, ) { if (source === 'notion') { @@ -99,6 +110,12 @@ export class RiskRuleService { } return validatedRecords; + } else if (source === 'database') { + const riskRules = ( + await this.riskRulePolicyService.formatRiskRuleWithRules(databaseId, options.projectIds) + ).filter(Boolean); + + return riskRules; } throw new Error('Unsupported source'); diff --git a/services/workflows-service/src/rule-engine/rule-engine.module.ts b/services/workflows-service/src/rule-engine/rule-engine.module.ts index 64cb72907a..c60fd50243 100644 --- a/services/workflows-service/src/rule-engine/rule-engine.module.ts +++ b/services/workflows-service/src/rule-engine/rule-engine.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { RuleEngineService } from './rule-engine.service'; import { NotionModule } from '@/notion/notion.module'; import { RiskRuleService } from '@/rule-engine/risk-rule.service'; +import { RiskRulePolicyModule } from '@/risk-rules/risk-rule-policy/risk-rule-policy.module'; @Module({ - imports: [NotionModule], + imports: [NotionModule, RiskRulePolicyModule], providers: [RuleEngineService, RiskRuleService], exports: [RuleEngineService, RiskRuleService], }) diff --git a/services/workflows-service/src/rule-engine/rule-engine.service.ts b/services/workflows-service/src/rule-engine/rule-engine.service.ts index 6ade54c04c..43a72adc71 100644 --- a/services/workflows-service/src/rule-engine/rule-engine.service.ts +++ b/services/workflows-service/src/rule-engine/rule-engine.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; -import { OperationHelpers, RuleSet } from '@ballerine/common'; +import { OperationHelpers, RuleSet, RuleSetWithChildren } from '@ballerine/common'; import { RuleEngine } from './core/rule-engine'; @Injectable() export class RuleEngineService { - public run(rules: RuleSet, formData: object) { + public run(rules: RuleSet | RuleSetWithChildren, formData: object) { const ruleEngine = RuleEngine(rules, OperationHelpers); return ruleEngine.run(formData); diff --git a/services/workflows-service/src/workflow/workflow.service.ts b/services/workflows-service/src/workflow/workflow.service.ts index 68249392f0..409b5a7b9e 100644 --- a/services/workflows-service/src/workflow/workflow.service.ts +++ b/services/workflows-service/src/workflow/workflow.service.ts @@ -1964,7 +1964,10 @@ export class WorkflowService { context: object, ruleStoreServiceOptions: TFindAllRulesOptions, ) => { - const rules = await this.riskRuleService.findAll(ruleStoreServiceOptions); + const rules = await this.riskRuleService.findAll(ruleStoreServiceOptions, { + projectIds: projectIds!, + shouldThrowOnValidation: false, + }); return rules.map(rule => { try {