@zeniai/client-epic-state 5.0.69-betaML31 → 5.0.69-betaML4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -2
  2. package/lib/commonStateTypes/viewAndReport/viewAndReport.js +2 -0
  3. package/lib/entity/account/accountSelector.d.ts +9 -0
  4. package/lib/entity/account/accountSelector.js +14 -1
  5. package/lib/entity/account/accountState.d.ts +1 -1
  6. package/lib/entity/accountGroup/accountGroupState.d.ts +1 -1
  7. package/lib/entity/class/classReducer.d.ts +4 -4
  8. package/lib/entity/class/classState.d.ts +1 -1
  9. package/lib/entity/forecast/forecastState.d.ts +1 -1
  10. package/lib/entity/sectionAccountsView/sectionAccountsView.d.ts +1 -1
  11. package/lib/entity/sectionClassesViewV2/sectionClassesView.d.ts +1 -1
  12. package/lib/entity/sectionClassesViewV2/sectionClassesViewReducer.d.ts +2 -2
  13. package/lib/entity/sectionProjectView/sectionProjectView.d.ts +1 -1
  14. package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +2 -2
  15. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  16. package/lib/entity/snackbar/snackbarTypes.js +1 -0
  17. package/lib/entity/transaction/payloadTypes/transactionPayload.d.ts +4 -0
  18. package/lib/entity/transaction/payloadTypes/transactionPayload.js +20 -58
  19. package/lib/entity/transaction/stateTypes/transaction.d.ts +3 -0
  20. package/lib/epic.d.ts +3 -2
  21. package/lib/epic.js +3 -2
  22. package/lib/esm/commonStateTypes/viewAndReport/viewAndReport.js +2 -0
  23. package/lib/esm/entity/account/accountSelector.js +11 -0
  24. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
  25. package/lib/esm/entity/transaction/payloadTypes/transactionPayload.js +20 -58
  26. package/lib/esm/epic.js +3 -2
  27. package/lib/esm/index.js +9 -8
  28. package/lib/esm/reducer.js +3 -0
  29. package/lib/esm/view/createTransferEntry/createTransferEntryReducer.js +56 -0
  30. package/lib/esm/view/createTransferEntry/createTransferEntrySelector.js +64 -0
  31. package/lib/esm/view/createTransferEntry/createTransferEntryState.js +17 -0
  32. package/lib/esm/view/createTransferEntry/epics/createTransferEntryEpic.js +112 -0
  33. package/lib/esm/view/createTransferEntry/epics/fetchTransferAccountsEpic.js +36 -0
  34. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
  35. package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +1 -21
  36. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +26 -117
  37. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +9 -59
  38. package/lib/esm/view/recommendation/recommendationHelper.js +0 -29
  39. package/lib/esm/view/recommendation/recommendationReducer.js +1 -36
  40. package/lib/esm/view/transactionDetail/epics/transactionDetailEpic.js +14 -2
  41. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +8 -122
  42. package/lib/esm/view/transactionDetail/transactionDetailSelector.js +6 -23
  43. package/lib/index.d.ts +9 -9
  44. package/lib/index.js +43 -36
  45. package/lib/reducer.d.ts +3 -0
  46. package/lib/reducer.js +3 -0
  47. package/lib/view/auditReportView/auditReportViewState.d.ts +1 -1
  48. package/lib/view/billPayCard/billPayCardSelector.d.ts +1 -1
  49. package/lib/view/cardBalance/cardBalanceSelector.d.ts +1 -1
  50. package/lib/view/cashBalance/cashBalanceSelector.d.ts +1 -1
  51. package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +1 -1
  52. package/lib/view/cashPosition/cashPositionSelector.d.ts +1 -1
  53. package/lib/view/createTransferEntry/createTransferEntryReducer.d.ts +32 -0
  54. package/lib/view/createTransferEntry/createTransferEntryReducer.js +60 -0
  55. package/lib/view/createTransferEntry/createTransferEntrySelector.d.ts +39 -0
  56. package/lib/view/createTransferEntry/createTransferEntrySelector.js +69 -0
  57. package/lib/view/createTransferEntry/createTransferEntryState.d.ts +30 -0
  58. package/lib/view/createTransferEntry/createTransferEntryState.js +20 -0
  59. package/lib/view/createTransferEntry/epics/createTransferEntryEpic.d.ts +11 -0
  60. package/lib/view/createTransferEntry/epics/createTransferEntryEpic.js +116 -0
  61. package/lib/view/createTransferEntry/epics/fetchTransferAccountsEpic.d.ts +14 -0
  62. package/lib/view/createTransferEntry/epics/fetchTransferAccountsEpic.js +40 -0
  63. package/lib/view/dashboard/dashboardReducer.d.ts +2 -2
  64. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.d.ts +2 -1
  65. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
  66. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -2
  67. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +2 -23
  68. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +4 -15
  69. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +26 -117
  70. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +4 -3
  71. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +1 -3
  72. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +11 -61
  73. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +0 -1
  74. package/lib/view/financeStatement/financeStatementReducer.d.ts +1 -1
  75. package/lib/view/globalMerchantView/globalMerchantViewReducer.d.ts +2 -2
  76. package/lib/view/recommendation/recommendationHelper.d.ts +0 -9
  77. package/lib/view/recommendation/recommendationHelper.js +1 -31
  78. package/lib/view/recommendation/recommendationReducer.d.ts +1 -18
  79. package/lib/view/recommendation/recommendationReducer.js +2 -37
  80. package/lib/view/reimbursementCard/reimbursementCardSelector.d.ts +1 -1
  81. package/lib/view/reportsResync/reportsResyncReducer.d.ts +2 -2
  82. package/lib/view/tasksCard/tasksCardSelector.d.ts +1 -1
  83. package/lib/view/topEx/topExSelector.d.ts +1 -1
  84. package/lib/view/transactionDetail/epics/transactionDetailEpic.d.ts +2 -1
  85. package/lib/view/transactionDetail/epics/transactionDetailEpic.js +14 -2
  86. package/lib/view/transactionDetail/transactionDetailPayload.d.ts +1 -1
  87. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +3 -12
  88. package/lib/view/transactionDetail/transactionDetailReducer.js +9 -123
  89. package/lib/view/transactionDetail/transactionDetailSelector.d.ts +6 -3
  90. package/lib/view/transactionDetail/transactionDetailSelector.js +7 -25
  91. package/lib/view/transactionDetail/transactionDetailTypes.d.ts +0 -1
  92. package/package.json +1 -1
  93. package/lib/esm/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +0 -83
  94. package/lib/tsconfig.typecheck.tsbuildinfo +0 -1
  95. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.d.ts +0 -9
  96. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +0 -87
