@zeniai/client-epic-state 5.2.34 → 5.2.35-beta10MM

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 (115) hide show
  1. package/lib/entity/actualMatching/actualMatchingPayload.d.ts +31 -0
  2. package/lib/entity/actualMatching/actualMatchingPayload.js +50 -0
  3. package/lib/entity/actualMatching/actualMatchingReducer.d.ts +9 -0
  4. package/lib/entity/actualMatching/actualMatchingReducer.js +33 -0
  5. package/lib/entity/actualMatching/actualMatchingSelector.d.ts +3 -0
  6. package/lib/entity/actualMatching/actualMatchingSelector.js +6 -0
  7. package/lib/entity/actualMatching/actualMatchingState.d.ts +23 -0
  8. package/lib/entity/actualMatching/actualMatchingState.js +2 -0
  9. package/lib/entity/actualMatching/actualMatchingTypes.d.ts +1 -0
  10. package/lib/entity/actualMatching/actualMatchingTypes.js +2 -0
  11. package/lib/entity/aiCfo/aiCfoReducer.js +78 -4
  12. package/lib/entity/aiCfo/aiCfoState.d.ts +16 -0
  13. package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +31 -0
  14. package/lib/entity/jeSchedules/jeSchedulesPayload.js +53 -0
  15. package/lib/entity/jeSchedules/jeSchedulesState.d.ts +33 -0
  16. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  17. package/lib/entity/snackbar/snackbarTypes.js +2 -0
  18. package/lib/epic.d.ts +8 -1
  19. package/lib/epic.js +14 -0
  20. package/lib/esm/entity/actualMatching/actualMatchingPayload.js +46 -0
  21. package/lib/esm/entity/actualMatching/actualMatchingReducer.js +30 -0
  22. package/lib/esm/entity/actualMatching/actualMatchingSelector.js +3 -0
  23. package/lib/esm/entity/actualMatching/actualMatchingState.js +1 -0
  24. package/lib/esm/entity/actualMatching/actualMatchingTypes.js +1 -0
  25. package/lib/esm/entity/aiCfo/aiCfoReducer.js +79 -5
  26. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +53 -0
  27. package/lib/esm/entity/snackbar/snackbarTypes.js +2 -0
  28. package/lib/esm/epic.js +14 -0
  29. package/lib/esm/index.js +7 -3
  30. package/lib/esm/reducer.js +6 -0
  31. package/lib/esm/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingEpic.js +44 -0
  32. package/lib/esm/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingMatchEpic.js +36 -0
  33. package/lib/esm/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingPageEpic.js +7 -0
  34. package/lib/esm/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +107 -0
  35. package/lib/esm/view/expenseAutomationView/epics/actualMatching/refreshActualMatchingList.js +16 -0
  36. package/lib/esm/view/expenseAutomationView/epics/actualMatching/searchMatchAccrualsEpic.js +54 -0
  37. package/lib/esm/view/expenseAutomationView/epics/actualMatching/searchMatchTransactionsEpic.js +74 -0
  38. package/lib/esm/view/expenseAutomationView/epics/actualMatching/undoReversalAccrualsEpic.js +76 -0
  39. package/lib/esm/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +25 -12
  40. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -3
  41. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +35 -15
  42. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +8 -3
  43. package/lib/esm/view/expenseAutomationView/expenseAutomationViewReducer.js +2 -1
  44. package/lib/esm/view/expenseAutomationView/expenseAutomationViewSelector.js +24 -6
  45. package/lib/esm/view/expenseAutomationView/expenseAutomationViewState.js +4 -2
  46. package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +68 -0
  47. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +434 -0
  48. package/lib/esm/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +2 -1
  49. package/lib/esm/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.js +1 -0
  50. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +426 -0
  51. package/lib/esm/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +4 -0
  52. package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +35 -0
  53. package/lib/esm/view/expenseAutomationView/types/jeSchedulesViewState.js +8 -1
  54. package/lib/esm/view/financeStatement/financeStatementReducer.js +17 -27
  55. package/lib/esm/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewPayload.js +4 -0
  56. package/lib/esm/view/spendManagement/billPay/billList/billListSelector.js +3 -3
  57. package/lib/index.d.ts +15 -6
  58. package/lib/index.js +82 -48
  59. package/lib/reducer.d.ts +2 -0
  60. package/lib/reducer.js +6 -0
  61. package/lib/rootStateTypes.d.ts +4 -0
  62. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingEpic.d.ts +9 -0
  63. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingEpic.js +48 -0
  64. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingMatchEpic.d.ts +12 -0
  65. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingMatchEpic.js +40 -0
  66. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingPageEpic.d.ts +5 -0
  67. package/lib/view/expenseAutomationView/epics/actualMatching/fetchActualMatchingPageEpic.js +11 -0
  68. package/lib/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.d.ts +9 -0
  69. package/lib/view/expenseAutomationView/epics/actualMatching/matchAndReverseAccrualsEpic.js +111 -0
  70. package/lib/view/expenseAutomationView/epics/actualMatching/refreshActualMatchingList.d.ts +3 -0
  71. package/lib/view/expenseAutomationView/epics/actualMatching/refreshActualMatchingList.js +19 -0
  72. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchAccrualsEpic.d.ts +9 -0
  73. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchAccrualsEpic.js +58 -0
  74. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchTransactionsEpic.d.ts +8 -0
  75. package/lib/view/expenseAutomationView/epics/actualMatching/searchMatchTransactionsEpic.js +78 -0
  76. package/lib/view/expenseAutomationView/epics/actualMatching/undoReversalAccrualsEpic.d.ts +9 -0
  77. package/lib/view/expenseAutomationView/epics/actualMatching/undoReversalAccrualsEpic.js +80 -0
  78. package/lib/view/expenseAutomationView/epics/common/fetchAllExpenseAutomationTabsEpic.js +25 -12
  79. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.d.ts +2 -1
  80. package/lib/view/expenseAutomationView/epics/jeSchedule/fetchJeSchedulePageEpic.js +13 -3
  81. package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.d.ts +5 -1
  82. package/lib/view/expenseAutomationView/epics/jeSchedule/prefetchOtherJeSchedulesTabEpic.js +34 -14
  83. package/lib/view/expenseAutomationView/epics/jeSchedule/switchJESchedulesTabEpic.js +8 -3
  84. package/lib/view/expenseAutomationView/expenseAutomationViewReducer.d.ts +2 -1
  85. package/lib/view/expenseAutomationView/expenseAutomationViewReducer.js +2 -1
  86. package/lib/view/expenseAutomationView/expenseAutomationViewSelector.js +24 -6
  87. package/lib/view/expenseAutomationView/expenseAutomationViewState.d.ts +2 -1
  88. package/lib/view/expenseAutomationView/expenseAutomationViewState.js +6 -3
  89. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  90. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +85 -0
  91. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +74 -0
  92. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +79 -0
  93. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +438 -0
  94. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.d.ts +6 -5
  95. package/lib/view/expenseAutomationView/reducers/jeSchedulesViewReducer.js +2 -1
  96. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +87 -0
  97. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.js +2 -0
  98. package/lib/view/expenseAutomationView/selectorTypes/expenseAutomationViewSelectorTypes.d.ts +11 -0
  99. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +38 -0
  100. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +445 -0
  101. package/lib/view/expenseAutomationView/selectors/jeSchedulesViewSelector.js +4 -0
  102. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +105 -0
  103. package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +42 -0
  104. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.d.ts +1 -1
  105. package/lib/view/expenseAutomationView/types/jeSchedulesViewState.js +8 -1
  106. package/lib/view/financeStatement/financeStatementReducer.js +17 -27
  107. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewPayload.d.ts +1 -11
  108. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewPayload.js +4 -0
  109. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewReducer.d.ts +1 -2
  110. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewSelector.d.ts +1 -2
  111. package/lib/view/invoicing/editInvoicingCustomerDetailView/editInvoicingCustomerDetailViewState.d.ts +15 -1
  112. package/lib/view/invoicing/invoicingCustomerListView/invoicingCustomerListViewReducer.d.ts +1 -1
  113. package/lib/view/invoicing/invoicingCustomerListView/invoicingCustomerListViewState.d.ts +1 -1
  114. package/lib/view/spendManagement/billPay/billList/billListSelector.js +2 -2
  115. package/package.json +1 -1
