@zeniai/client-epic-state 5.0.88-betaAR2 → 5.0.88-betaAS1

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.
@@ -90,13 +90,10 @@ const tenant = (0, toolkit_1.createSlice)({
90
90
  (newTenant != null && action.payload.tenantUpdated)) {
91
91
  // Note: If tenant data already exists, save it if the new data is the latest one.
92
92
  // we dont want to overwrite the user related data since that has not come in the output
93
- draft.tenantsById[tenantPayload.tenant_id] =
94
- applyTreasuryPromoUserFieldsFromPayload(tenantInStore, {
95
- ...newTenant,
96
- rewardsLastViewedTime: tenantInStore?.rewardsLastViewedTime,
97
- }, action.payload.userTenantPayload ?? {
98
- rewards_feature_last_viewed_time: null,
99
- });
93
+ draft.tenantsById[tenantPayload.tenant_id] = {
94
+ ...newTenant,
95
+ rewardsLastViewedTime: tenantInStore?.rewardsLastViewedTime,
96
+ };
100
97
  }
101
98
  });
102
99
  },
@@ -123,9 +120,6 @@ const tenant = (0, toolkit_1.createSlice)({
123
120
  draft.fetchState = 'Completed';
124
121
  draft.error = undefined;
125
122
  const [tenantsbyId, tenantIds, firstUser] = mapTenantsPayloadToState(tenants, user);
126
- tenantIds.forEach((tenantId) => {
127
- tenantsbyId[tenantId] = applyTreasuryPromoUserFieldsFromPayload(draft.tenantsById[tenantId], tenantsbyId[tenantId], user);
128
- });
129
123
  draft.tenantsById = tenantsbyId;
130
124
  draft.tenantIds = tenantIds;
131
125
  draft.loggedInUser = (0, assignWith_1.default)(draft.loggedInUser ?? {}, firstUser, (objValue, srcValue) => srcValue ?? objValue);
@@ -829,27 +823,6 @@ const tenant = (0, toolkit_1.createSlice)({
829
823
  });
830
824
  _a = tenant.actions, exports.updateTenants = _a.updateTenants, exports.fetchAllTenants = _a.fetchAllTenants, exports.updateTenantsSuccess = _a.updateTenantsSuccess, exports.updateTenantsFailure = _a.updateTenantsFailure, exports.fetchActiveTenant = _a.fetchActiveTenant, exports.updateTenantSuccess = _a.updateTenantSuccess, exports.updateTenantFailure = _a.updateTenantFailure, exports.updateCurrentTenant = _a.updateCurrentTenant, exports.fetchExcludedResources = _a.fetchExcludedResources, exports.updateExcludedResourcesSuccess = _a.updateExcludedResourcesSuccess, exports.updateExcludedResourcesFailure = _a.updateExcludedResourcesFailure, exports.doSignIn = _a.doSignIn, exports.doMagicLinkSignIn = _a.doMagicLinkSignIn, exports.magicLinkSignInSuccess = _a.magicLinkSignInSuccess, exports.magicLinkSignInFailure = _a.magicLinkSignInFailure, exports.sendEmailMagicLinkToUser = _a.sendEmailMagicLinkToUser, exports.sendEmailMagicLinkToUserSuccess = _a.sendEmailMagicLinkToUserSuccess, exports.sendEmailMagicLinkToUserFailure = _a.sendEmailMagicLinkToUserFailure, exports.updateSignInState = _a.updateSignInState, exports.doSignOut = _a.doSignOut, exports.signOutSuccess = _a.signOutSuccess, exports.sendSessionHeartbeat = _a.sendSessionHeartbeat, exports.sessionHeartbeatSuccess = _a.sessionHeartbeatSuccess, exports.sessionHeartbeatFailure = _a.sessionHeartbeatFailure, exports.updateLoggedInUser = _a.updateLoggedInUser, exports.fetchExternalConnections = _a.fetchExternalConnections, exports.saveExternalConnection = _a.saveExternalConnection, exports.saveExternalConnectionSuccess = _a.saveExternalConnectionSuccess, exports.saveExternalConnectionFailure = _a.saveExternalConnectionFailure, exports.fetchExternalConnectionsFailure = _a.fetchExternalConnectionsFailure, exports.fetchExternalConnectionsSuccess = _a.fetchExternalConnectionsSuccess, exports.saveAPIKeyConnection = _a.saveAPIKeyConnection, exports.saveAPIKeyConnectionSuccess = _a.saveAPIKeyConnectionSuccess, exports.saveAPIKeyConnectionFailure = _a.saveAPIKeyConnectionFailure, exports.saveOAuthConnection = _a.saveOAuthConnection, exports.saveOAuthConnectionSuccess = _a.saveOAuthConnectionSuccess, exports.saveOAuthConnectionFailure = _a.saveOAuthConnectionFailure, exports.saveConnectorCredentials = _a.saveConnectorCredentials, exports.saveConnectorCredentialsSuccess = _a.saveConnectorCredentialsSuccess, exports.saveConnectorCredentialsFailure = _a.saveConnectorCredentialsFailure, exports.deleteConnection = _a.deleteConnection, exports.deleteConnectionSuccess = _a.deleteConnectionSuccess, exports.deleteConnectionFailure = _a.deleteConnectionFailure, exports.clearAll = _a.clearAll, exports.fetchSubscriptionSummaryForTenant = _a.fetchSubscriptionSummaryForTenant, exports.updateSubscriptionSummaryForTenantSuccess = _a.updateSubscriptionSummaryForTenantSuccess, exports.updateSubscriptionSummaryForTenantFailure = _a.updateSubscriptionSummaryForTenantFailure, exports.updateOnboardingTenants = _a.updateOnboardingTenants, exports.removeOnboardingTenant = _a.removeOnboardingTenant, exports.updateTenantReimbursementInfo = _a.updateTenantReimbursementInfo, exports.updateReferViewedForLoggedInUser = _a.updateReferViewedForLoggedInUser, exports.resetSignInState = _a.resetSignInState, exports.trigger2FA = _a.trigger2FA, exports.updateTenantAccountingClassesEnabled = _a.updateTenantAccountingClassesEnabled, exports.updateTenantCapitalizationOnboardingDismissed = _a.updateTenantCapitalizationOnboardingDismissed, exports.updateTenantCapitalizationThreshold = _a.updateTenantCapitalizationThreshold, exports.updateTenantMasterTOSInfo = _a.updateTenantMasterTOSInfo, exports.verifyDeviceWithTwoFA = _a.verifyDeviceWithTwoFA, exports.verifyDeviceWithTwoFASuccess = _a.verifyDeviceWithTwoFASuccess, exports.verifyDeviceWithTwoFAFailure = _a.verifyDeviceWithTwoFAFailure, exports.updateTreasuryVideoViewedForLoggedInUser = _a.updateTreasuryVideoViewedForLoggedInUser, exports.updateTreasuryPromoRemindMeLaterClickedForLoggedInUser = _a.updateTreasuryPromoRemindMeLaterClickedForLoggedInUser, exports.updateTreasuryPromoIntroClosedByOutsideClickForLoggedInUser = _a.updateTreasuryPromoIntroClosedByOutsideClickForLoggedInUser, exports.resendVerifyDeviceOTP = _a.resendVerifyDeviceOTP, exports.resendVerifyDeviceOTPSuccess = _a.resendVerifyDeviceOTPSuccess, exports.resendVerifyDeviceOTPFailure = _a.resendVerifyDeviceOTPFailure;
831
825
  exports.default = tenant.reducer;
832
- /**
833
- * Applies treasury promo user fields from the tenants API user block when present.
834
- * When a field is omitted from the payload, keeps the existing tenant value so a
835
- * later fetch-all-tenants call does not reset values already loaded by fetch-active-tenant.
836
- */
837
- function applyTreasuryPromoUserFieldsFromPayload(existingTenant, mappedTenant, userTenantPayload) {
838
- if (existingTenant == null) {
839
- return mappedTenant;
840
- }
841
- return {
842
- ...mappedTenant,
843
- isTreasuryPromoVideoPlayed: userTenantPayload.is_treasury_promo_video_played ??
844
- existingTenant.isTreasuryPromoVideoPlayed,
845
- isTreasuryPromoVideoRemindMeLaterClicked: userTenantPayload.is_treasury_promo_video_remind_me_later_clicked ??
846
- existingTenant.isTreasuryPromoVideoRemindMeLaterClicked,
847
- loginCounterForTreasuryPromoVideo: userTenantPayload.login_counter_for_treasury_promo_video ??
848
- existingTenant.loginCounterForTreasuryPromoVideo,
849
- isTreasuryPromoIntroClosedByOutsideClick: userTenantPayload.is_treasury_promo_intro_closed_by_outside_click ??
850
- existingTenant.isTreasuryPromoIntroClosedByOutsideClick,
851
- };
852
- }
853
826
  /**
854
827
  * Converts tenants payload to Tenant and User State
855
828
  * @param payload tenants payload as received from backend API response
@@ -78,13 +78,10 @@ const tenant = createSlice({
78
78
  (newTenant != null && action.payload.tenantUpdated)) {
79
79
  // Note: If tenant data already exists, save it if the new data is the latest one.
80
80
  // we dont want to overwrite the user related data since that has not come in the output
81
- draft.tenantsById[tenantPayload.tenant_id] =
82
- applyTreasuryPromoUserFieldsFromPayload(tenantInStore, {
83
- ...newTenant,
84
- rewardsLastViewedTime: tenantInStore?.rewardsLastViewedTime,
85
- }, action.payload.userTenantPayload ?? {
86
- rewards_feature_last_viewed_time: null,
87
- });
81
+ draft.tenantsById[tenantPayload.tenant_id] = {
82
+ ...newTenant,
83
+ rewardsLastViewedTime: tenantInStore?.rewardsLastViewedTime,
84
+ };
88
85
  }
89
86
  });
90
87
  },
@@ -111,9 +108,6 @@ const tenant = createSlice({
111
108
  draft.fetchState = 'Completed';
112
109
  draft.error = undefined;
113
110
  const [tenantsbyId, tenantIds, firstUser] = mapTenantsPayloadToState(tenants, user);
114
- tenantIds.forEach((tenantId) => {
115
- tenantsbyId[tenantId] = applyTreasuryPromoUserFieldsFromPayload(draft.tenantsById[tenantId], tenantsbyId[tenantId], user);
116
- });
117
111
  draft.tenantsById = tenantsbyId;
118
112
  draft.tenantIds = tenantIds;
119
113
  draft.loggedInUser = assignWith(draft.loggedInUser ?? {}, firstUser, (objValue, srcValue) => srcValue ?? objValue);
@@ -817,27 +811,6 @@ const tenant = createSlice({
817
811
  });
818
812
  export const { updateTenants, fetchAllTenants, updateTenantsSuccess, updateTenantsFailure, fetchActiveTenant, updateTenantSuccess, updateTenantFailure, updateCurrentTenant, fetchExcludedResources, updateExcludedResourcesSuccess, updateExcludedResourcesFailure, doSignIn, doMagicLinkSignIn, magicLinkSignInSuccess, magicLinkSignInFailure, sendEmailMagicLinkToUser, sendEmailMagicLinkToUserSuccess, sendEmailMagicLinkToUserFailure, updateSignInState, doSignOut, signOutSuccess, sendSessionHeartbeat, sessionHeartbeatSuccess, sessionHeartbeatFailure, updateLoggedInUser, fetchExternalConnections, saveExternalConnection, saveExternalConnectionSuccess, saveExternalConnectionFailure, fetchExternalConnectionsFailure, fetchExternalConnectionsSuccess, saveAPIKeyConnection, saveAPIKeyConnectionSuccess, saveAPIKeyConnectionFailure, saveOAuthConnection, saveOAuthConnectionSuccess, saveOAuthConnectionFailure, saveConnectorCredentials, saveConnectorCredentialsSuccess, saveConnectorCredentialsFailure, deleteConnection, deleteConnectionSuccess, deleteConnectionFailure, clearAll, fetchSubscriptionSummaryForTenant, updateSubscriptionSummaryForTenantSuccess, updateSubscriptionSummaryForTenantFailure, updateOnboardingTenants, removeOnboardingTenant, updateTenantReimbursementInfo, updateReferViewedForLoggedInUser, resetSignInState, trigger2FA, updateTenantAccountingClassesEnabled, updateTenantCapitalizationOnboardingDismissed, updateTenantCapitalizationThreshold, updateTenantMasterTOSInfo, verifyDeviceWithTwoFA, verifyDeviceWithTwoFASuccess, verifyDeviceWithTwoFAFailure, updateTreasuryVideoViewedForLoggedInUser, updateTreasuryPromoRemindMeLaterClickedForLoggedInUser, updateTreasuryPromoIntroClosedByOutsideClickForLoggedInUser, resendVerifyDeviceOTP, resendVerifyDeviceOTPSuccess, resendVerifyDeviceOTPFailure, } = tenant.actions;
819
813
  export default tenant.reducer;
820
- /**
821
- * Applies treasury promo user fields from the tenants API user block when present.
822
- * When a field is omitted from the payload, keeps the existing tenant value so a
823
- * later fetch-all-tenants call does not reset values already loaded by fetch-active-tenant.
824
- */
825
- function applyTreasuryPromoUserFieldsFromPayload(existingTenant, mappedTenant, userTenantPayload) {
826
- if (existingTenant == null) {
827
- return mappedTenant;
828
- }
829
- return {
830
- ...mappedTenant,
831
- isTreasuryPromoVideoPlayed: userTenantPayload.is_treasury_promo_video_played ??
832
- existingTenant.isTreasuryPromoVideoPlayed,
833
- isTreasuryPromoVideoRemindMeLaterClicked: userTenantPayload.is_treasury_promo_video_remind_me_later_clicked ??
834
- existingTenant.isTreasuryPromoVideoRemindMeLaterClicked,
835
- loginCounterForTreasuryPromoVideo: userTenantPayload.login_counter_for_treasury_promo_video ??
836
- existingTenant.loginCounterForTreasuryPromoVideo,
837
- isTreasuryPromoIntroClosedByOutsideClick: userTenantPayload.is_treasury_promo_intro_closed_by_outside_click ??
838
- existingTenant.isTreasuryPromoIntroClosedByOutsideClick,
839
- };
840
- }
841
814
  /**
842
815
  * Converts tenants payload to Tenant and User State
843
816
  * @param payload tenants payload as received from backend API response
@@ -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
- // Handled per-line in `transactionMatchesCategory` see the note on
110
- // this function's JSDoc and `getLineAmountsForTransaction` above.
111
- return undefined;
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. Note: 'amount' never lands here
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
- // Handled per-line in `transactionMatchesCategory` see the note on
113
- // this function's JSDoc and `getLineAmountsForTransaction` above.
114
- return undefined;
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. Note: 'amount' never lands here
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-betaAR2",
3
+ "version": "5.0.88-betaAS1",
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",