@@ -0,0 +1,56 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+ import { initialCreateTransferEntryState, } from './createTransferEntryState';
3
+ export const initialState = initialCreateTransferEntryState;
4
+ const createTransferEntrySlice = createSlice({
5
+ name: 'createTransferEntry',
6
+ initialState,
7
+ reducers: {
8
+ fetchAccountsForTransferFlow(draft, action) {
9
+ draft.transferAccountsByType[action.payload.kind].fetchState =
10
+ 'In-Progress';
11
+ },
12
+ updateAccountsForTransferFlow(draft, action) {
13
+ const slice = draft.transferAccountsByType[action.payload.kind];
14
+ slice.fetchState = 'Completed';
15
+ slice.accountIds = action.payload.accountIds;
16
+ },
17
+ updateAccountsForTransferFlowFailure(draft, action) {
18
+ draft.transferAccountsByType[action.payload.kind].fetchState = 'Error';
19
+ },
20
+ createTransferEntry: {
21
+ prepare(payload) {
22
+ return { payload };
23
+ },
24
+ reducer(draft) {
25
+ draft.createTransferEntryStatus = 'In-Progress';
26
+ },
27
+ },
28
+ createTransferEntrySuccess(draft) {
29
+ draft.createTransferEntryStatus = 'Completed';
30
+ },
31
+ createTransferEntryFailure(draft) {
32
+ draft.createTransferEntryStatus = 'Error';
33
+ },
34
+ resetCreateTransferEntryStatus(draft) {
35
+ draft.createTransferEntryStatus = 'Not-Started';
36
+ },
37
+ createTransferEntryReplacedTransaction: {
38
+ prepare(previousTransactionId, newTransactionId) {
39
+ return { payload: { newTransactionId, previousTransactionId } };
40
+ },
41
+ reducer(draft, action) {
42
+ draft.lastTransferEntryReplacement = action.payload;
43
+ },
44
+ },
45
+ /**
46
+ * One-shot acknowledgement: the host has consumed the route-replacement
47
+ * signal and navigated to the new transaction url, so we can drop the
48
+ * record. Re-dispatching is idempotent.
49
+ */
50
+ clearTransferEntryRouteReplacement(draft) {
51
+ draft.lastTransferEntryReplacement = undefined;
52
+ },
53
+ },
54
+ });
55
+ export const { fetchAccountsForTransferFlow, updateAccountsForTransferFlow, updateAccountsForTransferFlowFailure, createTransferEntry, createTransferEntrySuccess, createTransferEntryFailure, resetCreateTransferEntryStatus, createTransferEntryReplacedTransaction, clearTransferEntryRouteReplacement, } = createTransferEntrySlice.actions;
56
+ export default createTransferEntrySlice.reducer;
@@ -0,0 +1,64 @@
1
+ import { createSelector } from '@reduxjs/toolkit';
2
+ import { getAccountBase, getAccountBaseForCreditCardTransferAccounts, getAccountBaseForTransferAccounts, } from '../../entity/account/accountSelector';
3
+ import { getAccountList } from '../accountList/accountListSelector';
4
+ export function getCreateTransferEntryStatus(state) {
5
+ return state.createTransferEntryState.createTransferEntryStatus;
6
+ }
7
+ export function getLastTransferEntryReplacement(state) {
8
+ return state.createTransferEntryState.lastTransferEntryReplacement;
9
+ }
10
+ /**
11
+ * Shared resolver for the credit/debit dropdowns in the transfer drawer.
12
+ *
13
+ * Resolution policy (intentional):
14
+ * 1. Prefer the dedicated API fetch results (`transferEntryEligible` for the
15
+ * debit pool, `creditCardOnlyEligible` for the credit-card-payment credit
16
+ * pool).
17
+ * 2. If the dedicated fetch returns an empty list while the COA is loaded,
18
+ * fall back to the COA: the full account list for `transferEntryEligible`,
19
+ * and COA accounts filtered to `credit_card` for `creditCardOnlyEligible`.
20
+ * This keeps the dropdown populated for older tenants where the dedicated
21
+ * endpoints haven't been backfilled yet.
22
+ *
23
+ * Intent — what this selector deliberately does NOT do:
24
+ * - Does NOT distinguish "fetch in-flight" / "fetch errored" / "fetch returned
25
+ * empty" — all three collapse to the COA fallback. The product preference
26
+ * is "always show something selectable" over "show a spinner that may never
27
+ * resolve". Loading/error UI belongs in the view layer; if you need a
28
+ * spinner, observe the `fetchState` on `transferAccountsByType[kind]`
29
+ * separately rather than changing the resolution policy here.
30
+ * - Does NOT enforce "API result is authoritative even if empty" — a tenant
31
+ * whose dedicated endpoint is correctly returning `[]` (e.g. no eligible
32
+ * transfer destinations) will still see the COA. That trade-off is
33
+ * accepted because the alternative blocks the user from picking anything;
34
+ * the backend distinguishes "no result" from "endpoint not implemented" by
35
+ * surfacing an error response, which the view layer handles separately.
36
+ *
37
+ * Memoized with reselect so consuming view-models don't recompute on every
38
+ * unrelated state change.
39
+ */
40
+ export const getResolvedTransferAccounts = createSelector([
41
+ (state) => state.accountState,
42
+ (state) => state.accountListState,
43
+ (state) => state.createTransferEntryState.transferAccountsByType
44
+ .transferEntryEligible.accountIds,
45
+ (state) => state.createTransferEntryState.transferAccountsByType
46
+ .creditCardOnlyEligible.accountIds,
47
+ ], (accountState, accountListState, transferEligibleIds, creditCardOnlyIds) => {
48
+ const accountList = getAccountList(accountState, accountListState, 'accountList');
49
+ const transferEligibleFromApi = transferEligibleIds
50
+ .map((id) => getAccountBaseForTransferAccounts(accountState, id))
51
+ .filter((account) => account != null);
52
+ const coaFallbackAllAccounts = accountList.accounts
53
+ .map((a) => getAccountBase(accountState, a.accountId, 'account_list'))
54
+ .filter((account) => account != null);
55
+ const transferAccountsList = transferEligibleFromApi.length > 0
56
+ ? transferEligibleFromApi
57
+ : coaFallbackAllAccounts;
58
+ const creditCardFromApi = creditCardOnlyIds
59
+ .map((id) => getAccountBaseForCreditCardTransferAccounts(accountState, id))
60
+ .filter((account) => account != null);
61
+ const coaFallbackCreditCards = coaFallbackAllAccounts.filter((a) => a.accountType === 'credit_card');
62
+ const creditCardTransferAccountsList = creditCardFromApi.length > 0 ? creditCardFromApi : coaFallbackCreditCards;
63
+ return { creditCardTransferAccountsList, transferAccountsList };
64
+ });
@@ -0,0 +1,17 @@
1
+ /** Which pool `fetchAccountsForTransferFlow` / transfer-flow epic should load. */
2
+ export const TRANSFER_ACCOUNTS_FETCH_TYPE = [
3
+ 'transferEntryEligible',
4
+ 'creditCardOnlyEligible',
5
+ ];
6
+ const initialTransferAccountsState = {
7
+ accountIds: [],
8
+ fetchState: 'Not-Started',
9
+ };
10
+ export const initialCreateTransferEntryState = {
11
+ createTransferEntryStatus: 'Not-Started',
12
+ lastTransferEntryReplacement: undefined,
13
+ transferAccountsByType: {
14
+ transferEntryEligible: initialTransferAccountsState,
15
+ creditCardOnlyEligible: initialTransferAccountsState,
16
+ },
17
+ };
@@ -0,0 +1,112 @@
1
+ import { concat, from, timer } from 'rxjs';
2
+ import { catchError, filter, mergeMap } from 'rxjs/operators';
3
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
4
+ import { toTransactionID } from '../../../entity/transaction/payloadTypes/transactionIDPayload';
5
+ import { removeTransaction, updateTransaction, } from '../../../entity/transaction/transactionReducer';
6
+ import { isSuccessResponse } from '../../../responsePayload';
7
+ import { removeTransactionFromAllTabs } from '../../expenseAutomationView/reducers/transactionsViewReducer';
8
+ import { initializeTransactionDetailLocalData, removeTransactionDetail, } from '../../transactionDetail/transactionDetailReducer';
9
+ import { getTransactionDetailKey } from '../../transactionDetail/transactionDetailState';
10
+ import { clearTransferEntryRouteReplacement, createTransferEntry, createTransferEntryFailure, createTransferEntryReplacedTransaction, createTransferEntrySuccess, resetCreateTransferEntryStatus, } from '../createTransferEntryReducer';
11
+ // ─── Pure helpers ────────────────────────────────────────────────────────────
12
+ function transactionIdsAreEqual(a, b) {
13
+ return a.id === b.id && a.type === b.type;
14
+ }
15
+ const successSnackbar = () => openSnackbar({
16
+ messageSection: 'create_transfer_entry',
17
+ messageText: 'success',
18
+ type: 'success',
19
+ });
20
+ const failureSnackbar = () => openSnackbar({
21
+ messageSection: 'create_transfer_entry',
22
+ messageText: 'failed',
23
+ type: 'error',
24
+ });
25
+ function buildFailureActions() {
26
+ return [createTransferEntryFailure(), failureSnackbar()];
27
+ }
28
+ function buildSuccessSideEffects(sourceTransactionId) {
29
+ return [
30
+ createTransferEntrySuccess(),
31
+ removeTransactionFromAllTabs(sourceTransactionId),
32
+ successSnackbar(),
33
+ ];
34
+ }
35
+ function buildPreviousTransactionCleanupActions(previousTransactionId) {
36
+ return [
37
+ removeTransaction(getTransactionDetailKey(previousTransactionId)),
38
+ removeTransactionDetail(previousTransactionId),
39
+ clearTransferEntryRouteReplacement(),
40
+ ];
41
+ }
42
+ /**
43
+ * Emits `resetCreateTransferEntryStatus()` (and, on the replacement path, the
44
+ * follow-up cleanup actions) one scheduler tick after the success actions.
45
+ *
46
+ * Why the `timer(0)` (and not synchronous emission):
47
+ * On the replacement path the host (web-app-ui) watches
48
+ * `lastTransferEntryReplacement`, navigates to the new transaction url, and
49
+ * then sees `clearTransferEntryRouteReplacement` clear the signal. If we
50
+ * dispatched the cleanup actions in the same tick as
51
+ * `createTransferEntryReplacedTransaction`, redux subscribers would observe
52
+ * `lastTransferEntryReplacement` flicker on then off in a single render, and
53
+ * the old transaction's state (which the still-mounted detail view may be
54
+ * reading) would be torn down before the host has had a chance to start
55
+ * navigating. Deferring by one microtask gives the host exactly one render
56
+ * pass to react before we tear down the previous transaction.
57
+ */
58
+ function delayedResetAndPreviousCleanup(previousTransactionId) {
59
+ return timer(0).pipe(mergeMap(() => {
60
+ const actions = [resetCreateTransferEntryStatus()];
61
+ if (previousTransactionId != null) {
62
+ actions.push(...buildPreviousTransactionCleanupActions(previousTransactionId));
63
+ }
64
+ return from(actions);
65
+ }));
66
+ }
67
+ // ─── Success-path branches ───────────────────────────────────────────────────
68
+ function handleSuccessWithoutTransactionBody(transactionId) {
69
+ return concat(from(buildSuccessSideEffects(transactionId)), delayedResetAndPreviousCleanup(undefined));
70
+ }
71
+ function handleSuccessWithTransactionBody(transactionPayload, vendorPayload, previousTransactionId, sourceTransactionId) {
72
+ const newTransactionId = toTransactionID(transactionPayload);
73
+ const replaced = !transactionIdsAreEqual(previousTransactionId, newTransactionId);
74
+ const immediate = [
75
+ updateTransaction(getTransactionDetailKey(newTransactionId), transactionPayload),
76
+ initializeTransactionDetailLocalData({
77
+ transactionId: newTransactionId,
78
+ vendorId: vendorPayload?.[0]?.vendor_id,
79
+ }),
80
+ ...buildSuccessSideEffects(sourceTransactionId),
81
+ ];
82
+ if (replaced) {
83
+ immediate.push(createTransferEntryReplacedTransaction(previousTransactionId, newTransactionId));
84
+ }
85
+ return concat(from(immediate), delayedResetAndPreviousCleanup(replaced ? previousTransactionId : undefined));
86
+ }
87
+ // ─── Top-level epic ──────────────────────────────────────────────────────────
88
+ const CREATE_TRANSFER_ENTRY_URL = `https://dev.api.zeni.ai/version/accounting/komal/1.0/transactions/transaction-type-update`;
89
+ export const createTransferEntryEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(createTransferEntry.match), mergeMap((action) => {
90
+ const { transactionId, creditAccountIntegrationId, debitAccountIntegrationId, memo, transactionType, sourceTransactionType, } = action.payload;
91
+ const previousTransactionId = {
92
+ id: transactionId,
93
+ type: sourceTransactionType,
94
+ };
95
+ return zeniAPI
96
+ .postAndGetJSON(CREATE_TRANSFER_ENTRY_URL, {
97
+ transaction_type: transactionType,
98
+ transaction_id: transactionId,
99
+ to_account_integration_id: creditAccountIntegrationId,
100
+ from_account_integration_id: debitAccountIntegrationId,
101
+ memo,
102
+ })
103
+ .pipe(mergeMap((response) => {
104
+ if (!isSuccessResponse(response)) {
105
+ return from(buildFailureActions());
106
+ }
107
+ if (response.data?.transaction != null) {
108
+ return handleSuccessWithTransactionBody(response.data.transaction, response.data.vendors, previousTransactionId, transactionId);
109
+ }
110
+ return handleSuccessWithoutTransactionBody(transactionId);
111
+ }), catchError(() => from(buildFailureActions())));
112
+ }));
@@ -0,0 +1,36 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, filter, groupBy, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { updateAccounts } from '../../../entity/account/accountReducer';
4
+ import { isSuccessResponse } from '../../../responsePayload';
5
+ import { fetchAccountsForTransferFlow, updateAccountsForTransferFlow, updateAccountsForTransferFlowFailure, } from '../createTransferEntryReducer';
6
+ const REPORT_ID_BY_TYPE = {
7
+ transferEntryEligible: 'transfer_accounts',
8
+ creditCardOnlyEligible: 'credit_card_transfer_accounts',
9
+ };
10
+ const QUERY_BY_TYPE = {
11
+ transferEntryEligible: { is_only_include_transfer_accounts: true },
12
+ creditCardOnlyEligible: { is_include_only_credit_card_accounts: true },
13
+ };
14
+ /**
15
+ * Pools fetched concurrently: `groupBy(kind)` keeps `transferEntryEligible`
16
+ * and `creditCardOnlyEligible` in their own streams so a back-to-back dispatch
17
+ * of both does not cancel the first. Within a single type, a fresh dispatch
18
+ * supersedes any in-flight request (per-group `switchMap`).
19
+ */
20
+ export const fetchTransferAccountsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchAccountsForTransferFlow.match), groupBy((action) => action.payload.kind), mergeMap((group$) => group$.pipe(switchMap((action) => {
21
+ const { kind } = action.payload;
22
+ const queryValue = QUERY_BY_TYPE[kind];
23
+ const reportId = REPORT_ID_BY_TYPE[kind];
24
+ const url = `https://dev.api.zeni.ai/version/accounting/komal/1.0/accounts?query=${encodeURIComponent(JSON.stringify(queryValue))}`;
25
+ return zeniAPI.getJSON(url).pipe(mergeMap((response) => {
26
+ if (isSuccessResponse(response) && response.data != null) {
27
+ const accountIds = response.data.accounts.map((account) => account.account_id);
28
+ const actions = [
29
+ updateAccounts(response.data.accounts, reportId),
30
+ updateAccountsForTransferFlow({ accountIds, kind }),
31
+ ];
32
+ return of(...actions);
33
+ }
34
+ return of(updateAccountsForTransferFlowFailure({ kind }));
35
+ }), catchError(() => of(updateAccountsForTransferFlowFailure({ kind }))));
36
+ }))));
@@ -4,6 +4,7 @@ import { toMonthYearPeriodId, } from '../../../../commonStateTypes/timePeriod';
4
4
  import { getIsAccountingProjectsEnabled } from '../../../../entity/tenant/tenantSelector';
