@zeniai/client-epic-state 5.0.90-betaAR1 → 5.0.90-betaAR3
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 +24 -21
- package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +40 -23
- 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 +27 -33
- package/lib/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.js +24 -21
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +7 -0
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +40 -23
- 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,19 +91,15 @@ 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. Translates the form-side amount criteria into
|
|
96
|
+
* the legacy `{rangeType, rangeEntity, range: {min, max}}` shape. The
|
|
97
|
+
* V1 backend does not understand vendor / department criteria or the
|
|
98
|
+
* rule-level 3.0 fields, so those are dropped on the wire.
|
|
99
99
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
* one-sided after rollback rather than collapsing into a closed range.
|
|
104
|
-
*
|
|
105
|
-
* The V1 backend does not understand vendor / department criteria or
|
|
106
|
-
* the rule-level 3.0 fields, so those are dropped on the wire.
|
|
100
|
+
* The form-side amount slot is always read as a `range`-style criteria
|
|
101
|
+
* here: V1 schemas only supported ranges, so an absent `min` or `max`
|
|
102
|
+
* falls back to `0` / `Number.MAX_SAFE_INTEGER`.
|
|
107
103
|
*/
|
|
108
104
|
export const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
109
105
|
const amount = approverViewUpdateData.criteria?.amount;
|
|
@@ -129,18 +125,25 @@ export const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
|
129
125
|
};
|
|
130
126
|
});
|
|
131
127
|
const isUpdate = approverViewUpdateData.approvalRuleId != null;
|
|
132
|
-
const criteriaEntry = {
|
|
133
|
-
type: 'range',
|
|
134
|
-
range_entity: 'currency',
|
|
135
|
-
min: minAmount?.amount ?? null,
|
|
136
|
-
max: maxAmount?.amount ?? null,
|
|
137
|
-
currency_code: currencyCode,
|
|
138
|
-
currency_symbol: currencySymbol,
|
|
139
|
-
};
|
|
140
128
|
const payload = {
|
|
141
129
|
steps,
|
|
142
130
|
is_applicable_on_pending_approval_entity: approverViewUpdateData.isApplicableOnPendingApprovalEntity ?? true,
|
|
143
|
-
criteria:
|
|
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
147
|
};
|
|
145
148
|
if (isUpdate) {
|
|
146
149
|
payload.approval_rule_id = approverViewUpdateData.approvalRuleId;
|
|
@@ -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,
|
|
@@ -681,29 +689,22 @@ export const getDefaultWithdrawFromAccount = (state, depositAccounts, companyId,
|
|
|
681
689
|
};
|
|
682
690
|
export const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser, billVendorId, billDepartmentId) => {
|
|
683
691
|
let isCreatorAlsoApprover = false;
|
|
684
|
-
// Approval Rules 3.0 — a rule
|
|
685
|
-
//
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
//
|
|
689
|
-
//
|
|
690
|
-
//
|
|
691
|
-
//
|
|
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.
|
|
692
701
|
const approvalRule = approvalRules.find((rule) => {
|
|
693
|
-
const amountCriteria = getAmountCriteria(rule.criteria);
|
|
694
|
-
if (amountCriteria == null) {
|
|
695
|
-
return false;
|
|
696
|
-
}
|
|
697
|
-
const { min, max } = amountCriteria;
|
|
698
|
-
const matchesMin = min == null || billAmount >= min.amount;
|
|
699
|
-
const matchesMax = max == null || billAmount <= max.amount;
|
|
700
|
-
if (!matchesMin || !matchesMax) {
|
|
701
|
-
return false;
|
|
702
|
-
}
|
|
703
702
|
const vendorCriteria = getVendorCriteria(rule.criteria);
|
|
704
703
|
if (vendorCriteria != null) {
|
|
705
|
-
|
|
706
|
-
|
|
704
|
+
if (billVendorId == null) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
const includesVendor = vendorCriteria.vendorIds.includes(billVendorId);
|
|
707
708
|
if (vendorCriteria.operator === 'is' && !includesVendor) {
|
|
708
709
|
return false;
|
|
709
710
|
}
|
|
@@ -713,8 +714,10 @@ export const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser
|
|
|
713
714
|
}
|
|
714
715
|
const departmentCriteria = getDepartmentCriteria(rule.criteria);
|
|
715
716
|
if (departmentCriteria != null) {
|
|
716
|
-
|
|
717
|
-
|
|
717
|
+
if (billDepartmentId == null) {
|
|
718
|
+
return false;
|
|
719
|
+
}
|
|
720
|
+
const includesDepartment = departmentCriteria.departmentIds.includes(billDepartmentId);
|
|
718
721
|
if (departmentCriteria.operator === 'is' && !includesDepartment) {
|
|
719
722
|
return false;
|
|
720
723
|
}
|
|
@@ -722,7 +725,21 @@ export const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser
|
|
|
722
725
|
return false;
|
|
723
726
|
}
|
|
724
727
|
}
|
|
725
|
-
|
|
728
|
+
const amountCriteria = getAmountCriteria(rule.criteria);
|
|
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.
|
|
734
|
+
return false;
|
|
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.
|
|
739
|
+
const { min, max } = amountCriteria;
|
|
740
|
+
const matchesMin = min == null || billAmount >= min.amount;
|
|
741
|
+
const matchesMax = max == null || billAmount <= max.amount;
|
|
742
|
+
return matchesMin && matchesMax;
|
|
726
743
|
});
|
|
727
744
|
if (approvalRule != null) {
|
|
728
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,48 +30,42 @@ interface ApprovalUpdatableInfoPayload {
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
32
|
* V1 / pre-3.0 wire shape. Kept so the rollback flag (gated by
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* The wire historically expected:
|
|
38
|
-
* criteria: [{type, range_entity, min, max, currency_code, currency_symbol}]
|
|
39
|
-
* with 'min' and 'max' as scalar numbers, and 'null' representing an
|
|
40
|
-
* absent bound for one-sided rules. See 'LegacyAmountRangeCriterionPayload'
|
|
41
|
-
* in '../../../../../entity/approvalRule/approvalRulePayload.ts' for the
|
|
42
|
-
* matching read-side shape.
|
|
33
|
+
* `approval_rule_v3_config` dynamic config) can target the original
|
|
34
|
+
* `/approval-rules` endpoint with the legacy `{rangeType, rangeEntity, range}`
|
|
35
|
+
* criteria. None of the 3.0 rule-level fields are sent.
|
|
43
36
|
*/
|
|
44
|
-
interface ApprovalUpdatableInfoPayloadV1CriterionEntry {
|
|
45
|
-
currency_code: string;
|
|
46
|
-
currency_symbol: string;
|
|
47
|
-
/** Upper bound. 'null' for a 'greater_than' rule. */
|
|
48
|
-
max: number | null;
|
|
49
|
-
/** Lower bound. 'null' for a 'less_than' rule. */
|
|
50
|
-
min: number | null;
|
|
51
|
-
range_entity: 'currency';
|
|
52
|
-
type: 'range';
|
|
53
|
-
}
|
|
54
37
|
interface ApprovalUpdatableInfoPayloadV1 {
|
|
55
|
-
criteria:
|
|
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
|
+
};
|
|
56
54
|
is_applicable_on_pending_approval_entity: boolean;
|
|
57
55
|
steps: ApproverViewStepPayload[];
|
|
58
56
|
approval_rule_id?: ID;
|
|
59
57
|
}
|
|
60
58
|
export declare const toApprovalChangableInfoPayload: (approverViewUpdateData: ApprovalRuleUpdateData | ApprovalRuleCreateData) => Partial<ApprovalUpdatableInfoPayload>;
|
|
61
59
|
/**
|
|
62
|
-
* V1 wire mapper — used when
|
|
63
|
-
* for the current tenant.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* Open-ended semantics are preserved on the wire: an absent 'min' or
|
|
69
|
-
* 'max' is sent as 'null' rather than synthesised to '0' or
|
|
70
|
-
* 'MAX_SAFE_INTEGER', so a 'greater_than' / 'less_than' rule stays
|
|
71
|
-
* one-sided after rollback rather than collapsing into a closed range.
|
|
60
|
+
* V1 wire mapper — used when `approval_rule_v3_config` resolves to OFF
|
|
61
|
+
* for the current tenant. Translates the form-side amount criteria into
|
|
62
|
+
* the legacy `{rangeType, rangeEntity, range: {min, max}}` shape. The
|
|
63
|
+
* V1 backend does not understand vendor / department criteria or the
|
|
64
|
+
* rule-level 3.0 fields, so those are dropped on the wire.
|
|
72
65
|
*
|
|
73
|
-
* The
|
|
74
|
-
*
|
|
66
|
+
* The form-side amount slot is always read as a `range`-style criteria
|
|
67
|
+
* here: V1 schemas only supported ranges, so an absent `min` or `max`
|
|
68
|
+
* falls back to `0` / `Number.MAX_SAFE_INTEGER`.
|
|
75
69
|
*/
|
|
76
70
|
export declare const toApprovalChangableInfoPayloadV1: (approverViewUpdateData: ApprovalRuleUpdateData | ApprovalRuleCreateData) => Partial<ApprovalUpdatableInfoPayloadV1>;
|
|
77
71
|
export {};
|
|
@@ -95,19 +95,15 @@ 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. Translates the form-side amount criteria into
|
|
100
|
+
* the legacy `{rangeType, rangeEntity, range: {min, max}}` shape. The
|
|
101
|
+
* V1 backend does not understand vendor / department criteria or the
|
|
102
|
+
* rule-level 3.0 fields, so those are dropped on the wire.
|
|
103
103
|
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* one-sided after rollback rather than collapsing into a closed range.
|
|
108
|
-
*
|
|
109
|
-
* The V1 backend does not understand vendor / department criteria or
|
|
110
|
-
* the rule-level 3.0 fields, so those are dropped on the wire.
|
|
104
|
+
* The form-side amount slot is always read as a `range`-style criteria
|
|
105
|
+
* here: V1 schemas only supported ranges, so an absent `min` or `max`
|
|
106
|
+
* falls back to `0` / `Number.MAX_SAFE_INTEGER`.
|
|
111
107
|
*/
|
|
112
108
|
const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
113
109
|
const amount = approverViewUpdateData.criteria?.amount;
|
|
@@ -133,18 +129,25 @@ const toApprovalChangableInfoPayloadV1 = (approverViewUpdateData) => {
|
|
|
133
129
|
};
|
|
134
130
|
});
|
|
135
131
|
const isUpdate = approverViewUpdateData.approvalRuleId != null;
|
|
136
|
-
const criteriaEntry = {
|
|
137
|
-
type: 'range',
|
|
138
|
-
range_entity: 'currency',
|
|
139
|
-
min: minAmount?.amount ?? null,
|
|
140
|
-
max: maxAmount?.amount ?? null,
|
|
141
|
-
currency_code: currencyCode,
|
|
142
|
-
currency_symbol: currencySymbol,
|
|
143
|
-
};
|
|
144
132
|
const payload = {
|
|
145
133
|
steps,
|
|
146
134
|
is_applicable_on_pending_approval_entity: approverViewUpdateData.isApplicableOnPendingApprovalEntity ?? true,
|
|
147
|
-
criteria:
|
|
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
151
|
};
|
|
149
152
|
if (isUpdate) {
|
|
150
153
|
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;
|
|
@@ -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,
|
|
@@ -698,29 +706,22 @@ const getDefaultWithdrawFromAccount = (state, depositAccounts, companyId, lastSe
|
|
|
698
706
|
exports.getDefaultWithdrawFromAccount = getDefaultWithdrawFromAccount;
|
|
699
707
|
const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser, billVendorId, billDepartmentId) => {
|
|
700
708
|
let isCreatorAlsoApprover = false;
|
|
701
|
-
// Approval Rules 3.0 — a rule
|
|
702
|
-
//
|
|
703
|
-
//
|
|
704
|
-
//
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
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.
|
|
709
718
|
const approvalRule = approvalRules.find((rule) => {
|
|
710
|
-
const amountCriteria = (0, approvalRuleSelector_1.getAmountCriteria)(rule.criteria);
|
|
711
|
-
if (amountCriteria == null) {
|
|
712
|
-
return false;
|
|
713
|
-
}
|
|
714
|
-
const { min, max } = amountCriteria;
|
|
715
|
-
const matchesMin = min == null || billAmount >= min.amount;
|
|
716
|
-
const matchesMax = max == null || billAmount <= max.amount;
|
|
717
|
-
if (!matchesMin || !matchesMax) {
|
|
718
|
-
return false;
|
|
719
|
-
}
|
|
720
719
|
const vendorCriteria = (0, approvalRuleSelector_1.getVendorCriteria)(rule.criteria);
|
|
721
720
|
if (vendorCriteria != null) {
|
|
722
|
-
|
|
723
|
-
|
|
721
|
+
if (billVendorId == null) {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
const includesVendor = vendorCriteria.vendorIds.includes(billVendorId);
|
|
724
725
|
if (vendorCriteria.operator === 'is' && !includesVendor) {
|
|
725
726
|
return false;
|
|
726
727
|
}
|
|
@@ -730,8 +731,10 @@ const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser, billV
|
|
|
730
731
|
}
|
|
731
732
|
const departmentCriteria = (0, approvalRuleSelector_1.getDepartmentCriteria)(rule.criteria);
|
|
732
733
|
if (departmentCriteria != null) {
|
|
733
|
-
|
|
734
|
-
|
|
734
|
+
if (billDepartmentId == null) {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
const includesDepartment = departmentCriteria.departmentIds.includes(billDepartmentId);
|
|
735
738
|
if (departmentCriteria.operator === 'is' && !includesDepartment) {
|
|
736
739
|
return false;
|
|
737
740
|
}
|
|
@@ -739,7 +742,21 @@ const checkIfCreatorIsApprover = (billAmount, approvalRules, signedInUser, billV
|
|
|
739
742
|
return false;
|
|
740
743
|
}
|
|
741
744
|
}
|
|
742
|
-
|
|
745
|
+
const amountCriteria = (0, approvalRuleSelector_1.getAmountCriteria)(rule.criteria);
|
|
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.
|
|
751
|
+
return false;
|
|
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.
|
|
756
|
+
const { min, max } = amountCriteria;
|
|
757
|
+
const matchesMin = min == null || billAmount >= min.amount;
|
|
758
|
+
const matchesMax = max == null || billAmount <= max.amount;
|
|
759
|
+
return matchesMin && matchesMax;
|
|
743
760
|
});
|
|
744
761
|
if (approvalRule != null) {
|
|
745
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-betaAR3",
|
|
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",
|