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

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 +100 -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 +100 -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,9 +397,25 @@ const expenseAutomationTransactionsView = createSlice({
387
397
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
388
398
  .transactionReviewLocalDataById, transactionId.id, undefined);
389
399
  if (transactionLocalData != null) {
400
+ const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
401
+ // Flip the per-line "fetching" flag to Completed for the lines this
402
+ // fetch covered. Reads as the success arm of the
403
+ // fetchEntityRecommendationsForCategorization → epic → reducer cycle.
404
+ lineIds.forEach((lineId) => {
405
+ const lineItem = newLocalData.lineItemById[lineId];
406
+ if (lineItem != null) {
407
+ newLocalData.lineItemById[lineId] = {
408
+ ...lineItem,
409
+ categoryClassRecommendationsFetchState: {
410
+ fetchState: 'Completed',
411
+ error: undefined,
412
+ },
413
+ };
414
+ }
415
+ });
390
416
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
391
417
  transactionId: transaction.id,
392
- transactionReviewLocalData: setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled),
418
+ transactionReviewLocalData: newLocalData,
393
419
  };
394
420
  // Mark transaction as dirty (autofill happened)
395
421
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
@@ -444,7 +470,13 @@ const expenseAutomationTransactionsView = createSlice({
444
470
  action.payload.status;
445
471
  },
446
472
  updateSelectedCustomerForTransaction(draft, action) {
447
- const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
473
+ const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab,
474
+ // uncategorizedAccounts, isAccountingClassesEnabled, and
475
+ // isUncategorizedExpenseCategoryEnabled remain in the payload type for
476
+ // backward compatibility but are no longer read here — the post-fetch
477
+ // success reducer (setEntityRecommendationForLineIdsForCategorization)
478
+ // owns the all-fields-filled check that drives auto-selection.
479
+ } = action.payload;
448
480
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
449
481
  .transactionReviewLocalDataById, transactionId, undefined);
450
482
  if (transactionLocalData != null) {
@@ -493,18 +525,13 @@ const expenseAutomationTransactionsView = createSlice({
493
525
  transactionReviewLocalData: newTransactionLocalData,
494
526
  transactionId,
495
527
  };
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
- }
528
+ // Auto-selection of the row checkbox is intentionally deferred to
529
+ // setEntityRecommendationForLineIdsForCategorization (success path of
530
+ // the recommendations fetch). On a payee change the existing
531
+ // category/class are stale, so auto-selecting against them would
532
+ // either trigger save flows on the wrong data or rely on a later
533
+ // recommendations write to fix it. Letting the success reducer do the
534
+ // auto-add keeps "checkbox flips on" aligned with "fresh recs are in".
508
535
  // Mark transaction as dirty (customer updated)
509
536
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
510
537
  draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
@@ -516,7 +543,11 @@ const expenseAutomationTransactionsView = createSlice({
516
543
  }
517
544
  },
518
545
  updateSelectedVendorForTransaction(draft, action) {
519
- const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
546
+ const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab,
547
+ // See updateSelectedCustomerForTransaction for why
548
+ // uncategorizedAccounts, isAccountingClassesEnabled, and
549
+ // isUncategorizedExpenseCategoryEnabled are no longer destructured.
550
+ } = action.payload;
520
551
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
521
552
  .transactionReviewLocalDataById, transactionId, undefined);
522
553
  if (transactionLocalData != null) {
@@ -569,18 +600,8 @@ const expenseAutomationTransactionsView = createSlice({
569
600
  },
570
601
  transactionId,
571
602
  };
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
- }
603
+ // See updateSelectedCustomerForTransaction above for why auto-select
604
+ // is deferred to setEntityRecommendationForLineIdsForCategorization.
584
605
  // Mark transaction as dirty (vendor updated)
585
606
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
586
607
  draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
@@ -793,6 +814,54 @@ const expenseAutomationTransactionsView = createSlice({
793
814
  },
794
815
  },
795
816
  },
