@zeniai/client-epic-state 5.2.14-betaRD0 → 5.2.14-betaRD1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,6 +46,19 @@ import { ApprovalRule, Criteria } from './approvalRuleState';
46
46
  export type RuleForOverlapCheck = Pick<ApprovalRule, 'approvalRuleId' | 'criteria' | 'isFallback'>;
47
47
  export interface RuleOverlap<TRule extends RuleForOverlapCheck = ApprovalRule> {
48
48
  conflictingRule: TRule;
49
+ /**
50
+ * 'duplicate' — every criterion matches exactly, including the
51
+ * amount interval (same comparator + min + max, not just an
52
+ * intersecting range). 'partial' — vendor/department match exactly
53
+ * (required for any overlap at all) but the amount intervals only
54
+ * intersect.
55
+ *
56
+ * Callers typically treat 'duplicate' as a blocking error (the
57
+ * candidate rule is unreachable — the existing rule already
58
+ * matches every bill/reimbursement it would) and 'partial' as a
59
+ * dismissible warning.
60
+ */
61
+ overlapKind: 'duplicate' | 'partial';
49
62
  }
50
63
  export interface DetectRuleOverlapInput<TRule extends RuleForOverlapCheck = ApprovalRule> {
51
64
  candidateCriteria: Criteria[];
@@ -23,7 +23,12 @@ function detectRuleOverlap({ candidateCriteria, existingRules, excludeRuleId, })
23
23
  }
24
24
  const ruleAmount = pickAmount(rule.criteria);
25
25
  if (amountsOverlap(candidateAmount, ruleAmount)) {
26
- overlaps.push({ conflictingRule: rule });
26
+ overlaps.push({
27
+ conflictingRule: rule,
28
+ overlapKind: areAmountCriteriaEqual(candidateAmount, ruleAmount)
29
+ ? 'duplicate'
30
+ : 'partial',
31
+ });
27
32
  }
28
33
  }
29
34
  return overlaps;
@@ -75,3 +80,13 @@ const amountsOverlap = (a, b) => {
75
80
  const bHigh = b.max?.amount ?? Number.POSITIVE_INFINITY;
76
81
  return Math.max(aLow, bLow) <= Math.min(aHigh, bHigh);
77
82
  };
83
+ // Only called once `amountsOverlap` is already true, so this decides
84
+ // duplicate vs partial rather than overlap vs no-overlap.
85
+ const areAmountCriteriaEqual = (a, b) => {
86
+ if (a == null || b == null) {
87
+ return a == null && b == null;
88
+ }
89
+ return (a.comparator === b.comparator &&
90
+ (a.min?.amount ?? null) === (b.min?.amount ?? null) &&
91
+ (a.max?.amount ?? null) === (b.max?.amount ?? null));
92
+ };
@@ -20,7 +20,12 @@ export function detectRuleOverlap({ candidateCriteria, existingRules, excludeRul
20
20
  }
21
21
  const ruleAmount = pickAmount(rule.criteria);
22
22
  if (amountsOverlap(candidateAmount, ruleAmount)) {
23
- overlaps.push({ conflictingRule: rule });
23
+ overlaps.push({
24
+ conflictingRule: rule,
25
+ overlapKind: areAmountCriteriaEqual(candidateAmount, ruleAmount)
26
+ ? 'duplicate'
27
+ : 'partial',
28
+ });
24
29
  }
25
30
  }
26
31
  return overlaps;
@@ -72,3 +77,13 @@ const amountsOverlap = (a, b) => {
72
77
  const bHigh = b.max?.amount ?? Number.POSITIVE_INFINITY;
73
78
  return Math.max(aLow, bLow) <= Math.min(aHigh, bHigh);
74
79
  };
80
+ // Only called once `amountsOverlap` is already true, so this decides
81
+ // duplicate vs partial rather than overlap vs no-overlap.
82
+ const areAmountCriteriaEqual = (a, b) => {
83
+ if (a == null || b == null) {
84
+ return a == null && b == null;
85
+ }
86
+ return (a.comparator === b.comparator &&
87
+ (a.min?.amount ?? null) === (b.min?.amount ?? null) &&
88
+ (a.max?.amount ?? null) === (b.max?.amount ?? null));
89
+ };
@@ -7,7 +7,7 @@ import { getIsAccountingClassesEnabled } from '../../../../entity/tenant/tenantS
7
7
  import { getUserByUserId } from '../../../../entity/user/userSelector';
