Skip to content

Commit

Permalink
Prevent overlapping state transitions in workflows (#2065)
Browse files Browse the repository at this point in the history
* add deepMergeWithOptions

* fix deepMergeWithOptions

* refactor `transformData` to include `workflowRuntimeId`

* add `DEEP_MERGE_CONTEXT` event to workflow-core

* update `UPDATE_CONTEXT` to replace the context instead of merging it

* update tests

* remove postUpdateEventName functionality

* make EntityRepository@updateById accept transaction param

* use transactions

* beginTransactionIfNotExistCurry util

* refactor CollectionFlow

* impl beginTransactionIfNotExistCurry

* fix UPDATE_CONTEXT signature

* refactor service.event

* refactor handleHookResponse

* refactors

* fixes

* fixes

* fix: fixes

* fix: fixes

* fix: fixes

* fix: fixes

* fix: fixes

* fix: fixes

* fix: pr comments

* fix: bug fixes

* fix: make `workflow.state.changed` dispatch only on state change

* fix: tests

* fix: resolve conflicts

---------

Co-authored-by: Alon Peretz <[email protected]>
  • Loading branch information
MatanYadaev and alonp99 authored Feb 29, 2024
1 parent f035381 commit 44a0dff
Show file tree
Hide file tree
Showing 62 changed files with 2,683 additions and 2,687 deletions.
1 change: 0 additions & 1 deletion apps/backoffice-v2/src/common/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export const Action = {
REJECT: 'reject',
APPROVE: 'approve',
REVISION: 'revision',
TASK_REVIEWED: 'TASK_REVIEWED',
CASE_REVIEWED: 'CASE_REVIEWED',
} as const;
export const Resource = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { workflowsQueryKeys } from '../../../../workflows/query-keys';
import { Action } from '../../../../../common/enums';
import { useFilterId } from '../../../../../common/hooks/useFilterId/useFilterId';

export const useApproveTaskByIdMutation = (workflowId: string, postUpdateEventName?: string) => {
export const useApproveTaskByIdMutation = (workflowId: string) => {
const queryClient = useQueryClient();
const filterId = useFilterId();
const workflowById = workflowsQueryKeys.byId({ workflowId, filterId });
Expand All @@ -24,7 +24,6 @@ export const useApproveTaskByIdMutation = (workflowId: string, postUpdateEventNa
documentId,
body: {
decision: Action.APPROVE,
postUpdateEventName,
},
contextUpdateMethod,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { workflowsQueryKeys } from '../../../../workflows/query-keys';
import { Action } from '../../../../../common/enums';
import { useFilterId } from '../../../../../common/hooks/useFilterId/useFilterId';

export const useRejectTaskByIdMutation = (workflowId: string, postUpdateEventName?: string) => {
export const useRejectTaskByIdMutation = (workflowId: string) => {
const queryClient = useQueryClient();
const filterId = useFilterId();
const workflowById = workflowsQueryKeys.byId({ workflowId, filterId });
Expand All @@ -19,7 +19,6 @@ export const useRejectTaskByIdMutation = (workflowId: string, postUpdateEventNam
body: {
decision: Action.REJECT,
reason,
postUpdateEventName,
},
}),
onMutate: async ({ documentId, reason }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import { TWorkflowById, updateWorkflowDecision } from '../../../../workflows/fet
import { workflowsQueryKeys } from '../../../../workflows/query-keys';
import { useFilterId } from '../../../../../common/hooks/useFilterId/useFilterId';

export const useRemoveDecisionTaskByIdMutation = (
workflowId: string,
postUpdateEventName?: string,
) => {
export const useRemoveDecisionTaskByIdMutation = (workflowId: string) => {
const queryClient = useQueryClient();
const filterId = useFilterId();
const workflowById = workflowsQueryKeys.byId({ workflowId, filterId });
Expand All @@ -27,7 +24,6 @@ export const useRemoveDecisionTaskByIdMutation = (
body: {
decision: null,
reason: null,
postUpdateEventName,
},
contextUpdateMethod,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { workflowsQueryKeys } from '../../../../workflows/query-keys';
import { useFilterId } from '../../../../../common/hooks/useFilterId/useFilterId';
import { Action } from '../../../../../common/enums';

export const useRevisionTaskByIdMutation = (postUpdateEventName?: string) => {
export const useRevisionTaskByIdMutation = () => {
const queryClient = useQueryClient();
const filterId = useFilterId();

Expand All @@ -29,7 +29,6 @@ export const useRevisionTaskByIdMutation = (postUpdateEventName?: string) => {
body: {
decision: Action.REVISION,
reason,
postUpdateEventName,
},
}),
onMutate: async ({ workflowId, documentId, reason }) => {
Expand Down
1 change: 0 additions & 1 deletion apps/backoffice-v2/src/domains/workflows/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,6 @@ export const updateWorkflowDecision = async ({
body: {
decision: string | null;
reason?: string;
postUpdateEventName?: string;
};
contextUpdateMethod: 'base' | 'director';
}) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,6 @@ export interface IUseCallToActionLogicParams {
};
}

export const getPostDecisionEventName = (workflow: TWorkflowById) => {
if (
!workflow?.workflowDefinition?.config?.workflowLevelResolution &&
workflow?.nextEvents?.includes(CommonWorkflowEvent.TASK_REVIEWED)
) {
return CommonWorkflowEvent.TASK_REVIEWED;
}
};
export const useCallToActionLegacyLogic = ({
contextUpdateMethod = 'base',
rejectionReasons,
Expand All @@ -48,12 +40,10 @@ export const useCallToActionLegacyLogic = ({
isLoadingReuploadNeeded,
dialog,
}: IUseCallToActionLogicParams) => {
const postUpdateEventName = getPostDecisionEventName(workflow);

const { mutate: mutateApproveTaskById, isLoading: isLoadingApproveTaskById } =
useApproveTaskByIdMutation(workflow?.id, postUpdateEventName);
useApproveTaskByIdMutation(workflow?.id);
const { mutate: mutateRejectTaskById, isLoading: isLoadingRejectTaskById } =
useRejectTaskByIdMutation(workflow?.id, postUpdateEventName);
useRejectTaskByIdMutation(workflow?.id);

const isLoadingTaskDecisionById =
isLoadingApproveTaskById || isLoadingRejectTaskById || isLoadingReuploadNeeded;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import {
import { motionBadgeProps } from '../../motion-badge-props';
import { useApproveTaskByIdMutation } from '@/domains/entities/hooks/mutations/useApproveTaskByIdMutation/useApproveTaskByIdMutation';
import { useRemoveDecisionTaskByIdMutation } from '@/domains/entities/hooks/mutations/useRemoveDecisionTaskByIdMutation/useRemoveDecisionTaskByIdMutation';
import { getPostRemoveDecisionEventName } from '@/pages/Entity/get-post-remove-decision-event-name';
import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState';
import { useAuthenticatedUserQuery } from '@/domains/auth/hooks/queries/useAuthenticatedUserQuery/useAuthenticatedUserQuery';
import { selectDirectorsDocuments } from '@/pages/Entity/selectors/selectDirectorsDocuments';
import { TWorkflowById } from '@/domains/workflows/fetchers';
import { useCaseDecision } from '@/pages/Entity/components/Case/hooks/useCaseDecision/useCaseDecision';
import { getPostDecisionEventName } from '../../components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed';
import { X } from 'lucide-react';
import { getRevisionReasonsForDocument } from '@/lib/blocks/components/DirectorsCallToAction/helpers';
Expand Down Expand Up @@ -44,10 +42,7 @@ export const useDirectorsBlocks = ({
reason?: string;
}) => () => void;
}) => {
const { mutate: removeDecisionById } = useRemoveDecisionTaskByIdMutation(
workflow?.id,
getPostRemoveDecisionEventName(workflow),
);
const { mutate: removeDecisionById } = useRemoveDecisionTaskByIdMutation(workflow?.id);

const { data: session } = useAuthenticatedUserQuery();
const caseState = useCaseState(session?.user, workflow);
Expand Down Expand Up @@ -78,9 +73,8 @@ export const useDirectorsBlocks = ({
});
}, [documents, removeDecisionById]);

const postApproveEventName = getPostDecisionEventName(workflow);
const { mutate: mutateApproveTaskById, isLoading: isLoadingApproveTaskById } =
useApproveTaskByIdMutation(workflow?.id, postApproveEventName);
useApproveTaskByIdMutation(workflow?.id);
const onMutateApproveTaskById = useCallback(
({
taskId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,13 @@ import { FunctionComponent, useCallback, useMemo } from 'react';
import { selectWorkflowDocuments } from '@/pages/Entity/selectors/selectWorkflowDocuments';
import { useStorageFilesQuery } from '@/domains/storage/hooks/queries/useStorageFilesQuery/useStorageFilesQuery';
import { useApproveTaskByIdMutation } from '@/domains/entities/hooks/mutations/useApproveTaskByIdMutation/useApproveTaskByIdMutation';
import { getPostRemoveDecisionEventName } from '@/pages/Entity/get-post-remove-decision-event-name';
import { useRemoveDecisionTaskByIdMutation } from '@/domains/entities/hooks/mutations/useRemoveDecisionTaskByIdMutation/useRemoveDecisionTaskByIdMutation';
import { CommonWorkflowStates, StateTag } from '@ballerine/common';
import { X } from 'lucide-react';
import { valueOrNA } from '@/common/utils/value-or-na/value-or-na';
import { createBlocksTyped } from '@/lib/blocks/create-blocks-typed/create-blocks-typed';
import { ctw } from '@/common/utils/ctw/ctw';
import { getDocumentsSchemas } from '@/pages/Entity/utils/get-documents-schemas/get-documents-schemas';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { useDocumentPageImages } from '@/lib/blocks/hooks/useDocumentPageImages';
import { motionBadgeProps } from '@/lib/blocks/motion-badge-props';
import { useRejectTaskByIdMutation } from '@/domains/entities/hooks/mutations/useRejectTaskByIdMutation/useRejectTaskByIdMutation';
Expand Down Expand Up @@ -70,7 +68,6 @@ export const useDocumentBlocks = ({
}) => {
const issuerCountryCode = extractCountryCodeFromWorkflow(workflow);
const documentsSchemas = getDocumentsSchemas(issuerCountryCode, workflow);
const postDecisionEventName = getPostDecisionEventName(workflow);
const documents = useMemo(() => selectWorkflowDocuments(workflow), [workflow]);
const documentPages = useMemo(
() => documents.flatMap(({ pages }) => pages?.map(({ ballerineFileId }) => ballerineFileId)),
Expand All @@ -80,11 +77,8 @@ export const useDocumentBlocks = ({
const documentPagesResults = useDocumentPageImages(documents, storageFilesQueryResult);

const { mutate: mutateApproveTaskById, isLoading: isLoadingApproveTaskById } =
useApproveTaskByIdMutation(workflow?.id, postDecisionEventName);
const { isLoading: isLoadingRejectTaskById } = useRejectTaskByIdMutation(
workflow?.id,
postDecisionEventName,
);
useApproveTaskByIdMutation(workflow?.id);
const { isLoading: isLoadingRejectTaskById } = useRejectTaskByIdMutation(workflow?.id);
const onMutateApproveTaskById = useCallback(
({
taskId,
Expand All @@ -97,11 +91,7 @@ export const useDocumentBlocks = ({
mutateApproveTaskById({ documentId: taskId, contextUpdateMethod }),
[mutateApproveTaskById],
);
const postRemoveDecisionEventName = getPostRemoveDecisionEventName(workflow);
const { mutate: onMutateRemoveDecisionById } = useRemoveDecisionTaskByIdMutation(
workflow?.id,
postRemoveDecisionEventName,
);
const { mutate: onMutateRemoveDecisionById } = useRemoveDecisionTaskByIdMutation(workflow?.id);

return (
documents?.flatMap(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState';
import { TWorkflowById } from '@/domains/workflows/fetchers';
import { CommonWorkflowEvent, DefaultContextSchema } from '@ballerine/common';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { CommonWorkflowEvent, CommonWorkflowStates, DefaultContextSchema } from '@ballerine/common';
import { checkCanMakeDecision } from '@/lib/blocks/hooks/useDocumentBlocks/utils/check-can-make-decision/check-can-make-decision';

export const checkCanApprove = ({
Expand All @@ -17,13 +16,14 @@ export const checkCanApprove = ({
decision: DefaultContextSchema['documents'][number]['decision'];
isLoadingApprove: boolean;
}) => {
const hasTaskReviewedEvent = !!getPostDecisionEventName(workflow);
const isStateManualReview = workflow.state === CommonWorkflowStates.MANUAL_REVIEW;

const hasApproveEvent = workflow?.nextEvents?.includes(CommonWorkflowEvent.APPROVE);
const canMakeDecision = checkCanMakeDecision({
caseState,
noAction,
decision,
});

return !isLoadingApprove && canMakeDecision && (hasTaskReviewedEvent || hasApproveEvent);
return !isLoadingApprove && canMakeDecision && (isStateManualReview || hasApproveEvent);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState';
import { TWorkflowById } from '@/domains/workflows/fetchers';
import { CommonWorkflowEvent, DefaultContextSchema } from '@ballerine/common';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { CommonWorkflowEvent, CommonWorkflowStates, DefaultContextSchema } from '@ballerine/common';
import { checkCanMakeDecision } from '@/lib/blocks/hooks/useDocumentBlocks/utils/check-can-make-decision/check-can-make-decision';

export const checkCanReject = ({
Expand All @@ -17,13 +16,14 @@ export const checkCanReject = ({
decision: DefaultContextSchema['documents'][number]['decision'];
isLoadingReject: boolean;
}) => {
const hasTaskReviewedEvent = !!getPostDecisionEventName(workflow);
const isStateManualReview = workflow.state === CommonWorkflowStates.MANUAL_REVIEW;

const hasRejectEvent = workflow?.nextEvents?.includes(CommonWorkflowEvent.REJECT);
const canMakeDecision = checkCanMakeDecision({
caseState,
noAction,
decision,
});

return !isLoadingReject && canMakeDecision && (hasTaskReviewedEvent || hasRejectEvent);
return !isLoadingReject && canMakeDecision && (isStateManualReview || hasRejectEvent);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/useCaseState';
import { TWorkflowById } from '@/domains/workflows/fetchers';
import { CommonWorkflowEvent, DefaultContextSchema } from '@ballerine/common';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { CommonWorkflowEvent, CommonWorkflowStates, DefaultContextSchema } from '@ballerine/common';
import { checkCanMakeDecision } from '@/lib/blocks/hooks/useDocumentBlocks/utils/check-can-make-decision/check-can-make-decision';

export const checkCanRevision = ({
Expand All @@ -17,13 +16,14 @@ export const checkCanRevision = ({
decision: DefaultContextSchema['documents'][number]['decision'];
isLoadingRevision: boolean;
}) => {
const hasTaskReviewedEvent = !!getPostDecisionEventName(workflow);
const isStateManualReview = workflow.state === CommonWorkflowStates.MANUAL_REVIEW;

const hasRevisionEvent = workflow?.nextEvents?.includes(CommonWorkflowEvent.REVISION);
const canMakeDecision = checkCanMakeDecision({
caseState,
noAction,
decision,
});

return !isLoadingRevision && canMakeDecision && (hasTaskReviewedEvent || hasRevisionEvent);
return !isLoadingRevision && canMakeDecision && (isStateManualReview || hasRevisionEvent);
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { Button } from '@/common/components/atoms/Button/Button';
import { ctw } from '@/common/utils/ctw/ctw';
import { Send } from 'lucide-react';
import { useAssociatedCompaniesInformationBlock } from '@/lib/blocks/hooks/useAssociatedCompaniesInformationBlock/useAssociatedCompaniesInformationBlock';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { useDocumentPageImages } from '@/lib/blocks/hooks/useDocumentPageImages';
import { useRegistryInfoBlock } from '@/lib/blocks/hooks/useRegistryInfoBlock/useRegistryInfoBlock';
import { useKybRegistryInfoBlock } from '@/lib/blocks/hooks/useKybRegistryInfoBlock/useKybRegistryInfoBlock';
Expand Down Expand Up @@ -63,9 +62,8 @@ export const useDefaultBlocksLogic = () => {
const isWorkflowLevelResolution =
workflow?.workflowDefinition?.config?.workflowLevelResolution ??
workflow?.context?.entity?.type === 'business';
const postDecisionEventName = getPostDecisionEventName(workflow);
const { mutate: mutateRevisionTaskById, isLoading: isLoadingReuploadNeeded } =
useRevisionTaskByIdMutation(postDecisionEventName);
useRevisionTaskByIdMutation();
const onReuploadNeeded = useCallback(
({
workflowId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { ctw } from '@/common/utils/ctw/ctw';
import { ExternalLink, Send } from 'lucide-react';
import { useDocumentPageImages } from '@/lib/blocks/hooks/useDocumentPageImages';
import { associatedCompanyAdapter } from '@/lib/blocks/hooks/useAssosciatedCompaniesBlock/associated-company-adapter';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { useEntityInfoBlock } from '@/lib/blocks/hooks/useEntityInfoBlock/useEntityInfoBlock';
import { useDocumentBlocks } from '@/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks';
import { useMainRepresentativeBlock } from '@/lib/blocks/hooks/useMainRepresentativeBlock/useMainRepresentativeBlock';
Expand Down Expand Up @@ -79,9 +78,8 @@ export const useKybExampleBlocksLogic = () => {
},
[mutateEvent],
);
const postDecisionEventName = getPostDecisionEventName(workflow);
const { mutate: mutateRevisionTaskById, isLoading: isLoadingReuploadNeeded } =
useRevisionTaskByIdMutation(postDecisionEventName);
useRevisionTaskByIdMutation();
const onReuploadNeeded = useCallback(
({
workflowId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { useCaseState } from '@/pages/Entity/components/Case/hooks/useCaseState/
import { useRevisionTaskByIdMutation } from '@/domains/entities/hooks/mutations/useRevisionTaskByIdMutation/useRevisionTaskByIdMutation';
import { useCallback, useMemo } from 'react';
import toast from 'react-hot-toast';
import { getPostDecisionEventName } from '@/lib/blocks/components/CallToActionLegacy/hooks/useCallToActionLegacyLogic/useCallToActionLegacyLogic';
import { useEntityInfoBlock } from '@/lib/blocks/hooks/useEntityInfoBlock/useEntityInfoBlock';
import { useDocumentBlocks } from '@/lib/blocks/hooks/useDocumentBlocks/useDocumentBlocks';

Expand All @@ -30,9 +29,8 @@ export const useManualReviewBlocksLogic = () => {
openCorporate: _openCorporate,
...entityDataAdditionalInfo
} = workflow?.context?.entity?.data?.additionalInfo ?? {};
const postDecisionEventName = getPostDecisionEventName(workflow);
const { mutate: mutateRevisionTaskById, isLoading: isLoadingReuploadNeeded } =
useRevisionTaskByIdMutation(postDecisionEventName);
useRevisionTaskByIdMutation();
const onReuploadNeeded = useCallback(
({
workflowId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface IPendingEvent {
workflowId: string;
workflowState: string;
documentId: string;
eventName: string;
token: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { TWorkflowById } from '@/domains/workflows/fetchers';
import { useCallback, useMemo } from 'react';
import { calculateAllWorkflowPendingEvents } from '@/pages/Entity/components/Case/hooks/usePendingRevisionEvents/utils/calculate-pending-workflow-events';
import { CommonWorkflowEvent } from '@ballerine/common';
import { CommonWorkflowEvent, CommonWorkflowStates } from '@ballerine/common';
import { checkIsKybExampleVariant } from '@/lib/blocks/variants/variant-checkers';
import { useRevisionCaseMutation } from '@/domains/workflows/hooks/mutations/useRevisionCaseMutation/useRevisionCaseMutation';
import { IPendingEvent } from './interfaces';
Expand All @@ -18,7 +18,7 @@ const composeUniqueWorkflowEvents = (

const isPendingEventIsRevision = (pendingWorkflowEvent: IPendingEvent) =>
pendingWorkflowEvent?.eventName === CommonWorkflowEvent.REVISION ||
pendingWorkflowEvent?.eventName === CommonWorkflowEvent.TASK_REVIEWED;
pendingWorkflowEvent?.workflowState === CommonWorkflowStates.MANUAL_REVIEW;

export const usePendingRevisionEvents = (
mutateRevisionCase: ReturnType<typeof useRevisionCaseMutation>['mutate'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const calculatePendingWorkflowEvents = (workflow: TWorkflowById): Array<I
.map(document => {
return {
workflowId: workflow.id,
workflowState: workflow.state,
documentId: document?.id as string,
eventName: calculateWorkflowRevisionableEvent(workflow, document?.decision?.status),
token: workflow?.context?.metadata?.token,
Expand All @@ -31,9 +32,7 @@ export const calculatePendingWorkflowEvents = (workflow: TWorkflowById): Array<I
.filter((a): a is NonNullable<IPendingEvent> => !!a && !!a.eventName);
};

export const calculateAllWorkflowPendingEvents = (
workflow: TWorkflowById,
): Array<IPendingEvent> => {
export const calculateAllWorkflowPendingEvents = (workflow: TWorkflowById): IPendingEvent[] => {
return [
...calculatePendingWorkflowEvents(workflow),
...(workflow.childWorkflows?.flatMap(childWorkflow =>
Expand Down
Loading

0 comments on commit 44a0dff

Please sign in to comment.