@zeniai/client-epic-state 5.0.64-betaSS1 → 5.0.65-betaML1

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 (35) hide show
  1. package/lib/epic.d.ts +2 -1
  2. package/lib/epic.js +2 -1
  3. package/lib/esm/epic.js +2 -1
  4. package/lib/esm/index.js +6 -6
  5. package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +21 -1
  6. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +137 -21
  7. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -0
  8. package/lib/esm/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +78 -0
  9. package/lib/esm/view/recommendation/recommendationHelper.js +29 -0
  10. package/lib/esm/view/recommendation/recommendationReducer.js +36 -1
  11. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +94 -1
  12. package/lib/esm/view/transactionDetail/transactionDetailSelector.js +22 -0
  13. package/lib/index.d.ts +7 -6
  14. package/lib/index.js +35 -32
  15. package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
  16. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -0
  17. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +23 -2
  18. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +14 -1
  19. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +137 -21
  20. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +2 -0
  21. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +3 -0
  22. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +29 -0
  23. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +6 -0
  24. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.d.ts +9 -0
  25. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +82 -0
  26. package/lib/view/recommendation/recommendationHelper.d.ts +9 -0
  27. package/lib/view/recommendation/recommendationHelper.js +31 -1
  28. package/lib/view/recommendation/recommendationReducer.d.ts +18 -1
  29. package/lib/view/recommendation/recommendationReducer.js +37 -2
  30. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +11 -1
  31. package/lib/view/transactionDetail/transactionDetailReducer.js +95 -2
  32. package/lib/view/transactionDetail/transactionDetailSelector.d.ts +2 -0
  33. package/lib/view/transactionDetail/transactionDetailSelector.js +24 -1
  34. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +1 -0
  35. package/package.json +1 -1
