diff --git a/apps/backoffice-v2/src/common/components/molecules/ProcessTracker/hooks/useProcessTracker/process-tracker-adapters/collection-flow.process-tracker.ts b/apps/backoffice-v2/src/common/components/molecules/ProcessTracker/hooks/useProcessTracker/process-tracker-adapters/collection-flow.process-tracker.ts index 57ad97e6e7..efe2bf5c20 100644 --- a/apps/backoffice-v2/src/common/components/molecules/ProcessTracker/hooks/useProcessTracker/process-tracker-adapters/collection-flow.process-tracker.ts +++ b/apps/backoffice-v2/src/common/components/molecules/ProcessTracker/hooks/useProcessTracker/process-tracker-adapters/collection-flow.process-tracker.ts @@ -28,12 +28,20 @@ export class CollectionFlowProcessTracker implements IProcessTracker { } private getSteps() { - return Object.keys(this.workflow?.context?.collectionFlow?.state?.progress ?? {})?.sort( + const steps = this.workflow?.context?.collectionFlow?.config?.steps; + + if (!steps?.length) return []; + + // Create a map of stateName to orderNumber for efficient lookup + const stateOrderMap = new Map(steps.map(step => [step.stateName, step.orderNumber])); + + // Get progress states and sort them by their corresponding orderNumber + return Object.keys(this.workflow?.context?.collectionFlow?.state?.progress ?? {}).sort( (a, b) => { - return ( - (this.workflow?.context?.collectionFlow?.state?.progress?.[a]?.isCompleted ?? 0) - - (this.workflow?.context?.collectionFlow?.state?.progress?.[b]?.isCompleted ?? 0) - ); + const orderA = stateOrderMap.get(a) ?? 0; + const orderB = stateOrderMap.get(b) ?? 0; + + return orderA - orderB; }, ); } diff --git a/apps/backoffice-v2/src/domains/workflows/fetchers.ts b/apps/backoffice-v2/src/domains/workflows/fetchers.ts index cc422ca960..5e771ad231 100644 --- a/apps/backoffice-v2/src/domains/workflows/fetchers.ts +++ b/apps/backoffice-v2/src/domains/workflows/fetchers.ts @@ -119,6 +119,7 @@ export const BaseWorkflowByIdSchema = z.object({ .object({ config: z.object({ apiUrl: z.string().url(), + steps: z.array(z.object({ stateName: z.string(), orderNumber: z.number() })), }), state: z.object({ uiState: z.string(), diff --git a/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx b/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx index 1c5bd8a3e3..a5279378aa 100644 --- a/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx +++ b/apps/kyb-app/src/components/layouts/AppShell/Navigation.tsx @@ -12,12 +12,16 @@ import { ctw } from '@ballerine/ui'; export const Navigation = () => { const { state } = useDynamicUIContext(); const { t } = useTranslation(); - const { stateApi } = useStateManagerContext(); + const { stateApi, payload } = useStateManagerContext(); const { currentPage } = usePageResolverContext(); const { customer } = useCustomer(); const { exit, isExitAvailable } = useAppExit(); - const isFirstStep = currentPage?.number === 1; + const currentPageNumber = payload?.collectionFlow?.config?.steps?.find( + step => step.stateName === currentPage?.stateName, + )?.orderNumber; + + const isFirstStep = currentPageNumber === 1; const isDisabled = state.isLoading; const onPrevious = useCallback(async () => { diff --git a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts index edf2a329be..b954c73952 100644 --- a/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts +++ b/apps/kyb-app/src/components/organisms/DynamicUI/Page/hooks/usePageErrors/usePageErrors.ts @@ -1,6 +1,7 @@ import { ErrorField } from '@/components/organisms/DynamicUI/rule-engines'; import { findDocumentDefinitionById } from '@/components/organisms/UIRenderer/elements/JSONForm/helpers/findDefinitionByName'; import { Document, UIElement, UIPage } from '@/domains/collection-flow'; +import { CollectionFlowContext } from '@/domains/collection-flow/types/flow-context.types'; import { AnyObject } from '@ballerine/ui'; import { useMemo } from 'react'; @@ -22,11 +23,15 @@ export const selectDirectorsDocuments = (context: unknown): Document[] => ?.filter(Boolean) ?.flat() || []; -export const usePageErrors = (context: AnyObject, pages: UIPage[]): PageError[] => { +export const usePageErrors = (context: CollectionFlowContext, pages: UIPage[]): PageError[] => { return useMemo(() => { const pagesWithErrors: PageError[] = pages.map(page => { + const pageNumber = context?.collectionFlow?.config?.steps?.find( + step => step.stateName === page.stateName, + )?.orderNumber; + const pageErrorBase: PageError = { - page: page.number, + page: pageNumber || page.number, pageName: page.name, stateName: page.stateName, errors: [], diff --git a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx index f8bf0b3903..6052f1deee 100644 --- a/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx +++ b/apps/kyb-app/src/pages/CollectionFlow/CollectionFlow.tsx @@ -72,7 +72,7 @@ export const CollectionFlow = withSessionProtected(() => { const elements = schema?.uiSchema?.elements; const definition = schema?.definition.definition; - const pageErrors = usePageErrors(context ?? {}, elements || []); + const pageErrors = usePageErrors(context ?? ({} as CollectionFlowContext), elements || []); const isRevision = useMemo( () => context?.collectionFlow?.state?.collectionFlowState === CollectionFlowStates.revision, [context], diff --git a/packages/common/src/schemas/documents/default-context-schema.ts b/packages/common/src/schemas/documents/default-context-schema.ts index d7da213459..2d01ab8414 100644 --- a/packages/common/src/schemas/documents/default-context-schema.ts +++ b/packages/common/src/schemas/documents/default-context-schema.ts @@ -46,6 +46,14 @@ export const defaultContextSchema = Type.Composite([ config: Type.Optional( Type.Object({ apiUrl: Type.String(), + steps: Type.Optional( + Type.Array( + Type.Object({ + stateName: Type.String(), + orderNumber: Type.Number(), + }), + ), + ), }), ), state: Type.Optional( diff --git a/packages/common/src/utils/collection-flow-manager/collection-flow-manager.ts b/packages/common/src/utils/collection-flow-manager/collection-flow-manager.ts index 9a6dcdf006..f6f04d5c97 100644 --- a/packages/common/src/utils/collection-flow-manager/collection-flow-manager.ts +++ b/packages/common/src/utils/collection-flow-manager/collection-flow-manager.ts @@ -9,8 +9,6 @@ import { export class CollectionFlowManager { constructor(public context: TContext, private readonly _config?: CollectionFlowManagerConfig) { - console.log('config', _config); - if (_config && !collectionFlowConfigValidationSchema(_config)) { throw new Error('Invalid collection flow manager config.'); } @@ -29,6 +27,7 @@ export class CollectionFlowManager { const config: NonNullable['config'] = { apiUrl: this._config?.apiUrl || '', + steps: this._config?.steps || [], }; console.log('Collection Flow Context initiated with config: ', config); diff --git a/packages/common/src/utils/collection-flow-manager/helpers/config-helper.ts b/packages/common/src/utils/collection-flow-manager/helpers/config-helper.ts index ad5da887e5..b415a8e04f 100644 --- a/packages/common/src/utils/collection-flow-manager/helpers/config-helper.ts +++ b/packages/common/src/utils/collection-flow-manager/helpers/config-helper.ts @@ -1,4 +1,5 @@ import { DefaultContextSchema } from '@/schemas'; +import { CollectionFlowManagerConfig } from '../schemas/config-schema'; export class ConfigHelper { constructor(private context: DefaultContextSchema) {} @@ -16,6 +17,14 @@ export class ConfigHelper { this.context.collectionFlow.config.apiUrl = apiUrl; } + get steps(): CollectionFlowManagerConfig['steps'] { + return this.context.collectionFlow?.config?.steps || []; + } + + set steps(_) { + throw new Error('Setting steps is not allowed after initialization.'); + } + override(config: NonNullable['config']>) { this.context.collectionFlow = { config, diff --git a/packages/common/src/utils/collection-flow-manager/schemas/config-schema.ts b/packages/common/src/utils/collection-flow-manager/schemas/config-schema.ts index 5a44135144..89cddcfb7f 100644 --- a/packages/common/src/utils/collection-flow-manager/schemas/config-schema.ts +++ b/packages/common/src/utils/collection-flow-manager/schemas/config-schema.ts @@ -5,6 +5,7 @@ const ajv = new Ajv(); const TCollectionFlowStepSchema = Type.Object({ stateName: Type.String(), + orderNumber: Type.Number(), }); export const CollectionFlowManagerConfigSchema = Type.Object({ diff --git a/services/workflows-service/package.json b/services/workflows-service/package.json index 589f3487c3..5629876ce5 100644 --- a/services/workflows-service/package.json +++ b/services/workflows-service/package.json @@ -14,7 +14,7 @@ "prod": "npm run db:migrate-up && node dist/src/main", "prod:next": "npm run db:migrate-up && npm run db:data-sync && node dist/src/main", "start:watch": "nest start --watch", - "start:debug": "nest start --debug", + "start:debug": "nest start --debug --watch", "build": "nest build --path=tsconfig.build.json", "test": "jest --runInBand", "test:unit": "cross-env SKIP_DB_SETUP_TEARDOWN=true jest --testRegex '.*\\.unit\\.test\\.ts$'", diff --git a/services/workflows-service/prisma/data-migrations b/services/workflows-service/prisma/data-migrations index 65491e06eb..995e0369a8 160000 --- a/services/workflows-service/prisma/data-migrations +++ b/services/workflows-service/prisma/data-migrations @@ -1 +1 @@ -Subproject commit 65491e06eb6f7a6fecfeebb9f23a5254fa7a0f20 +Subproject commit 995e0369a8d27b118900dc5ddacd4ec7bc58efd5 diff --git a/services/workflows-service/scripts/workflows/runtime/generate-initial-collection-flow-example.ts b/services/workflows-service/scripts/workflows/runtime/generate-initial-collection-flow-example.ts index 8083ebeb40..95734e97eb 100644 --- a/services/workflows-service/scripts/workflows/runtime/generate-initial-collection-flow-example.ts +++ b/services/workflows-service/scripts/workflows/runtime/generate-initial-collection-flow-example.ts @@ -1,7 +1,34 @@ import { CollectionFlowManager } from '@ballerine/common'; -import { PrismaClient } from '@prisma/client'; +import { createWorkflow } from '@ballerine/workflow-core'; +import { Prisma, PrismaClient, UiDefinition } from '@prisma/client'; import { env } from '../../../src/env'; +export const getStepsInOrder = async (uiDefinition: UiDefinition) => { + if (!uiDefinition?.uiSchema) return []; + + const { uiSchema = {}, definition } = uiDefinition as Prisma.JsonObject; + const { elements } = uiSchema as Prisma.JsonObject; + + if (!elements || !definition) return []; + + const stepsInOrder: string[] = []; + + const stateMachine = createWorkflow({ + runtimeId: '', + definition: (definition as Prisma.JsonObject).definition as any, + definitionType: 'statechart-json', + extensions: {}, + workflowContext: {}, + }); + + while (!stateMachine.getSnapshot().done) { + stepsInOrder.push(stateMachine.getSnapshot().value); + await stateMachine.sendEvent({ type: 'NEXT' }); + } + + return stepsInOrder.map((stepName, index) => ({ stateName: stepName, orderNumber: index + 1 })); +}; + export const generateInitialCollectionFlowExample = async ( prismaClient: PrismaClient, { @@ -38,9 +65,15 @@ export const generateInitialCollectionFlowExample = async ( }, }; + const uiDefinition = await prismaClient.uiDefinition.findFirst({ + where: { + workflowDefinitionId, + }, + }); + const collectionFlowManager = new CollectionFlowManager(initialContext, { apiUrl: env.APP_API_URL, - steps: [], + steps: await getStepsInOrder(uiDefinition as UiDefinition), }); collectionFlowManager.start(); diff --git a/services/workflows-service/src/collection-flow/helpers/get-steps-in-order.ts b/services/workflows-service/src/collection-flow/helpers/get-steps-in-order.ts new file mode 100644 index 0000000000..7a2cdfdb56 --- /dev/null +++ b/services/workflows-service/src/collection-flow/helpers/get-steps-in-order.ts @@ -0,0 +1,28 @@ +import { createWorkflow } from '@ballerine/workflow-core'; +import { Prisma, UiDefinition } from '@prisma/client'; + +export const getStepsInOrder = async (uiDefinition: UiDefinition) => { + if (!uiDefinition?.uiSchema) return []; + + const { uiSchema = {}, definition } = uiDefinition as Prisma.JsonObject; + const { elements } = uiSchema as Prisma.JsonObject; + + if (!elements || !definition) return []; + + const stepsInOrder: string[] = []; + + const stateMachine = createWorkflow({ + runtimeId: '', + definition: (definition as Prisma.JsonObject).definition as any, + definitionType: 'statechart-json', + extensions: {}, + workflowContext: {}, + }); + + while (!stateMachine.getSnapshot().done) { + stepsInOrder.push(stateMachine.getSnapshot().value); + await stateMachine.sendEvent({ type: 'NEXT' }); + } + + return stepsInOrder.map((stepName, index) => ({ stateName: stepName, orderNumber: index + 1 })); +}; diff --git a/services/workflows-service/src/workflow/workflow.service.ts b/services/workflows-service/src/workflow/workflow.service.ts index d8b202ce9a..a444a4a045 100644 --- a/services/workflows-service/src/workflow/workflow.service.ts +++ b/services/workflows-service/src/workflow/workflow.service.ts @@ -2,6 +2,7 @@ import { WorkflowTokenService } from '@/auth/workflow-token/workflow-token.servi import { BusinessReportService } from '@/business-report/business-report.service'; import { BusinessRepository } from '@/business/business.repository'; import { BusinessService } from '@/business/business.service'; +import { getStepsInOrder } from '@/collection-flow/helpers/get-steps-in-order'; import { ajv } from '@/common/ajv/ajv.validator'; import { AppLoggerService } from '@/common/app-logger/app-logger.service'; import { EntityRepository } from '@/common/entity/entity.repository'; @@ -89,6 +90,7 @@ import { EndUser, Prisma, PrismaClient, + UiDefinition, UiDefinitionContext, User, WorkflowDefinition, @@ -1467,8 +1469,6 @@ export class WorkflowService { } } - const uiSchema = (uiDefinition as Record)?.uiSchema; - // Initializing Collection Flow const collectionFlowManager = new CollectionFlowManager( { @@ -1482,7 +1482,7 @@ export class WorkflowService { }, { apiUrl: env.APP_API_URL, - steps: uiSchema?.elements || [], + steps: await getStepsInOrder(uiDefinition as UiDefinition), }, );