5
5
  import { fetchAccountList } from '../../../accountList/accountListReducer';
6
6
  import { fetchClassList } from '../../../classList/classListReducer';
7
+ import { fetchAccountsForTransferFlow } from '../../../createTransferEntry/createTransferEntryReducer';
7
8
  import { fetchOwnerList } from '../../../ownerList/ownerListReducer';
8
9
  import { fetchProjectList } from '../../../projectList/projectListReducer';
9
10
  import { fetchTransactionCategorization, fetchTransactionCategorizationView, } from '../../reducers/transactionsViewReducer';
@@ -23,6 +24,13 @@ export const fetchTransactionCategorizationViewEpic = (actions$, state$) => acti
23
24
  accountList.fetchState === 'Not-Started') {
24
25
  updateActions.push(fetchAccountList('accountList', true));
25
26
  }
27
+ const { transferEntryEligible, creditCardOnlyEligible } = state$.value.createTransferEntryState.transferAccountsByType;
28
+ if (transferEntryEligible.fetchState === 'Not-Started') {
29
+ updateActions.push(fetchAccountsForTransferFlow({ kind: 'transferEntryEligible' }));
30
+ }
31
+ if (creditCardOnlyEligible.fetchState === 'Not-Started') {
32
+ updateActions.push(fetchAccountsForTransferFlow({ kind: 'creditCardOnlyEligible' }));
33
+ }
26
34
  const classList = state$.value.classListState;
