@zeniai/client-epic-state 5.0.87 → 5.0.88-betaAR1
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,28 +28,69 @@ 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
|
+
* Defensive `?.` / `?? 0` on every nested access: the static types declare
|
|
53
|
+
* `transaction`, `transaction.transaction`, and `transaction.amount` as
|
|
54
|
+
* required, but in practice this selector chain can be fed partial
|
|
55
|
+
* payloads — e.g. while state is initializing, while a transaction is
|
|
56
|
+
* mid-sync after a Pusher delta, or when a `TransactionView` is
|
|
57
|
+
* constructed from a server response that hasn't fully resolved its
|
|
58
|
+
* embedded `amount` envelope. A missing amount field collapses to `0`
|
|
59
|
+
* rather than throwing; an absent line is treated as `0` so the operator
|
|
60
|
+
* comparison still produces a deterministic boolean (e.g. `greater_than
|
|
61
|
+
* 100` won't include a transaction just because one of its lines failed
|
|
62
|
+
* to deserialize).
|
|
63
|
+
*/
|
|
64
|
+
const getLineAmountsForTransaction = (transaction) => {
|
|
65
|
+
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
66
|
+
if (localData?.lineItemById != null) {
|
|
67
|
+
const lineItems = Object.values(localData.lineItemById);
|
|
68
|
+
if (lineItems.length > 0) {
|
|
69
|
+
return lineItems.map((lineItem) => lineItem?.amount?.amount ?? 0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (transaction?.transaction?.lines != null &&
|
|
73
|
+
transaction.transaction.lines.length > 0) {
|
|
74
|
+
return transaction.transaction.lines.map((line) => line?.amount?.amount ?? 0);
|
|
75
|
+
}
|
|
76
|
+
return [transaction?.amount?.amount ?? 0];
|
|
77
|
+
};
|
|
31
78
|
/**
|
|
32
79
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
33
80
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
34
81
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
82
|
+
*
|
|
83
|
+
* NOTE: the `'amount'` field is NOT handled here. Amount matching is
|
|
84
|
+
* per-line (see `getLineAmountsForTransaction` + the early-return in
|
|
85
|
+
* `transactionMatchesCategory`) and would otherwise need to return a
|
|
86
|
+
* non-scalar value that doesn't fit this helper's contract.
|
|
35
87
|
*/
|
|
36
88
|
const getCategoryValueForTransaction = (key, transaction) => {
|
|
37
89
|
switch (key) {
|
|
38
90
|
case 'amount': {
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
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;
|
|
91
|
+
// Handled per-line in `transactionMatchesCategory` — see the note on
|
|
92
|
+
// this function's JSDoc and `getLineAmountsForTransaction` above.
|
|
93
|
+
return undefined;
|
|
53
94
|
}
|
|
54
95
|
case 'payee': {
|
|
55
96
|
if (isNonEmptyString(transaction.vendorName)) {
|
|
@@ -187,6 +228,27 @@ const matchAmount = (amount, category) => {
|
|
|
187
228
|
return op === 'not_equal' ? !matched : matched;
|
|
188
229
|
};
|
|
189
230
|
const transactionMatchesCategory = (transaction, category) => {
|
|
231
|
+
if (category.field === 'amount') {
|
|
232
|
+
// Per-line matching: the transaction is included if ANY of its lines
|
|
233
|
+
// satisfies the operator. This is what the user expects when a
|
|
234
|
+
// multi-line transaction contains lines at different amounts — they
|
|
235
|
+
// want the row to surface if even one line falls in the filter's
|
|
236
|
+
// range. The full row (with non-matching lines too) renders
|
|
237
|
+
// downstream; this matcher only decides row inclusion. Same
|
|
238
|
+
// operator + sign semantics as the aggregate version, just applied
|
|
239
|
+
// per line.
|
|
240
|
+
//
|
|
241
|
+
// `not_equal` deserves a sanity-check: `matchAmount` already inverts
|
|
242
|
+
// internally for that operator, so for a line whose amount is NOT
|
|
243
|
+
// in the filter's `values`, matchAmount returns true; for a line
|
|
244
|
+
// whose amount IS in the values, it returns false. `.some()` over
|
|
245
|
+
// lines therefore means "the transaction has at least one line that
|
|
246
|
+
// is not equal to any of the filter values" — which is the right
|
|
247
|
+
// match-any-line interpretation of `not_equal` (a transaction
|
|
248
|
+
// matches if it has a line that satisfies the negation).
|
|
249
|
+
const lineAmounts = getLineAmountsForTransaction(transaction);
|
|
250
|
+
return lineAmounts.some((lineAmount) => matchAmount(lineAmount, category));
|
|
251
|
+
}
|
|
190
252
|
const value = getCategoryValueForTransaction(category.field, transaction);
|
|
191
253
|
if (value == null) {
|
|
192
254
|
// Absent-value semantics — pinning these down explicitly so the
|
|
@@ -206,12 +268,10 @@ const transactionMatchesCategory = (transaction, category) => {
|
|
|
206
268
|
// are filtered out.
|
|
207
269
|
//
|
|
208
270
|
// Only the `not_equal` branch returns `true` here; everything else
|
|
209
|
-
// falls through to the `false` below.
|
|
271
|
+
// falls through to the `false` below. Note: 'amount' never lands here
|
|
272
|
+
// because it short-circuits to the per-line path above.
|
|
210
273
|
return category.matchingOperator === 'not_equal';
|
|
211
274
|
}
|
|
212
|
-
if (category.field === 'amount') {
|
|
213
|
-
return matchAmount(Number(value), category);
|
|
214
|
-
}
|
|
215
275
|
const valueStr = value.toString();
|
|
216
276
|
const valueInList = category.values.some((v) => v.toString() === valueStr);
|
|
217
277
|
return category.matchingOperator === 'not_equal' ? !valueInList : valueInList;
|
|
@@ -31,28 +31,69 @@ 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
|
+
* Defensive `?.` / `?? 0` on every nested access: the static types declare
|
|
56
|
+
* `transaction`, `transaction.transaction`, and `transaction.amount` as
|
|
57
|
+
* required, but in practice this selector chain can be fed partial
|
|
58
|
+
* payloads — e.g. while state is initializing, while a transaction is
|
|
59
|
+
* mid-sync after a Pusher delta, or when a `TransactionView` is
|
|
60
|
+
* constructed from a server response that hasn't fully resolved its
|
|
61
|
+
* embedded `amount` envelope. A missing amount field collapses to `0`
|
|
62
|
+
* rather than throwing; an absent line is treated as `0` so the operator
|
|
63
|
+
* comparison still produces a deterministic boolean (e.g. `greater_than
|
|
64
|
+
* 100` won't include a transaction just because one of its lines failed
|
|
65
|
+
* to deserialize).
|
|
66
|
+
*/
|
|
67
|
+
const getLineAmountsForTransaction = (transaction) => {
|
|
68
|
+
const localData = transaction?.transactionLocalData?.transactionReviewLocalData;
|
|
69
|
+
if (localData?.lineItemById != null) {
|
|
70
|
+
const lineItems = Object.values(localData.lineItemById);
|
|
71
|
+
if (lineItems.length > 0) {
|
|
72
|
+
return lineItems.map((lineItem) => lineItem?.amount?.amount ?? 0);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (transaction?.transaction?.lines != null &&
|
|
76
|
+
transaction.transaction.lines.length > 0) {
|
|
77
|
+
return transaction.transaction.lines.map((line) => line?.amount?.amount ?? 0);
|
|
78
|
+
}
|
|
79
|
+
return [transaction?.amount?.amount ?? 0];
|
|
80
|
+
};
|
|
34
81
|
/**
|
|
35
82
|
* Resolve the comparable value for a transaction against a given filter field.
|
|
36
83
|
* Returns undefined when the transaction doesn't carry the field — callers
|
|
37
84
|
* decide whether absence counts as a match (for 'not_equal' it does).
|
|
85
|
+
*
|
|
86
|
+
* NOTE: the `'amount'` field is NOT handled here. Amount matching is
|
|
87
|
+
* per-line (see `getLineAmountsForTransaction` + the early-return in
|
|
88
|
+
* `transactionMatchesCategory`) and would otherwise need to return a
|
|
89
|
+
* non-scalar value that doesn't fit this helper's contract.
|
|
38
90
|
*/
|
|
39
91
|
const getCategoryValueForTransaction = (key, transaction) => {
|
|
40
92
|
switch (key) {
|
|
41
93
|
case 'amount': {
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
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;
|
|
94
|
+
// Handled per-line in `transactionMatchesCategory` — see the note on
|
|
95
|
+
// this function's JSDoc and `getLineAmountsForTransaction` above.
|
|
96
|
+
return undefined;
|
|
56
97
|
}
|
|
57
98
|
case 'payee': {
|
|
58
99
|
if (isNonEmptyString(transaction.vendorName)) {
|
|
@@ -190,6 +231,27 @@ const matchAmount = (amount, category) => {
|
|
|
190
231
|
return op === 'not_equal' ? !matched : matched;
|
|
191
232
|
};
|
|
192
233
|
const transactionMatchesCategory = (transaction, category) => {
|
|
234
|
+
if (category.field === 'amount') {
|
|
235
|
+
// Per-line matching: the transaction is included if ANY of its lines
|
|
236
|
+
// satisfies the operator. This is what the user expects when a
|
|
237
|
+
// multi-line transaction contains lines at different amounts — they
|
|
238
|
+
// want the row to surface if even one line falls in the filter's
|
|
239
|
+
// range. The full row (with non-matching lines too) renders
|
|
240
|
+
// downstream; this matcher only decides row inclusion. Same
|
|
241
|
+
// operator + sign semantics as the aggregate version, just applied
|
|
242
|
+
// per line.
|
|
243
|
+
//
|
|
244
|
+
// `not_equal` deserves a sanity-check: `matchAmount` already inverts
|
|
245
|
+
// internally for that operator, so for a line whose amount is NOT
|
|
246
|
+
// in the filter's `values`, matchAmount returns true; for a line
|
|
247
|
+
// whose amount IS in the values, it returns false. `.some()` over
|
|
248
|
+
// lines therefore means "the transaction has at least one line that
|
|
249
|
+
// is not equal to any of the filter values" — which is the right
|
|
250
|
+
// match-any-line interpretation of `not_equal` (a transaction
|
|
251
|
+
// matches if it has a line that satisfies the negation).
|
|
252
|
+
const lineAmounts = getLineAmountsForTransaction(transaction);
|
|
253
|
+
return lineAmounts.some((lineAmount) => matchAmount(lineAmount, category));
|
|
254
|
+
}
|
|
193
255
|
const value = getCategoryValueForTransaction(category.field, transaction);
|
|
194
256
|
if (value == null) {
|
|
195
257
|
// Absent-value semantics — pinning these down explicitly so the
|
|
@@ -209,12 +271,10 @@ const transactionMatchesCategory = (transaction, category) => {
|
|
|
209
271
|
// are filtered out.
|
|
210
272
|
//
|
|
211
273
|
// Only the `not_equal` branch returns `true` here; everything else
|
|
212
|
-
// falls through to the `false` below.
|
|
274
|
+
// falls through to the `false` below. Note: 'amount' never lands here
|
|
275
|
+
// because it short-circuits to the per-line path above.
|
|
213
276
|
return category.matchingOperator === 'not_equal';
|
|
214
277
|
}
|
|
215
|
-
if (category.field === 'amount') {
|
|
216
|
-
return matchAmount(Number(value), category);
|
|
217
|
-
}
|
|
218
278
|
const valueStr = value.toString();
|
|
219
279
|
const valueInList = category.values.some((v) => v.toString() === valueStr);
|
|
220
280
|
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.
|
|
3
|
+
"version": "5.0.88-betaAR1",
|
|
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",
|