@@ -42,6 +42,7 @@ import invoicingSubscription, { initialState as initialInvoicingSubscriptionStat
42
42
  import invoicingTransaction, { initialState as initialInvoicingTransactionState, } from './entity/invoicing/invoicingTransaction/invoicingTransactionReducer';
43
43
  import invoicingPlan, { initialState as initialInvoicingPlanState, } from './entity/invoicing/plan/planReducer';
44
44
  import invoicingProduct, { initialState as initialInvoicingProductState, } from './entity/invoicing/product/productReducer';
45
+ import actualMatching, { initialState as initialActualMatchingState, } from './entity/actualMatching/actualMatchingReducer';
45
46
  import jeSchedules, { initialState as initialJESchedulesState, } from './entity/jeSchedules/jeSchedulesReducer';
46
47
  import merchant, { initialState as initialMerchantState, } from './entity/merchant/merchantReducer';
47
48
  import monthEndCloseChecks, { initialState as initialMonthEndCloseChecksState, } from './entity/monthEndCloseChecks/monthEndCloseChecksReducer';
@@ -118,6 +119,7 @@ import creditAgentView, { initialState as initialCreditAgentViewState, } from '.
118
119
  import dashboard, { initialState as initialDashboardState, } from './view/dashboard/dashboardReducer';
119
120
  import dashboardLayout, { initialState as initialDashboardLayoutState, } from './view/dashboardLayout/dashboardLayoutReducer';
120
121
  import expenseAutomationView, { initialState as initialExpenseAutomationViewState, } from './view/expenseAutomationView/expenseAutomationViewReducer';
122
+ import expenseAutomationActualMatchingView, { initialState as initialExpenseAutomationActualMatchingViewState, } from './view/expenseAutomationView/reducers/actualMatchingViewReducer';
121
123
  import expenseAutomationFluxAnalysisView, { initialState as initialExpenseAutomationFluxAnalysisViewState, } from './view/expenseAutomationView/reducers/fluxAnalysisViewReducer';
122
124
  import expenseAutomationJESchedulesView, { initialState as initialExpenseAutomationJESchedulesViewState, } from './view/expenseAutomationView/reducers/jeSchedulesViewReducer';
123
125
  import expenseAutomationMissingReceiptsView, { initialState as initialExpenseAutomationMissingReceiptsViewState, } from './view/expenseAutomationView/reducers/missingReceiptsViewReducer';
@@ -279,6 +281,7 @@ const initialEntitiesState = {
279
281
  accountingSummaryState: initialAccountingSummaryState,
280
282
  accountReconState: initialAccountReconState,
281
283
  accountState: initialAccountState,
284
+ actualMatchingState: initialActualMatchingState,
282
285
  addressState: initialAddressState,
283
286
  aiAccountantCustomerState: initialAiAccountantCustomerState,
284
287
  aiCfoState: initialAiCfoState,
@@ -421,6 +424,7 @@ const initialViewsState = {
421
424
  entityAutoCompleteState: initialEntityAutoCompleteState,
422
425
  entityRecommendationState: initialEntityRecommendationState,
423
426
  expenseAutomationFluxAnalysisViewState: initialExpenseAutomationFluxAnalysisViewState,
427
+ expenseAutomationActualMatchingViewState: initialExpenseAutomationActualMatchingViewState,
424
428
  expenseAutomationJESchedulesViewState: initialExpenseAutomationJESchedulesViewState,
425
429
  expenseAutomationMissingReceiptsViewState: initialExpenseAutomationMissingReceiptsViewState,
426
430
  expenseAutomationReconciliationViewState: initialExpenseAutomationReconciliationViewState,
@@ -563,6 +567,7 @@ const entityReducers = {
563
567
  accountReconState: accountRecon,
564
568
  accountingSummaryState: accountingSummary,
565
569
  accountState: account,
570
+ actualMatchingState: actualMatching,
566
571
  addressState: address,
567
572
  aiAccountantCustomerState: aiAccountantCustomer,
568
573
  aiCfoState: aiCfo,
@@ -705,6 +710,7 @@ const viewReducers = {
705
710
  entityAutoCompleteState: entityAutoComplete,
706
711
  entityRecommendationState: recommendation,
707
712
  expenseAutomationFluxAnalysisViewState: expenseAutomationFluxAnalysisView,
713
+ expenseAutomationActualMatchingViewState: expenseAutomationActualMatchingView,
708
714
  expenseAutomationJESchedulesViewState: expenseAutomationJESchedulesView,
709
715
  expenseAutomationMissingReceiptsViewState: expenseAutomationMissingReceiptsView,
710
716
  expenseAutomationReconciliationViewState: expenseAutomationReconciliationView,
@@ -0,0 +1,44 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
+ import { updateActualMatchingMatches } from '../../../../entity/actualMatching/actualMatchingReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { clearAll } from '../../../../rootActions';
6
+ import { toServiceMonthFromPeriod, } from '../../payload/actualMatchingPayload';
7
+ import { fetchActualMatching, fetchActualMatchingFailure, fetchActualMatchingSuccess, } from '../../reducers/actualMatchingViewReducer';
8
+ import { getSelectedMonthYearForCurrentTenant, getTenantDisplayCurrency, } from '../../selectors/actualMatchingViewSelector';
9
+ export const fetchActualMatchingEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchActualMatching.match), switchMap((action) => {
10
+ const state = state$.value;
11
+ const { period, refreshViewInBackground } = action.payload;
12
+ const selectedPeriod = getSelectedMonthYearForCurrentTenant(state) ?? {
13
+ month: period.start.month,
14
+ year: period.start.year,
15
+ };
16
+ const serviceMonth = toServiceMonthFromPeriod(period);
17
+ return zeniAPI
18
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/actual-matching?service_month=${encodeURIComponent(serviceMonth)}`)
19
+ .pipe(mergeMap((response) => {
20
+ if (isSuccessResponse(response) && response.data != null) {
21
+ const matches = response.data.matches;
22
+ const updateActions = [];
23
+ if (matches.length > 0) {
24
+ updateActions.push(updateActualMatchingMatches(matches, getTenantDisplayCurrency(state)));
25
+ }
26
+ updateActions.push(fetchActualMatchingSuccess({
27
+ matchIds: matches.map((match) => match.match_id),
28
+ selectedPeriod,
29
+ refreshViewInBackground,
30
+ }));
31
+ return from(updateActions);
32
+ }
33
+ return of(fetchActualMatchingFailure({
34
+ status: response.status,
35
+ selectedPeriod,
36
+ refreshViewInBackground,
37
+ }));
38
+ }), catchError((error) => of(fetchActualMatchingFailure({
39
+ status: createZeniAPIStatus('Unexpected Error', 'fetch actual matching REST API call errored out' +
40
+ JSON.stringify(error)),
41
+ selectedPeriod,
42
+ refreshViewInBackground,
43
+ }))), takeUntil(actions$.pipe(filter(clearAll.match))));
44
+ }));
@@ -0,0 +1,36 @@
1
+ import { from } from 'rxjs';
2
+ import { catchError, filter, mergeMap, takeUntil } from 'rxjs/operators';
3
+ import { updateActualMatchingMatches } from '../../../../entity/actualMatching/actualMatchingReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { clearAll } from '../../../../rootActions';
6
+ import { fetchActualMatchingMatch, fetchActualMatchingMatchFailure, fetchActualMatchingMatchSuccess, } from '../../reducers/actualMatchingViewReducer';
7
+ import { getTenantDisplayCurrency } from '../../selectors/actualMatchingViewSelector';
8
+ export const fetchActualMatchingMatchEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchActualMatchingMatch.match), mergeMap((action) => {
9
+ const { matchId, refreshViewInBackground } = action.payload;
10
+ return zeniAPI
11
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/actual-matching/${encodeURIComponent(matchId)}`)
12
+ .pipe(mergeMap((response) => {
13
+ if (isSuccessResponse(response) && response.data != null) {
14
+ return from([
15
+ updateActualMatchingMatches([response.data], getTenantDisplayCurrency(state$.value)),
16
+ fetchActualMatchingMatchSuccess({
17
+ matchId,
18
+ refreshViewInBackground,
19
+ }),
20
+ ]);
21
+ }
22
+ return from([
23
+ fetchActualMatchingMatchFailure({
24
+ matchId,
25
+ refreshViewInBackground,
26
+ status: response.status,
27
+ }),
28
+ ]);
29
+ }), catchError((error) => from([
30
+ fetchActualMatchingMatchFailure({
31
+ matchId,
32
+ refreshViewInBackground,
33
+ status: createZeniAPIStatus('Unexpected Error', `Fetch actual matching match REST API call errored out ${JSON.stringify(error)}`),
34
+ }),
35
+ ])), takeUntil(actions$.pipe(filter(clearAll.match))));
36
+ }));
@@ -0,0 +1,7 @@
1
+ import { of } from 'rxjs';
2
+ import { filter, mergeMap } from 'rxjs/operators';
3
+ import { fetchActualMatching, fetchActualMatchingPage, } from '../../reducers/actualMatchingViewReducer';
4
+ export const fetchActualMatchingPageEpic = (actions$) => actions$.pipe(filter(fetchActualMatchingPage.match), mergeMap((action) => {
5
+ const { period, refreshViewInBackground } = action.payload;
6
+ return of(fetchActualMatching(period, refreshViewInBackground));
7
+ }));
@@ -0,0 +1,107 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, exhaustMap, filter, mergeMap, takeUntil } from 'rxjs/operators';
3
+ import { getActualMatchingMatchById } from '../../../../entity/actualMatching/actualMatchingSelector';
4
+ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
+ import { createZeniAPIStatus, isSuccessResponse } from '../../../../responsePayload';
6
+ import { clearAll } from '../../../../rootActions';
7
+ import { toMatchAndReverseResult, } from '../../payload/actualMatchingPayload';
8
+ import { clearMatchNewModal, matchAndReverseAccruals, matchAndReverseAccrualsFailure, matchAndReverseAccrualsSuccess, } from '../../reducers/actualMatchingViewReducer';
9
+ import { refreshActualMatchingList } from './refreshActualMatchingList';
10
+ function isMatchCompleteStatus(status) {
11
+ return status === 'reversed' || status === 'noop';
12
+ }
13
+ function isMatchFailedStatus(status) {
14
+ return (status === 'conflict' || status === 'claim_lost' || status === 'error');
15
+ }
16
+ function matchAndReverseSnackbar(results, vendorName) {
17
+ const reversedCount = results.filter((result) => result.status === 'reversed').length;
18
+ const failedCount = results.filter((result) => isMatchFailedStatus(result.status)).length;
19
+ const variables = [
20
+ { variableName: '_count_', variableValue: String(reversedCount) },
21
+ { variableName: '_vendor_name_', variableValue: vendorName },
22
+ ];
23
+ if (results.length === 0 || (reversedCount === 0 && failedCount > 0)) {
24
+ return openSnackbar({
25
+ messageSection: 'actual_matching_match_and_reverse',
26
+ messageText: 'failed',
27
+ type: 'error',
28
+ variables,
29
+ });
30
+ }
31
+ if (reversedCount > 0 && failedCount === 0) {
32
+ return openSnackbar({
33
+ messageSection: 'actual_matching_match_and_reverse',
34
+ messageText: 'success',
35
+ type: 'success',
36
+ variables,
37
+ });
38
+ }
39
+ if (reversedCount > 0 && failedCount > 0) {
40
+ return openSnackbar({
41
+ messageSection: 'actual_matching_match_and_reverse',
42
+ messageText: 'notification',
43
+ type: 'warning',
44
+ variables,
45
+ });
46
+ }
47
+ return null;
48
+ }
49
+ function matchAndReverseHttpFailSnackbar(vendorName) {
50
+ return openSnackbar({
51
+ messageSection: 'actual_matching_match_and_reverse',
52
+ messageText: 'failed',
53
+ type: 'error',
54
+ variables: [
55
+ { variableName: '_count_', variableValue: '0' },
56
+ { variableName: '_vendor_name_', variableValue: vendorName },
57
+ ],
58
+ });
59
+ }
60
+ function matchVendorName(state, matchId) {
61
+ return getActualMatchingMatchById(state.actualMatchingState, matchId)?.vendorName ?? '';
62
+ }
63
+ export const matchAndReverseAccrualsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(matchAndReverseAccruals.match),
64
+ // Posts reversing JEs; drop duplicate clicks while a request is in flight.
65
+ exhaustMap((action) => {
66
+ const { transactionId, transactionIntegrationId, transactionType, lineIds, accrualIds, transactionSyncToken, } = action.payload;
67
+ const body = {
68
+ transaction_id: transactionId,
69
+ transaction_integration_id: transactionIntegrationId,
70
+ transaction_type: transactionType,
71
+ line_ids: lineIds,
72
+ transaction_sync_token: transactionSyncToken,
73
+ accrual_ids: accrualIds,
74
+ };
75
+ // Key leftover results and snackbar vendor to the confirmed bill, not the
76
+ // row Match New was opened from (user can pick a different txn).
77
+ const matchId = `${transactionType.toLowerCase()}:${transactionIntegrationId}`;
78
+ const vendorName = matchVendorName(state$.value, matchId);
79
+ return zeniAPI
80
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/match-and-reverse`, body)
81
+ .pipe(mergeMap((response) => {
82
+ if (isSuccessResponse(response) && response.data != null) {
83
+ const results = response.data.results.map(toMatchAndReverseResult);
84
+ const successActions = [
85
+ matchAndReverseAccrualsSuccess({ matchId, results }),
86
+ ];
87
+ const occupancyChanged = results.some((result) => isMatchCompleteStatus(result.status));
88
+ const allComplete = results.length > 0 &&
89
+ results.every((result) => isMatchCompleteStatus(result.status));
90
+ if (occupancyChanged) {
91
+ successActions.push(...refreshActualMatchingList(state$.value));
92
+ }
93
+ const snackbar = matchAndReverseSnackbar(results, vendorName);
94
+ if (snackbar != null) {
95
+ successActions.push(snackbar);
96
+ }
97
+ if (allComplete) {
98
+ successActions.push(clearMatchNewModal());
99
+ }
100
+ return from(successActions);
101
+ }
102
+ return of(matchAndReverseAccrualsFailure({ status: response.status }), matchAndReverseHttpFailSnackbar(vendorName));
103
+ }), catchError((error) => of(matchAndReverseAccrualsFailure({
104
+ status: createZeniAPIStatus('Unexpected Error', 'Match and reverse accruals REST API call errored out' +
105
+ JSON.stringify(error)),
106
+ }), matchAndReverseHttpFailSnackbar(vendorName))), takeUntil(actions$.pipe(filter(clearAll.match))));
107
+ }));
@@ -0,0 +1,16 @@
1
+ import { convertToPeriod, toAbsoluteDay, } from '../../../../commonStateTypes/timePeriod';
2
+ import { fetchActualMatching } from '../../reducers/actualMatchingViewReducer';
3
+ import { getSelectedMonthYearForCurrentTenant } from '../../selectors/actualMatchingViewSelector';
4
+ export function refreshActualMatchingList(state) {
5
+ const selectedMonthYear = getSelectedMonthYearForCurrentTenant(state);
6
+ if (selectedMonthYear == null) {
7
+ return [];
8
+ }
9
+ const period = convertToPeriod(selectedMonthYear);
10
+ const start = toAbsoluteDay(period.start);
11
+ const end = toAbsoluteDay(period.end);
12
+ if (start == null || end == null) {
13
+ return [];
14
+ }
15
+ return [fetchActualMatching({ start, end }, true)];
16
+ }
@@ -0,0 +1,54 @@
1
+ import { from, merge, of } from 'rxjs';
2
+ import { catchError, debounceTime, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
+ import { withInheritedAccrualCurrency } from '../../../../entity/actualMatching/actualMatchingPayload';
4
+ import { updateJEOneTimeAccruals } from '../../../../entity/jeSchedules/jeSchedulesReducer';
5
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
+ import { clearAll } from '../../../../rootActions';
7
+ import { searchMatchAccruals, searchMatchAccrualsFailure, searchMatchAccrualsSuccess, } from '../../reducers/actualMatchingViewReducer';
8
+ import { getTenantDisplayCurrency } from '../../selectors/actualMatchingViewSelector';
9
+ import { MATCH_ACCRUAL_SEARCH_LIMIT, MATCH_SEARCH_DEBOUNCE_MS, } from '../../types/actualMatchingViewState';
10
+ export const searchMatchAccrualsEpic = (actions$, state$, zeniAPI) => {
11
+ const searchActions$ = actions$.pipe(filter(searchMatchAccruals.match));
12
+ const pagination$ = searchActions$.pipe(filter((action) => action.payload.pageToken != null));
13
+ const newSearchDebounced$ = searchActions$.pipe(filter((action) => action.payload.pageToken == null), debounceTime(MATCH_SEARCH_DEBOUNCE_MS));
14
+ return merge(pagination$, newSearchDebounced$).pipe(switchMap((action) => {
15
+ const { query, pageToken } = action.payload;
16
+ const offset = pageToken != null && pageToken.length > 0 ? Number(pageToken) : 0;
17
+ const searchText = encodeURIComponent(query);
18
+ const limit = MATCH_ACCRUAL_SEARCH_LIMIT;
19
+ return zeniAPI
20
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals?status=posted&reversal_policy=on_bill_match&search_text=${searchText}&limit=${limit}&offset=${offset}`)
21
+ .pipe(mergeMap((response) => {
22
+ if (isSuccessResponse(response) && response.data != null) {
23
+ const accruals = response.data.accruals;
24
+ const append = pageToken != null;
25
+ const nextOffset = offset + accruals.length;
26
+ const total = response.data.total ?? nextOffset;
27
+ const nextPageToken = nextOffset < total ? String(nextOffset) : null;
28
+ // toJEOneTimeAccrual still defaults a missing code to USD. Stamp
29
+ // tenant currency first, same as list members.
30
+ const fallbackCurrencyCode = getTenantDisplayCurrency(state$.value)?.currencyCode ?? '';
31
+ const stampedAccruals = accruals.map((accrual) => withInheritedAccrualCurrency(accrual, fallbackCurrencyCode));
32
+ const actionsOut = [];
33
+ if (stampedAccruals.length > 0) {
34
+ actionsOut.push(updateJEOneTimeAccruals(stampedAccruals));
35
+ }
36
+ actionsOut.push(searchMatchAccrualsSuccess({
37
+ append,
38
+ nextPageToken,
39
+ results: stampedAccruals,
40
+ }));
41
+ return from(actionsOut);
42
+ }
43
+ return of(searchMatchAccrualsFailure({
44
+ error: response.status,
45
+ }));
46
+ }), catchError((error) => of(searchMatchAccrualsFailure({
47
+ error: createZeniAPIStatus('Unexpected Error', 'Search match accruals REST API call errored out' +
48
+ JSON.stringify(error)),
49
+ }))),
50
+ // On the HTTP stream only. Wrapping the outer merge would complete
51
+ // the epic on the first `clearAll` and drop later searches.
52
+ takeUntil(actions$.pipe(filter(clearAll.match))));
53
+ }));
54
+ };
@@ -0,0 +1,74 @@
1
+ import { from, merge, of } from 'rxjs';
2
+ import { catchError, debounceTime, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
+ import { getActualMatchingMatchById } from '../../../../entity/actualMatching/actualMatchingSelector';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
+ import { clearAll } from '../../../../rootActions';
6
+ import { toActualMatchingTransactionSearchResult, } from '../../payload/actualMatchingPayload';
7
+ import { openMatchNewModal, searchMatchAccruals, searchMatchTransactions, searchMatchTransactionsFailure, searchMatchTransactionsSuccess, } from '../../reducers/actualMatchingViewReducer';
8
+ import { getTenantDisplayCurrency, parseActualMatchingMatchId, } from '../../selectors/actualMatchingViewSelector';
9
+ import { MATCH_SEARCH_DEBOUNCE_MS, MATCH_TRANSACTION_SEARCH_PAGE_SIZE, } from '../../types/actualMatchingViewState';
10
+ export const searchMatchTransactionsEpic = (actions$, state$, zeniAPI) => {
11
+ const openModal$ = actions$.pipe(filter(openMatchNewModal.match), mergeMap((action) => {
12
+ const matchId = action.payload.matchId;
13
+ if (matchId == null || matchId === '') {
14
+ return from([
15
+ searchMatchTransactions(''),
16
+ searchMatchAccruals(''),
17
+ ]);
18
+ }
19
+ const match = getActualMatchingMatchById(state$.value.actualMatchingState, matchId);
20
+ const parsed = parseActualMatchingMatchId(matchId);
21
+ const transactionType = match?.transactionType != null && match.transactionType !== ''
22
+ ? match.transactionType
23
+ : parsed?.transactionType;
24
+ const transactionIntegrationId = match?.transactionIntegrationId != null &&
25
+ match.transactionIntegrationId !== ''
26
+ ? match.transactionIntegrationId
27
+ : parsed?.transactionIntegrationId;
28
+ return from([
29
+ searchMatchTransactions('', undefined, transactionType, transactionIntegrationId),
30
+ ]);
31
+ }));
32
+ const searchActions$ = actions$.pipe(filter(searchMatchTransactions.match));
33
+ const pagination$ = searchActions$.pipe(filter((action) => action.payload.pageToken != null));
34
+ const newSearchDebounced$ = searchActions$.pipe(filter((action) => action.payload.pageToken == null), debounceTime(MATCH_SEARCH_DEBOUNCE_MS));
35
+ const search$ = merge(pagination$, newSearchDebounced$).pipe(switchMap((action) => {
36
+ const { query, pageToken, transactionType, transactionIntegrationId } = action.payload;
37
+ const queryParam = {
38
+ search_text: query,
39
+ page_size: MATCH_TRANSACTION_SEARCH_PAGE_SIZE,
40
+ ...(pageToken != null ? { page_token: pageToken } : {}),
41
+ ...(transactionType != null && transactionType !== ''
42
+ ? { transaction_type: transactionType }
43
+ : {}),
44
+ ...(transactionIntegrationId != null && transactionIntegrationId !== ''
45
+ ? { transaction_integration_id: transactionIntegrationId }
46
+ : {}),
47
+ };
48
+ return zeniAPI
49
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/match-transaction-search?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
50
+ .pipe(mergeMap((response) => {
51
+ if (isSuccessResponse(response) && response.data != null) {
52
+ const { transactions, next_page_token } = response.data;
53
+ const append = pageToken != null;
54
+ const fallbackCurrency = getTenantDisplayCurrency(state$.value);
55
+ const results = transactions.map((transaction) => toActualMatchingTransactionSearchResult(transaction, fallbackCurrency));
56
+ return of(searchMatchTransactionsSuccess({
57
+ append,
58
+ nextPageToken: next_page_token ?? null,
59
+ results,
60
+ }));
61
+ }
62
+ return of(searchMatchTransactionsFailure({
63
+ error: response.status,
64
+ }));
65
+ }), catchError((error) => of(searchMatchTransactionsFailure({
66
+ error: createZeniAPIStatus('Unexpected Error', 'Search match transactions REST API call errored out' +
67
+ JSON.stringify(error)),
68
+ }))),
69
+ // On the HTTP stream only. Wrapping `merge(openModal$, search$)`
70
+ // would complete the epic on the first `clearAll` and drop later searches.
71
+ takeUntil(actions$.pipe(filter(clearAll.match))));
72
+ }));
73
+ return merge(openModal$, search$);
74
+ };
@@ -0,0 +1,76 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, exhaustMap, filter, mergeMap, takeUntil, } from 'rxjs/operators';
3
+ import { getActualMatchingMatchById } from '../../../../entity/actualMatching/actualMatchingSelector';
4
+ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
+ import { clearAll } from '../../../../rootActions';
7
+ import { toUndoReversalResult, } from '../../payload/actualMatchingPayload';
8
+ import { fetchActualMatchingMatch, undoReversalAccruals, undoReversalAccrualsFailure, undoReversalAccrualsSuccess, } from '../../reducers/actualMatchingViewReducer';
9
+ import { refreshActualMatchingList } from './refreshActualMatchingList';
10
+ function undoReversalSnackbar(results, vendorName) {
11
+ const undoneCount = results.filter((result) => result.status === 'undone').length;
12
+ const failedCount = results.length - undoneCount;
13
+ const variables = [
14
+ { variableName: '_count_', variableValue: String(undoneCount) },
15
+ { variableName: '_vendor_name_', variableValue: vendorName },
16
+ ];
17
+ if (results.length > 0 && failedCount === 0) {
18
+ return openSnackbar({
19
+ messageSection: 'actual_matching_undo_reversal',
20
+ messageText: 'success',
21
+ type: 'success',
22
+ variables,
23
+ });
24
+ }
25
+ if (undoneCount > 0 && failedCount > 0) {
26
+ return openSnackbar({
27
+ messageSection: 'actual_matching_undo_reversal',
28
+ messageText: 'notification',
29
+ type: 'warning',
30
+ variables,
31
+ });
32
+ }
33
+ return openSnackbar({
34
+ messageSection: 'actual_matching_undo_reversal',
35
+ messageText: 'failed',
36
+ type: 'error',
37
+ variables,
38
+ });
39
+ }
40
+ function matchVendorName(state, matchId) {
41
+ return (getActualMatchingMatchById(state.actualMatchingState, matchId)?.vendorName ??
42
+ '');
43
+ }
44
+ export const undoReversalAccrualsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(undoReversalAccruals.match),
45
+ // Posts QBO deletes; drop duplicate clicks while the first request is in flight.
46
+ exhaustMap((action) => {
47
+ const { accrualIds, matchId } = action.payload;
48
+ const body = {
49
+ accrual_ids: accrualIds,
50
+ };
51
+ const vendorName = matchVendorName(state$.value, matchId);
52
+ return zeniAPI
53
+ .postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/undo-reversal`, body)
54
+ .pipe(mergeMap((response) => {
55
+ if (isSuccessResponse(response) && response.data != null) {
56
+ const results = response.data.results.map(toUndoReversalResult);
57
+ const successActions = [
58
+ undoReversalAccrualsSuccess(matchId, results),
59
+ ];
60
+ const anyUndone = results.some((result) => result.status === 'undone');
61
+ if (anyUndone) {
62
+ successActions.push(...refreshActualMatchingList(state$.value), fetchActualMatchingMatch(matchId, true));
63
+ }
64
+ successActions.push(undoReversalSnackbar(results, vendorName));
65
+ return from(successActions);
66
+ }
67
+ return of(undoReversalAccrualsFailure({
68
+ matchId,
69
+ status: response.status,
70
+ }), undoReversalSnackbar([], vendorName));
71
+ }), catchError((error) => of(undoReversalAccrualsFailure({
72
+ matchId,
73
+ status: createZeniAPIStatus('Unexpected Error', 'Undo reversal accruals REST API call errored out' +
74
+ JSON.stringify(error)),
75
+ }), undoReversalSnackbar([], vendorName))), takeUntil(actions$.pipe(filter(clearAll.match))));
76
+ }));
@@ -10,7 +10,7 @@ import { fetchReconciliation } from '../../reducers/reconciliationViewReducer';
10
10
  import { fetchTransactionCategorizationView } from '../../reducers/transactionsViewReducer';
11
11
  import { getJESchedulePeriodTabKey } from '../../types/jeSchedulesViewState';
12
12
  export const fetchAllExpenseAutomationTabsEpic = (actions$, state$) => actions$.pipe(filter(fetchAllExpenseAutomationTabs.match), switchMap((action) => {
13
- const { cacheOverride, refreshViewInBackground } = action.payload;
13
+ const { cacheOverride, refreshViewInBackground, isAccrualEnabled } = action.payload;
14
14
  const state = state$.value;
15
15
  const { monthEndCloseChecksState } = state;
16
16
  const { monthEndCloseChecksByTenantId } = monthEndCloseChecksState;
@@ -35,7 +35,7 @@ export const fetchAllExpenseAutomationTabsEpic = (actions$, state$) => actions$.
35
35
  amount: 75.0,
36
36
  currencyCode: 'USD',
37
37
  currencySymbol: '$',
38
- }, cacheOverride, refreshViewInBackground), fetchFluxAnalysisView(selectedPeriod, cacheOverride, refreshViewInBackground, null), fetchJeSchedulesPage({ start: timePeriodStart, end: timePeriodEnd }, cacheOverride));
38
+ }, cacheOverride, refreshViewInBackground), fetchFluxAnalysisView(selectedPeriod, cacheOverride, refreshViewInBackground, null), fetchJeSchedulesPage({ start: timePeriodStart, end: timePeriodEnd }, cacheOverride, isAccrualEnabled));
39
39
  if (reconciliationEnabled) {
40
40
  actions.push(fetchReconciliation(selectedPeriod, undefined, cacheOverride, refreshViewInBackground));
41
41
  }
@@ -61,16 +61,29 @@ export const fetchAllExpenseAutomationTabsEpic = (actions$, state$) => actions$.
61
61
  fetchActions.push(fetchFluxAnalysisView(selectedPeriod, cacheOverride, refreshViewInBackground, null));
62
62
  }
63
63
  // The two JE tabs cache separately, so only the tab actually being shown decides whether a
64
- // fetch is still needed.
65
- const jeSchedulesPeriodTabKey = getJESchedulePeriodTabKey(state.expenseAutomationJESchedulesViewState.uiState.selectedPageTab, monthYearPeriodId);
66
- if (state.expenseAutomationJESchedulesViewState.fetchState !==
67
- 'In-Progress' &&
68
- (!state.expenseAutomationJESchedulesViewState.hasValidState() ||
69
- state.expenseAutomationJESchedulesViewState
70
- .jeScheduleTransactionKeysByPeriodAndTab[jeSchedulesPeriodTabKey] == null ||
71
- state.expenseAutomationJESchedulesViewState
72
- .failedJeScheduleTransactionKeysByPeriodAndTab[jeSchedulesPeriodTabKey] == null)) {
73
- fetchActions.push(fetchJeSchedulesPage({ start: timePeriodStart, end: timePeriodEnd }, cacheOverride));
64
+ // fetch is still needed. Actual Matching is a third tab on the same page with its own
65
+ // endpoint; fetchJeSchedulesPage carries isAccrualEnabled so the prefetch epic can warm AM.
66
+ const selectedPageTab = state.expenseAutomationJESchedulesViewState.uiState.selectedPageTab;
67
+ if (selectedPageTab === 'actual_matching') {
68
+ const actualMatchingPeriodKeys = state.expenseAutomationActualMatchingViewState.matchIdsByPeriod[monthYearPeriodId];
69
+ if (state.expenseAutomationActualMatchingViewState.fetchState !==
70
+ 'In-Progress' &&
71
+ (!state.expenseAutomationActualMatchingViewState.hasValidState() ||
72
+ actualMatchingPeriodKeys == null)) {
73
+ fetchActions.push(fetchJeSchedulesPage({ start: timePeriodStart, end: timePeriodEnd }, cacheOverride, isAccrualEnabled));
74
+ }
75
+ }
76
+ else {
77
+ const jeSchedulesPeriodTabKey = getJESchedulePeriodTabKey(selectedPageTab, monthYearPeriodId);
78
+ if (state.expenseAutomationJESchedulesViewState.fetchState !==
79
+ 'In-Progress' &&
80
+ (!state.expenseAutomationJESchedulesViewState.hasValidState() ||
81
+ state.expenseAutomationJESchedulesViewState
82
+ .jeScheduleTransactionKeysByPeriodAndTab[jeSchedulesPeriodTabKey] == null ||
83
+ state.expenseAutomationJESchedulesViewState
84
+ .failedJeScheduleTransactionKeysByPeriodAndTab[jeSchedulesPeriodTabKey] == null)) {
85
+ fetchActions.push(fetchJeSchedulesPage({ start: timePeriodStart, end: timePeriodEnd }, cacheOverride, isAccrualEnabled));
86
+ }
74
87
  }
75
88
  if (state.expenseAutomationReconciliationViewState.fetchState !==
76
89
  'In-Progress' &&
@@ -2,24 +2,34 @@ import { concat, from } from 'rxjs';
2
2
  import { filter, mergeMap } from 'rxjs/operators';
3
3
  import { fetchAccountList } from '../../../accountList/accountListReducer';
4
4
  import { getAccountsTypesForScheduleEpics } from '../../../scheduleView/scheduleListView/scheduleListHelper';
5
+ import { fetchActualMatchingPage } from '../../reducers/actualMatchingViewReducer';
5
6
  import { fetchJeSchedules, fetchJeSchedulesPage, initializeAccountSettingsView, } from '../../reducers/jeSchedulesViewReducer';
6
7
  export const fetchJeSchedulesPageEpic = (actions$, state$) => actions$.pipe(filter(fetchJeSchedulesPage.match), mergeMap((action) => {
7
8
  const jeSchedulePageActions = [];
8
9
  const { period, refreshViewInBackground } = action.payload;
10
+ const state = state$.value;
11
+ const { selectedPageTab } = state.expenseAutomationJESchedulesViewState.uiState;
9
12
  const accountType = getAccountsTypesForScheduleEpics();
10
- const accountListState = state$.value.accountListState;
13
+ const accountListState = state.accountListState;
11
14
  if (accountListState.byReportId[accountType.accountListKey].fetchState ===
12
15
  'Not-Started') {
13
16
  // call this only if the account list for schedules is not fetched already
14
17
  jeSchedulePageActions.push(fetchAccountList(accountType.accountListKey, true, false, false, false, accountType.allowedAccountList));
15
18
  }
16
- jeSchedulePageActions.push(fetchJeSchedules(period, refreshViewInBackground));
19
+ // AM is chrome on this page but a different endpoint. fetchJeSchedules with
20
+ // selectedPageTab === 'actual_matching' would hit /1.0/schedules and write a bogus cache slot.
21
+ if (selectedPageTab === 'actual_matching') {
22
+ jeSchedulePageActions.push(fetchActualMatchingPage(period, refreshViewInBackground));
23
+ }
24
+ else {
25
+ jeSchedulePageActions.push(fetchJeSchedules(period, refreshViewInBackground));
26
+ }
17
27
  // Only on the first page load. Account settings cannot change from a tab switch or a
18
28
  // refresh, and re-initializing puts accountSettingsView back to 'In-Progress' -- which the
19
29
  // page reads as "still loading" and answers with a full skeleton over rows it already has.
20
30
  // The action also resets localData, so re-dispatching it blindly would empty the settings
21
31
  // form until the refetch lands. Saving account settings still re-initializes explicitly.
22
- if (!state$.value.expenseAutomationJESchedulesViewState.accountSettings
32
+ if (!state.expenseAutomationJESchedulesViewState.accountSettings
23
33
  .hasLoadedAccountSettings) {
24
34
  jeSchedulePageActions.push(initializeAccountSettingsView());
25
35
  }
@@ -1,7 +1,8 @@
1
- import { EMPTY, of } from 'rxjs';
1
+ import { EMPTY, from } from 'rxjs';
2
2
  import { filter, mergeMap } from 'rxjs/operators';
3
3
  import { toMonthYearPeriodId } from '../../../../commonStateTypes/timePeriod';
4
4
  import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
5
+ import { fetchActualMatchingPage } from '../../reducers/actualMatchingViewReducer';
5
6
  import { fetchJeSchedules, fetchJeSchedulesPage, } from '../../reducers/jeSchedulesViewReducer';
6
7
  import { ALL_JE_PAGE_TABS, getJESchedulePeriodTabKey, } from '../../types/jeSchedulesViewState';
7
8
  /**
@@ -9,27 +10,46 @@ import { ALL_JE_PAGE_TABS, getJESchedulePeriodTabKey, } from '../../types/jeSche
9
10
  *
10
11
  * Listens for `fetchJeSchedulesPage` rather than `fetchJeSchedules` on purpose: the prefetch emits
11
12
  * `fetchJeSchedules`, so listening for that would make this epic feed itself.
13
+ *
14
+ * Review <-> Existing share `/1.0/schedules`. Actual Matching is a different endpoint; it is
15
+ * warmed with `fetchActualMatchingPage` when the page action carries `isAccrualEnabled`.
12
16
  */
13
17
  export const prefetchOtherJeSchedulesTabEpic = (actions$, state$) => actions$.pipe(filter(fetchJeSchedulesPage.match), mergeMap((action) => {
14
18
  const state = state$.value;
15
- const { period } = action.payload;
19
+ const { period, isAccrualEnabled } = action.payload;
16
20
  const { selectedPageTab } = state.expenseAutomationJESchedulesViewState.uiState;
17
- const otherPageTab = ALL_JE_PAGE_TABS.find((pageTab) => pageTab !== selectedPageTab);
18
- if (otherPageTab == null) {
19
- return EMPTY;
20
- }
21
+ const actions = [];
22
+ // AM is chrome-only on ALL_JE_PAGE_TABS; warming it with fetchJeSchedules
23
+ // would hit the wrong endpoint.
24
+ const otherPageTab = ALL_JE_PAGE_TABS.find((pageTab) => pageTab !== selectedPageTab && pageTab !== 'actual_matching');
21
25
  const currentTenant = getCurrentTenant(state);
22
26
  const selectedPeriod = state.expenseAutomationViewState.selectedPeriodByTenantId[currentTenant.tenantId];
23
- if (selectedPeriod == null) {
24
- return EMPTY;
27
+ if (otherPageTab != null && selectedPeriod != null) {
28
+ const otherPeriodTabKey = getJESchedulePeriodTabKey(otherPageTab, toMonthYearPeriodId(selectedPeriod));
29
+ const isOtherTabAlreadyLoaded = state.expenseAutomationJESchedulesViewState
30
+ .jeScheduleTransactionKeysByPeriodAndTab[otherPeriodTabKey] != null;
31
+ if (!isOtherTabAlreadyLoaded) {
32
+ actions.push(fetchJeSchedules(period, true, otherPageTab));
33
+ }
34
+ }
35
+ // GET /actual-matching is not Statsig-gated; CES has no Statsig. The screen
36
+ // passes is_accrual_enabled so disabled tenants do not pay for a list they
37
+ // cannot open. Visible AM is fetchJeSchedulesPageEpic's job.
38
+ if (isAccrualEnabled === true &&
39
+ selectedPageTab !== 'actual_matching') {
40
+ const periodId = toMonthYearPeriodId({
41
+ month: period.start.month,
42
+ year: period.start.year,
43
+ });
44
+ // `fetchState` is global across months. Skipping on In-Progress would drop
45
+ // October prefetch while September is still loading; September's success
46
+ // then marks the slice Completed and October looks loaded with no ids.
47
+ if (state.expenseAutomationActualMatchingViewState.matchIdsByPeriod[periodId] == null) {
48
+ actions.push(fetchActualMatchingPage(period, true));
49
+ }
25
50
  }
26
- // Already warm for this period: without this check every tab switch would refetch the tab
27
- // just left, since a switch also fires fetchJeSchedulesPage.
28
- const otherPeriodTabKey = getJESchedulePeriodTabKey(otherPageTab, toMonthYearPeriodId(selectedPeriod));
29
- const isOtherTabAlreadyLoaded = state.expenseAutomationJESchedulesViewState
30
- .jeScheduleTransactionKeysByPeriodAndTab[otherPeriodTabKey] != null;
31
- if (isOtherTabAlreadyLoaded) {
51
+ if (actions.length === 0) {
32
52
  return EMPTY;
33
53
  }
34
- return of(fetchJeSchedules(period, true, otherPageTab));
54
+ return from(actions);
35
55
  }));