8
8
  import { getUserRoleByUserId } from '../../../../entity/userRole/userRoleSelector';
9
9
  export const getBillPaySetupApproverView = createSelector((state) => state.billPaySetupApproverViewState, (state) => state.approvalRuleState, (state) => state.userState, (billPaySetupApproverViewState, approvalRuleState, userState) => {
10
- const { approvalRulesDeleteStatusById, approvalRuleIds, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = billPaySetupApproverViewState;
10
+ const { approvalRulesDeleteStatusById, approvalRuleIds, approverListState, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = billPaySetupApproverViewState;
11
11
  const deleteStateList = Object.values(approvalRulesDeleteStatusById);
12
12
  const approvalRules = getApprovalRulesByIds(approvalRuleState, approvalRuleIds);
13
13
  const allApprovalRules = approvalRules.map((approvalRule) => {
@@ -35,10 +35,15 @@ export const getBillPaySetupApproverView = createSelector((state) => state.billP
35
35
  name: 'Require Info',
36
36
  code: 'require_info',
37
37
  };
38
+ const activeApproverIds = new Set(approverListState.approverIds);
39
+ const referencedApproverIds = new Set(allApprovalRules.flatMap((approvalRule) => approvalRule.steps.flatMap((step) => step.actors.map((actor) => actor.userId))));
40
+ const inactiveApproverIds = Array.from(referencedApproverIds).filter((userId) => !activeApproverIds.has(userId));
38
41
  return {
39
42
  allApprovalRules,
40
43
  deleteStatus: reduceAnyFetchState(deleteStateList),
41
44
  approvalRulesStatus,
45
+ approverListFetchStatus: approverListState.fetchStatus,
46
+ inactiveApproverIds,
42
47
  fetchState,
43
48
  error,
44
49
  listeningToPusherEvent,
@@ -57,6 +62,19 @@ export const getBillPaySetupApproverUpdateDataView = createSelector((state) => s
57
62
  };
58
63
  })
59
64
  .filter((approver) => approver.user != null);
65
+ const activeApproverIds = new Set(approverIds);
66
+ const currentRuleSteps = billPaySetupApproverViewUpdateData?.data?.steps ?? [];
67
+ const inactiveActorUserIds = new Set(currentRuleSteps
68
+ .flatMap((step) => step.actors.map((actor) => actor.userId))
69
+ .filter((userId) => userId != null && !activeApproverIds.has(userId)));
70
+ const inactiveActorApprovers = Array.from(inactiveActorUserIds)
71
+ .map((userId) => {
72
+ return {
73
+ user: getUserByUserId(userState, userId),
74
+ role: getUserRoleByUserId(userRoleState, userId),
75
+ };
76
+ })
77
+ .filter((approver) => approver.user != null);
60
78
  const allAttributes = attributes;
61
79
  const filteredApprovers = cloneDeep(allApprovers);
62
80
  const filteredAttributes = cloneDeep(attributes);
@@ -90,6 +108,7 @@ export const getBillPaySetupApproverUpdateDataView = createSelector((state) => s
90
108
  allApprovalRules,
91
109
  allApprovers,
92
110
  filteredApprovers,
111
+ inactiveActorApprovers,
93
112
  allAttributes,
94
113
  filteredAttributes,
95
114
  billPaySetupApproverViewUpdateData,
@@ -7,7 +7,7 @@ import { getUserByUserId } from '../../../../entity/user/userSelector';
7
7
  import { getUserRoleByUserId } from '../../../../entity/userRole/userRoleSelector';
8
8
  export const getRemiSetupApproverView = (state, filterAdmins = true) => {
9
9
  const { remiSetupApproverViewState, approvalRuleState, userState } = state;
10
- const { approvalRulesDeleteStatusById, approvalRuleIds, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = remiSetupApproverViewState;
10
+ const { approvalRulesDeleteStatusById, approvalRuleIds, approverListState, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = remiSetupApproverViewState;
11
11
  const deleteStateList = Object.values(approvalRulesDeleteStatusById);
12
12
  const approvalRules = getApprovalRulesByIds(approvalRuleState, approvalRuleIds);
13
13
  const allApprovalRules = approvalRules.map((approvalRule) => {
@@ -36,10 +36,15 @@ export const getRemiSetupApproverView = (state, filterAdmins = true) => {
36
36
  name: 'Require Info',
37
37
  code: 'require_info',
38
38
  };
39
+ const activeApproverIds = new Set(approverListState.approverIds);
40
+ const referencedApproverIds = new Set(allApprovalRules.flatMap((approvalRule) => approvalRule.steps.flatMap((step) => step.actors.map((actor) => actor.userId))));
41
+ const inactiveApproverIds = Array.from(referencedApproverIds).filter((userId) => !activeApproverIds.has(userId));
39
42
  return {
40
43
  allApprovalRules,
41
44
  deleteStatus: reduceAnyFetchState(deleteStateList),
42
45
  approvalRulesStatus,
46
+ approverListFetchStatus: approverListState.fetchStatus,
47
+ inactiveApproverIds,
43
48
  fetchState,
44
49
  error,
45
50
  listeningToPusherEvent,
@@ -74,6 +79,19 @@ export const getRemiSetupApproverUpdateDataView = createSelector((state) => stat
74
79
  };
75
80
  })
76
81
  .filter((approver) => approver.user != null);
82
+ const activeApproverIds = new Set(approverIds);
83
+ const currentRuleSteps = remiSetupApproverViewUpdateData?.data?.steps ?? [];
84
+ const inactiveActorUserIds = new Set(currentRuleSteps
85
+ .flatMap((step) => step.actors.map((actor) => actor.userId))
86
+ .filter((userId) => userId != null && !activeApproverIds.has(userId)));
87
+ const inactiveActorApprovers = Array.from(inactiveActorUserIds)
88
+ .map((userId) => {
89
+ return {
90
+ user: getUserByUserId(userState, userId),
91
+ role: getUserRoleByUserId(userRoleState, userId),
92
+ };
93
+ })
94
+ .filter((approver) => approver.user != null);
77
95
  const allAttributes = attributes;
78
96
  const filteredApprovers = cloneDeep(allApprovers);
79
97
  const filteredAttributes = cloneDeep(attributes);
@@ -108,6 +126,7 @@ export const getRemiSetupApproverUpdateDataView = createSelector((state) => stat
108
126
  allAttributes,
109
127
  filteredApprovers,
110
128
  filteredAttributes,
129
+ inactiveActorApprovers,
111
130
  allAccountingClasses,
112
131
  remiSetupApproverViewUpdateData,
113
132
  approversDetailsFetchStatus: reduceAllFetchState([
@@ -13,7 +13,7 @@ const tenantSelector_1 = require("../../../../entity/tenant/tenantSelector");
13
13
  const userSelector_1 = require("../../../../entity/user/userSelector");
14
14
  const userRoleSelector_1 = require("../../../../entity/userRole/userRoleSelector");
15
15
  exports.getBillPaySetupApproverView = (0, toolkit_1.createSelector)((state) => state.billPaySetupApproverViewState, (state) => state.approvalRuleState, (state) => state.userState, (billPaySetupApproverViewState, approvalRuleState, userState) => {
16
- const { approvalRulesDeleteStatusById, approvalRuleIds, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = billPaySetupApproverViewState;
16
+ const { approvalRulesDeleteStatusById, approvalRuleIds, approverListState, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = billPaySetupApproverViewState;
17
17
  const deleteStateList = Object.values(approvalRulesDeleteStatusById);
18
18
  const approvalRules = (0, approvalRuleSelector_1.getApprovalRulesByIds)(approvalRuleState, approvalRuleIds);
19
19
  const allApprovalRules = approvalRules.map((approvalRule) => {
@@ -41,10 +41,15 @@ exports.getBillPaySetupApproverView = (0, toolkit_1.createSelector)((state) => s
41
41
  name: 'Require Info',
42
42
  code: 'require_info',
43
43
  };
44
+ const activeApproverIds = new Set(approverListState.approverIds);
45
+ const referencedApproverIds = new Set(allApprovalRules.flatMap((approvalRule) => approvalRule.steps.flatMap((step) => step.actors.map((actor) => actor.userId))));
46
+ const inactiveApproverIds = Array.from(referencedApproverIds).filter((userId) => !activeApproverIds.has(userId));
44
47
  return {
45
48
  allApprovalRules,
46
49
  deleteStatus: (0, reduceFetchState_1.reduceAnyFetchState)(deleteStateList),
47
50
  approvalRulesStatus,
51
+ approverListFetchStatus: approverListState.fetchStatus,
52
+ inactiveApproverIds,
48
53
  fetchState,
49
54
  error,
50
55
  listeningToPusherEvent,
@@ -63,6 +68,19 @@ exports.getBillPaySetupApproverUpdateDataView = (0, toolkit_1.createSelector)((s
63
68
  };
64
69
  })
65
70
  .filter((approver) => approver.user != null);
71
+ const activeApproverIds = new Set(approverIds);
72
+ const currentRuleSteps = billPaySetupApproverViewUpdateData?.data?.steps ?? [];
73
+ const inactiveActorUserIds = new Set(currentRuleSteps
74
+ .flatMap((step) => step.actors.map((actor) => actor.userId))
75
+ .filter((userId) => userId != null && !activeApproverIds.has(userId)));
76
+ const inactiveActorApprovers = Array.from(inactiveActorUserIds)
77
+ .map((userId) => {
78
+ return {
79
+ user: (0, userSelector_1.getUserByUserId)(userState, userId),
80
+ role: (0, userRoleSelector_1.getUserRoleByUserId)(userRoleState, userId),
81
+ };
82
+ })
83
+ .filter((approver) => approver.user != null);
66
84
  const allAttributes = attributes;
67
85
  const filteredApprovers = (0, cloneDeep_1.default)(allApprovers);
68
86
  const filteredAttributes = (0, cloneDeep_1.default)(attributes);
@@ -96,6 +114,7 @@ exports.getBillPaySetupApproverUpdateDataView = (0, toolkit_1.createSelector)((s
96
114
  allApprovalRules,
97
115
  allApprovers,
98
116
  filteredApprovers,
117
+ inactiveActorApprovers,
99
118
  allAttributes,
100
119
  filteredAttributes,
101
120
  billPaySetupApproverViewUpdateData,
@@ -49,7 +49,9 @@ export interface ApprovalRuleWithUser extends Omit<ApprovalRule, 'steps'> {
49
49
  export interface BillPaySetupApproverView extends SelectorView {
50
50
  allApprovalRules: ApprovalRuleWithUser[];
51
51
  approvalRulesStatus: AllowedValueWithCode;
52
+ approverListFetchStatus: FetchStateAndError;
52
53
  deleteStatus: FetchStateAndError;
54
+ inactiveApproverIds: ID[];
53
55
  listeningToPusherEvent?: boolean;
54
56
  newApprovalRuleId?: ID;
55
57
  }
@@ -61,6 +63,7 @@ export interface BillPaySetupApproverUpdateDataView extends SelectorView {
61
63
  approverListFetchStatus: FetchStateAndError;
62
64
  filteredApprovers: UserAndRole[];
63
65
  filteredAttributes: AttributeType[];
66
+ inactiveActorApprovers: UserAndRole[];
64
67
  affectedEntityIds?: ID[];
65
68
  billPaySetupApproverViewUpdateData?: ApproverViewUpdateData;
66
69
  listeningToPusherEvent?: boolean;
@@ -13,7 +13,7 @@ const userSelector_1 = require("../../../../entity/user/userSelector");
13
13
  const userRoleSelector_1 = require("../../../../entity/userRole/userRoleSelector");
14
14
  const getRemiSetupApproverView = (state, filterAdmins = true) => {
15
15
  const { remiSetupApproverViewState, approvalRuleState, userState } = state;
16
- const { approvalRulesDeleteStatusById, approvalRuleIds, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = remiSetupApproverViewState;
16
+ const { approvalRulesDeleteStatusById, approvalRuleIds, approverListState, fetchState, error, listeningToPusherEvent, newApprovalRuleId, } = remiSetupApproverViewState;
17
17
  const deleteStateList = Object.values(approvalRulesDeleteStatusById);
18
18
  const approvalRules = (0, approvalRuleSelector_1.getApprovalRulesByIds)(approvalRuleState, approvalRuleIds);
19
19
  const allApprovalRules = approvalRules.map((approvalRule) => {
@@ -42,10 +42,15 @@ const getRemiSetupApproverView = (state, filterAdmins = true) => {
42
42
  name: 'Require Info',
43
43
  code: 'require_info',
44
44
  };
45
+ const activeApproverIds = new Set(approverListState.approverIds);
46
+ const referencedApproverIds = new Set(allApprovalRules.flatMap((approvalRule) => approvalRule.steps.flatMap((step) => step.actors.map((actor) => actor.userId))));
47
+ const inactiveApproverIds = Array.from(referencedApproverIds).filter((userId) => !activeApproverIds.has(userId));
45
48
  return {
46
49
  allApprovalRules,
47
50
  deleteStatus: (0, reduceFetchState_1.reduceAnyFetchState)(deleteStateList),
48
51
  approvalRulesStatus,
52
+ approverListFetchStatus: approverListState.fetchStatus,
53
+ inactiveApproverIds,
49
54
  fetchState,
50
55
  error,
51
56
  listeningToPusherEvent,
@@ -81,6 +86,19 @@ exports.getRemiSetupApproverUpdateDataView = (0, toolkit_1.createSelector)((stat
81
86
  };
82
87
  })
83
88
  .filter((approver) => approver.user != null);
89
+ const activeApproverIds = new Set(approverIds);
90
+ const currentRuleSteps = remiSetupApproverViewUpdateData?.data?.steps ?? [];
91
+ const inactiveActorUserIds = new Set(currentRuleSteps
92
+ .flatMap((step) => step.actors.map((actor) => actor.userId))
93
+ .filter((userId) => userId != null && !activeApproverIds.has(userId)));
94
+ const inactiveActorApprovers = Array.from(inactiveActorUserIds)
95
+ .map((userId) => {
96
+ return {
97
+ user: (0, userSelector_1.getUserByUserId)(userState, userId),
98
+ role: (0, userRoleSelector_1.getUserRoleByUserId)(userRoleState, userId),
99
+ };
100
+ })
101
+ .filter((approver) => approver.user != null);
84
102
  const allAttributes = attributes;
85
103
  const filteredApprovers = (0, cloneDeep_1.default)(allApprovers);
86
104
  const filteredAttributes = (0, cloneDeep_1.default)(attributes);
@@ -115,6 +133,7 @@ exports.getRemiSetupApproverUpdateDataView = (0, toolkit_1.createSelector)((stat
115
133
  allAttributes,
116
134
  filteredApprovers,
117
135
  filteredAttributes,
136
+ inactiveActorApprovers,
118
137
  allAccountingClasses,
119
138
  remiSetupApproverViewUpdateData,
120
139
  approversDetailsFetchStatus: (0, reduceFetchState_1.reduceAllFetchState)([
@@ -48,7 +48,9 @@ export interface ApprovalRuleWithUser extends Omit<ApprovalRule, 'steps'> {
48
48
  export interface RemiSetupApproverView extends SelectorView {
49
49
  allApprovalRules: ApprovalRuleWithUser[];
50
50
  approvalRulesStatus: AllowedValueWithCode;
51
+ approverListFetchStatus: FetchStateAndError;
51
52
  deleteStatus: FetchStateAndError;
53
+ inactiveApproverIds: ID[];
52
54
  listeningToPusherEvent?: boolean;
53
55
  newApprovalRuleId?: ID;
54
56
  }
@@ -60,6 +62,7 @@ export interface RemiSetupApproverUpdateDataView extends SelectorView {
60
62
  approversDetailsFetchStatus: FetchStateAndError;
61
63
  filteredApprovers: UserAndRole[];
62
64
  filteredAttributes: AttributeType[];
65
+ inactiveActorApprovers: UserAndRole[];
63
66
  affectedEntityIds?: ID[];
64
67
  listeningToPusherEvent?: boolean;
65
68
  remiSetupApproverViewUpdateData?: ApproverViewUpdateData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.14-betaRD0",
3
+ "version": "5.2.14-betaRD1",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",