@zeniai/client-epic-state 5.0.52 → 5.0.53-betaML4

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.
Files changed (124) hide show
  1. package/lib/commonStateTypes/recommendationBase.d.ts +2 -1
  2. package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -2
  3. package/lib/commonStateTypes/viewAndReport/viewAndReport.js +1 -0
  4. package/lib/entity/account/accountState.d.ts +1 -1
  5. package/lib/entity/accountGroup/accountGroupState.d.ts +1 -1
  6. package/lib/entity/class/classReducer.d.ts +4 -4
  7. package/lib/entity/class/classState.d.ts +1 -1
  8. package/lib/entity/forecast/forecastState.d.ts +1 -1
  9. package/lib/entity/project/projectPayload.d.ts +8 -1
  10. package/lib/entity/project/projectPayload.js +15 -1
  11. package/lib/entity/project/projectState.d.ts +4 -2
  12. package/lib/entity/sectionAccountsView/sectionAccountsView.d.ts +1 -1
  13. package/lib/entity/sectionClassesViewV2/sectionClassesView.d.ts +1 -1
  14. package/lib/entity/sectionClassesViewV2/sectionClassesViewReducer.d.ts +2 -2
  15. package/lib/entity/sectionProjectView/sectionProjectView.d.ts +1 -1
  16. package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +2 -2
  17. package/lib/entity/tenant/clearAllEpic.d.ts +2 -1
  18. package/lib/entity/tenant/clearAllEpic.js +2 -0
  19. package/lib/entity/transaction/payloadTypes/transactionLinePayload.d.ts +2 -0
  20. package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +9 -0
  21. package/lib/entity/transaction/payloadTypes/transactionPayload.d.ts +2 -0
  22. package/lib/entity/transaction/payloadTypes/transactionPayload.js +6 -0
  23. package/lib/entity/transaction/stateTypes/transaction.d.ts +2 -0
  24. package/lib/entity/transaction/stateTypes/transactionLine.d.ts +2 -0
  25. package/lib/epic.d.ts +4 -1
  26. package/lib/epic.js +5 -1
  27. package/lib/esm/commonStateTypes/viewAndReport/viewAndReport.js +1 -0
  28. package/lib/esm/entity/project/projectPayload.js +12 -0
  29. package/lib/esm/entity/tenant/clearAllEpic.js +2 -0
  30. package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +9 -0
  31. package/lib/esm/entity/transaction/payloadTypes/transactionPayload.js +6 -0
  32. package/lib/esm/epic.js +5 -1
  33. package/lib/esm/index.js +11 -7
  34. package/lib/esm/reducer.js +3 -0
  35. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
  36. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/updateTransactionCategorizationEpic.js +3 -0
  37. package/lib/esm/view/expenseAutomationView/helpers/saveTransactionHelper.js +1 -0
  38. package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +11 -2
  39. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +127 -31
  40. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +13 -2
  41. package/lib/esm/view/expenseAutomationView/types/transactionsViewState.js +1 -0
  42. package/lib/esm/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.js +44 -0
  43. package/lib/esm/view/projectList/fetchProjectListEpic.js +38 -0
  44. package/lib/esm/view/projectList/projectListPayload.js +1 -0
  45. package/lib/esm/view/projectList/projectListReducer.js +39 -0
  46. package/lib/esm/view/projectList/projectListSelector.js +9 -0
  47. package/lib/esm/view/projectList/projectListState.js +1 -0
  48. package/lib/esm/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.js +83 -0
  49. package/lib/esm/view/recommendation/recommendationReducer.js +52 -1
  50. package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +1 -0
  51. package/lib/esm/view/transactionDetail/epics/transactionDetailEpic.js +6 -0
  52. package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -0
  53. package/lib/esm/view/transactionDetail/transactionDetailSelector.js +14 -2
  54. package/lib/esm/view/transactionList/fetchTransactionListByProjectEpic.js +45 -0
  55. package/lib/esm/view/transactionList/parallelFetchProjectTransactionListEpic.js +32 -0
  56. package/lib/esm/view/transactionList/transactionListByProjectSelector.js +58 -0
  57. package/lib/esm/view/transactionList/transactionListReducer.js +83 -2
  58. package/lib/esm/view/transactionList/transactionListState.js +10 -0
  59. package/lib/index.d.ts +13 -9
  60. package/lib/index.js +42 -27
  61. package/lib/reducer.d.ts +3 -0
  62. package/lib/reducer.js +3 -0
  63. package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
  64. package/lib/view/auditReportView/auditReportViewState.d.ts +1 -1
  65. package/lib/view/billPayCard/billPayCardSelector.d.ts +1 -1
  66. package/lib/view/cardBalance/cardBalanceSelector.d.ts +1 -1
  67. package/lib/view/cashBalance/cashBalanceSelector.d.ts +1 -1
  68. package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +1 -1
  69. package/lib/view/cashPosition/cashPositionSelector.d.ts +1 -1
  70. package/lib/view/dashboard/dashboardReducer.d.ts +2 -2
  71. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.d.ts +2 -1
  72. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
  73. package/lib/view/expenseAutomationView/epics/transactionCategorization/updateTransactionCategorizationEpic.js +3 -0
  74. package/lib/view/expenseAutomationView/helpers/saveTransactionHelper.js +1 -0
  75. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +2 -1
  76. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +11 -2
  77. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +3 -1
  78. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +127 -31
  79. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +4 -0
  80. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +12 -1
  81. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +5 -1
  82. package/lib/view/expenseAutomationView/types/transactionsViewState.js +1 -0
  83. package/lib/view/financeStatement/financeStatementReducer.d.ts +1 -1
  84. package/lib/view/globalMerchantView/globalMerchantViewReducer.d.ts +2 -2
  85. package/lib/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.d.ts +15 -1
  86. package/lib/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.js +45 -0
  87. package/lib/view/projectList/fetchProjectListEpic.d.ts +8 -0
  88. package/lib/view/projectList/fetchProjectListEpic.js +42 -0
  89. package/lib/view/projectList/projectListPayload.d.ts +7 -0
  90. package/lib/view/projectList/projectListPayload.js +2 -0
  91. package/lib/view/projectList/projectListReducer.d.ts +9 -0
  92. package/lib/view/projectList/projectListReducer.js +43 -0
  93. package/lib/view/projectList/projectListSelector.d.ts +8 -0
  94. package/lib/view/projectList/projectListSelector.js +13 -0
  95. package/lib/view/projectList/projectListState.d.ts +4 -0
  96. package/lib/view/projectList/projectListState.js +2 -0
  97. package/lib/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.d.ts +8 -0
  98. package/lib/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.js +87 -0
  99. package/lib/view/recommendation/recommendationReducer.d.ts +21 -1
  100. package/lib/view/recommendation/recommendationReducer.js +53 -2
  101. package/lib/view/reimbursementCard/reimbursementCardSelector.d.ts +1 -1
  102. package/lib/view/reportsResync/reportsResyncReducer.d.ts +2 -2
  103. package/lib/view/tasksCard/tasksCardSelector.d.ts +1 -1
  104. package/lib/view/topEx/topExSelector.d.ts +1 -1
  105. package/lib/view/transactionDetail/epics/saveTransactionHelper.js +1 -0
  106. package/lib/view/transactionDetail/epics/transactionDetailEpic.d.ts +2 -1
  107. package/lib/view/transactionDetail/epics/transactionDetailEpic.js +6 -0
  108. package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -0
  109. package/lib/view/transactionDetail/transactionDetailSelector.d.ts +3 -0
  110. package/lib/view/transactionDetail/transactionDetailSelector.js +13 -1
  111. package/lib/view/transactionDetail/transactionDetailState.d.ts +2 -0
  112. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +2 -0
  113. package/lib/view/transactionList/fetchTransactionListByProjectEpic.d.ts +8 -0
  114. package/lib/view/transactionList/fetchTransactionListByProjectEpic.js +52 -0
  115. package/lib/view/transactionList/parallelFetchProjectTransactionListEpic.d.ts +6 -0
  116. package/lib/view/transactionList/parallelFetchProjectTransactionListEpic.js +36 -0
  117. package/lib/view/transactionList/transactionListByProjectSelector.d.ts +22 -0
  118. package/lib/view/transactionList/transactionListByProjectSelector.js +66 -0
  119. package/lib/view/transactionList/transactionListPayload.d.ts +15 -0
  120. package/lib/view/transactionList/transactionListReducer.d.ts +25 -2
  121. package/lib/view/transactionList/transactionListReducer.js +83 -2
  122. package/lib/view/transactionList/transactionListState.d.ts +15 -0
  123. package/lib/view/transactionList/transactionListState.js +13 -1
  124. package/package.json +1 -1
