@zeniai/client-epic-state 5.0.90-betaAR2 → 5.0.90-betaAR4
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.
- package/lib/esm/view/spendManagement/billPay/billPayReview/billPayReviewSelector.js +14 -0
- package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewReducer.js +11 -1
- package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.js +22 -24
- package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +56 -5
- package/lib/esm/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js +7 -2
- package/lib/view/spendManagement/billPay/billPayReview/billPayReviewSelector.d.ts +7 -0
- package/lib/view/spendManagement/billPay/billPayReview/billPayReviewSelector.js +14 -0
- package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewReducer.js +11 -1
- package/lib/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.d.ts +36 -27
- package/lib/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.js +22 -24
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +8 -1
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +55 -4
- package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js +7 -2
- package/package.json +1 -1
|
@@ -52,6 +52,12 @@ export const getReviewPageBillDetail = (state, billId) => {
|
|
|
52
52
|
let recurringConfig;
|
|
53
53
|
let billPayInfo;
|
|
54
54
|
let isBillMarkedForRetry = false;
|
|
55
|
+
// Bill-level accounting class id. Approval Rules 3.0 'department'
|
|
56
|
+
// criteria match against this. Sourced from BillTransaction so
|
|
57
|
+
// any selector that needs it (e.g. creator-as-approver lookup)
|
|
58
|
+
// can read it off the view rather than reaching into
|
|
59
|
+
// 'state.billTransactionState' itself.
|
|
60
|
+
let classId;
|
|
55
61
|
const zeniAssistRetryStatus = billId != null
|
|
56
62
|
? billDetailViewState.billDetailById[key].zeniAssist.retryStatus
|
|
57
63
|
: undefined;
|
|
@@ -62,6 +68,13 @@ export const getReviewPageBillDetail = (state, billId) => {
|
|
|
62
68
|
transaction?.billPayInfo.isCollectToAccountInfoFromVendorContact ?? false;
|
|
63
69
|
isRecurring = transaction?.isRecurring ?? false;
|
|
64
70
|
billPayInfo = transaction?.billPayInfo;
|
|
71
|
+
// Normalise empty-string to undefined so downstream readers
|
|
72
|
+
// treat "no class" uniformly regardless of whether the wire
|
|
73
|
+
// used '' or omitted the field.
|
|
74
|
+
classId =
|
|
75
|
+
transaction?.classId != null && transaction.classId !== ''
|
|
76
|
+
? transaction.classId
|
|
77
|
+
: undefined;
|
|
65
78
|
if (isRecurring &&
|
|
66
79
|
transaction?.recurringBillConfigId != null &&
|
|
67
80
|
transaction.recurringBillInstance != null) {
|
|
@@ -178,5 +191,6 @@ export const getReviewPageBillDetail = (state, billId) => {
|
|
|
178
191
|
zeniAssistRetryStatus,
|
|
179
192
|
billPayInfo,
|
|
180
193
|
outsideZeniPaymentInfo,
|
|
194
|
+
classId,
|
|
181
195
|
};
|
|
182
196
|
};
|
|
@@ -165,13 +165,23 @@ const billPaySetupApproverView = createSlice({
|
|
|
165
165
|
reorderBillPayApprovalRules: {
|
|
166
166
|
reducer(draft, action) {
|
|
167
167
|
const { approvalRuleIds } = action.payload;
|
|
168
|
+
const wasReorderInFlight = draft.reorderStatus.fetchState === 'In-Progress';
|
|
168
169
|
draft.reorderStatus = { fetchState: 'In-Progress', error: undefined };
|
|
169
170
|
// Stash the pre-reorder order so 'Failure' can restore it
|
|
170
171
|
// synchronously. The epic's refetch on failure remains as a
|
|
171
172
|
// safety net (covers cases where the server's truth has
|
|
172
173
|
// drifted from the local snapshot), but the snappy UX path
|
|
173
174
|
// is the in-memory rollback.
|
|
174
|
-
|
|
175
|
+
//
|
|
176
|
+
// Only capture a fresh snapshot when no reorder is already in
|
|
177
|
+
// flight. If a second drag fires before the first PUT lands,
|
|
178
|
+
// the existing snapshot already holds the last server-backed
|
|
179
|
+
// order; overwriting it with the first reorder's optimistic
|
|
180
|
+
// result would mean a Failure rolls back to an uncommitted
|
|
181
|
+
// intermediate order instead of the last good list.
|
|
182
|
+
if (!wasReorderInFlight) {
|
|
183
|
+
draft.reorderRollbackIds = draft.approvalRuleIds;
|
|
184
|
+
}
|
|
175
185
|
// Optimistic: render new order immediately. On failure, the
|
|
176
186
|
// reducer restores 'reorderRollbackIds' and the epic refetch
|
|
177
187
|
// reconciles any drift.
|
package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.js
CHANGED
|
@@ -91,15 +91,20 @@ export const toApprovalChangableInfoPayload = (approverViewUpdateData) => {
|
|
|
91
91
|
return payload;
|
|
92
92
|
};
|
|
93
93
|
/**
|
|
94
|
-
* V1 wire mapper — used when
|
|
95
|
-
* for the current tenant.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
94
|
+
* V1 wire mapper — used when 'approval_rule_v3_config' resolves to OFF
|
|
95
|
+
* for the current tenant. Emits the legacy snake_case array shape that
|
|
96
|
+
* '/approval-rules' historically consumed (see the V1 fixture in
|
|
97
|
+
* 'entity/entityApprovalStatus/__mocks__/json/entityApprovalStatusPayload.json'):
|
|
98
|
+
* criteria: [{type: 'range', range_entity: 'currency', min, max,
|
|
99
|
+
* currency_code, currency_symbol}]
|
|
99
100
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
101
|
+
* Open-ended semantics are preserved on the wire: an absent 'min' or
|
|
102
|
+
* 'max' is sent as 'null' rather than synthesised to '0' or
|
|
103
|
+
* 'MAX_SAFE_INTEGER', so a 'greater_than' / 'less_than' rule stays
|
|
104
|
+
* one-sided after rollback rather than collapsing into a closed range.
|
|
105
|
+
*
|
|
106
|
+
* The V1 backend does not understand vendor / department criteria or
|
|
107
|
+
* the rule-level 3.0 fields, so those are dropped on the wire.
|
|
103
108
|
*/
|
|
104
109
|
export const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
105
110
|
const amount = approverViewUpdateData.criteria?.amount;
|
|
@@ -125,25 +130,18 @@ export const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
|
125
130
|
};
|
|
126
131
|
});
|
|
127
132
|
const isUpdate = approverViewUpdateData.approvalRuleId != null;
|
|
133
|
+
const criteriaEntry = {
|
|
134
|
+
type: 'range',
|
|
135
|
+
range_entity: 'currency',
|
|
136
|
+
min: minAmount?.amount ?? null,
|
|
137
|
+
max: maxAmount?.amount ?? null,
|
|
138
|
+
currency_code: currencyCode,
|
|
139
|
+
currency_symbol: currencySymbol,
|
|
140
|
+
};
|
|
128
141
|
const payload = {
|
|
129
142
|
steps,
|
|
130
143
|
is_applicable_on_pending_approval_entity: approverViewUpdateData.isApplicableOnPendingApprovalEntity ?? true,
|
|
131
|
-
criteria:
|
|
132
|
-
rangeType: 'range',
|
|
133
|
-
rangeEntity: 'currency',
|
|
134
|
-
range: {
|
|
135
|
-
min: {
|
|
136
|
-
amount: minAmount?.amount ?? 0,
|
|
137
|
-
currencyCode,
|
|
138
|
-
currencySymbol,
|
|
139
|
-
},
|
|
140
|
-
max: {
|
|
141
|
-
amount: maxAmount?.amount ?? Number.MAX_SAFE_INTEGER,
|
|
142
|
-
currencyCode,
|
|
143
|
-
currencySymbol,
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
},
|
|
144
|
+
criteria: [criteriaEntry],
|
|
147
145
|
};
|
|
148
146
|
if (isUpdate) {
|
|
149
147
|
payload.approval_rule_id = approverViewUpdateData.approvalRuleId;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import recordGet from 'lodash/get';
|
|
2
2
|
import has from 'lodash/has';
|
|
3
|
-
import { getAmountCriteria } from '../../../../entity/approvalRule/approvalRuleSelector';
|
|
3
|
+
import { getAmountCriteria, getDepartmentCriteria, getVendorCriteria, } from '../../../../entity/approvalRule/approvalRuleSelector';
|
|
4
4
|
import { reduceFetchState } from '../../../../commonStateTypes/reduceFetchState';
|
|
5
5
|
import { getBankAccountByBankAccountId, getBankAccountsByBankAccountIds, } from '../../../../entity/bankAccount/bankAccountSelector';
|
|
6
6
|
import { getContactById, getContactsByIds, } from '../../../../entity/billPay/contact/contactSelector';
|
|
@@ -357,6 +357,14 @@ export const getEditBillDetail = (state, isVendorRecommendationFeatureEnabled, i
|
|
|
357
357
|
isDeleted,
|
|
358
358
|
showAutofill,
|
|
359
359
|
addBillAutoFields,
|
|
360
|
+
// Bill-level accounting class. Approval Rules 3.0 'department'
|
|
361
|
+
// criteria match against this. Lives on BillTransaction; not on
|
|
362
|
+
// any sub-section. Empty-string is normalised to 'undefined'
|
|
363
|
+
// so downstream readers treat "no class" uniformly regardless
|
|
364
|
+
// of whether the wire used '' or omitted the field.
|
|
365
|
+
classId: transaction?.classId != null && transaction.classId !== ''
|
|
366
|
+
? transaction.classId
|
|
367
|
+
: undefined,
|
|
360
368
|
billStage,
|
|
361
369
|
deleteStatus,
|
|
362
370
|
billActivities,
|
|
@@ -679,16 +687,59 @@ export const getDefaultWithdrawFromAccount = (state, depositAccounts, companyId,
|
|
|
679
687
|
}
|
|
680
688
|
return undefined;
|
|
681
689
|
};
|
|
682
|
-
export const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser) => {
|
|
690
|
+
export const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser, billVendorId, billDepartmentId) => {
|
|
683
691
|
let isCreatorAlsoApprover = false;
|
|
692
|
+
// Approval Rules 3.0 — a rule applies to a bill only when all of
|
|
693
|
+
// its present criteria are satisfied. Criteria not declared on
|
|
694
|
+
// the rule are treated as "no narrowing on this axis".
|
|
695
|
+
//
|
|
696
|
+
// Strict-match semantics for vendor / department: if the rule
|
|
697
|
+
// narrows by an axis but the bill has no value on that axis, the
|
|
698
|
+
// rule does NOT apply (an absent bill-side value can't satisfy
|
|
699
|
+
// either 'is' or 'is_not'). Amount is always present on a rule
|
|
700
|
+
// (the form enforces this), but bounds may be one-sided.
|
|
684
701
|
const approvalRule = approvalRules.find((rule) => {
|
|
702
|
+
const vendorCriteria = getVendorCriteria(rule.criteria);
|
|
703
|
+
if (vendorCriteria != null) {
|
|
704
|
+
if (billVendorId == null) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
const includesVendor = vendorCriteria.vendorIds.includes(billVendorId);
|
|
708
|
+
if (vendorCriteria.operator === 'is' && !includesVendor) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
if (vendorCriteria.operator === 'is_not' && includesVendor) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const departmentCriteria = getDepartmentCriteria(rule.criteria);
|
|
716
|
+
if (departmentCriteria != null) {
|
|
717
|
+
if (billDepartmentId == null) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
const includesDepartment = departmentCriteria.departmentIds.includes(billDepartmentId);
|
|
721
|
+
if (departmentCriteria.operator === 'is' && !includesDepartment) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
if (departmentCriteria.operator === 'is_not' && includesDepartment) {
|
|
725
|
+
return false;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
685
728
|
const amountCriteria = getAmountCriteria(rule.criteria);
|
|
686
|
-
if (amountCriteria
|
|
729
|
+
if (amountCriteria == null) {
|
|
730
|
+
// The form guarantees an amount criterion on every rule, so
|
|
731
|
+
// this branch is defensive against backend drift. Keep
|
|
732
|
+
// pre-3.0 behaviour: a rule that doesn't declare amount
|
|
733
|
+
// doesn't participate here.
|
|
687
734
|
return false;
|
|
688
735
|
}
|
|
736
|
+
// Bounds use the wire's inclusive 'gte' / 'lte' semantics, and
|
|
737
|
+
// either side may be absent for a one-sided 'greater_than' /
|
|
738
|
+
// 'less_than' rule.
|
|
689
739
|
const { min, max } = amountCriteria;
|
|
690
|
-
|
|
691
|
-
|
|
740
|
+
const matchesMin = min == null || billAmount >= min.amount;
|
|
741
|
+
const matchesMax = max == null || billAmount <= max.amount;
|
|
742
|
+
return matchesMin && matchesMax;
|
|
692
743
|
});
|
|
693
744
|
if (approvalRule != null) {
|
|
694
745
|
const requireApprovalSteps = approvalRule.steps.filter((step) => step.action === 'require_approval');
|
|
@@ -182,10 +182,15 @@ const remiSetupApproverView = createSlice({
|
|
|
182
182
|
reorderRemiApprovalRules: {
|
|
183
183
|
reducer(draft, action) {
|
|
184
184
|
const { approvalRuleIds } = action.payload;
|
|
185
|
+
const wasReorderInFlight = draft.reorderStatus.fetchState === 'In-Progress';
|
|
185
186
|
draft.reorderStatus = { fetchState: 'In-Progress', error: undefined };
|
|
186
187
|
// Stash the pre-reorder order for synchronous rollback on
|
|
187
|
-
// 'Failure'. See the BillPay reducer for the rationale
|
|
188
|
-
|
|
188
|
+
// 'Failure'. See the BillPay reducer for the rationale,
|
|
189
|
+
// including the in-flight guard: a second drag before the
|
|
190
|
+
// first PUT lands must not overwrite the snapshot.
|
|
191
|
+
if (!wasReorderInFlight) {
|
|
192
|
+
draft.reorderRollbackIds = draft.approvalRuleIds;
|
|
193
|
+
}
|
|
189
194
|
draft.approvalRuleIds = approvalRuleIds;
|
|
190
195
|
},
|
|
191
196
|
prepare(approvalRuleIds, useV3 = false) {
|
|
@@ -24,6 +24,13 @@ export interface BillPayReviewSelectorView extends SelectorView {
|
|
|
24
24
|
vendor: Vendor | undefined;
|
|
25
25
|
zeniAssistRetryStatus: FetchStateAndError | undefined;
|
|
26
26
|
billPayInfo?: BillPayInfo;
|
|
27
|
+
/**
|
|
28
|
+
* Bill-level accounting class id. Approval Rules 3.0 'department'
|
|
29
|
+
* criteria match against this — readers needing it (e.g. the
|
|
30
|
+
* creator-as-approver lookup) should read it off the view rather
|
|
31
|
+
* than digging through 'state.billTransactionState'.
|
|
32
|
+
*/
|
|
33
|
+
classId?: ID;
|
|
27
34
|
invoiceNumber?: string;
|
|
28
35
|
isBillMarkedForRetry?: boolean;
|
|
29
36
|
outsideZeniPaymentInfo?: OutsideZeniPaymentLocalData;
|
|
@@ -59,6 +59,12 @@ const getReviewPageBillDetail = (state, billId) => {
|
|
|
59
59
|
let recurringConfig;
|
|
60
60
|
let billPayInfo;
|
|
61
61
|
let isBillMarkedForRetry = false;
|
|
62
|
+
// Bill-level accounting class id. Approval Rules 3.0 'department'
|
|
63
|
+
// criteria match against this. Sourced from BillTransaction so
|
|
64
|
+
// any selector that needs it (e.g. creator-as-approver lookup)
|
|
65
|
+
// can read it off the view rather than reaching into
|
|
66
|
+
// 'state.billTransactionState' itself.
|
|
67
|
+
let classId;
|
|
62
68
|
const zeniAssistRetryStatus = billId != null
|
|
63
69
|
? billDetailViewState.billDetailById[key].zeniAssist.retryStatus
|
|
64
70
|
: undefined;
|
|
@@ -69,6 +75,13 @@ const getReviewPageBillDetail = (state, billId) => {
|
|
|
69
75
|
transaction?.billPayInfo.isCollectToAccountInfoFromVendorContact ?? false;
|
|
70
76
|
isRecurring = transaction?.isRecurring ?? false;
|
|
71
77
|
billPayInfo = transaction?.billPayInfo;
|
|
78
|
+
// Normalise empty-string to undefined so downstream readers
|
|
79
|
+
// treat "no class" uniformly regardless of whether the wire
|
|
80
|
+
// used '' or omitted the field.
|
|
81
|
+
classId =
|
|
82
|
+
transaction?.classId != null && transaction.classId !== ''
|
|
83
|
+
? transaction.classId
|
|
84
|
+
: undefined;
|
|
72
85
|
if (isRecurring &&
|
|
73
86
|
transaction?.recurringBillConfigId != null &&
|
|
74
87
|
transaction.recurringBillInstance != null) {
|
|
@@ -185,6 +198,7 @@ const getReviewPageBillDetail = (state, billId) => {
|
|
|
185
198
|
zeniAssistRetryStatus,
|
|
186
199
|
billPayInfo,
|
|
187
200
|
outsideZeniPaymentInfo,
|
|
201
|
+
classId,
|
|
188
202
|
};
|
|
189
203
|
};
|
|
190
204
|
exports.getReviewPageBillDetail = getReviewPageBillDetail;
|
package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewReducer.js
CHANGED
|
@@ -169,13 +169,23 @@ const billPaySetupApproverView = (0, toolkit_1.createSlice)({
|
|
|
169
169
|
reorderBillPayApprovalRules: {
|
|
170
170
|
reducer(draft, action) {
|
|
171
171
|
const { approvalRuleIds } = action.payload;
|
|
172
|
+
const wasReorderInFlight = draft.reorderStatus.fetchState === 'In-Progress';
|
|
172
173
|
draft.reorderStatus = { fetchState: 'In-Progress', error: undefined };
|
|
173
174
|
// Stash the pre-reorder order so 'Failure' can restore it
|
|
174
175
|
// synchronously. The epic's refetch on failure remains as a
|
|
175
176
|
// safety net (covers cases where the server's truth has
|
|
176
177
|
// drifted from the local snapshot), but the snappy UX path
|
|
177
178
|
// is the in-memory rollback.
|
|
178
|
-
|
|
179
|
+
//
|
|
180
|
+
// Only capture a fresh snapshot when no reorder is already in
|
|
181
|
+
// flight. If a second drag fires before the first PUT lands,
|
|
182
|
+
// the existing snapshot already holds the last server-backed
|
|
183
|
+
// order; overwriting it with the first reorder's optimistic
|
|
184
|
+
// result would mean a Failure rolls back to an uncommitted
|
|
185
|
+
// intermediate order instead of the last good list.
|
|
186
|
+
if (!wasReorderInFlight) {
|
|
187
|
+
draft.reorderRollbackIds = draft.approvalRuleIds;
|
|
188
|
+
}
|
|
179
189
|
// Optimistic: render new order immediately. On failure, the
|
|
180
190
|
// reducer restores 'reorderRollbackIds' and the epic refetch
|
|
181
191
|
// reconciles any drift.
|
|
@@ -30,42 +30,51 @@ interface ApprovalUpdatableInfoPayload {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* V1 / pre-3.0 wire shape. Kept so the rollback flag (gated by
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* 'approval_rule_v3_config' dynamic config) can target the original
|
|
34
|
+
* '/approval-rules' endpoint with the legacy snake_case array of
|
|
35
|
+
* range entries. None of the 3.0 rule-level fields are sent.
|
|
36
|
+
*
|
|
37
|
+
* Reference: 'entityApprovalStatusPayload.json' fixture, which carries
|
|
38
|
+
* a real V1 approval rule from the legacy backend:
|
|
39
|
+
* criteria: [{type: 'range', range_entity: 'currency',
|
|
40
|
+
* min, max, currency_code, currency_symbol}]
|
|
41
|
+
* with 'min' / 'max' as scalar numbers and 'null' representing an
|
|
42
|
+
* absent bound for one-sided rules. The read-side mapper in
|
|
43
|
+
* 'approvalRulePayload.ts' (see 'LegacyAmountRangeCriterionPayload')
|
|
44
|
+
* decodes the same shape — write-side has to match.
|
|
36
45
|
*/
|
|
46
|
+
interface ApprovalUpdatableInfoPayloadV1CriterionEntry {
|
|
47
|
+
currency_code: string;
|
|
48
|
+
currency_symbol: string;
|
|
49
|
+
/** Upper bound. 'null' for a 'greater_than' (open-upper) rule. */
|
|
50
|
+
max: number | null;
|
|
51
|
+
/** Lower bound. 'null' for a 'less_than' (open-lower) rule. */
|
|
52
|
+
min: number | null;
|
|
53
|
+
range_entity: 'currency';
|
|
54
|
+
type: 'range';
|
|
55
|
+
}
|
|
37
56
|
interface ApprovalUpdatableInfoPayloadV1 {
|
|
38
|
-
criteria:
|
|
39
|
-
range: {
|
|
40
|
-
max: {
|
|
41
|
-
amount: number;
|
|
42
|
-
currencyCode: string;
|
|
43
|
-
currencySymbol: string;
|
|
44
|
-
};
|
|
45
|
-
min: {
|
|
46
|
-
amount: number;
|
|
47
|
-
currencyCode: string;
|
|
48
|
-
currencySymbol: string;
|
|
49
|
-
};
|
|
50
|
-
};
|
|
51
|
-
rangeEntity: 'currency';
|
|
52
|
-
rangeType: 'range';
|
|
53
|
-
};
|
|
57
|
+
criteria: ApprovalUpdatableInfoPayloadV1CriterionEntry[];
|
|
54
58
|
is_applicable_on_pending_approval_entity: boolean;
|
|
55
59
|
steps: ApproverViewStepPayload[];
|
|
56
60
|
approval_rule_id?: ID;
|
|
57
61
|
}
|
|
58
62
|
export declare const toApprovalChangableInfoPayload: (approverViewUpdateData: ApprovalRuleUpdateData | ApprovalRuleCreateData) => Partial<ApprovalUpdatableInfoPayload>;
|
|
59
63
|
/**
|
|
60
|
-
* V1 wire mapper — used when
|
|
61
|
-
* for the current tenant.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
64
|
+
* V1 wire mapper — used when 'approval_rule_v3_config' resolves to OFF
|
|
65
|
+
* for the current tenant. Emits the legacy snake_case array shape that
|
|
66
|
+
* '/approval-rules' historically consumed (see the V1 fixture in
|
|
67
|
+
* 'entity/entityApprovalStatus/__mocks__/json/entityApprovalStatusPayload.json'):
|
|
68
|
+
* criteria: [{type: 'range', range_entity: 'currency', min, max,
|
|
69
|
+
* currency_code, currency_symbol}]
|
|
70
|
+
*
|
|
71
|
+
* Open-ended semantics are preserved on the wire: an absent 'min' or
|
|
72
|
+
* 'max' is sent as 'null' rather than synthesised to '0' or
|
|
73
|
+
* 'MAX_SAFE_INTEGER', so a 'greater_than' / 'less_than' rule stays
|
|
74
|
+
* one-sided after rollback rather than collapsing into a closed range.
|
|
65
75
|
*
|
|
66
|
-
* The
|
|
67
|
-
*
|
|
68
|
-
* falls back to `0` / `Number.MAX_SAFE_INTEGER`.
|
|
76
|
+
* The V1 backend does not understand vendor / department criteria or
|
|
77
|
+
* the rule-level 3.0 fields, so those are dropped on the wire.
|
|
69
78
|
*/
|
|
70
79
|
export declare const toApprovalChangableInfoPayloadV1: (approverViewUpdateData: ApprovalRuleUpdateData | ApprovalRuleCreateData) => Partial<ApprovalUpdatableInfoPayloadV1>;
|
|
71
80
|
export {};
|
|
@@ -95,15 +95,20 @@ const toApprovalChangableInfoPayload = (approverViewUpdateData) => {
|
|
|
95
95
|
};
|
|
96
96
|
exports.toApprovalChangableInfoPayload = toApprovalChangableInfoPayload;
|
|
97
97
|
/**
|
|
98
|
-
* V1 wire mapper — used when
|
|
99
|
-
* for the current tenant.
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
98
|
+
* V1 wire mapper — used when 'approval_rule_v3_config' resolves to OFF
|
|
99
|
+
* for the current tenant. Emits the legacy snake_case array shape that
|
|
100
|
+
* '/approval-rules' historically consumed (see the V1 fixture in
|
|
101
|
+
* 'entity/entityApprovalStatus/__mocks__/json/entityApprovalStatusPayload.json'):
|
|
102
|
+
* criteria: [{type: 'range', range_entity: 'currency', min, max,
|
|
103
|
+
* currency_code, currency_symbol}]
|
|
103
104
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
105
|
+
* Open-ended semantics are preserved on the wire: an absent 'min' or
|
|
106
|
+
* 'max' is sent as 'null' rather than synthesised to '0' or
|
|
107
|
+
* 'MAX_SAFE_INTEGER', so a 'greater_than' / 'less_than' rule stays
|
|
108
|
+
* one-sided after rollback rather than collapsing into a closed range.
|
|
109
|
+
*
|
|
110
|
+
* The V1 backend does not understand vendor / department criteria or
|
|
111
|
+
* the rule-level 3.0 fields, so those are dropped on the wire.
|
|
107
112
|
*/
|
|
108
113
|
const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
109
114
|
const amount = approverViewUpdateData.criteria?.amount;
|
|
@@ -129,25 +134,18 @@ const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
|
129
134
|
};
|
|
130
135
|
});
|
|
131
136
|
const isUpdate = approverViewUpdateData.approvalRuleId != null;
|
|
137
|
+
const criteriaEntry = {
|
|
138
|
+
type: 'range',
|
|
139
|
+
range_entity: 'currency',
|
|
140
|
+
min: minAmount?.amount ?? null,
|
|
141
|
+
max: maxAmount?.amount ?? null,
|
|
142
|
+
currency_code: currencyCode,
|
|
143
|
+
currency_symbol: currencySymbol,
|
|
144
|
+
};
|
|
132
145
|
const payload = {
|
|
133
146
|
steps,
|
|
134
147
|
is_applicable_on_pending_approval_entity: approverViewUpdateData.isApplicableOnPendingApprovalEntity ?? true,
|
|
135
|
-
criteria:
|
|
136
|
-
rangeType: 'range',
|
|
137
|
-
rangeEntity: 'currency',
|
|
138
|
-
range: {
|
|
139
|
-
min: {
|
|
140
|
-
amount: minAmount?.amount ?? 0,
|
|
141
|
-
currencyCode,
|
|
142
|
-
currencySymbol,
|
|
143
|
-
},
|
|
144
|
-
max: {
|
|
145
|
-
amount: maxAmount?.amount ?? Number.MAX_SAFE_INTEGER,
|
|
146
|
-
currencyCode,
|
|
147
|
-
currencySymbol,
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
148
|
+
criteria: [criteriaEntry],
|
|
151
149
|
};
|
|
152
150
|
if (isUpdate) {
|
|
153
151
|
payload.approval_rule_id = approverViewUpdateData.approvalRuleId;
|
|
@@ -100,6 +100,13 @@ export interface EditBillDetailSelectorView extends SelectorView {
|
|
|
100
100
|
withdrawFromList: FundingAccount[];
|
|
101
101
|
billPayInfo?: BillPayInfo;
|
|
102
102
|
bookCloseDate?: ZeniDate;
|
|
103
|
+
/**
|
|
104
|
+
* Bill-level accounting class id. Approval Rules 3.0 'department'
|
|
105
|
+
* criteria match against this — selectors needing to evaluate a
|
|
106
|
+
* department-narrowed rule against the current bill should read
|
|
107
|
+
* this rather than digging through 'state.billTransactionState'.
|
|
108
|
+
*/
|
|
109
|
+
classId?: ID;
|
|
103
110
|
createTime?: ZeniDate;
|
|
104
111
|
editBillInitialDetails?: EditBillInitialDetails;
|
|
105
112
|
internationalWireOnboardingStatus?: AllowedValueWithCode;
|
|
@@ -157,4 +164,4 @@ export declare const getDefaultWithdrawFromAccount: (state: RootState, depositAc
|
|
|
157
164
|
accountId: ID;
|
|
158
165
|
accountType: AccountTypeSubConfigCodeKeyType;
|
|
159
166
|
}) => FundingAccount | undefined;
|
|
160
|
-
export declare const checkIfCreatorIsApprover: (billAmount: number, approvalRules: ApprovalRuleWithUser[], signedInUser?: LoggedInUser) => boolean;
|
|
167
|
+
export declare const checkIfCreatorIsApprover: (billAmount: number, approvalRules: ApprovalRuleWithUser[], signedInUser?: LoggedInUser, billVendorId?: ID, billDepartmentId?: ID) => boolean;
|
|
@@ -366,6 +366,14 @@ const getEditBillDetail = (state, isVendorRecommendationFeatureEnabled, isZeniAc
|
|
|
366
366
|
isDeleted,
|
|
367
367
|
showAutofill,
|
|
368
368
|
addBillAutoFields,
|
|
369
|
+
// Bill-level accounting class. Approval Rules 3.0 'department'
|
|
370
|
+
// criteria match against this. Lives on BillTransaction; not on
|
|
371
|
+
// any sub-section. Empty-string is normalised to 'undefined'
|
|
372
|
+
// so downstream readers treat "no class" uniformly regardless
|
|
373
|
+
// of whether the wire used '' or omitted the field.
|
|
374
|
+
classId: transaction?.classId != null && transaction.classId !== ''
|
|
375
|
+
? transaction.classId
|
|
376
|
+
: undefined,
|
|
369
377
|
billStage,
|
|
370
378
|
deleteStatus,
|
|
371
379
|
billActivities,
|
|
@@ -696,16 +704,59 @@ const getDefaultWithdrawFromAccount = (state, depositAccounts, companyId, lastSe
|
|
|
696
704
|
return undefined;
|
|
697
705
|
};
|
|
698
706
|
exports.getDefaultWithdrawFromAccount = getDefaultWithdrawFromAccount;
|
|
699
|
-
const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser) => {
|
|
707
|
+
const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser, billVendorId, billDepartmentId) => {
|
|
700
708
|
let isCreatorAlsoApprover = false;
|
|
709
|
+
// Approval Rules 3.0 — a rule applies to a bill only when all of
|
|
710
|
+
// its present criteria are satisfied. Criteria not declared on
|
|
711
|
+
// the rule are treated as "no narrowing on this axis".
|
|
712
|
+
//
|
|
713
|
+
// Strict-match semantics for vendor / department: if the rule
|
|
714
|
+
// narrows by an axis but the bill has no value on that axis, the
|
|
715
|
+
// rule does NOT apply (an absent bill-side value can't satisfy
|
|
716
|
+
// either 'is' or 'is_not'). Amount is always present on a rule
|
|
717
|
+
// (the form enforces this), but bounds may be one-sided.
|
|
701
718
|
const approvalRule = approvalRules.find((rule) => {
|
|
719
|
+
const vendorCriteria = (0, approvalRuleSelector_1.getVendorCriteria)(rule.criteria);
|
|
720
|
+
if (vendorCriteria != null) {
|
|
721
|
+
if (billVendorId == null) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
const includesVendor = vendorCriteria.vendorIds.includes(billVendorId);
|
|
725
|
+
if (vendorCriteria.operator === 'is' && !includesVendor) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
if (vendorCriteria.operator === 'is_not' && includesVendor) {
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const departmentCriteria = (0, approvalRuleSelector_1.getDepartmentCriteria)(rule.criteria);
|
|
733
|
+
if (departmentCriteria != null) {
|
|
734
|
+
if (billDepartmentId == null) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const includesDepartment = departmentCriteria.departmentIds.includes(billDepartmentId);
|
|
738
|
+
if (departmentCriteria.operator === 'is' && !includesDepartment) {
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
if (departmentCriteria.operator === 'is_not' && includesDepartment) {
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
702
745
|
const amountCriteria = (0, approvalRuleSelector_1.getAmountCriteria)(rule.criteria);
|
|
703
|
-
if (amountCriteria
|
|
746
|
+
if (amountCriteria == null) {
|
|
747
|
+
// The form guarantees an amount criterion on every rule, so
|
|
748
|
+
// this branch is defensive against backend drift. Keep
|
|
749
|
+
// pre-3.0 behaviour: a rule that doesn't declare amount
|
|
750
|
+
// doesn't participate here.
|
|
704
751
|
return false;
|
|
705
752
|
}
|
|
753
|
+
// Bounds use the wire's inclusive 'gte' / 'lte' semantics, and
|
|
754
|
+
// either side may be absent for a one-sided 'greater_than' /
|
|
755
|
+
// 'less_than' rule.
|
|
706
756
|
const { min, max } = amountCriteria;
|
|
707
|
-
|
|
708
|
-
|
|
757
|
+
const matchesMin = min == null || billAmount >= min.amount;
|
|
758
|
+
const matchesMax = max == null || billAmount <= max.amount;
|
|
759
|
+
return matchesMin && matchesMax;
|
|
709
760
|
});
|
|
710
761
|
if (approvalRule != null) {
|
|
711
762
|
const requireApprovalSteps = approvalRule.steps.filter((step) => step.action === 'require_approval');
|
package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js
CHANGED
|
@@ -186,10 +186,15 @@ const remiSetupApproverView = (0, toolkit_1.createSlice)({
|
|
|
186
186
|
reorderRemiApprovalRules: {
|
|
187
187
|
reducer(draft, action) {
|
|
188
188
|
const { approvalRuleIds } = action.payload;
|
|
189
|
+
const wasReorderInFlight = draft.reorderStatus.fetchState === 'In-Progress';
|
|
189
190
|
draft.reorderStatus = { fetchState: 'In-Progress', error: undefined };
|
|
190
191
|
// Stash the pre-reorder order for synchronous rollback on
|
|
191
|
-
// 'Failure'. See the BillPay reducer for the rationale
|
|
192
|
-
|
|
192
|
+
// 'Failure'. See the BillPay reducer for the rationale,
|
|
193
|
+
// including the in-flight guard: a second drag before the
|
|
194
|
+
// first PUT lands must not overwrite the snapshot.
|
|
195
|
+
if (!wasReorderInFlight) {
|
|
196
|
+
draft.reorderRollbackIds = draft.approvalRuleIds;
|
|
197
|
+
}
|
|
193
198
|
draft.approvalRuleIds = approvalRuleIds;
|
|
194
199
|
},
|
|
195
200
|
prepare(approvalRuleIds, useV3 = false) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.0.90-
|
|
3
|
+
"version": "5.0.90-betaAR4",
|
|
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",
|