@@ -27,6 +27,7 @@ export declare const removeTransactionFromTabView: (tabView: TransactionsTabView
27
27
  */
28
28
  export declare const filterAutoTabLineItems: (tabView: TransactionsTabViewState, transactionId: ID, updatedTransaction: SupportedTransactionWithCOT) => void;
29
29
  export declare const MAX_SELECTION_LIMIT = 5;
30
+ export declare const shouldAutoSelectAndAdd: (transaction: SupportedTransactionWithCOT, localData: TransactionReviewLocalData, selectedCheckBoxTransactionIds: ID[], uncategorizedAccounts: UncategorizedAccounts, isUncategorizedExpenseCategoryEnabled?: boolean, isAccountingClassesEnabled?: boolean) => boolean;
30
31
  export declare const toTransactionDetailLocalData: (transaction: SupportedTransactionWithCOT, selectedTab: TransactionsTab, lineItemsByTransactionIds: string[], uncategorizedIncomeExpense: UncategorizedAccounts, isUncategorizedExpenseCategoryEnabled?: boolean) => TransactionReviewLocalData;
31
32
  export declare const toSetAllLineItemsToCategoryClass: (draft: TransactionsTabViewState, transaction: SupportedTransactionWithCOT, transactionId: ID, transactionLineId: ID, transactionDetailLocalData: TransactionReviewLocalData, updatedItem: "category" | "class" | "project", uncategorizedAccounts: UncategorizedAccounts, selectedTab: TransactionsTab, vendorId?: ID, customerId?: ID, selectedAccount?: AccountBase, selectedClassBase?: ClassBase, selectedProjectBase?: ProjectBase, isUncategorizedExpenseCategoryEnabled?: boolean, isAccountingClassesEnabled?: boolean) => {
32
33
  localData: TransactionReviewLocalData;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hasTransactionLocalDataChanges = exports.isAnyItemWithUncategorizedExpenseAccount = exports.getSnackbarMessageForTransactionReview = exports.mergeTabSpecificLineItems = exports.getLineItemsByTransactionIdsFromLocalData = exports.getLineItemsByTransactionsIdsFromResponse = exports.checkIfAllLineItemsAreCategoryClassFilled = exports.setEntityRecommendationForLineIdInLocalData = exports.toSetAllLineItemsToCategoryClass = exports.toTransactionDetailLocalData = exports.MAX_SELECTION_LIMIT = exports.filterAutoTabLineItems = exports.removeTransactionFromTabView = exports.getPendingReviewLineIdsFromTransaction = void 0;
3
+ exports.hasTransactionLocalDataChanges = exports.isAnyItemWithUncategorizedExpenseAccount = exports.getSnackbarMessageForTransactionReview = exports.mergeTabSpecificLineItems = exports.getLineItemsByTransactionIdsFromLocalData = exports.getLineItemsByTransactionsIdsFromResponse = exports.checkIfAllLineItemsAreCategoryClassFilled = exports.setEntityRecommendationForLineIdInLocalData = exports.toSetAllLineItemsToCategoryClass = exports.toTransactionDetailLocalData = exports.shouldAutoSelectAndAdd = exports.MAX_SELECTION_LIMIT = exports.filterAutoTabLineItems = exports.removeTransactionFromTabView = exports.getPendingReviewLineIdsFromTransaction = void 0;
4
4
  const vendorTransaction_1 = require("../../../entity/transaction/stateTypes/vendorTransaction");
5
5
  const transactionDetailLocalDataHelper_1 = require("../../transactionDetail/transactionDetailLocalDataHelper");
6
6
  const transactionDetailLocalDataHelper_2 = require("../../transactionDetail/transactionDetailLocalDataHelper");
@@ -65,6 +65,23 @@ const filterAutoTabLineItems = (tabView, transactionId, updatedTransaction) => {
65
65
  };
66
66
  exports.filterAutoTabLineItems = filterAutoTabLineItems;
67
67
  exports.MAX_SELECTION_LIMIT = 5;
68
+ // Returns true when the transaction should be auto-added to the
69
+ // selectedCheckBoxTransactionIds list after a categorization-related
70
+ // state mutation: all line items are filled, the selection limit has
71
+ // not been hit, and the id isn't already in the list. Consolidates the
72
+ // triplet that was inlined at four sites (updateSelectedCustomer/Vendor,
73
+ // setAllItemsToCategoryClassInLocalDataForCategorization, and
74
+ // setEntityRecommendationForLineIdInLocalData below).
75
+ const shouldAutoSelectAndAdd = (transaction, localData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
76
+ if (selectedCheckBoxTransactionIds.includes(transaction.id)) {
77
+ return false;
78
+ }
79
+ if (selectedCheckBoxTransactionIds.length >= exports.MAX_SELECTION_LIMIT) {
80
+ return false;
81
+ }
82
+ return (0, exports.checkIfAllLineItemsAreCategoryClassFilled)(transaction, localData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
83
+ };
84
+ exports.shouldAutoSelectAndAdd = shouldAutoSelectAndAdd;
68
85
  const toTransactionDetailLocalData = (transaction, selectedTab, lineItemsByTransactionIds, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled) => {
69
86
  const transactionLocal = {
70
87
  ...transactionsViewState_1.initialSupportedTransactionCategorization.transactionReviewLocalData,
@@ -552,10 +569,14 @@ const setEntityRecommendationForLineIdInLocalData = (draft, transaction, transac
552
569
  selectedEntity: transactionDetailLocalData.selectedEntity,
553
570
  tabSpecificLineItems: transactionDetailLocalData.tabSpecificLineItems,
554
571
  };
572
+ // Auto-uncheck if not all line items are filled (the row's
573
+ // category/class became stale after the entity update). Otherwise
574
+ // route through shouldAutoSelectAndAdd so the MAX_SELECTION_LIMIT
575
+ // gate is consistently applied across all auto-add sites.
555
576
  if (!(0, exports.checkIfAllLineItemsAreCategoryClassFilled)(transaction, newLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
556
577
  selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds.filter((id) => id !== transaction.id);
557
578
  }
558
- else if (selectedCheckBoxTransactionIds.includes(transaction.id) === false) {
579
+ else if ((0, exports.shouldAutoSelectAndAdd)(transaction, newLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
559
580
  selectedCheckBoxTransactionIds.push(transaction.id);
560
581
  }
561
582
  draft.selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds;
@@ -157,7 +157,20 @@ export declare const fetchTransactionCategorization: import("@reduxjs/toolkit").
157
157
  uncategorizedAccounts: UncategorizedAccounts;
158
158
  isAccountingClassesEnabled: boolean | undefined;
159
159
  isUncategorizedExpenseCategoryEnabled: boolean | undefined;
160
- }, "expenseAutomationTransactionsView/setEntityRecommendationForLineIdsForCategorization", never, never>, updateSelectedTransactionId: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
160
+ }, "expenseAutomationTransactionsView/setEntityRecommendationForLineIdsForCategorization", never, never>, markCategoryClassRecommendationsInProgressForCategorization: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", transactionId: TransactionID, lineIds: string[]], {
161
+ selectedTab: "review" | "autoCategorized";
162
+ transactionId: TransactionID;
163
+ lineIds: string[];
164
+ }, "expenseAutomationTransactionsView/markCategoryClassRecommendationsInProgressForCategorization", never, never>, markCategoryClassRecommendationsCompletedForCategorization: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", transactionId: TransactionID, lineIds: string[]], {
165
+ selectedTab: "review" | "autoCategorized";
166
+ transactionId: TransactionID;
167
+ lineIds: string[];
168
+ }, "expenseAutomationTransactionsView/markCategoryClassRecommendationsCompletedForCategorization", never, never>, markCategoryClassRecommendationsFailureForCategorization: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[selectedTab: "review" | "autoCategorized", transactionId: TransactionID, lineIds: string[], error: ZeniAPIStatus<Record<string, unknown>>], {
169
+ selectedTab: "review" | "autoCategorized";
170
+ transactionId: TransactionID;
171
+ lineIds: string[];
172
+ error: ZeniAPIStatus<Record<string, unknown>>;
173
+ }, "expenseAutomationTransactionsView/markCategoryClassRecommendationsFailureForCategorization", never, never>, updateSelectedTransactionId: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
161
174
  lineId: ID;
162
175
  selectedTab: TransactionsTab;
163
176
  transactionId: ID;
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  var _a;
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.uploadTransactionCategorizationReceiptSuccess = exports.updateTransactionCategorizationUploadReceiptState = exports.backgroundRefetchReviewTab = exports.syncTransactionCategorizationFromDetailSave = exports.updateSelectedTransactionId = exports.setEntityRecommendationForLineIdsForCategorization = exports.clearExpenseAutomationTransactionsView = exports.clearExpenseAutomationTransactionsViewPerTabView = exports.updateSelectedCheckboxTransactionIds = exports.fetchTransactionCategorizationView = exports.resetOtherTabsFetchState = exports.fetchTransactionCategorizationSuccess = exports.updateParentTotalCountForTab = exports.updateTotalCountForTransactionCategorization = exports.setAllItemsToCategoryClassInLocalDataForCategorization = exports.updateSelectedCustomerForTransaction = exports.updateSelectedVendorForTransaction = exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = exports.markTransactionAsNotMiscategorized = exports.updateTransactionCategorizationSaveStatus = exports.updateTransactionCategorizationCompletedSubTab = exports.updateCurrentSelectedTransactionCategorizationTab = exports.updateTransactionCategorization = exports.saveTransactionCategorization = exports.fetchTransactionCategorizationFailure = exports.saveTransactionCategorizationLocalData = exports.initializeTransactionCategorizationViewLocalData = exports.updateTransactionCategorizationUIState = exports.fetchTransactionCategorization = exports.initialState = exports.initialTransactionTabViewState = void 0;
7
+ exports.uploadTransactionCategorizationReceiptSuccess = exports.updateTransactionCategorizationUploadReceiptState = exports.backgroundRefetchReviewTab = exports.syncTransactionCategorizationFromDetailSave = exports.updateSelectedTransactionId = exports.markCategoryClassRecommendationsFailureForCategorization = exports.markCategoryClassRecommendationsCompletedForCategorization = exports.markCategoryClassRecommendationsInProgressForCategorization = exports.setEntityRecommendationForLineIdsForCategorization = exports.clearExpenseAutomationTransactionsView = exports.clearExpenseAutomationTransactionsViewPerTabView = exports.updateSelectedCheckboxTransactionIds = exports.fetchTransactionCategorizationView = exports.resetOtherTabsFetchState = exports.fetchTransactionCategorizationSuccess = exports.updateParentTotalCountForTab = exports.updateTotalCountForTransactionCategorization = exports.setAllItemsToCategoryClassInLocalDataForCategorization = exports.updateSelectedCustomerForTransaction = exports.updateSelectedVendorForTransaction = exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = exports.markTransactionAsNotMiscategorized = exports.updateTransactionCategorizationSaveStatus = exports.updateTransactionCategorizationCompletedSubTab = exports.updateCurrentSelectedTransactionCategorizationTab = exports.updateTransactionCategorization = exports.saveTransactionCategorization = exports.fetchTransactionCategorizationFailure = exports.saveTransactionCategorizationLocalData = exports.initializeTransactionCategorizationViewLocalData = exports.updateTransactionCategorizationUIState = exports.fetchTransactionCategorization = exports.initialState = exports.initialTransactionTabViewState = void 0;
8
8
  const toolkit_1 = require("@reduxjs/toolkit");
9
9
  const get_1 = __importDefault(require("lodash/get"));
10
10
  const uniq_1 = __importDefault(require("lodash/uniq"));
@@ -72,15 +72,20 @@ exports.initialState = {
72
72
  *
73
73
  * Snapshot scope (deliberately narrow):
74
74
  * - Captured: `pageToken`, `totalCount`, `transactionIdsBySelectedPeriod`,
75
- * `fetchState`, `error`. These are the keys the fetch epic's cache check
76
- * reads to decide whether to short-circuit a sub-tab revisit.
75
+ * `fetchState`, `error`, `scrollPosition`. The first five are the keys the
76
+ * fetch epic's cache check reads to decide whether to short-circuit a
77
+ * sub-tab revisit; `scrollPosition` is captured per sub-tab so revisiting
78
+ * restores the user's last offset and switching to a never-visited sub-tab
79
+ * starts at the top instead of inheriting the outgoing sub-tab's offset
80
+ * (which would land the user at a meaningless row index against a
81
+ * completely different list).
77
82
  * - Intentionally NOT captured (left in place on `autoCat` and inherited by
78
83
  * the incoming sub-tab):
79
84
  * - `uiState.searchString`, `uiState.sortKey`, `uiState.sortOrder`,
80
- * `uiState.scrollPosition`, `uiState.nodeCollapseState`,
81
- * `uiState.limit` search and sort are user-applied preferences that
82
- * persist across sub-tabs by design (a search query stays active when
83
- * the user toggles between Manual / AI Accountant).
85
+ * `uiState.nodeCollapseState`, `uiState.limit` — search and sort are
86
+ * user-applied preferences that persist across sub-tabs by design (a
87
+ * search query stays active when the user toggles between Manual / AI
88
+ * Accountant).
84
89
  * - `selectedCheckBoxTransactionIds` — selection state is intentionally
85
90
  * cleared at the action source (the fetch epic / save flow), not
86
91
  * by the snapshot path. Carrying selection across sub-tabs is safe
@@ -107,6 +112,7 @@ function snapshotAutoCategorizedTab(autoCat) {
107
112
  error: autoCat.error,
108
113
  fetchState: normalisedFetchState,
109
114
  pageToken: autoCat.uiState.pageToken,
115
+ scrollPosition: autoCat.uiState.scrollPosition,
110
116
  totalCount: { ...autoCat.uiState.totalCount },
111
117
  transactionIdsBySelectedPeriod,
112
118
  };
@@ -343,6 +349,7 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
343
349
  if (cached != null) {
344
350
  autoCat.uiState.pageToken = cached.pageToken;
345
351
  autoCat.uiState.totalCount = { ...cached.totalCount };
352
+ autoCat.uiState.scrollPosition = cached.scrollPosition;
346
353
  autoCat.transactionIdsBySelectedPeriod = {
347
354
  ...cached.transactionIdsBySelectedPeriod,
348
355
  };
@@ -353,9 +360,15 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
353
360
  // Sub-tab has never been visited — clear the active slot so the
354
361
  // view-epic's `fetchState === 'Not-Started' || transactionIds.length
355
362
  // === 0` cache check falls through and triggers a fresh page-1
356
- // fetch for the new dataset.
363
+ // fetch for the new dataset. `scrollPosition` is reset to the top
364
+ // so the incoming list isn't anchored to the outgoing sub-tab's
365
+ // offset (different rows → previous offset is meaningless).
357
366
  autoCat.uiState.pageToken = null;
358
367
  autoCat.uiState.totalCount = {};
368
+ autoCat.uiState.scrollPosition = {
369
+ scrollTop: 0,
370
+ scrollLeft: undefined,
371
+ };
359
372
  autoCat.transactionIdsBySelectedPeriod = {};
360
373
  autoCat.fetchState = 'Not-Started';
361
374
  autoCat.error = undefined;
@@ -414,12 +427,9 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
414
427
  const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
415
428
  .selectedCheckBoxTransactionIds;
416
429
  if (selectedTransaction != null &&
417
- (0, transactionCategorizationLocalDataHelper_1.checkIfAllLineItemsAreCategoryClassFilled)(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
418
- selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
419
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
430
+ (0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
420
431
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
421
- ...draft.transactionCategorizationView[selectedTab]
422
- .selectedCheckBoxTransactionIds,
432
+ ...selectedCheckBoxTransactionIds,
423
433
  transactionId,
424
434
  ];
425
435
  }
@@ -502,9 +512,26 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
502
512
  const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
503
513
  .transactionReviewLocalDataById, transactionId.id, undefined);
504
514
  if (transactionLocalData != null) {
515
+ const newLocalData = (0, transactionCategorizationLocalDataHelper_1.setEntityRecommendationForLineIdInLocalData)(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
516
+ // Release the per-line fetch flag for every line this fetch
517
+ // covered. Done in-place on the freshly returned local data
518
+ // (the helper produces a new mutable object), before we write
519
+ // it back to the draft.
520
+ lineIds.forEach((lineId) => {
521
+ const lineItem = newLocalData.lineItemById[lineId];
522
+ if (lineItem != null) {
523
+ newLocalData.lineItemById[lineId] = {
524
+ ...lineItem,
525
+ categoryClassRecommendationsFetchState: {
526
+ fetchState: 'Completed',
527
+ error: undefined,
528
+ },
529
+ };
530
+ }
531
+ });
505
532
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
506
533
  transactionId: transaction.id,
507
- transactionReviewLocalData: (0, transactionCategorizationLocalDataHelper_1.setEntityRecommendationForLineIdInLocalData)(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled),
534
+ transactionReviewLocalData: newLocalData,
508
535
  };
509
536
  // Mark transaction as dirty (autofill happened)
510
537
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
@@ -517,6 +544,99 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
517
544
  }
518
545
  },
519
546
  },
547
+ // Start arm of the line-update recommendations fetch. Flips covered
548
+ // lines to In-Progress AND removes the transaction from
549
+ // selectedCheckBoxTransactionIds — the row is unchecked + disabled
550
+ // until either the success arm (recheck if limit allows) or the
551
+ // failure arm releases the per-line flag.
552
+ markCategoryClassRecommendationsInProgressForCategorization: {
553
+ reducer(draft, action) {
554
+ const { selectedTab, transactionId, lineIds } = action.payload;
555
+ const tab = draft.transactionCategorizationView[selectedTab];
556
+ const transactionLocalData = (0, get_1.default)(tab.transactionReviewLocalDataById, transactionId.id, undefined);
557
+ if (transactionLocalData != null) {
558
+ lineIds.forEach((lineId) => {
559
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
560
+ if (lineItem != null) {
561
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
562
+ ...lineItem,
563
+ categoryClassRecommendationsFetchState: {
564
+ fetchState: 'In-Progress',
565
+ error: undefined,
566
+ },
567
+ };
568
+ }
569
+ });
570
+ }
571
+ // Uncheck on fetch start: the row's existing category/class are
572
+ // stale after a payee change, so we drop the auto-selection until
573
+ // fresh recommendations land.
574
+ if (tab.selectedCheckBoxTransactionIds.includes(transactionId.id)) {
575
+ tab.selectedCheckBoxTransactionIds =
576
+ tab.selectedCheckBoxTransactionIds.filter((id) => id !== transactionId.id);
577
+ }
578
+ },
579
+ prepare(selectedTab, transactionId, lineIds) {
580
+ return { payload: { selectedTab, transactionId, lineIds } };
581
+ },
582
+ },
583
+ // Completed arm. Used by the epic when the HTTP call succeeds but the
584
+ // server returned no per-line recommendations — we still need to
585
+ // release the In-Progress flag so the skeleton resolves. Successful
586
+ // responses with recommendations go through
587
+ // setEntityRecommendationForLineIdsForCategorization instead (which
588
+ // also flips the lines to Completed).
589
+ markCategoryClassRecommendationsCompletedForCategorization: {
590
+ reducer(draft, action) {
591
+ const { selectedTab, transactionId, lineIds } = action.payload;
592
+ const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
593
+ .transactionReviewLocalDataById, transactionId.id, undefined);
594
+ if (transactionLocalData != null) {
595
+ lineIds.forEach((lineId) => {
596
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
597
+ if (lineItem != null) {
598
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
599
+ ...lineItem,
600
+ categoryClassRecommendationsFetchState: {
601
+ fetchState: 'Completed',
602
+ error: undefined,
603
+ },
604
+ };
605
+ }
606
+ });
607
+ }
608
+ },
609
+ prepare(selectedTab, transactionId, lineIds) {
610
+ return { payload: { selectedTab, transactionId, lineIds } };
611
+ },
612
+ },
613
+ // Failure arm. Flips covered lines to Error so the skeleton resolves
614
+ // and downstream UI can react to the failure. The transaction stays
615
+ // unchecked from the start-arm action.
616
+ markCategoryClassRecommendationsFailureForCategorization: {
617
+ reducer(draft, action) {
618
+ const { selectedTab, transactionId, lineIds, error } = action.payload;
619
+ const transactionLocalData = (0, get_1.default)(draft.transactionCategorizationView[selectedTab]
620
+ .transactionReviewLocalDataById, transactionId.id, undefined);
621
+ if (transactionLocalData != null) {
622
+ lineIds.forEach((lineId) => {
623
+ const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
624
+ if (lineItem != null) {
625
+ transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
626
+ ...lineItem,
627
+ categoryClassRecommendationsFetchState: {
628
+ fetchState: 'Error',
629
+ error,
630
+ },
631
+ };
632
+ }
633
+ });
634
+ }
635
+ },
636
+ prepare(selectedTab, transactionId, lineIds, error) {
637
+ return { payload: { selectedTab, transactionId, lineIds, error } };
638
+ },
639
+ },
520
640
  fetchTransactionCategorizationSuccess(draft, action) {
521
641
  const { selectedTab, refreshViewInBackground } = action.payload;
522
642
  if (draft.transactionCategorizationView[selectedTab].fetchState ===
@@ -612,9 +732,7 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
612
732
  .selectedCheckBoxTransactionIds;
613
733
  if (newTransactionLocalData.customer != null &&
614
734
  newTransactionLocalData.customer.id != null &&
615
- (0, transactionCategorizationLocalDataHelper_1.checkIfAllLineItemsAreCategoryClassFilled)(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
616
- selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
617
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
735
+ (0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
618
736
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
619
737
  ...selectedCheckBoxTransactionIds,
620
738
  transactionId,
@@ -688,9 +806,7 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
688
806
  .selectedCheckBoxTransactionIds;
689
807
  if (newTransactionLocalData.vendor != null &&
690
808
  newTransactionLocalData.vendor.id != null &&
691
- (0, transactionCategorizationLocalDataHelper_1.checkIfAllLineItemsAreCategoryClassFilled)(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
692
- selectedCheckBoxTransactionIds.length < transactionCategorizationLocalDataHelper_1.MAX_SELECTION_LIMIT &&
693
- selectedCheckBoxTransactionIds.includes(transactionId) === false) {
809
+ (0, transactionCategorizationLocalDataHelper_1.shouldAutoSelectAndAdd)(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
694
810
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
695
811
  ...selectedCheckBoxTransactionIds,
696
812
  transactionId,
@@ -923,5 +1039,5 @@ const expenseAutomationTransactionsView = (0, toolkit_1.createSlice)({
923
1039
  },
924
1040
  },
925
1041
  });
926
- _a = expenseAutomationTransactionsView.actions, exports.fetchTransactionCategorization = _a.fetchTransactionCategorization, exports.updateTransactionCategorizationUIState = _a.updateTransactionCategorizationUIState, exports.initializeTransactionCategorizationViewLocalData = _a.initializeTransactionCategorizationViewLocalData, exports.saveTransactionCategorizationLocalData = _a.saveTransactionCategorizationLocalData, exports.fetchTransactionCategorizationFailure = _a.fetchTransactionCategorizationFailure, exports.saveTransactionCategorization = _a.saveTransactionCategorization, exports.updateTransactionCategorization = _a.updateTransactionCategorization, exports.updateCurrentSelectedTransactionCategorizationTab = _a.updateCurrentSelectedTransactionCategorizationTab, exports.updateTransactionCategorizationCompletedSubTab = _a.updateTransactionCategorizationCompletedSubTab, exports.updateTransactionCategorizationSaveStatus = _a.updateTransactionCategorizationSaveStatus, exports.markTransactionAsNotMiscategorized = _a.markTransactionAsNotMiscategorized, exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = _a.updateStatusForTransactionNotMiscategorizedUpdateForCategorization, exports.updateSelectedVendorForTransaction = _a.updateSelectedVendorForTransaction, exports.updateSelectedCustomerForTransaction = _a.updateSelectedCustomerForTransaction, exports.setAllItemsToCategoryClassInLocalDataForCategorization = _a.setAllItemsToCategoryClassInLocalDataForCategorization, exports.updateTotalCountForTransactionCategorization = _a.updateTotalCountForTransactionCategorization, exports.updateParentTotalCountForTab = _a.updateParentTotalCountForTab, exports.fetchTransactionCategorizationSuccess = _a.fetchTransactionCategorizationSuccess, exports.resetOtherTabsFetchState = _a.resetOtherTabsFetchState, exports.fetchTransactionCategorizationView = _a.fetchTransactionCategorizationView, exports.updateSelectedCheckboxTransactionIds = _a.updateSelectedCheckboxTransactionIds, exports.clearExpenseAutomationTransactionsViewPerTabView = _a.clearExpenseAutomationTransactionsViewPerTabView, exports.clearExpenseAutomationTransactionsView = _a.clearExpenseAutomationTransactionsView, exports.setEntityRecommendationForLineIdsForCategorization = _a.setEntityRecommendationForLineIdsForCategorization, exports.updateSelectedTransactionId = _a.updateSelectedTransactionId, exports.syncTransactionCategorizationFromDetailSave = _a.syncTransactionCategorizationFromDetailSave, exports.backgroundRefetchReviewTab = _a.backgroundRefetchReviewTab, exports.updateTransactionCategorizationUploadReceiptState = _a.updateTransactionCategorizationUploadReceiptState, exports.uploadTransactionCategorizationReceiptSuccess = _a.uploadTransactionCategorizationReceiptSuccess;
1042
+ _a = expenseAutomationTransactionsView.actions, exports.fetchTransactionCategorization = _a.fetchTransactionCategorization, exports.updateTransactionCategorizationUIState = _a.updateTransactionCategorizationUIState, exports.initializeTransactionCategorizationViewLocalData = _a.initializeTransactionCategorizationViewLocalData, exports.saveTransactionCategorizationLocalData = _a.saveTransactionCategorizationLocalData, exports.fetchTransactionCategorizationFailure = _a.fetchTransactionCategorizationFailure, exports.saveTransactionCategorization = _a.saveTransactionCategorization, exports.updateTransactionCategorization = _a.updateTransactionCategorization, exports.updateCurrentSelectedTransactionCategorizationTab = _a.updateCurrentSelectedTransactionCategorizationTab, exports.updateTransactionCategorizationCompletedSubTab = _a.updateTransactionCategorizationCompletedSubTab, exports.updateTransactionCategorizationSaveStatus = _a.updateTransactionCategorizationSaveStatus, exports.markTransactionAsNotMiscategorized = _a.markTransactionAsNotMiscategorized, exports.updateStatusForTransactionNotMiscategorizedUpdateForCategorization = _a.updateStatusForTransactionNotMiscategorizedUpdateForCategorization, exports.updateSelectedVendorForTransaction = _a.updateSelectedVendorForTransaction, exports.updateSelectedCustomerForTransaction = _a.updateSelectedCustomerForTransaction, exports.setAllItemsToCategoryClassInLocalDataForCategorization = _a.setAllItemsToCategoryClassInLocalDataForCategorization, exports.updateTotalCountForTransactionCategorization = _a.updateTotalCountForTransactionCategorization, exports.updateParentTotalCountForTab = _a.updateParentTotalCountForTab, exports.fetchTransactionCategorizationSuccess = _a.fetchTransactionCategorizationSuccess, exports.resetOtherTabsFetchState = _a.resetOtherTabsFetchState, exports.fetchTransactionCategorizationView = _a.fetchTransactionCategorizationView, exports.updateSelectedCheckboxTransactionIds = _a.updateSelectedCheckboxTransactionIds, exports.clearExpenseAutomationTransactionsViewPerTabView = _a.clearExpenseAutomationTransactionsViewPerTabView, exports.clearExpenseAutomationTransactionsView = _a.clearExpenseAutomationTransactionsView, exports.setEntityRecommendationForLineIdsForCategorization = _a.setEntityRecommendationForLineIdsForCategorization, exports.markCategoryClassRecommendationsInProgressForCategorization = _a.markCategoryClassRecommendationsInProgressForCategorization, exports.markCategoryClassRecommendationsCompletedForCategorization = _a.markCategoryClassRecommendationsCompletedForCategorization, exports.markCategoryClassRecommendationsFailureForCategorization = _a.markCategoryClassRecommendationsFailureForCategorization, exports.updateSelectedTransactionId = _a.updateSelectedTransactionId, exports.syncTransactionCategorizationFromDetailSave = _a.syncTransactionCategorizationFromDetailSave, exports.backgroundRefetchReviewTab = _a.backgroundRefetchReviewTab, exports.updateTransactionCategorizationUploadReceiptState = _a.updateTransactionCategorizationUploadReceiptState, exports.uploadTransactionCategorizationReceiptSuccess = _a.uploadTransactionCategorizationReceiptSuccess;
927
1043
  exports.default = expenseAutomationTransactionsView.reducer;
@@ -20,6 +20,8 @@ export interface ExpenseAutomationTransactionViewSelector extends SelectorView {
20
20
  classList: ClassBase[];
21
21
  completionStatus: CompletionStatusType;
22
22
  fetchStateByTransactionTabs: Record<TransactionsTab, FetchStateAndError>;
23
+ hasInFlightCategoryClassRecommendations: boolean;
24
+ inFlightCategoryClassRecommendationsByTransactionId: Record<ID, boolean>;
23
25
  isAccountingProjectsEnabled: boolean;
24
26
  markAsNotMiscategorizedStatus: FetchStateAndError;
25
27
  /**
@@ -1,3 +1,6 @@
1
+ import { ID } from '../../../commonStateTypes/common';
1
2
  import { RootState } from '../../../reducer';
2
3
  import { ExpenseAutomationTransactionViewSelector } from '../selectorTypes/transactionsViewSelectorTypes';
4
+ export declare const getCategorizationInFlightRecommendationsByTransactionId: (state: RootState) => Record<ID, boolean>;
5
+ export declare const getCategorizationHasInFlightRecommendations: (state: RootState) => boolean;
3
6
  export declare function getExpenseAutomationTransactionView(state: RootState): ExpenseAutomationTransactionViewSelector;
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getCategorizationHasInFlightRecommendations = exports.getCategorizationInFlightRecommendationsByTransactionId = void 0;
6
7
  exports.getExpenseAutomationTransactionView = getExpenseAutomationTransactionView;
8
+ const toolkit_1 = require("@reduxjs/toolkit");
7
9
  const omit_1 = __importDefault(require("lodash/omit"));
8
10
  const reduceFetchState_1 = require("../../../commonStateTypes/reduceFetchState");
9
11
  const timePeriod_1 = require("../../../commonStateTypes/timePeriod");
@@ -14,6 +16,29 @@ const accountListSelector_1 = require("../../accountList/accountListSelector");
14
16
  const classListSelector_1 = require("../../classList/classListSelector");
15
17
  const projectListSelector_1 = require("../../projectList/projectListSelector");
16
18
  const expenseAutomationViewSelectorTypes_1 = require("../selectorTypes/expenseAutomationViewSelectorTypes");
19
+ // Per-transaction "any line is fetching category/class recommendations?"
20
+ // map for the active categorization tab. Memoized on the active tab's
21
+ // transactionReviewLocalDataById reference, so steady state (no fetches
22
+ // in flight) recomputes only when local data changes — and the row-table
23
+ // can read a single map lookup per row instead of re-iterating the
24
+ // row's lineItemById in the render path.
25
+ exports.getCategorizationInFlightRecommendationsByTransactionId = (0, toolkit_1.createSelector)((state) => state.expenseAutomationTransactionsViewState
26
+ .selectedTransactionCategorizationTab, (state) => state.expenseAutomationTransactionsViewState
27
+ .transactionCategorizationView, (selectedTab, transactionCategorizationView) => {
28
+ const tab = transactionCategorizationView[selectedTab];
29
+ const result = {};
30
+ for (const [transactionId, data] of Object.entries(tab.transactionReviewLocalDataById)) {
31
+ const lineItems = Object.values(data.transactionReviewLocalData.lineItemById);
32
+ if (lineItems.some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
33
+ 'In-Progress')) {
34
+ result[transactionId] = true;
35
+ }
36
+ }
37
+ return result;
38
+ });
39
+ // Page-level "any line on the active tab is in flight?" flag. Drives the
40
+ // list-page Save disable.
41
+ exports.getCategorizationHasInFlightRecommendations = (0, toolkit_1.createSelector)(exports.getCategorizationInFlightRecommendationsByTransactionId, (inFlightByTransactionId) => Object.keys(inFlightByTransactionId).length > 0);
17
42
  function getExpenseAutomationTransactionView(state) {
18
43
  const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
19
44
  const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
@@ -109,6 +134,8 @@ function getExpenseAutomationTransactionView(state) {
109
134
  : [];
110
135
  const transactionCompletionStatus = allSteps.find((step) => step.step === 'transaction_categorization')?.completionStatus;
111
136
  const completionStatus = transactionCompletionStatus ?? 'incomplete';
137
+ const inFlightCategoryClassRecommendationsByTransactionId = (0, exports.getCategorizationInFlightRecommendationsByTransactionId)(state);
138
+ const hasInFlightCategoryClassRecommendations = (0, exports.getCategorizationHasInFlightRecommendations)(state);
112
139
  return {
113
140
  version: 0,
114
141
  fetchState: fetchStatus.fetchState,
@@ -122,6 +149,8 @@ function getExpenseAutomationTransactionView(state) {
122
149
  classList: allClasses,
123
150
  accountsHierarchyList,
124
151
  classHierarchyList,
152
+ hasInFlightCategoryClassRecommendations,
153
+ inFlightCategoryClassRecommendationsByTransactionId,
125
154
  isAccountingProjectsEnabled,
126
155
  projectList,
127
156
  transactionLocalData,
@@ -48,6 +48,7 @@ export interface TransactionCategorizationLineItemData extends TransactionLineBa
48
48
  transactionId: ID;
49
49
  account?: AccountBase;
50
50
  categorizationStatus?: CategorizationStatusType;
51
+ categoryClassRecommendationsFetchState?: FetchStateAndError;
51
52
  class?: ClassBase;
52
53
  customer?: CustomerBase;
53
54
  item?: ProductOrService;
@@ -112,11 +113,16 @@ export interface TransactionsTabViewState extends FetchedState {
112
113
  * (mirrors parent-tab caching). The snapshot is period-spanning — both the
113
114
  * `totalCount` map and `transactionIdsBySelectedPeriod` map preserve every
114
115
  * period the user has paged through under that sub-tab.
116
+ *
117
+ * `scrollPosition` is captured per sub-tab so revisiting a sub-tab restores
118
+ * the user's last scroll offset; switching to a fresh / never-visited sub-tab
119
+ * starts at the top instead of inheriting the previous sub-tab's offset.
115
120
  */
116
121
  export interface AutoCategorizedSubTabSnapshot {
117
122
  error: ZeniAPIStatus | undefined;
118
123
  fetchState: FetchState;
119
124
  pageToken: PageToken;
125
+ scrollPosition: TransactionsViewUIState['scrollPosition'];
120
126
  totalCount: Record<MonthYearPeriodId, number>;
121
127
  transactionIdsBySelectedPeriod: Record<MonthYearPeriodId, ID[]>;
122
128
  }
@@ -0,0 +1,9 @@
1
+ import { ActionsObservable, StateObservable } from 'redux-observable';
2
+ import { Observable } from 'rxjs';
3
+ import { RootState } from '../../reducer';
4
+ import { ZeniAPI } from '../../zeniAPI';
5
+ import { markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, markCategoryClassRecommendationsInProgressForCategorization, setEntityRecommendationForLineIdsForCategorization } from '../expenseAutomationView/reducers/transactionsViewReducer';
6
+ import { markCategoryClassRecommendationsCompletedForTransactionDetail, markCategoryClassRecommendationsFailureForTransactionDetail, markCategoryClassRecommendationsInProgressForTransactionDetail, setEntityRecommendationForLineIdsForTransactionDetail } from '../transactionDetail/transactionDetailReducer';
7
+ import { fetchEntityRecommendationsForLineUpdate } from './recommendationReducer';
8
+ export type ActionType = ReturnType<typeof fetchEntityRecommendationsForLineUpdate> | ReturnType<typeof markCategoryClassRecommendationsInProgressForCategorization> | ReturnType<typeof markCategoryClassRecommendationsCompletedForCategorization> | ReturnType<typeof markCategoryClassRecommendationsFailureForCategorization> | ReturnType<typeof markCategoryClassRecommendationsInProgressForTransactionDetail> | ReturnType<typeof markCategoryClassRecommendationsCompletedForTransactionDetail> | ReturnType<typeof markCategoryClassRecommendationsFailureForTransactionDetail> | ReturnType<typeof setEntityRecommendationForLineIdsForCategorization> | ReturnType<typeof setEntityRecommendationForLineIdsForTransactionDetail>;
9
+ export declare const fetchEntityRecommendationsForLineUpdateEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchEntityRecommendationsForLineUpdateEpic = void 0;
4
+ const rxjs_1 = require("rxjs");
5
+ const operators_1 = require("rxjs/operators");
6
+ const tenantSelector_1 = require("../../entity/tenant/tenantSelector");
7
+ const transactionHelper_1 = require("../../entity/transaction/transactionHelper");
8
+ const responsePayload_1 = require("../../responsePayload");
9
+ const accountListSelector_1 = require("../accountList/accountListSelector");
10
+ const transactionsViewReducer_1 = require("../expenseAutomationView/reducers/transactionsViewReducer");
11
+ const transactionDetailReducer_1 = require("../transactionDetail/transactionDetailReducer");
12
+ const recommendationHelper_1 = require("./recommendationHelper");
13
+ const recommendationReducer_1 = require("./recommendationReducer");
14
+ // Close the per-target groupBy bucket after this much idle time so inactive
15
+ // groups don't accumulate forever (each group holds a Subject + a chain of
16
+ // active subscribers; without a duration selector groupBy retains them for
17
+ // the lifetime of the source observable, which is the lifetime of the app).
18
+ // Picked at 60s to leave a generous window for re-fetches on the same
19
+ // payee/transaction/line target while still bounding memory.
20
+ const GROUP_INACTIVITY_TIMEOUT_MS = 60000;
21
+ // Build the groupBy key. Same flow + same transaction + same line set
22
+ // share a target so switchMap can cancel an in-flight prior fetch. Use
23
+ // NUL (\u0000) as separator because it can never appear in transaction
24
+ // or line ids (vs the `|` used in an earlier WIP which collides with
25
+ // pipe-delimited synthetic ids).
26
+ const SEPARATOR = '\u0000';
27
+ const toGroupKey = (flowType, transactionId, lineIds) => `${flowType}${SEPARATOR}${transactionId}${SEPARATOR}${[...lineIds]
28
+ .sort((a, b) => a.localeCompare(b))
29
+ .join(SEPARATOR)}`;
30
+ // Targeted recommendations fetch that drives the per-line skeleton, the
31
+ // list-page row-checkbox uncheck/recheck, and the page-level Save
32
+ // disable.
33
+ //
34
+ // Concurrency model:
35
+ // - groupBy by (flowType, transactionId, sorted lineIds) so distinct
36
+ // targets run in parallel.
37
+ // - inside each group, switchMap so a re-fetch on the SAME target
38
+ // cancels any prior in-flight request for that target.
39
+ // - groupBy duration selector closes idle groups to bound memory.
40
+ const fetchEntityRecommendationsForLineUpdateEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(recommendationReducer_1.fetchEntityRecommendationsForLineUpdate.match), (0, operators_1.groupBy)((action) => toGroupKey(action.payload.flowType, action.payload.transactionDetails.transactionId.id, action.payload.transactionDetails.lineIds), undefined, (group$) => group$.pipe((0, operators_1.debounce)(() => (0, rxjs_1.timer)(GROUP_INACTIVITY_TIMEOUT_MS)))), (0, operators_1.mergeMap)((group$) => group$.pipe((0, operators_1.switchMap)((action) => {
41
+ const { entity, amount, transactionDetails, flowType, selectedTab, isFetchCOT, isUncategorizedExpenseCategoryEnabled, } = action.payload;
42
+ const { transactionId, transaction, lineIds } = transactionDetails;
43
+ // Synchronously emit the start arm so the per-line skeleton +
44
+ // row uncheck happen before the HTTP request begins.
45
+ const startAction = flowType === 'categorization' && selectedTab != null
46
+ ? (0, transactionsViewReducer_1.markCategoryClassRecommendationsInProgressForCategorization)(selectedTab, transactionId, lineIds)
47
+ : (0, transactionDetailReducer_1.markCategoryClassRecommendationsInProgressForTransactionDetail)(transactionId, lineIds);
48
+ const url = (0, recommendationHelper_1.buildRecommendationUrl)(zeniAPI.apiEndPoints.accountMicroServiceBaseUrl, entity, amount, transactionDetails, isFetchCOT === true);
49
+ const httpStream$ = zeniAPI
50
+ .getJSON(url)
51
+ .pipe((0, operators_1.mergeMap)((response) => {
52
+ if (!(0, responsePayload_1.isSuccessResponse)(response) || response.data == null) {
53
+ return (0, rxjs_1.of)(flowType === 'categorization' && selectedTab != null
54
+ ? (0, transactionsViewReducer_1.markCategoryClassRecommendationsFailureForCategorization)(selectedTab, transactionId, lineIds, response.status)
55
+ : (0, transactionDetailReducer_1.markCategoryClassRecommendationsFailureForTransactionDetail)(transactionId, lineIds, response.status));
56
+ }
57
+ const recommendationWithCOTByLineId = (0, recommendationHelper_1.getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT)(response.data.recommendations, lineIds);
58
+ // HTTP 200 with no per-line recommendations: NOT an error.
59
+ // Flip lines to Completed so the skeleton resolves and
60
+ // downstream UI can treat this as "we tried, nothing to
61
+ // apply".
62
+ if (recommendationWithCOTByLineId == null) {
63
+ return (0, rxjs_1.of)(flowType === 'categorization' && selectedTab != null
64
+ ? (0, transactionsViewReducer_1.markCategoryClassRecommendationsCompletedForCategorization)(selectedTab, transactionId, lineIds)
65
+ : (0, transactionDetailReducer_1.markCategoryClassRecommendationsCompletedForTransactionDetail)(transactionId, lineIds));
66
+ }
67
+ const transactionWithCOT = isFetchCOT === true
68
+ ? (0, transactionHelper_1.getTransactionWithCOTFromRecommendations)(transaction, recommendationWithCOTByLineId)
69
+ : (0, transactionHelper_1.getTransactionWithCOT)(transaction);
70
+ const uncategorizedIncomeExpense = (0, accountListSelector_1.getUncategorizedAccounts)(state$.value.accountState, state$.value.accountListState, 'accountList');
71
+ if (flowType === 'categorization' && selectedTab != null) {
72
+ return (0, rxjs_1.of)((0, transactionsViewReducer_1.setEntityRecommendationForLineIdsForCategorization)(selectedTab, transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled, (0, tenantSelector_1.getIsAccountingClassesEnabled)(state$.value)));
73
+ }
74
+ return (0, rxjs_1.of)((0, transactionDetailReducer_1.setEntityRecommendationForLineIdsForTransactionDetail)(transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense));
75
+ }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)(flowType === 'categorization' && selectedTab != null
76
+ ? (0, transactionsViewReducer_1.markCategoryClassRecommendationsFailureForCategorization)(selectedTab, transactionId, lineIds, (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'fetchEntityRecommendationsForLineUpdate failed: ' +
77
+ JSON.stringify(error)))
78
+ : (0, transactionDetailReducer_1.markCategoryClassRecommendationsFailureForTransactionDetail)(transactionId, lineIds, (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'fetchEntityRecommendationsForLineUpdate failed: ' +
79
+ JSON.stringify(error))))));
80
+ return (0, rxjs_1.concat)((0, rxjs_1.of)(startAction), httpStream$);
81
+ }))));
82
+ exports.fetchEntityRecommendationsForLineUpdateEpic = fetchEntityRecommendationsForLineUpdateEpic;
@@ -1,8 +1,17 @@
1
1
  import { RecommendationPayload, RecommendationPayloadWithCOT } from '../../commonPayloadTypes/recommendationPayload';
2
2
  import { ID } from '../../commonStateTypes/common';
3
3
  import { Recommendation, RecommendationWithCOT } from '../../commonStateTypes/recommendationBase';
4
+ import { Entity } from '../../entity/genericEntity/entity';
4
5
  import { RecommendationByLineId, RecommendationWithCOTByLineId } from '../../entity/transaction/stateTypes/transaction';
6
+ import { SupportedTransaction } from '../../entity/transaction/transactionState';
5
7
  export declare const getRecommendationFromRecommendationPayload: (payload: RecommendationPayload[]) => Recommendation[] | undefined;
6
8
  export declare const getRecommendationByLineIdFromRecommendationPayload: (recommendationPayload: RecommendationPayload[], lineIds: ID[]) => RecommendationByLineId | undefined;
7
9
  export declare const getRecommendationWithCOTFromRecommendationPayloadWithCOT: (payload: RecommendationPayloadWithCOT[]) => RecommendationWithCOT[] | undefined;
10
+ export declare const buildRecommendationUrl: (accountMicroServiceBaseUrl: string, entity: Entity, amount: number, transactionDetails: {
11
+ lineIds: ID[];
12
+ transaction: SupportedTransaction;
13
+ transactionId: {
14
+ id: string;
15
+ };
16
+ }, isFetchCOT: boolean) => string;
8
17
  export declare const getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT: (recommendationPayload: RecommendationPayloadWithCOT[], lineIds: ID[]) => RecommendationWithCOTByLineId | undefined;
@@ -1,7 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT = exports.getRecommendationWithCOTFromRecommendationPayloadWithCOT = exports.getRecommendationByLineIdFromRecommendationPayload = exports.getRecommendationFromRecommendationPayload = void 0;
3
+ exports.getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT = exports.buildRecommendationUrl = exports.getRecommendationWithCOTFromRecommendationPayloadWithCOT = exports.getRecommendationByLineIdFromRecommendationPayload = exports.getRecommendationFromRecommendationPayload = void 0;
4
4
  const transactionPayload_1 = require("../../entity/transaction/payloadTypes/transactionPayload");
5
+ const transactionDetailState_1 = require("../transactionDetail/transactionDetailState");
5
6
  const getRecommendationFromRecommendationPayload = (payload) => {
6
7
  if (payload == null || payload.length === 0) {
7
8
  return undefined;
@@ -43,6 +44,35 @@ const getRecommendationWithCOTFromRecommendationPayloadWithCOT = (payload) => {
43
44
  return recommendations;
44
45
  };
45
46
  exports.getRecommendationWithCOTFromRecommendationPayloadWithCOT = getRecommendationWithCOTFromRecommendationPayloadWithCOT;
47
+ // Pluralized URL segment for entity types. Mirrors the legacy
48
+ // fetchEntityRecommendationsByTransactionIdEpic.toEntityTypeURL.
49
+ const toEntityTypeUrlSegment = (entityType) => `${entityType}s`;
50
+ // Build the recommendations URL for a single entity + transaction target.
51
+ // Centralizes the URL shape so the legacy
52
+ // fetchEntityRecommendationsByTransactionIdEpic and the new
53
+ // fetchEntityRecommendationsForLineUpdateEpic stay in lockstep.
54
+ const buildRecommendationUrl = (accountMicroServiceBaseUrl, entity, amount, transactionDetails, isFetchCOT) => {
55
+ const entityQuery = {
56
+ entity_name: entity.name,
57
+ entity_type: entity.type,
58
+ };
59
+ // Entity-id-null case: server expects entity_name / entity_type query
60
+ // instead of the {entityType}/{qboId}/recommendations path.
61
+ if (entity.qboId === entity.name) {
62
+ return `${accountMicroServiceBaseUrl}/1.0/recommendations?query=${encodeURIComponent(JSON.stringify(entityQuery))}`;
63
+ }
64
+ const query = {
65
+ amount,
66
+ transaction_id: (0, transactionDetailState_1.getActualTransactionID)(transactionDetails.transactionId.id),
67
+ transaction_type: transactionDetails.transaction.type,
68
+ line_ids: transactionDetails.lineIds,
69
+ };
70
+ if (isFetchCOT === true) {
71
+ query.include_cot = true;
72
+ }
73
+ return `${accountMicroServiceBaseUrl}/1.0/${toEntityTypeUrlSegment(entity.type)}/${entity.qboId}/recommendations?query=${encodeURIComponent(JSON.stringify(query))}`;
74
+ };
75
+ exports.buildRecommendationUrl = buildRecommendationUrl;
46
76
  const getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT = (recommendationPayload, lineIds) => {
47
77
  if (recommendationPayload.length === 0) {
48
78
  return undefined;