@@ -183,6 +183,7 @@ const buildLineItemById = (transaction, uncategorizedIncomeExpense, recommendati
183
183
  const lineId = line.id;
184
184
  record.account = accountLine.account;
185
185
  record.class = accountLine.class;
186
+ record.project = accountLine.project;
186
187
  record.vendor =
187
188
  accountLine.vendor != null
188
189
  ? {
@@ -305,7 +306,7 @@ const buildLineItemById = (transaction, uncategorizedIncomeExpense, recommendati
305
306
  });
306
307
  return lineItemData;
307
308
  };
308
- export const toSetAllLineItemsToCategoryClass = (draft, transaction, transactionId, transactionLineId, transactionDetailLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
309
+ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transactionId, transactionLineId, transactionDetailLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
309
310
  let newLineItems = {};
310
311
  let selectedCheckBoxTransactionIds = draft.selectedCheckBoxTransactionIds;
311
312
  let similarTransactionUpdated = false;
@@ -325,7 +326,7 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
325
326
  const record = {
326
327
  ...transactionDetailLocalData.lineItemById[lineId],
327
328
  };
328
- // if the line is the one that is being updated, then set the account and class
329
+ // if the line is the one that is being updated, then set the account, class, and project
329
330
  if (transactionId === record.transactionId &&
330
331
  transactionLineId === lineId) {
331
332
  if (selectedAccount != null) {
@@ -334,6 +335,9 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
334
335
  if (selectedClassBase != null) {
335
336
  record.class = selectedClassBase;
336
337
  }
338
+ if (selectedProjectBase != null) {
339
+ record.project = selectedProjectBase;
340
+ }
337
341
  const isClassSatisfied = isAccountingClassesEnabled === false || record.class != null;
338
342
  if (isClassSatisfied && record.account != null) {
339
343
  if (selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
@@ -385,6 +389,10 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
385
389
  selectedCheckBoxTransactionIds.push(transaction.id);
386
390
  }
387
391
  }
392
+ // Project intentionally does NOT propagate to other rows of
393
+ // the same vendor: each line can carry a different project,
394
+ // and auto-filling siblings (the way class does) leads to
395
+ // unexpected bulk selection.
388
396
  }
389
397
  }
390
398
  if (line.type === 'transaction_with_product_or_service_line') {
@@ -514,6 +522,7 @@ export const setEntityRecommendationForLineIdInLocalData = (draft, transaction,
514
522
  ...newLineItem,
515
523
  class: matchingRecord.class,
516
524
  account: matchingRecord.account,
525
+ project: matchingRecord.project,
517
526
  lineEntity: matchingRecord.lineEntity,
518
527
  };
519
528
  }
@@ -1,6 +1,7 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import recordGet from 'lodash/get';
3
3
  import uniq from 'lodash/uniq';
4
+ import { fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, } from '../../recommendation/recommendationReducer';
4
5
  import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
5
6
  import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
6
7
  import { MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
@@ -282,7 +283,7 @@ const expenseAutomationTransactionsView = createSlice({
282
283
  },
283
284
  setAllItemsToCategoryClassInLocalDataForCategorization: {
284
285
  reducer(draft, action) {
285
- const { vendorId, customerId, selectedAccount, selectedClassBase, transactions, transactionId, lineId, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
286
+ const { vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, transactions, transactionId, lineId, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
286
287
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
287
288
  .transactionReviewLocalDataById, transactionId, undefined);
288
289
  const existingAccount = transactionLocalData?.transactionReviewLocalData.lineItemById[lineId]
@@ -296,6 +297,7 @@ const expenseAutomationTransactionsView = createSlice({
296
297
  ...transactionLocalData.transactionReviewLocalData.lineItemById[lineId],
297
298
  account: selectedAccount,
298
299
  class: selectedClassBase,
300
+ project: selectedProjectBase,
299
301
  },
300
302
  },
301
303
  };
@@ -324,9 +326,16 @@ const expenseAutomationTransactionsView = createSlice({
324
326
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
325
327
  .transactionReviewLocalDataById, transaction.id, undefined);
326
328
  if (transactionLocalData != null) {
327
- const { localData, similarTransactionUpdated } = toSetAllLineItemsToCategoryClass(draft.transactionCategorizationView[selectedTab], transaction, transactionId, lineId, transactionLocalData.transactionReviewLocalData, existingAccount?.accountId !== selectedAccount?.accountId
329
+ const existingClassId = transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.class?.classId;
330
+ const existingProjectId = transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.project?.projectId;
331
+ const updatedItem = existingAccount?.accountId !== selectedAccount?.accountId
328
332
  ? 'category'
329
- : 'class', uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
333
+ : existingClassId !== selectedClassBase?.classId
334
+ ? 'class'
335
+ : existingProjectId !== selectedProjectBase?.projectId
336
+ ? 'project'
337
+ : 'class';
338
+ const { localData, similarTransactionUpdated } = toSetAllLineItemsToCategoryClass(draft.transactionCategorizationView[selectedTab], transaction, transactionId, lineId, transactionLocalData.transactionReviewLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
330
339
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
331
340
  transactionId: transaction.id,
332
341
  transactionReviewLocalData: localData,
@@ -345,7 +354,7 @@ const expenseAutomationTransactionsView = createSlice({
345
354
  ...dirtyIds,
346
355
  ]);
347
356
  },
348
- prepare(selectedTab, transactionId, lineId, transactions, uncategorizedAccounts, vendorId, customerId, selectedAccount, selectedClassBase, lineEntity, recommendations, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) {
357
+ prepare(selectedTab, transactionId, lineId, transactions, uncategorizedAccounts, vendorId, customerId, selectedAccount, selectedClassBase, lineEntity, recommendations, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled, selectedProjectBase) {
349
358
  return {
350
359
  payload: {
351
360
  selectedTab,
@@ -357,6 +366,7 @@ const expenseAutomationTransactionsView = createSlice({
357
366
  lineEntity,
358
367
  selectedAccount,
359
368
  selectedClassBase,
369
+ selectedProjectBase,
360
370
  recommendations,
361
371
  uncategorizedAccounts,
362
372
  isAccountingClassesEnabled,
@@ -387,10 +397,53 @@ const expenseAutomationTransactionsView = createSlice({
387
397
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
388
398
  .transactionReviewLocalDataById, transactionId.id, undefined);
389
399
  if (transactionLocalData != null) {
400
+ // Snapshot the pre-fetch selection. setEntityRecommendationForLineIdInLocalData
401
+ // mutates selectedCheckBoxTransactionIds internally (auto-add when
402
+ // all-filled, auto-remove otherwise) but does not enforce
403
+ // MAX_SELECTION_LIMIT. We restore from this snapshot below and let
404
+ // the reducer-level gate own the selection contract so the cap is
405
+ // honored and the gate mirrors the original auto-add gate from
406
+ // updateSelectedVendor/CustomerForTransaction.
407
+ const selectionBeforeFetch = [
408
+ ...draft.transactionCategorizationView[selectedTab]
409
+ .selectedCheckBoxTransactionIds,
410
+ ];
411
+ const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
412
+ // Flip the per-line "fetching" flag to Completed for the lines this
413
+ // fetch covered. Reads as the success arm of the
414
+ // fetchEntityRecommendationsForCategorization → epic → reducer cycle.
415
+ lineIds.forEach((lineId) => {
416
+ const lineItem = newLocalData.lineItemById[lineId];
417
+ if (lineItem != null) {
418
+ newLocalData.lineItemById[lineId] = {
419
+ ...lineItem,
420
+ categoryClassRecommendationsFetchState: {
421
+ fetchState: 'Completed',
422
+ error: undefined,
423
+ },
424
+ };
425
+ }
426
+ });
390
427
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
391
428
  transactionId: transaction.id,
392
- transactionReviewLocalData: setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled),
429
+ transactionReviewLocalData: newLocalData,
393
430
  };
431
+ // Auto-add the transaction to the bulk-save selection now that
432
+ // fresh recs have landed and all required fields are filled. This
433
+ // is the success-arm of the deferral noted in
434
+ // updateSelectedVendor/CustomerForTransaction: their auto-add was
435
+ // suppressed because pre-fetch category/class were stale; we run
436
+ // the same gate here against newLocalData so the checkbox flips
437
+ // on against fresh recs.
438
+ draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = selectionBeforeFetch;
439
+ if (checkIfAllLineItemsAreCategoryClassFilled(transaction, newLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true &&
440
+ selectionBeforeFetch.length < MAX_SELECTION_LIMIT &&
441
+ selectionBeforeFetch.includes(transaction.id) === false) {
442
+ draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
443
+ ...selectionBeforeFetch,
444
+ transaction.id,
445
+ ];
446
+ }
394
447
  // Mark transaction as dirty (autofill happened)
395
448
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
396
449
  draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
@@ -444,7 +497,13 @@ const expenseAutomationTransactionsView = createSlice({
444
497
  action.payload.status;
445
498
  },
446
499
  updateSelectedCustomerForTransaction(draft, action) {
447
- const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
500
+ const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab,
501
+ // uncategorizedAccounts, isAccountingClassesEnabled, and
502
+ // isUncategorizedExpenseCategoryEnabled remain in the payload type for
503
+ // backward compatibility but are no longer read here — the post-fetch
504
+ // success reducer (setEntityRecommendationForLineIdsForCategorization)
505
+ // owns the all-fields-filled check that drives auto-selection.
506
+ } = action.payload;
448
507
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
449
508
  .transactionReviewLocalDataById, transactionId, undefined);
450
509
  if (transactionLocalData != null) {
@@ -493,18 +552,13 @@ const expenseAutomationTransactionsView = createSlice({
493
552
  transactionReviewLocalData: newTransactionLocalData,
494
553
  transactionId,
495
554
  };
496
- const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
497
- .selectedCheckBoxTransactionIds;
498
- if (newTransactionLocalData.customer != null &&
499
- newTransactionLocalData.customer.id != null &&
500
- checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
501
- selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
502
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
503
- draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
504
- ...selectedCheckBoxTransactionIds,
505
- transactionId,
506
- ];
507
- }
555
+ // Auto-selection of the row checkbox is intentionally deferred to
556
+ // setEntityRecommendationForLineIdsForCategorization (success path of
557
+ // the recommendations fetch). On a payee change the existing
558
+ // category/class are stale, so auto-selecting against them would
559
+ // either trigger save flows on the wrong data or rely on a later
560
+ // recommendations write to fix it. Letting the success reducer do the
561
+ // auto-add keeps "checkbox flips on" aligned with "fresh recs are in".
508
562
  // Mark transaction as dirty (customer updated)
509
563
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
510
564
  draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
@@ -516,7 +570,11 @@ const expenseAutomationTransactionsView = createSlice({
516
570
  }
517
571
  },
518
572
  updateSelectedVendorForTransaction(draft, action) {
519
- const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
573
+ const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab,
574
+ // See updateSelectedCustomerForTransaction for why
575
+ // uncategorizedAccounts, isAccountingClassesEnabled, and
576
+ // isUncategorizedExpenseCategoryEnabled are no longer destructured.
577
+ } = action.payload;
520
578
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
521
579
  .transactionReviewLocalDataById, transactionId, undefined);
522
580
  if (transactionLocalData != null) {
@@ -569,18 +627,8 @@ const expenseAutomationTransactionsView = createSlice({
569
627
  },
570
628
  transactionId,
571
629
  };
572
- const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
573
- .selectedCheckBoxTransactionIds;
574
- if (newTransactionLocalData.vendor != null &&
575
- newTransactionLocalData.vendor.id != null &&
576
- checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
577
- selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
578
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
579
- draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
580
- ...selectedCheckBoxTransactionIds,
581
- transactionId,
582
- ];
583
- }
630
+ // See updateSelectedCustomerForTransaction above for why auto-select
631
+ // is deferred to setEntityRecommendationForLineIdsForCategorization.
584
632
  // Mark transaction as dirty (vendor updated)
585
633
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
586
634
  draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
@@ -793,6 +841,54 @@ const expenseAutomationTransactionsView = createSlice({
793
841
  },
794
842
  },
795
843
  },
844
+ extraReducers: (builder) => {
845
+ // Start arm of the categorization-only recommendations fetch. Marks every
846
+ // affected line's per-line flag as In-Progress. The epic uses
847
+ // groupBy(transactionId+lineIds) + switchMap, so concurrent fetches for
848
+ // distinct (transaction, lineIds-set) targets do not cancel each other —
849
+ // there's no need for a global cleanup sweep here.
850
+ builder.addCase(fetchEntityRecommendationsForCategorization, (draft, action) => {
851
+ const { selectedTab, transactionDetails } = action.payload;
852
+ const { transactionId, lineIds } = transactionDetails;
853
+ const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
854
+ .transactionReviewLocalDataById, transactionId.id, undefined);
855
+ if (transactionLocalData != null) {
856
+ lineIds.forEach((lineId) => {
857
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
858
+ if (lineItem != null) {
859
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
860
+ ...lineItem,
861
+ categoryClassRecommendationsFetchState: {
862
+ fetchState: 'In-Progress',
863
+ error: undefined,
864
+ },
865
+ };
866
+ }
867
+ });
868
+ }
869
+ });
870
+ // Failure arm. Flips the affected lines' per-line flag to Error so the
871
+ // skeleton resolves and the queued checkbox re-evaluation drops the entry.
872
+ builder.addCase(setCategoryClassRecommendationsForCategorizationFailure, (draft, action) => {
873
+ const { selectedTab, transactionId, lineIds, error } = action.payload;
874
+ const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
875
+ .transactionReviewLocalDataById, transactionId.id, undefined);
876
+ if (transactionLocalData != null) {
877
+ lineIds.forEach((lineId) => {
878
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
879
+ if (lineItem != null) {
880
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
881
+ ...lineItem,
882
+ categoryClassRecommendationsFetchState: {
883
+ fetchState: 'Error',
884
+ error,
885
+ },
886
+ };
887
+ }
888
+ });
889
+ }
890
+ });
891
+ },
796
892
  });
797
893
  export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
798
894
  export default expenseAutomationTransactionsView.reducer;
@@ -1,14 +1,15 @@
1
1
  import omit from 'lodash/omit';
2
2
  import { reduceAllFetchState, reduceAnyFetchState, } from '../../../commonStateTypes/reduceFetchState';
3
3
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
4
- import { getIsAccountingClassesEnabled, getCurrentTenant, } from '../../../entity/tenant/tenantSelector';
4
+ import { getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
5
5
  import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
6
6
  import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
7
7
  import { getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
8
8
  import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
9
+ import { getProjectList } from '../../projectList/projectListSelector';
9
10
  import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
10
11
  export function getExpenseAutomationTransactionView(state) {
11
- const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
12
+ const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
12
13
  const { selectedTransactionCategorizationTab } = expenseAutomationTransactionsViewState;
13
14
  const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
14
15
  const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
@@ -17,6 +18,8 @@ export function getExpenseAutomationTransactionView(state) {
17
18
  const allClasses = classList.classes.map((classData) => {
18
19
  return omit(classData, ['accountIds', 'key']);
19
20
  });
21
+ const projectList = getProjectList(projectState, projectListState);
22
+ const isAccountingProjectsEnabled = getIsAccountingProjectsEnabled(state);
20
23
  const accountsHierarchyList = getNestedAccountListHierarchy(accountListState, 'accountList');
21
24
  const classHierarchyList = getNestedClassListHierarchy(classListState);
22
25
  const currentTenant = getCurrentTenant(state);
@@ -39,6 +42,11 @@ export function getExpenseAutomationTransactionView(state) {
39
42
  return null;
40
43
  })
41
44
  .filter((transaction) => transaction != null);
45
+ // True if any visible line is currently fetching category/class
46
+ // recommendations because the user changed the payee. Used to disable Save
47
+ // and to defer manual checkbox selection.
48
+ const hasInFlightCategoryClassRecommendations = transactionLocalData.some((transaction) => Object.values(transaction.transactionReviewLocalData.lineItemById).some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
49
+ 'In-Progress'));
42
50
  const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
43
51
  const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
44
52
  const monthEndFetchState = monthYearPeriodId != null
@@ -96,6 +104,8 @@ export function getExpenseAutomationTransactionView(state) {
96
104
  classList: allClasses,
97
105
  accountsHierarchyList,
98
106
  classHierarchyList,
107
+ isAccountingProjectsEnabled,
108
+ projectList,
99
109
  transactionLocalData,
100
110
  uiState,
101
111
  refreshStatus,
@@ -107,5 +117,6 @@ export function getExpenseAutomationTransactionView(state) {
107
117
  uploadReceiptStatusById,
108
118
  selectedTransactionId,
109
119
  selectedTransactionLineId,
120
+ hasInFlightCategoryClassRecommendations,
110
121
  };
111
122
  }
@@ -8,6 +8,7 @@ const TRANSACTIONS_SORT_KEYS = [
8
8
  'date',
9
9
  'category',
10
10
  'class',
11
+ 'project',
11
12
  'memo_and_receipt',
12
13
  ];
13
14
  export const toTransactionsSortKey = (v) => stringToUnion(v, TRANSACTIONS_SORT_KEYS);
@@ -115,6 +115,50 @@ function buildHorizontalClassColumnBalancesSlice(report, classAmounts, totalAmou
115
115
  },
116
116
  };
117
117
  }
118
+ /**
119
+ * Sum balances across descendant accounts (recursive).
120
+ *
121
+ * Parent rows in the horizontal pivot only carry their own direct postings
122
+ * (zero for "container" accounts), so callers rendering "Total {parent}" rows
123
+ * must aggregate from descendants to get the real per-class totals.
124
+ *
125
+ * Returns one `COABalance` per class column + total (same shape as the
126
+ * enriched `balancesInTimeframe.balances`), with `amount` summed and `type`
127
+ * set to `default_with_null_balance` for zeros. Returns `[]` when no
128
+ * descendants have enriched data.
129
+ */
130
+ export function aggregateHorizontalDescendantBalances(accounts, childIndices, childrenByParent) {
131
+ let template;
132
+ const sums = new Map();
133
+ const walk = (idx) => {
134
+ const childRow = accounts[idx];
135
+ const childBalances = childRow?.accountReportData?.balancesInTimeframe.balances;
136
+ if (childBalances != null) {
137
+ if (template == null) {
138
+ template = childBalances;
139
+ }
140
+ childBalances.forEach((b, i) => {
141
+ sums.set(i, (sums.get(i) ?? 0) + (b.balance.amount ?? 0));
142
+ });
143
+ }
144
+ // Always recurse — a missing intermediate node should not prune its
145
+ // descendants from the aggregate.
146
+ const grandChildren = childrenByParent[idx] ?? [];
147
+ grandChildren.forEach(walk);
148
+ };
149
+ childIndices.forEach(walk);
150
+ if (template == null) {
151
+ return [];
152
+ }
153
+ return template.map((tpl, i) => {
154
+ const amount = sums.get(i) ?? 0;
155
+ return {
156
+ ...tpl,
157
+ balance: { ...tpl.balance, amount },
158
+ type: amount === 0 ? 'default_with_null_balance' : 'default',
159
+ };
160
+ });
161
+ }
118
162
  /**
119
163
  * Parent index per DFS row — O(n). Matches the scan in the former
120
164
  * `findHorizontalAccountParentIndex` (depth &lt;= 2 rows have no parent).
@@ -0,0 +1,38 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap, withLatestFrom, } from 'rxjs/operators';
3
+ import { updateProjects } from '../../entity/project/projectReducer';
4
+ import { getIsAccountingProjectsEnabled } from '../../entity/tenant/tenantSelector';
5
+ import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
6
+ import { fetchProjectList, updateProjectList, updateProjectListFailure, } from './projectListReducer';
7
+ const buildProjectListQuery = (isIncludeDeleted) => {
8
+ const query = {
9
+ is_deleted: isIncludeDeleted,
10
+ sort_by: 'project_name',
11
+ sort_order: 'asc',
12
+ is_full: true,
13
+ };
14
+ return `?query=${encodeURIComponent(JSON.stringify(query))}`;
15
+ };
16
+ export const fetchProjectListEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchProjectList.match), withLatestFrom(state$), switchMap(([action, state]) => {
17
+ if (!getIsAccountingProjectsEnabled(state)) {
18
+ return of(updateProjectList({
19
+ status: createZeniAPIStatus('Success', ''),
20
+ data: { projects: [] },
21
+ }));
22
+ }
23
+ const { isIncludeDeleted } = action.payload;
24
+ const queryParams = buildProjectListQuery(isIncludeDeleted);
25
+ return zeniAPI
26
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/projects${queryParams}`)
27
+ .pipe(mergeMap((response) => {
28
+ if (isSuccessResponse(response)) {
29
+ return of(updateProjects(response.data?.projects ?? []), updateProjectList(response));
30
+ }
31
+ else {
32
+ return of(updateProjectListFailure(response.status));
33
+ }
34
+ }), catchError((error) => {
35
+ return of(updateProjectListFailure(createZeniAPIStatus('Unexpected Error', 'Fetch Projects REST API call errored out' +
36
+ JSON.stringify(error))));
37
+ }));
38
+ }));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ export const initialState = {
3
+ fetchState: 'Not-Started',
4
+ error: undefined,
5
+ projectIds: [],
6
+ hasValidState() {
7
+ return this.fetchState == 'Completed';
8
+ },
9
+ };
10
+ const projectList = createSlice({
11
+ name: 'projectList',
12
+ initialState,
13
+ reducers: {
14
+ fetchProjectList: {
15
+ reducer(draft) {
16
+ draft.fetchState = 'In-Progress';
17
+ draft.error = undefined;
18
+ },
19
+ prepare(isIncludeDeleted) {
20
+ return { payload: { isIncludeDeleted: isIncludeDeleted ?? false } };
21
+ },
22
+ },
23
+ updateProjectList(draft, action) {
24
+ draft.projectIds =
25
+ action.payload.data?.projects?.map((project) => project.zeni_project_id) ?? [];
26
+ draft.fetchState = 'Completed';
27
+ draft.error = undefined;
28
+ },
29
+ updateProjectListFailure(draft, action) {
30
+ draft.fetchState = 'Error';
31
+ draft.error = action.payload;
32
+ },
33
+ clearProjectList(draft) {
34
+ Object.assign(draft, initialState);
35
+ },
36
+ },
37
+ });
38
+ export const { fetchProjectList, updateProjectList, updateProjectListFailure, clearProjectList, } = projectList.actions;
39
+ export default projectList.reducer;
@@ -0,0 +1,9 @@
1
+ import { getProjectsByIds } from '../../entity/project/projectSelector';
2
+ export const getProjectList = (projectState, projectListState) => {
3
+ return {
4
+ projects: getProjectsByIds(projectState, projectListState.projectIds),
5
+ fetchState: projectListState.fetchState,
6
+ error: projectListState.error,
7
+ version: 0,
8
+ };
9
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,83 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, groupBy, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { getIsAccountingClassesEnabled } from '../../entity/tenant/tenantSelector';
4
+ import { getTransactionWithCOT, getTransactionWithCOTFromRecommendations, } from '../../entity/transaction/transactionHelper';
5
+ import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
6
+ import { setEntityRecommendationForLineIdsForCategorization } from '../../view/expenseAutomationView/reducers/transactionsViewReducer';
7
+ import { getUncategorizedAccounts } from '../accountList/accountListSelector';
8
+ import { getActualTransactionID } from '../transactionDetail/transactionDetailState';
9
+ import { getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT, getRecommendationFromRecommendationPayload, } from './recommendationHelper';
10
+ import { fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, updateRecommendationForEntity, } from './recommendationReducer';
11
+ // Categorization-list-only recommendations fetch.
12
+ //
13
+ // Concurrency model: groupBy on (transactionId.id, sorted lineIds) → switchMap
14
+ // inside each group → mergeMap to flatten groups. This means:
15
+ // • Same target (same transaction + same lineIds-set) — newer fetch cancels
16
+ // the prior in-flight fetch. Avoids the same-line stale-response race.
17
+ // • Different targets — run fully in parallel. Avoids the silent
18
+ // "stuck spinner" cancellation that the global-switchMap epic suffers
19
+ // from when the user moves between rows or JE lines mid-flight.
20
+ //
21
+ // The transactionsViewReducer's per-line categoryClassRecommendationsFetchState
22
+ // flag is set to In-Progress when the action enters this epic (start arm in
23
+ // extraReducers), Completed via setEntityRecommendationForLineIdsForCategorization,
24
+ // or Error via setCategoryClassRecommendationsForCategorizationFailure.
25
+ export const fetchEntityRecommendationsForCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchEntityRecommendationsForCategorization.match), groupBy((action) => {
26
+ const { transactionId, lineIds } = action.payload.transactionDetails;
27
+ return `${transactionId.id}|${[...lineIds].sort().join('|')}`;
28
+ }), mergeMap((group$) => group$.pipe(switchMap((action) => {
29
+ const { entity, amount, transactionDetails, isFetchCOT, selectedTab, isUncategorizedExpenseCategoryEnabled, } = action.payload;
30
+ const query = {
31
+ amount,
32
+ transaction_id: getActualTransactionID(transactionDetails.transactionId.id),
33
+ transaction_type: transactionDetails.transaction.type,
34
+ line_ids: transactionDetails.lineIds,
35
+ };
36
+ if (isFetchCOT === true) {
37
+ query.include_cot = true;
38
+ }
39
+ const entityQuery = {
40
+ entity_name: entity.name,
41
+ entity_type: entity.type,
42
+ };
43
+ const recommendationUrl = entity.qboId === entity.name // entity id null case
44
+ ? `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/recommendations?query=${encodeURIComponent(JSON.stringify(entityQuery))}`
45
+ : `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/${toEntityTypeURL(entity.type)}/${entity.qboId}/recommendations?query=${encodeURIComponent(JSON.stringify(query))}`;
46
+ return zeniAPI
47
+ .getJSON(recommendationUrl)
48
+ .pipe(mergeMap((response) => {
49
+ if (isSuccessResponse(response) && response.data != null) {
50
+ const actions = [];
51
+ const { transactionId, transaction, lineIds } = transactionDetails;
52
+ const recommendationWithCOTByLineId = getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT(response.data.recommendations, lineIds);
53
+ const recommendations = getRecommendationFromRecommendationPayload(response.data.recommendations);
54
+ const transactionWithCOT = isFetchCOT === true &&
55
+ recommendationWithCOTByLineId != null
56
+ ? getTransactionWithCOTFromRecommendations(transaction, recommendationWithCOTByLineId)
57
+ : getTransactionWithCOT(transaction);
58
+ actions.push(updateRecommendationForEntity({
59
+ entityId: entity.id,
60
+ entityType: entity.type,
61
+ recommendations,
62
+ }));
63
+ if (recommendationWithCOTByLineId != null) {
64
+ const uncategorizedIncomeExpense = getUncategorizedAccounts(state$.value.accountState, state$.value.accountListState, 'accountList');
65
+ actions.push(setEntityRecommendationForLineIdsForCategorization(selectedTab, transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled, getIsAccountingClassesEnabled(state$.value)));
66
+ }
67
+ else {
68
+ // Successful HTTP but no per-line recommendations — still
69
+ // need to clear the In-Progress flag, otherwise the
70
+ // skeleton sticks. Treat as a "no recommendations" success
71
+ // by firing the failure action with a benign error so the
72
+ // line flag flips out of In-Progress.
73
+ actions.push(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionId, lineIds, createZeniAPIStatus('Unexpected Error', 'Recommendations response missing per-line data')));
74
+ }
75
+ return from(actions);
76
+ }
77
+ else {
78
+ return of(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionDetails.transactionId, transactionDetails.lineIds, response.status));
79
+ }
80
+ }), catchError((error) => of(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionDetails.transactionId, transactionDetails.lineIds, createZeniAPIStatus('Unexpected Error', 'Fetch recommendations for categorization REST API call errored out' +
81
+ JSON.stringify(error))))));
82
+ }))));
83
+ const toEntityTypeURL = (entityType) => `${entityType}s`;