@zeniai/client-epic-state 5.0.88-betaAR2 → 5.0.88-betaAS2
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.
|
@@ -28,87 +28,28 @@ export const TRANSACTION_FILTER_CATEGORIES = [
|
|
|
28
28
|
{ value: 'class', type: 'dropdown' },
|
|
29
29
|
{ value: 'amount', type: 'numberRange' },
|
|
30
30
|
];
|
|
31
|
-
/**
|
|
32
|
-
* Per-line amounts for a transaction, preserving the canonical source
|
|
33
|
-
* precedence:
|
|
34
|
-
*
|
|
35
|
-
* 1. `transactionLocalData.transactionReviewLocalData.lineItemById` —
|
|
36
|
-
* the locally-edited line items the user sees in the grid. Reflects
|
|
37
|
-
* unsaved edits so the filter matches what the user actually has on
|
|
38
|
-
* screen, not the pre-edit server state.
|
|
39
|
-
* 2. `transaction.transaction.lines` — persisted lines from the
|
|
40
|
-
* transaction payload.
|
|
41
|
-
* 3. `[transaction.amount.amount]` — single-element fallback for
|
|
42
|
-
* transactions that don't have a `lines` array at all (treat the
|
|
43
|
-
* transaction-level amount as a one-line transaction).
|
|
44
|
-
*
|
|
45
|
-
* Returned as an array so the amount filter in `transactionMatchesCategory`
|
|
46
|
-
* can OR-combine per-line `matchAmount` results: a transaction is included
|
|
47
|
-
* if ANY of its lines satisfies the operator, and the full row (with all
|
|
48
|
-
* lines, matching or not) renders downstream. Before the per-line change
|
|
49
|
-
* this helper's predecessor in `getCategoryValueForTransaction` summed
|
|
50
|
-
* these values into a single aggregate and compared the sum.
|
|
51
|
-
*
|
|
52
|
-
* **Missing amounts.** Lines (or transactions) whose amount field is
|
|
53
|
-
* `null` / `undefined` / `NaN` are *dropped* from the returned array
|
|
54
|
-
* rather than coerced to `0`. Coercing to `0` would silently make a
|
|
55
|
-
* row with no amount data match operators like `less_than 100`, even
|
|
56
|
-
* though we genuinely don't know what the amount is — a semantic
|
|
57
|
-
* regression flagged in PR review. By filtering instead, the empty-array
|
|
58
|
-
* case bubbles up to `transactionMatchesCategory`, which falls back to
|
|
59
|
-
* the same "absent value: only `not_equal` matches" contract used by
|
|
60
|
-
* non-amount fields.
|
|
61
|
-
*
|
|
62
|
-
* The defensive `?.` chains stay in place so a partial payload (state
|
|
63
|
-
* init, Pusher mid-sync, partially-resolved server response) doesn't
|
|
64
|
-
* throw; the difference is `?? 0` → filter-out.
|
|
65
|
-
*/
|
|
66
|
-
const getLineAmountsForTransaction = (transaction) => {
|
|
67
|
-
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
68
|
-
if (localData?.lineItemById != null) {
|
|
69
|
-
const lineItems = Object.values(localData.lineItemById);
|
|
70
|
-
if (lineItems.length > 0) {
|
|
71
|
-
return lineItems
|
|
72
|
-
.map((lineItem) => lineItem?.amount?.amount)
|
|
73
|
-
.filter(isFiniteNumber);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
if (transaction?.transaction?.lines != null &&
|
|
77
|
-
transaction.transaction.lines.length > 0) {
|
|
78
|
-
return transaction.transaction.lines
|
|
79
|
-
.map((line) => line?.amount?.amount)
|
|
80
|
-
.filter(isFiniteNumber);
|
|
81
|
-
}
|
|
82
|
-
const txAmount = transaction?.amount?.amount;
|
|
83
|
-
return isFiniteNumber(txAmount) ? [txAmount] : [];
|
|
84
|
-
};
|
|
85
|
-
/**
|
|
86
|
-
* Type guard that survives an `Array<number | undefined>.filter(...)` and
|
|
87
|
-
* narrows the result to `number[]`. Also rejects `NaN` so downstream
|
|
88
|
-
* `matchAmount` never sees a value that fails every operator silently
|
|
89
|
-
* (because every comparison with `NaN` is false, `NaN < 100`, `NaN > 100`,
|
|
90
|
-
* `NaN === N` all yield `false` — the row would silently slip through
|
|
91
|
-
* `not_equal` but fail every other operator, which is exactly the
|
|
92
|
-
* absent-value contract we want to apply explicitly via an empty array
|
|
93
|
-
* instead).
|
|
94
|
-
*/
|
|
95
|
-
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
96
31
|
/**
|
|
97
32
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
98
33
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
99
34
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
100
|
-
*
|
|
101
|
-
* NOTE: the `'amount'` field is NOT handled here. Amount matching is
|
|
102
|
-
* per-line (see `getLineAmountsForTransaction` + the early-return in
|
|
103
|
-
* `transactionMatchesCategory`) and would otherwise need to return a
|
|
104
|
-
* non-scalar value that doesn't fit this helper's contract.
|
|
105
35
|
*/
|
|
106
36
|
const getCategoryValueForTransaction = (key, transaction) => {
|
|
107
37
|
switch (key) {
|
|
108
38
|
case 'amount': {
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
39
|
+
// Prefer transactionLocalData line items (sum). Fall back to lines on
|
|
40
|
+
// the transaction itself. Final fallback: transaction-level amount.
|
|
41
|
+
const localDataForAmount = transaction.transactionLocalData?.transactionReviewLocalData;
|
|
42
|
+
if (localDataForAmount?.lineItemById) {
|
|
43
|
+
const lineItems = Object.values(localDataForAmount.lineItemById);
|
|
44
|
+
if (lineItems.length > 0) {
|
|
45
|
+
return lineItems.reduce((sum, lineItem) => sum + (lineItem.amount?.amount ?? 0), 0);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (transaction.transaction.lines &&
|
|
49
|
+
transaction.transaction.lines.length > 0) {
|
|
50
|
+
return transaction.transaction.lines.reduce((sum, line) => sum + (line.amount?.amount ?? 0), 0);
|
|
51
|
+
}
|
|
52
|
+
return transaction.amount.amount;
|
|
112
53
|
}
|
|
113
54
|
case 'payee': {
|
|
114
55
|
if (isNonEmptyString(transaction.vendorName)) {
|
|
@@ -246,37 +187,6 @@ const matchAmount = (amount, category) => {
|
|
|
246
187
|
return op === 'not_equal' ? !matched : matched;
|
|
247
188
|
};
|
|
248
189
|
const transactionMatchesCategory = (transaction, category) => {
|
|
249
|
-
if (category.field === 'amount') {
|
|
250
|
-
// Per-line matching: the transaction is included if ANY of its lines
|
|
251
|
-
// satisfies the operator. This is what the user expects when a
|
|
252
|
-
// multi-line transaction contains lines at different amounts — they
|
|
253
|
-
// want the row to surface if even one line falls in the filter's
|
|
254
|
-
// range. The full row (with non-matching lines too) renders
|
|
255
|
-
// downstream; this matcher only decides row inclusion. Same
|
|
256
|
-
// operator + sign semantics as the aggregate version, just applied
|
|
257
|
-
// per line.
|
|
258
|
-
//
|
|
259
|
-
// `not_equal` deserves a sanity-check: `matchAmount` already inverts
|
|
260
|
-
// internally for that operator, so for a line whose amount is NOT
|
|
261
|
-
// in the filter's `values`, matchAmount returns true; for a line
|
|
262
|
-
// whose amount IS in the values, it returns false. `.some()` over
|
|
263
|
-
// lines therefore means "the transaction has at least one line that
|
|
264
|
-
// is not equal to any of the filter values" — which is the right
|
|
265
|
-
// match-any-line interpretation of `not_equal` (a transaction
|
|
266
|
-
// matches if it has a line that satisfies the negation).
|
|
267
|
-
const lineAmounts = getLineAmountsForTransaction(transaction);
|
|
268
|
-
if (lineAmounts.length === 0) {
|
|
269
|
-
// No valid (finite) amount data on the transaction or its lines —
|
|
270
|
-
// apply the same absent-value contract the generic branch below
|
|
271
|
-
// uses for non-amount fields: only `not_equal` matches a missing
|
|
272
|
-
// value, every other operator excludes it. Coercing missing
|
|
273
|
-
// amounts to `0` upstream (the previous behavior) would have made
|
|
274
|
-
// such rows silently match `less_than 100`, `in_between [0, 50]`,
|
|
275
|
-
// etc., which the PR review correctly flagged as a regression.
|
|
276
|
-
return category.matchingOperator === 'not_equal';
|
|
277
|
-
}
|
|
278
|
-
return lineAmounts.some((lineAmount) => matchAmount(lineAmount, category));
|
|
279
|
-
}
|
|
280
190
|
const value = getCategoryValueForTransaction(category.field, transaction);
|
|
281
191
|
if (value == null) {
|
|
282
192
|
// Absent-value semantics — pinning these down explicitly so the
|
|
@@ -296,10 +206,12 @@ const transactionMatchesCategory = (transaction, category) => {
|
|
|
296
206
|
// are filtered out.
|
|
297
207
|
//
|
|
298
208
|
// Only the `not_equal` branch returns `true` here; everything else
|
|
299
|
-
// falls through to the `false` below.
|
|
300
|
-
// because it short-circuits to the per-line path above.
|
|
209
|
+
// falls through to the `false` below.
|
|
301
210
|
return category.matchingOperator === 'not_equal';
|
|
302
211
|
}
|
|
212
|
+
if (category.field === 'amount') {
|
|
213
|
+
return matchAmount(Number(value), category);
|
|
214
|
+
}
|
|
303
215
|
const valueStr = value.toString();
|
|
304
216
|
const valueInList = category.values.some((v) => v.toString() === valueStr);
|
|
305
217
|
return category.matchingOperator === 'not_equal' ? !valueInList : valueInList;
|
|
@@ -31,87 +31,28 @@ exports.TRANSACTION_FILTER_CATEGORIES = [
|
|
|
31
31
|
{ value: 'class', type: 'dropdown' },
|
|
32
32
|
{ value: 'amount', type: 'numberRange' },
|
|
33
33
|
];
|
|
34
|
-
/**
|
|
35
|
-
* Per-line amounts for a transaction, preserving the canonical source
|
|
36
|
-
* precedence:
|
|
37
|
-
*
|
|
38
|
-
* 1. `transactionLocalData.transactionReviewLocalData.lineItemById` —
|
|
39
|
-
* the locally-edited line items the user sees in the grid. Reflects
|
|
40
|
-
* unsaved edits so the filter matches what the user actually has on
|
|
41
|
-
* screen, not the pre-edit server state.
|
|
42
|
-
* 2. `transaction.transaction.lines` — persisted lines from the
|
|
43
|
-
* transaction payload.
|
|
44
|
-
* 3. `[transaction.amount.amount]` — single-element fallback for
|
|
45
|
-
* transactions that don't have a `lines` array at all (treat the
|
|
46
|
-
* transaction-level amount as a one-line transaction).
|
|
47
|
-
*
|
|
48
|
-
* Returned as an array so the amount filter in `transactionMatchesCategory`
|
|
49
|
-
* can OR-combine per-line `matchAmount` results: a transaction is included
|
|
50
|
-
* if ANY of its lines satisfies the operator, and the full row (with all
|
|
51
|
-
* lines, matching or not) renders downstream. Before the per-line change
|
|
52
|
-
* this helper's predecessor in `getCategoryValueForTransaction` summed
|
|
53
|
-
* these values into a single aggregate and compared the sum.
|
|
54
|
-
*
|
|
55
|
-
* **Missing amounts.** Lines (or transactions) whose amount field is
|
|
56
|
-
* `null` / `undefined` / `NaN` are *dropped* from the returned array
|
|
57
|
-
* rather than coerced to `0`. Coercing to `0` would silently make a
|
|
58
|
-
* row with no amount data match operators like `less_than 100`, even
|
|
59
|
-
* though we genuinely don't know what the amount is — a semantic
|
|
60
|
-
* regression flagged in PR review. By filtering instead, the empty-array
|
|
61
|
-
* case bubbles up to `transactionMatchesCategory`, which falls back to
|
|
62
|
-
* the same "absent value: only `not_equal` matches" contract used by
|
|
63
|
-
* non-amount fields.
|
|
64
|
-
*
|
|
65
|
-
* The defensive `?.` chains stay in place so a partial payload (state
|
|
66
|
-
* init, Pusher mid-sync, partially-resolved server response) doesn't
|
|
67
|
-
* throw; the difference is `?? 0` → filter-out.
|
|
68
|
-
*/
|
|
69
|
-
const getLineAmountsForTransaction = (transaction) => {
|
|
70
|
-
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
71
|
-
if (localData?.lineItemById != null) {
|
|
72
|
-
const lineItems = Object.values(localData.lineItemById);
|
|
73
|
-
if (lineItems.length > 0) {
|
|
74
|
-
return lineItems
|
|
75
|
-
.map((lineItem) => lineItem?.amount?.amount)
|
|
76
|
-
.filter(isFiniteNumber);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
if (transaction?.transaction?.lines != null &&
|
|
80
|
-
transaction.transaction.lines.length > 0) {
|
|
81
|
-
return transaction.transaction.lines
|
|
82
|
-
.map((line) => line?.amount?.amount)
|
|
83
|
-
.filter(isFiniteNumber);
|
|
84
|
-
}
|
|
85
|
-
const txAmount = transaction?.amount?.amount;
|
|
86
|
-
return isFiniteNumber(txAmount) ? [txAmount] : [];
|
|
87
|
-
};
|
|
88
|
-
/**
|
|
89
|
-
* Type guard that survives an `Array<number | undefined>.filter(...)` and
|
|
90
|
-
* narrows the result to `number[]`. Also rejects `NaN` so downstream
|
|
91
|
-
* `matchAmount` never sees a value that fails every operator silently
|
|
92
|
-
* (because every comparison with `NaN` is false, `NaN < 100`, `NaN > 100`,
|
|
93
|
-
* `NaN === N` all yield `false` — the row would silently slip through
|
|
94
|
-
* `not_equal` but fail every other operator, which is exactly the
|
|
95
|
-
* absent-value contract we want to apply explicitly via an empty array
|
|
96
|
-
* instead).
|
|
97
|
-
*/
|
|
98
|
-
const isFiniteNumber = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
99
34
|
/**
|
|
100
35
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
101
36
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
102
37
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
103
|
-
*
|
|
104
|
-
* NOTE: the `'amount'` field is NOT handled here. Amount matching is
|
|
105
|
-
* per-line (see `getLineAmountsForTransaction` + the early-return in
|
|
106
|
-
* `transactionMatchesCategory`) and would otherwise need to return a
|
|
107
|
-
* non-scalar value that doesn't fit this helper's contract.
|
|
108
38
|
*/
|
|
109
39
|
const getCategoryValueForTransaction = (key, transaction) => {
|
|
110
40
|
switch (key) {
|
|
111
41
|
case 'amount': {
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
|
|
42
|
+
// Prefer transactionLocalData line items (sum). Fall back to lines on
|
|
43
|
+
// the transaction itself. Final fallback: transaction-level amount.
|
|
44
|
+
const localDataForAmount = transaction.transactionLocalData?.transactionReviewLocalData;
|
|
45
|
+
if (localDataForAmount?.lineItemById) {
|
|
46
|
+
const lineItems = Object.values(localDataForAmount.lineItemById);
|
|
47
|
+
if (lineItems.length > 0) {
|
|
48
|
+
return lineItems.reduce((sum, lineItem) => sum + (lineItem.amount?.amount ?? 0), 0);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (transaction.transaction.lines &&
|
|
52
|
+
transaction.transaction.lines.length > 0) {
|
|
53
|
+
return transaction.transaction.lines.reduce((sum, line) => sum + (line.amount?.amount ?? 0), 0);
|
|
54
|
+
}
|
|
55
|
+
return transaction.amount.amount;
|
|
115
56
|
}
|
|
116
57
|
case 'payee': {
|
|
117
58
|
if (isNonEmptyString(transaction.vendorName)) {
|
|
@@ -249,37 +190,6 @@ const matchAmount = (amount, category) => {
|
|
|
249
190
|
return op === 'not_equal' ? !matched : matched;
|
|
250
191
|
};
|
|
251
192
|
const transactionMatchesCategory = (transaction, category) => {
|
|
252
|
-
if (category.field === 'amount') {
|
|
253
|
-
// Per-line matching: the transaction is included if ANY of its lines
|
|
254
|
-
// satisfies the operator. This is what the user expects when a
|
|
255
|
-
// multi-line transaction contains lines at different amounts — they
|
|
256
|
-
// want the row to surface if even one line falls in the filter's
|
|
257
|
-
// range. The full row (with non-matching lines too) renders
|
|
258
|
-
// downstream; this matcher only decides row inclusion. Same
|
|
259
|
-
// operator + sign semantics as the aggregate version, just applied
|
|
260
|
-
// per line.
|
|
261
|
-
//
|
|
262
|
-
// `not_equal` deserves a sanity-check: `matchAmount` already inverts
|
|
263
|
-
// internally for that operator, so for a line whose amount is NOT
|
|
264
|
-
// in the filter's `values`, matchAmount returns true; for a line
|
|
265
|
-
// whose amount IS in the values, it returns false. `.some()` over
|
|
266
|
-
// lines therefore means "the transaction has at least one line that
|
|
267
|
-
// is not equal to any of the filter values" — which is the right
|
|
268
|
-
// match-any-line interpretation of `not_equal` (a transaction
|
|
269
|
-
// matches if it has a line that satisfies the negation).
|
|
270
|
-
const lineAmounts = getLineAmountsForTransaction(transaction);
|
|
271
|
-
if (lineAmounts.length === 0) {
|
|
272
|
-
// No valid (finite) amount data on the transaction or its lines —
|
|
273
|
-
// apply the same absent-value contract the generic branch below
|
|
274
|
-
// uses for non-amount fields: only `not_equal` matches a missing
|
|
275
|
-
// value, every other operator excludes it. Coercing missing
|
|
276
|
-
// amounts to `0` upstream (the previous behavior) would have made
|
|
277
|
-
// such rows silently match `less_than 100`, `in_between [0, 50]`,
|
|
278
|
-
// etc., which the PR review correctly flagged as a regression.
|
|
279
|
-
return category.matchingOperator === 'not_equal';
|
|
280
|
-
}
|
|
281
|
-
return lineAmounts.some((lineAmount) => matchAmount(lineAmount, category));
|
|
282
|
-
}
|
|
283
193
|
const value = getCategoryValueForTransaction(category.field, transaction);
|
|
284
194
|
if (value == null) {
|
|
285
195
|
// Absent-value semantics — pinning these down explicitly so the
|
|
@@ -299,10 +209,12 @@ const transactionMatchesCategory = (transaction, category) => {
|
|
|
299
209
|
// are filtered out.
|
|
300
210
|
//
|
|
301
211
|
// Only the `not_equal` branch returns `true` here; everything else
|
|
302
|
-
// falls through to the `false` below.
|
|
303
|
-
// because it short-circuits to the per-line path above.
|
|
212
|
+
// falls through to the `false` below.
|
|
304
213
|
return category.matchingOperator === 'not_equal';
|
|
305
214
|
}
|
|
215
|
+
if (category.field === 'amount') {
|
|
216
|
+
return matchAmount(Number(value), category);
|
|
217
|
+
}
|
|
306
218
|
const valueStr = value.toString();
|
|
307
219
|
const valueInList = category.values.some((v) => v.toString() === valueStr);
|
|
308
220
|
return category.matchingOperator === 'not_equal' ? !valueInList : valueInList;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeniai/client-epic-state",
|
|
3
|
-
"version": "5.0.88-
|
|
3
|
+
"version": "5.0.88-betaAS2",
|
|
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",
|