27
35
  if (classList.hasValidState() === false &&
28
36
  classList.fetchState === 'Not-Started') {
@@ -59,22 +59,6 @@ export const filterAutoTabLineItems = (tabView, transactionId, updatedTransactio
59
59
  }
60
60
  };
61
61
  export const MAX_SELECTION_LIMIT = 5;
62
- // Returns true when the transaction should be auto-added to the
63
- // selectedCheckBoxTransactionIds list after a categorization-related
64
- // state mutation: all line items are filled, the selection limit has
65
- // not been hit, and the id isn't already in the list. Consolidates the
66
- // triplet that was inlined at four sites (updateSelectedCustomer/Vendor,
67
- // setAllItemsToCategoryClassInLocalDataForCategorization, and
68
- // setEntityRecommendationForLineIdInLocalData below).
69
- export const shouldAutoSelectAndAdd = (transaction, localData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
70
- if (selectedCheckBoxTransactionIds.includes(transaction.id)) {
71
- return false;
72
- }
73
- if (selectedCheckBoxTransactionIds.length >= MAX_SELECTION_LIMIT) {
74
- return false;
75
- }
76
- return checkIfAllLineItemsAreCategoryClassFilled(transaction, localData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
77
- };
78
62
  export const toTransactionDetailLocalData = (transaction, selectedTab, lineItemsByTransactionIds, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled) => {
79
63
  const transactionLocal = {
80
64
  ...initialSupportedTransactionCategorization.transactionReviewLocalData,
@@ -560,14 +544,10 @@ export const setEntityRecommendationForLineIdInLocalData = (draft, transaction,
560
544
  selectedEntity: transactionDetailLocalData.selectedEntity,
561
545
  tabSpecificLineItems: transactionDetailLocalData.tabSpecificLineItems,
562
546
  };
563
- // Auto-uncheck if not all line items are filled (the row's
564
- // category/class became stale after the entity update). Otherwise
565
- // route through shouldAutoSelectAndAdd so the MAX_SELECTION_LIMIT
566
- // gate is consistently applied across all auto-add sites.
567
547
  if (!checkIfAllLineItemsAreCategoryClassFilled(transaction, newLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
568
548
  selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds.filter((id) => id !== transaction.id);
569
549
  }
570
- else if (shouldAutoSelectAndAdd(transaction, newLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
550
+ else if (selectedCheckBoxTransactionIds.includes(transaction.id) === false) {
571
551
  selectedCheckBoxTransactionIds.push(transaction.id);
572
552
  }
573
553
  draft.selectedCheckBoxTransactionIds = selectedCheckBoxTransactionIds;
@@ -3,8 +3,9 @@ import recordGet from 'lodash/get';
3
3
  import uniq from 'lodash/uniq';
4
4
  import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
5
5
  import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
6
- import { filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, shouldAutoSelectAndAdd, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
6
+ import { MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
7
7
  import { DEFAULT_COMPLETED_SUB_TAB, } from '../types/completedSubTab';
8
+ import { TRANSACTIONS_TABS, } from '../types/transactionsViewState';
8
9
  export const initialTransactionTabViewState = {
9
10
  saveStatus: {
10
11
  fetchState: 'Not-Started',
@@ -420,9 +421,12 @@ const expenseAutomationTransactionsView = createSlice({
420
421
  const selectedCheckBoxTransactionIds = draft.transactionCategorizationView[selectedTab]
421
422
  .selectedCheckBoxTransactionIds;
422
423
  if (selectedTransaction != null &&
423
- shouldAutoSelectAndAdd(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
424
+ checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
425
+ selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
426
+ selectedCheckBoxTransactionIds.includes(transactionId) === false) {
424
427
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
425
- ...selectedCheckBoxTransactionIds,
428
+ ...draft.transactionCategorizationView[selectedTab]
429
+ .selectedCheckBoxTransactionIds,
426
430
  transactionId,
427
431
  ];
428
432
  }
@@ -505,26 +509,9 @@ const expenseAutomationTransactionsView = createSlice({
505
509
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
506
510
  .transactionReviewLocalDataById, transactionId.id, undefined);
507
511
  if (transactionLocalData != null) {
508
- const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
509
- // Release the per-line fetch flag for every line this fetch
510
- // covered. Done in-place on the freshly returned local data
511
- // (the helper produces a new mutable object), before we write
512
- // it back to the draft.
513
- lineIds.forEach((lineId) => {
514
- const lineItem = newLocalData.lineItemById[lineId];
515
- if (lineItem != null) {
516
- newLocalData.lineItemById[lineId] = {
517
- ...lineItem,
518
- categoryClassRecommendationsFetchState: {
519
- fetchState: 'Completed',
520
- error: undefined,
521
- },
522
- };
523
- }
524
- });
525
512
  draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
526
513
  transactionId: transaction.id,
527
- transactionReviewLocalData: newLocalData,
514
+ transactionReviewLocalData: setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled),
528
515
  };
529
516
  // Mark transaction as dirty (autofill happened)
530
517
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
@@ -537,99 +524,6 @@ const expenseAutomationTransactionsView = createSlice({
537
524
  }
538
525
  },
539
526
  },
540
- // Start arm of the line-update recommendations fetch. Flips covered
541
- // lines to In-Progress AND removes the transaction from
542
- // selectedCheckBoxTransactionIds — the row is unchecked + disabled
543
- // until either the success arm (recheck if limit allows) or the
544
- // failure arm releases the per-line flag.
545
- markCategoryClassRecommendationsInProgressForCategorization: {
546
- reducer(draft, action) {
547
- const { selectedTab, transactionId, lineIds } = action.payload;
548
- const tab = draft.transactionCategorizationView[selectedTab];
549
- const transactionLocalData = recordGet(tab.transactionReviewLocalDataById, transactionId.id, undefined);
550
- if (transactionLocalData != null) {
551
- lineIds.forEach((lineId) => {
552
- const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
553
- if (lineItem != null) {
554
- transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
555
- ...lineItem,
556
- categoryClassRecommendationsFetchState: {
557
- fetchState: 'In-Progress',
558
- error: undefined,
559
- },
560
- };
561
- }
562
- });
563
- }
564
- // Uncheck on fetch start: the row's existing category/class are
565
- // stale after a payee change, so we drop the auto-selection until
566
- // fresh recommendations land.
567
- if (tab.selectedCheckBoxTransactionIds.includes(transactionId.id)) {
568
- tab.selectedCheckBoxTransactionIds =
569
- tab.selectedCheckBoxTransactionIds.filter((id) => id !== transactionId.id);
570
- }
571
- },
572
- prepare(selectedTab, transactionId, lineIds) {
573
- return { payload: { selectedTab, transactionId, lineIds } };
574
- },
575
- },
576
- // Completed arm. Used by the epic when the HTTP call succeeds but the
577
- // server returned no per-line recommendations — we still need to
578
- // release the In-Progress flag so the skeleton resolves. Successful
579
- // responses with recommendations go through
580
- // setEntityRecommendationForLineIdsForCategorization instead (which
581
- // also flips the lines to Completed).
582
- markCategoryClassRecommendationsCompletedForCategorization: {
583
- reducer(draft, action) {
584
- const { selectedTab, transactionId, lineIds } = action.payload;
585
- const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
586
- .transactionReviewLocalDataById, transactionId.id, undefined);
587
- if (transactionLocalData != null) {
588
- lineIds.forEach((lineId) => {
589
- const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
590
- if (lineItem != null) {
591
- transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
592
- ...lineItem,
593
- categoryClassRecommendationsFetchState: {
594
- fetchState: 'Completed',
595
- error: undefined,
596
- },
597
- };
598
- }
599
- });
600
- }
601
- },
602
- prepare(selectedTab, transactionId, lineIds) {
603
- return { payload: { selectedTab, transactionId, lineIds } };
604
- },
605
- },
606
- // Failure arm. Flips covered lines to Error so the skeleton resolves
607
- // and downstream UI can react to the failure. The transaction stays
608
- // unchecked from the start-arm action.
609
- markCategoryClassRecommendationsFailureForCategorization: {
610
- reducer(draft, action) {
611
- const { selectedTab, transactionId, lineIds, error } = action.payload;
612
- const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
613
- .transactionReviewLocalDataById, transactionId.id, undefined);
614
- if (transactionLocalData != null) {
615
- lineIds.forEach((lineId) => {
616
- const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
617
- if (lineItem != null) {
618
- transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
619
- ...lineItem,
620
- categoryClassRecommendationsFetchState: {
621
- fetchState: 'Error',
622
- error,
623
- },
624
- };
625
- }
626
- });
627
- }
628
- },
629
- prepare(selectedTab, transactionId, lineIds, error) {
630
- return { payload: { selectedTab, transactionId, lineIds, error } };
631
- },
632
- },
633
527
  fetchTransactionCategorizationSuccess(draft, action) {
634
528
  const { selectedTab, refreshViewInBackground } = action.payload;
635
529
  if (draft.transactionCategorizationView[selectedTab].fetchState ===
@@ -725,7 +619,9 @@ const expenseAutomationTransactionsView = createSlice({
725
619
  .selectedCheckBoxTransactionIds;
726
620
  if (newTransactionLocalData.customer != null &&
727
621
  newTransactionLocalData.customer.id != null &&
728
- shouldAutoSelectAndAdd(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
622
+ checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
623
+ selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
624
+ selectedCheckBoxTransactionIds.includes(transactionId) === false) {
729
625
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
730
626
  ...selectedCheckBoxTransactionIds,
731
627
  transactionId,
@@ -799,7 +695,9 @@ const expenseAutomationTransactionsView = createSlice({
799
695
  .selectedCheckBoxTransactionIds;
800
696
  if (newTransactionLocalData.vendor != null &&
801
697
  newTransactionLocalData.vendor.id != null &&
802
- shouldAutoSelectAndAdd(selectedTransaction, newTransactionLocalData, selectedCheckBoxTransactionIds, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled)) {
698
+ checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
699
+ selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
700
+ selectedCheckBoxTransactionIds.includes(transactionId) === false) {
803
701
  draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
804
702
  ...selectedCheckBoxTransactionIds,
805
703
  transactionId,
@@ -1030,7 +928,18 @@ const expenseAutomationTransactionsView = createSlice({
1030
928
  };
1031
929
  },
1032
930
  },
931
+ removeTransactionFromAllTabs: {
932
+ prepare(transactionId) {
933
+ return { payload: { transactionId } };
934
+ },
935
+ reducer(draft, action) {
936
+ const { transactionId } = action.payload;
937
+ for (const tab of TRANSACTIONS_TABS) {
938
+ removeTransactionFromTabView(draft.transactionCategorizationView[tab], transactionId);
939
+ }
940
+ },
941
+ },
1033
942
  },
1034
943
  });
1035
- export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, updateParentTotalCountForTab, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, markCategoryClassRecommendationsInProgressForCategorization, markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
944
+ export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationCompletedSubTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, updateParentTotalCountForTab, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, removeTransactionFromAllTabs, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
1036
945
  export default expenseAutomationTransactionsView.reducer;