@zeniai/client-epic-state 5.0.85-betaAR2 → 5.0.85-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.
@@ -28,28 +28,57 @@ 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
+ const getLineAmountsForTransaction = (transaction) => {
53
+ const localData = transaction.transactionLocalData?.transactionReviewLocalData;
54
+ if (localData?.lineItemById != null) {
55
+ const lineItems = Object.values(localData.lineItemById);
56
+ if (lineItems.length > 0) {
57
+ return lineItems.map((lineItem) => lineItem.amount?.amount ?? 0);
58
+ }
59
+ }
60
+ if (transaction.transaction.lines != null &&
61
+ transaction.transaction.lines.length > 0) {
62
+ return transaction.transaction.lines.map((line) => line.amount?.amount ?? 0);
63
+ }
64
+ return [transaction.amount.amount];
65
+ };
31
66
  /**
32
67
  * Resolve the comparable value for a transaction against a given filter field.
33
68
  * Returns undefined when the transaction doesn't carry the field — callers
34
69
  * decide whether absence counts as a match (for 'not_equal' it does).
70
+ *
71
+ * NOTE: the `'amount'` field is NOT handled here. Amount matching is
72
+ * per-line (see `getLineAmountsForTransaction` + the early-return in
73
+ * `transactionMatchesCategory`) and would otherwise need to return a
74
+ * non-scalar value that doesn't fit this helper's contract.
35
75
  */