817
+ extraReducers: (builder) => {
818
+ // Start arm of the categorization-only recommendations fetch. Marks every
819
+ // affected line's per-line flag as In-Progress. The epic uses
820
+ // groupBy(transactionId+lineIds) + switchMap, so concurrent fetches for
821
+ // distinct (transaction, lineIds-set) targets do not cancel each other —
822
+ // there's no need for a global cleanup sweep here.
823
+ builder.addCase(fetchEntityRecommendationsForCategorization, (draft, action) => {
824
+ const { selectedTab, transactionDetails } = action.payload;
825
+ const { transactionId, lineIds } = transactionDetails;
826
+ const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
827
+ .transactionReviewLocalDataById, transactionId.id, undefined);
828
+ if (transactionLocalData != null) {
829
+ lineIds.forEach((lineId) => {
830
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
831
+ if (lineItem != null) {
832
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
833
+ ...lineItem,
834
+ categoryClassRecommendationsFetchState: {
835
+ fetchState: 'In-Progress',
836
+ error: undefined,
837
+ },
838
+ };
839
+ }
840
+ });
841
+ }
842
+ });
843
+ // Failure arm. Flips the affected lines' per-line flag to Error so the
844
+ // skeleton resolves and the queued checkbox re-evaluation drops the entry.
845
+ builder.addCase(setCategoryClassRecommendationsForCategorizationFailure, (draft, action) => {
846
+ const { selectedTab, transactionId, lineIds, error } = action.payload;
847
+ const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
848
+ .transactionReviewLocalDataById, transactionId.id, undefined);
849
+ if (transactionLocalData != null) {
850
+ lineIds.forEach((lineId) => {
851
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
852
+ if (lineItem != null) {
853
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
854
+ ...lineItem,
855
+ categoryClassRecommendationsFetchState: {
856
+ fetchState: 'Error',
857
+ error,
858
+ },
859
+ };
860
+ }
861
+ });
862
+ }
863
+ });
864
+ },
796
865
  });
797
866
  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
867
  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`;
@@ -106,12 +106,63 @@ const recommendation = createSlice({
106
106
  };
107
107
  },
108
108
  },
109
+ // Categorization-list-only fetch action. Mirrors the
110
+ // fetchEntityRecommendationsByTransactionId payload but is observed by a
111
+ // separate epic (with groupBy + switchMap, keyed on
112
+ // transactionId+lineIds-set) so concurrent fetches for distinct targets do
113
+ // not cancel each other. Reducer hooks in transactionsViewReducer use this
114
+ // to drive the per-line categoryClassRecommendationsFetchState flag.
115
+ fetchEntityRecommendationsForCategorization: {
116
+ reducer(draft, action) {
117
+ const { entity } = action.payload;
118
+ const key = getEntityRecommendationKey(entity.type, entity.id);
119
+ const existingRecommendation = draft.recommendationByEntity[key];
120
+ if (existingRecommendation == null) {
121
+ draft.recommendationByEntity[key] = {
122
+ ...initialEntityRecommendationState,
123
+ fetchState: 'In-Progress',
124
+ };
125
+ }
126
+ },
127
+ prepare(entity, amount, transactionDetails, selectedTab, isFetchCOT = false, isUncategorizedExpenseCategoryEnabled) {
128
+ return {
129
+ payload: {
130
+ entity,
131
+ amount,
132
+ transactionDetails,
133
+ selectedTab,
134
+ isFetchCOT,
135
+ isUncategorizedExpenseCategoryEnabled,
136
+ },
137
+ };
138
+ },
139
+ },
140
+ setCategoryClassRecommendationsForCategorizationFailure: {
141
+ reducer(
142
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
143
+ _draft,
144
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
145
+ _action) {
146
+ // The transactionsViewReducer handles the per-line flag flip; this
147
+ // reducer slice has no recommendation cache to update for the failure.
148
+ },
149
+ prepare(selectedTab, transactionId, lineIds, error) {
150
+ return {
151
+ payload: {
152
+ selectedTab,
153
+ transactionId,
154
+ lineIds,
155
+ error,
156
+ },
157
+ };
158
+ },
159
+ },
109
160
  clearRecommendation(draft) {
110
161
  Object.assign(draft, initialState);
111
162
  },
112
163
  },
113
164
  });
114
- export const { fetchEntityRecommendationsByTransactionId, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
165
+ export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
115
166
  export default recommendation.reducer;
116
167
  const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
117
168
  const key = getEntityRecommendationKey(entityType, entityId);