36
76
  const getCategoryValueForTransaction = (key, transaction) => {
37
77
  switch (key) {
38
78
  case 'amount': {
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;
79
+ // Handled per-line in `transactionMatchesCategory` see the note on
80
+ // this function's JSDoc and `getLineAmountsForTransaction` above.
81
+ return undefined;
53
82
  }
54
83
  case 'payee': {
55
84
  if (isNonEmptyString(transaction.vendorName)) {
@@ -187,6 +216,27 @@ const matchAmount = (amount, category) => {
187
216
  return op === 'not_equal' ? !matched : matched;
188
217
  };
189
218
  const transactionMatchesCategory = (transaction, category) => {
219
+ if (category.field === 'amount') {
220
+ // Per-line matching: the transaction is included if ANY of its lines
221
+ // satisfies the operator. This is what the user expects when a
222
+ // multi-line transaction contains lines at different amounts — they
223
+ // want the row to surface if even one line falls in the filter's
224
+ // range. The full row (with non-matching lines too) renders
225
+ // downstream; this matcher only decides row inclusion. Same
226
+ // operator + sign semantics as the aggregate version, just applied
227
+ // per line.
228
+ //
229
+ // `not_equal` deserves a sanity-check: `matchAmount` already inverts
230
+ // internally for that operator, so for a line whose amount is NOT
231
+ // in the filter's `values`, matchAmount returns true; for a line
232
+ // whose amount IS in the values, it returns false. `.some()` over
233
+ // lines therefore means "the transaction has at least one line that
234
+ // is not equal to any of the filter values" — which is the right
235
+ // match-any-line interpretation of `not_equal` (a transaction
236
+ // matches if it has a line that satisfies the negation).
237
+ const lineAmounts = getLineAmountsForTransaction(transaction);
238
+ return lineAmounts.some((lineAmount) => matchAmount(lineAmount, category));
239
+ }
190
240
  const value = getCategoryValueForTransaction(category.field, transaction);
191
241
  if (value == null) {
192
242
  // Absent-value semantics — pinning these down explicitly so the
@@ -206,12 +256,10 @@ const transactionMatchesCategory = (transaction, category) => {
206
256
  // are filtered out.
207
257
  //
208
258
  // Only the `not_equal` branch returns `true` here; everything else
209
- // falls through to the `false` below.
259
+ // falls through to the `false` below. Note: 'amount' never lands here
260
+ // because it short-circuits to the per-line path above.
210
261
  return category.matchingOperator === 'not_equal';
211
262
  }
212
- if (category.field === 'amount') {
213
- return matchAmount(Number(value), category);
214
- }
215
263
  const valueStr = value.toString();
216
264
  const valueInList = category.values.some((v) => v.toString() === valueStr);
217
265
  return category.matchingOperator === 'not_equal' ? !valueInList : valueInList;
@@ -31,28 +31,57 @@ 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
+ const getLineAmountsForTransaction = (transaction) => {
56
+ const localData = transaction.transactionLocalData?.transactionReviewLocalData;
57
+ if (localData?.lineItemById != null) {
58
+ const lineItems = Object.values(localData.lineItemById);
59
+ if (lineItems.length > 0) {
60
+ return lineItems.map((lineItem) => lineItem.amount?.amount ?? 0);
61
+ }
62
+ }
63
+ if (transaction.transaction.lines != null &&
64
+ transaction.transaction.lines.length > 0) {
65
+ return transaction.transaction.lines.map((line) => line.amount?.amount ?? 0);
66
+ }
67
+ return [transaction.amount.amount];
68
+ };
34
69
  /**
35
70
  * Resolve the comparable value for a transaction against a given filter field.
36
71
  * Returns undefined when the transaction doesn't carry the field — callers
37
72
  * decide whether absence counts as a match (for 'not_equal' it does).
73
+ *
74
+ * NOTE: the `'amount'` field is NOT handled here. Amount matching is
75
+ * per-line (see `getLineAmountsForTransaction` + the early-return in
76
+ * `transactionMatchesCategory`) and would otherwise need to return a
77
+ * non-scalar value that doesn't fit this helper's contract.
38
78
  */
39
79
  const getCategoryValueForTransaction = (key, transaction) => {
40
80
  switch (key) {
41
81
  case 'amount': {
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;
82
+ // Handled per-line in `transactionMatchesCategory` see the note on
83
+ // this function's JSDoc and `getLineAmountsForTransaction` above.
84
+ return undefined;
56
85
  }
57
86
  case 'payee': {
58
87
  if (isNonEmptyString(transaction.vendorName)) {
@@ -190,6 +219,27 @@ const matchAmount = (amount, category) => {
190
219
  return op === 'not_equal' ? !matched : matched;
191
220
  };
192
221
  const transactionMatchesCategory = (transaction, category) => {
222
+ if (category.field === 'amount') {
223
+ // Per-line matching: the transaction is included if ANY of its lines
224
+ // satisfies the operator. This is what the user expects when a
225
+ // multi-line transaction contains lines at different amounts — they
226
+ // want the row to surface if even one line falls in the filter's
227
+ // range. The full row (with non-matching lines too) renders
228
+ // downstream; this matcher only decides row inclusion. Same
229
+ // operator + sign semantics as the aggregate version, just applied
230
+ // per line.
231
+ //
232
+ // `not_equal` deserves a sanity-check: `matchAmount` already inverts
233
+ // internally for that operator, so for a line whose amount is NOT
234
+ // in the filter's `values`, matchAmount returns true; for a line
235
+ // whose amount IS in the values, it returns false. `.some()` over
236
+ // lines therefore means "the transaction has at least one line that
237
+ // is not equal to any of the filter values" — which is the right
238
+ // match-any-line interpretation of `not_equal` (a transaction
239
+ // matches if it has a line that satisfies the negation).
240
+ const lineAmounts = getLineAmountsForTransaction(transaction);
241
+ return lineAmounts.some((lineAmount) => matchAmount(lineAmount, category));
242
+ }
193
243
  const value = getCategoryValueForTransaction(category.field, transaction);
194
244
  if (value == null) {
195
245
  // Absent-value semantics — pinning these down explicitly so the
@@ -209,12 +259,10 @@ const transactionMatchesCategory = (transaction, category) => {
209
259
  // are filtered out.
210
260
  //
211
261
  // Only the `not_equal` branch returns `true` here; everything else
212
- // falls through to the `false` below.
262
+ // falls through to the `false` below. Note: 'amount' never lands here
263
+ // because it short-circuits to the per-line path above.
213
264
  return category.matchingOperator === 'not_equal';
214
265
  }
215
- if (category.field === 'amount') {
216
- return matchAmount(Number(value), category);
217
- }
218
266
  const valueStr = value.toString();
219
267
  const valueInList = category.values.some((v) => v.toString() === valueStr);
220
268
  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.85-betaAR2",
3
+ "version": "5.0.85-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",