@zeniai/client-epic-state 5.0.81 → 5.0.82-betaAK1
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.
- package/lib/commonStateTypes/amount.d.ts +2 -1
- package/lib/commonStateTypes/amount.js +5 -2
- package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -2
- package/lib/commonStateTypes/viewAndReport/viewAndReport.js +2 -0
- package/lib/commonStateTypes/workingDayHelper.d.ts +11 -0
- package/lib/commonStateTypes/workingDayHelper.js +31 -1
- package/lib/entity/account/accountSelector.d.ts +9 -0
- package/lib/entity/account/accountSelector.js +14 -1
- package/lib/entity/account/accountState.d.ts +1 -1
- package/lib/entity/accountGroup/accountGroupState.d.ts +1 -1
- package/lib/entity/class/classReducer.d.ts +4 -4
- package/lib/entity/class/classState.d.ts +1 -1
- package/lib/entity/forecast/forecastState.d.ts +1 -1
- package/lib/entity/sectionAccountsView/sectionAccountsView.d.ts +1 -1
- package/lib/entity/sectionClassesViewV2/sectionClassesView.d.ts +1 -1
- package/lib/entity/sectionClassesViewV2/sectionClassesViewReducer.d.ts +2 -2
- package/lib/entity/sectionProjectView/sectionProjectView.d.ts +1 -1
- package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +2 -2
- package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/entity/tenant/clearAllEpic.d.ts +2 -1
- package/lib/entity/tenant/clearAllEpic.js +2 -0
- package/lib/entity/transaction/payloadTypes/transactionPayload.d.ts +4 -0
- package/lib/entity/transaction/payloadTypes/transactionPayload.js +18 -10
- package/lib/entity/transaction/stateTypes/transaction.d.ts +3 -0
- package/lib/epic.d.ts +7 -1
- package/lib/epic.js +7 -1
- package/lib/esm/commonStateTypes/amount.js +3 -1
- package/lib/esm/commonStateTypes/viewAndReport/viewAndReport.js +2 -0
- package/lib/esm/commonStateTypes/workingDayHelper.js +29 -0
- package/lib/esm/entity/account/accountSelector.js +11 -0
- package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
- package/lib/esm/entity/tenant/clearAllEpic.js +2 -0
- package/lib/esm/entity/transaction/payloadTypes/transactionPayload.js +18 -10
- package/lib/esm/epic.js +7 -1
- package/lib/esm/index.js +12 -6
- package/lib/esm/reducer.js +9 -0
- package/lib/esm/view/createTransferEntry/createTransferEntryReducer.js +82 -0
- package/lib/esm/view/createTransferEntry/createTransferEntrySelector.js +64 -0
- package/lib/esm/view/createTransferEntry/createTransferEntryState.js +17 -0
- package/lib/esm/view/createTransferEntry/epics/createTransferEntryEpic.js +130 -0
- package/lib/esm/view/createTransferEntry/epics/fetchTransferAccountsEpic.js +36 -0
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +13 -1
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +9 -1
- package/lib/esm/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowPayload.js +24 -0
- package/lib/esm/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowReducer.js +55 -0
- package/lib/esm/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowSelector.js +15 -0
- package/lib/esm/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowState.js +45 -0
- package/lib/esm/view/spendManagement/cashManagement/autoSweepFlow/epics/fetchCashManagementSettingsEpic.js +28 -0
- package/lib/esm/view/spendManagement/cashManagement/autoSweepFlow/epics/saveAutoSweepSettingsEpic.js +27 -0
- package/lib/esm/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewPayload.js +37 -0
- package/lib/esm/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewReducer.js +49 -0
- package/lib/esm/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewSelector.js +179 -0
- package/lib/esm/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewState.js +5 -0
- package/lib/esm/view/spendManagement/cashManagement/cashManagementOverview/epics/fetchCashManagementBannerEpic.js +28 -0
- package/lib/esm/view/spendManagement/cashManagement/cashManagementOverview/epics/fetchCashManagementOverviewPageEpic.js +39 -0
- package/lib/esm/view/transactionDetail/transactionDetailSelector.js +6 -1
- package/lib/index.d.ts +15 -6
- package/lib/index.js +62 -33
- package/lib/reducer.d.ts +9 -0
- package/lib/reducer.js +9 -0
- package/lib/view/auditReportView/auditReportViewState.d.ts +1 -1
- package/lib/view/billPayCard/billPayCardSelector.d.ts +1 -1
- package/lib/view/cardBalance/cardBalanceSelector.d.ts +1 -1
- package/lib/view/cashBalance/cashBalanceSelector.d.ts +1 -1
- package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +1 -1
- package/lib/view/cashPosition/cashPositionSelector.d.ts +1 -1
- package/lib/view/createTransferEntry/createTransferEntryReducer.d.ts +32 -0
- package/lib/view/createTransferEntry/createTransferEntryReducer.js +86 -0
- package/lib/view/createTransferEntry/createTransferEntrySelector.d.ts +39 -0
- package/lib/view/createTransferEntry/createTransferEntrySelector.js +69 -0
- package/lib/view/createTransferEntry/createTransferEntryState.d.ts +30 -0
- package/lib/view/createTransferEntry/createTransferEntryState.js +20 -0
- package/lib/view/createTransferEntry/epics/createTransferEntryEpic.d.ts +11 -0
- package/lib/view/createTransferEntry/epics/createTransferEntryEpic.js +134 -0
- package/lib/view/createTransferEntry/epics/fetchTransferAccountsEpic.d.ts +14 -0
- package/lib/view/createTransferEntry/epics/fetchTransferAccountsEpic.js +40 -0
- package/lib/view/dashboard/dashboardReducer.d.ts +2 -2
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +3 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +14 -2
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +4 -1
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.d.ts +1 -0
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +11 -2
- package/lib/view/financeStatement/financeStatementReducer.d.ts +1 -1
- package/lib/view/globalMerchantView/globalMerchantViewReducer.d.ts +2 -2
- package/lib/view/reimbursementCard/reimbursementCardSelector.d.ts +1 -1
- package/lib/view/reportsResync/reportsResyncReducer.d.ts +2 -2
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowPayload.d.ts +52 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowPayload.js +29 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowReducer.d.ts +21 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowReducer.js +59 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowSelector.d.ts +16 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowSelector.js +19 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowState.d.ts +44 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowState.js +51 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/epics/fetchCashManagementSettingsEpic.d.ts +16 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/epics/fetchCashManagementSettingsEpic.js +32 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/epics/saveAutoSweepSettingsEpic.d.ts +14 -0
- package/lib/view/spendManagement/cashManagement/autoSweepFlow/epics/saveAutoSweepSettingsEpic.js +31 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewPayload.d.ts +32 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewPayload.js +42 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewReducer.d.ts +15 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewReducer.js +53 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewSelector.d.ts +41 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewSelector.js +184 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewState.d.ts +71 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewState.js +8 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/epics/fetchCashManagementBannerEpic.d.ts +16 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/epics/fetchCashManagementBannerEpic.js +32 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/epics/fetchCashManagementOverviewPageEpic.d.ts +10 -0
- package/lib/view/spendManagement/cashManagement/cashManagementOverview/epics/fetchCashManagementOverviewPageEpic.js +43 -0
- package/lib/view/tasksCard/tasksCardSelector.d.ts +1 -1
- package/lib/view/topEx/topExSelector.d.ts +1 -1
- package/lib/view/transactionDetail/transactionDetailPayload.d.ts +1 -1
- package/lib/view/transactionDetail/transactionDetailSelector.d.ts +6 -1
- package/lib/view/transactionDetail/transactionDetailSelector.js +6 -1
- package/package.json +34 -62
package/lib/esm/reducer.js
CHANGED
|
@@ -98,6 +98,7 @@ import companyHealthMetricView, { initialState as initialCompanyHealthMetricView
|
|
|
98
98
|
import companyMonthEndReportView, { initialState as initialCompanyMonthEndReportViewState, } from './view/companyMonthEndReportView/companyMonthEndReportViewReducer';
|
|
99
99
|
import companyTaskManagerView, { initialState as initialCompanyTaskManagerViewState, } from './view/companyTaskManagerView/companyTaskManagerViewReducer';
|
|
100
100
|
import companyView, { initialState as initialCompanyViewState, } from './view/companyView/companyViewReducer';
|
|
101
|
+
import createTransferEntry, { initialState as initialCreateTransferEntryState, } from './view/createTransferEntry/createTransferEntryReducer';
|
|
101
102
|
import creditAgentView, { initialState as initialCreditAgentViewState, } from './view/creditAgentView/creditAgentViewReducer';
|
|
102
103
|
import dashboard, { initialState as initialDashboardState, } from './view/dashboard/dashboardReducer';
|
|
103
104
|
import dashboardLayout, { initialState as initialDashboardLayoutState, } from './view/dashboardLayout/dashboardLayoutReducer';
|
|
@@ -157,6 +158,8 @@ import internationalWireVerification, { initialState as initialInternationalWire
|
|
|
157
158
|
import magicLinkView, { initialState as initialMagicLinkViewState, } from './view/spendManagement/billPay/magicLinkView/magicLinkViewReducer';
|
|
158
159
|
import previousBills, { initialState as initialPreviousBillsState, } from './view/spendManagement/billPay/previousBills/previousBillsReducer';
|
|
159
160
|
import wiseRedirect, { initialState as initialWiseRedirectState, } from './view/spendManagement/billPay/wiseRedirectView/wiseRedirectReducer';
|
|
161
|
+
import autoSweepFlow, { initialState as initialAutoSweepFlowState, } from './view/spendManagement/cashManagement/autoSweepFlow/autoSweepFlowReducer';
|
|
162
|
+
import cashManagementOverview, { initialState as initialCashManagementOverviewState, } from './view/spendManagement/cashManagement/cashManagementOverview/cashManagementOverviewReducer';
|
|
160
163
|
import cardUserOnboarding, { initialState as initialCardUserOnboardingState, } from './view/spendManagement/chargeCards/cardUserOnboarding/cardUserOnboardingReducer';
|
|
161
164
|
import cashbackDetail, { initialState as initialCashbackDetailState, } from './view/spendManagement/chargeCards/cashbackDetail/cashbackDetailReducer';
|
|
162
165
|
import chargeCardConfig, { initialState as initialChargeCardConfigState, } from './view/spendManagement/chargeCards/chargeCardConfig/chargeCardConfigReducer';
|
|
@@ -308,6 +311,7 @@ const initialViewsState = {
|
|
|
308
311
|
arAgingState: initialArAgingState,
|
|
309
312
|
auditReportViewState: initialAuditReportViewState,
|
|
310
313
|
authenticationViewState: initialAuthenticationViewState,
|
|
314
|
+
autoSweepFlowState: initialAutoSweepFlowState,
|
|
311
315
|
autotransferRulesState: initialAutotransferRulesState,
|
|
312
316
|
balanceSheetState: initialBalanceSheetState,
|
|
313
317
|
bankAccountViewState: initialBankAccountView,
|
|
@@ -326,6 +330,7 @@ const initialViewsState = {
|
|
|
326
330
|
cashBalanceState: initialCashBalanceState,
|
|
327
331
|
cashFlowState: initialCashFlowState,
|
|
328
332
|
cashInCashOutState: initialCashInCashOutState,
|
|
333
|
+
cashManagementOverviewState: initialCashManagementOverviewState,
|
|
329
334
|
cashPositionState: initialCashPositionState,
|
|
330
335
|
chargeCardConfigState: initialChargeCardConfigState,
|
|
331
336
|
chargeCardDetailState: initialChargeCardDetailState,
|
|
@@ -360,6 +365,7 @@ const initialViewsState = {
|
|
|
360
365
|
expenseAutomationReconciliationViewState: initialExpenseAutomationReconciliationViewState,
|
|
361
366
|
expenseAutomationTransactionsViewState: initialExpenseAutomationTransactionsViewState,
|
|
362
367
|
expenseAutomationViewState: initialExpenseAutomationViewState,
|
|
368
|
+
createTransferEntryState: initialCreateTransferEntryState,
|
|
363
369
|
expressPayState: initialExpressPayState,
|
|
364
370
|
featureNotificationViewState: initialFeatureNotificationViewState,
|
|
365
371
|
fileViewState: initialFileViewState,
|
|
@@ -544,6 +550,7 @@ const viewReducers = {
|
|
|
544
550
|
arAgingState: arAging,
|
|
545
551
|
auditReportViewState: auditReportView,
|
|
546
552
|
authenticationViewState: authentication,
|
|
553
|
+
autoSweepFlowState: autoSweepFlow,
|
|
547
554
|
autotransferRulesState: autotransferRules,
|
|
548
555
|
balanceSheetState: balanceSheet,
|
|
549
556
|
bankAccountViewState: bankAccountView,
|
|
@@ -561,6 +568,7 @@ const viewReducers = {
|
|
|
561
568
|
cashBalanceState: cashBalance,
|
|
562
569
|
cashFlowState: cashFlow,
|
|
563
570
|
cashInCashOutState: cashInCashOut,
|
|
571
|
+
cashManagementOverviewState: cashManagementOverview,
|
|
564
572
|
cashPositionState: cashPosition,
|
|
565
573
|
chargeCardConfigState: chargeCardConfig,
|
|
566
574
|
chargeCardDetailState: chargeCardDetail,
|
|
@@ -595,6 +603,7 @@ const viewReducers = {
|
|
|
595
603
|
expenseAutomationReconciliationViewState: expenseAutomationReconciliationView,
|
|
596
604
|
expenseAutomationTransactionsViewState: expenseAutomationTransactionsView,
|
|
597
605
|
expenseAutomationViewState: expenseAutomationView,
|
|
606
|
+
createTransferEntryState: createTransferEntry,
|
|
598
607
|
expressPayState: expressPay,
|
|
599
608
|
featureNotificationViewState: featureNotificationView,
|
|
600
609
|
fileViewState: fileView,
|
|
@@ -0,0 +1,82 @@
|
|
|
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
|
+
const slice = draft.transferAccountsByType[action.payload.kind];
|
|
10
|
+
// Idempotent: a duplicate dispatch must not briefly downgrade
|
|
11
|
+
// 'Completed' back to 'In-Progress' (would clear loaded data from a
|
|
12
|
+
// consumer's perspective) nor restart the in-flight epic stream.
|
|
13
|
+
// The hook (`useFetchTransferAccountsForFlow`) is the sole caller and
|
|
14
|
+
// already dedupes via `store.getState()`; this guard is defense in
|
|
15
|
+
// depth for any future caller.
|
|
16
|
+
if (slice.fetchState === 'In-Progress' ||
|
|
17
|
+
slice.fetchState === 'Completed') {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
slice.fetchState = 'In-Progress';
|
|
21
|
+
},
|
|
22
|
+
updateAccountsForTransferFlow(draft, action) {
|
|
23
|
+
const slice = draft.transferAccountsByType[action.payload.kind];
|
|
24
|
+
slice.fetchState = 'Completed';
|
|
25
|
+
slice.accountIds = action.payload.accountIds;
|
|
26
|
+
},
|
|
27
|
+
updateAccountsForTransferFlowFailure(draft, action) {
|
|
28
|
+
draft.transferAccountsByType[action.payload.kind].fetchState = 'Error';
|
|
29
|
+
},
|
|
30
|
+
createTransferEntry: {
|
|
31
|
+
prepare(payload) {
|
|
32
|
+
return { payload };
|
|
33
|
+
},
|
|
34
|
+
/**
|
|
35
|
+
* Defense-in-depth: clear any stale `lastTransferEntryReplacement` on a
|
|
36
|
+
* fresh dispatch so a second `createTransferEntry` never inherits the
|
|
37
|
+
* previous run's replacement signal (e.g. if the prior `timer(0)` cleanup
|
|
38
|
+
* never fired). Mirrors the host hook's `consumedReplacementRef` guard
|
|
39
|
+
* (see `useTransferEntryRouteReplacement` in zeni-web-app-ui).
|
|
40
|
+
*/
|
|
41
|
+
reducer(draft) {
|
|
42
|
+
draft.createTransferEntryStatus = 'In-Progress';
|
|
43
|
+
draft.lastTransferEntryReplacement = undefined;
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
createTransferEntrySuccess(draft) {
|
|
47
|
+
draft.createTransferEntryStatus = 'Completed';
|
|
48
|
+
},
|
|
49
|
+
createTransferEntryFailure(draft) {
|
|
50
|
+
draft.createTransferEntryStatus = 'Error';
|
|
51
|
+
},
|
|
52
|
+
resetCreateTransferEntryStatus(draft) {
|
|
53
|
+
draft.createTransferEntryStatus = 'Not-Started';
|
|
54
|
+
},
|
|
55
|
+
createTransferEntryReplacedTransaction: {
|
|
56
|
+
prepare(previousTransactionId, newTransactionId) {
|
|
57
|
+
return { payload: { newTransactionId, previousTransactionId } };
|
|
58
|
+
},
|
|
59
|
+
reducer(draft, action) {
|
|
60
|
+
draft.lastTransferEntryReplacement = action.payload;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* One-shot acknowledgement: the host has consumed the route-replacement
|
|
65
|
+
* signal and navigated to the new transaction url, so we can drop the
|
|
66
|
+
* record. Re-dispatching is idempotent.
|
|
67
|
+
*/
|
|
68
|
+
clearTransferEntryRouteReplacement(draft) {
|
|
69
|
+
draft.lastTransferEntryReplacement = undefined;
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Full slice reset. Dispatched from `clearAllEpic` on tenant switch so
|
|
73
|
+
* stale per-tenant state (account-id pools, status, replacement signal)
|
|
74
|
+
* doesn't bleed across tenants.
|
|
75
|
+
*/
|
|
76
|
+
clearCreateTransferEntry(draft) {
|
|
77
|
+
Object.assign(draft, initialState);
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
export const { fetchAccountsForTransferFlow, updateAccountsForTransferFlow, updateAccountsForTransferFlowFailure, createTransferEntry, createTransferEntrySuccess, createTransferEntryFailure, resetCreateTransferEntryStatus, createTransferEntryReplacedTransaction, clearTransferEntryRouteReplacement, clearCreateTransferEntry, } = createTransferEntrySlice.actions;
|
|
82
|
+
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,130 @@
|
|
|
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
|
+
/**
|
|
29
|
+
* Defensive guard: a BE response that returns 200 with `transaction != null`
|
|
30
|
+
* but where the body lacks a usable id/type is unsafe to seed into the store —
|
|
31
|
+
* downstream code would store under a garbage key and the host hook would try
|
|
32
|
+
* to navigate to a broken url. Treat that case as failure so the source row
|
|
33
|
+
* stays visible and the user can retry.
|
|
34
|
+
*/
|
|
35
|
+
function isWellFormedTransactionPayload(t) {
|
|
36
|
+
return (t != null &&
|
|
37
|
+
typeof t.transaction_id === 'string' &&
|
|
38
|
+
t.transaction_id !== '' &&
|
|
39
|
+
typeof t.transaction_type === 'string' &&
|
|
40
|
+
t.transaction_type !== '');
|
|
41
|
+
}
|
|
42
|
+
function buildSuccessSideEffects(sourceTransactionId) {
|
|
43
|
+
return [
|
|
44
|
+
createTransferEntrySuccess(),
|
|
45
|
+
removeTransactionFromAllTabs(sourceTransactionId),
|
|
46
|
+
successSnackbar(),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
function buildPreviousTransactionCleanupActions(previousTransactionId) {
|
|
50
|
+
return [
|
|
51
|
+
removeTransaction(getTransactionDetailKey(previousTransactionId)),
|
|
52
|
+
removeTransactionDetail(previousTransactionId),
|
|
53
|
+
clearTransferEntryRouteReplacement(),
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Emits `resetCreateTransferEntryStatus()` (and, on the replacement path, the
|
|
58
|
+
* follow-up cleanup actions) one scheduler tick after the success actions.
|
|
59
|
+
*
|
|
60
|
+
* Why the `timer(0)` (and not synchronous emission):
|
|
61
|
+
* On the replacement path the host (web-app-ui) watches
|
|
62
|
+
* `lastTransferEntryReplacement`, navigates to the new transaction url, and
|
|
63
|
+
* then sees `clearTransferEntryRouteReplacement` clear the signal. If we
|
|
64
|
+
* dispatched the cleanup actions in the same tick as
|
|
65
|
+
* `createTransferEntryReplacedTransaction`, redux subscribers would observe
|
|
66
|
+
* `lastTransferEntryReplacement` flicker on then off in a single render, and
|
|
67
|
+
* the old transaction's state (which the still-mounted detail view may be
|
|
68
|
+
* reading) would be torn down before the host has had a chance to start
|
|
69
|
+
* navigating. Deferring by one microtask gives the host exactly one render
|
|
70
|
+
* pass to react before we tear down the previous transaction.
|
|
71
|
+
*/
|
|
72
|
+
function delayedResetAndPreviousCleanup(previousTransactionId) {
|
|
73
|
+
return timer(0).pipe(mergeMap(() => {
|
|
74
|
+
const actions = [resetCreateTransferEntryStatus()];
|
|
75
|
+
if (previousTransactionId != null) {
|
|
76
|
+
actions.push(...buildPreviousTransactionCleanupActions(previousTransactionId));
|
|
77
|
+
}
|
|
78
|
+
return from(actions);
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
// ─── Success-path branches ───────────────────────────────────────────────────
|
|
82
|
+
function handleSuccessWithoutTransactionBody(transactionId) {
|
|
83
|
+
return concat(from(buildSuccessSideEffects(transactionId)), delayedResetAndPreviousCleanup(undefined));
|
|
84
|
+
}
|
|
85
|
+
function handleSuccessWithTransactionBody(transactionPayload, vendorPayload, previousTransactionId, sourceTransactionId) {
|
|
86
|
+
const newTransactionId = toTransactionID(transactionPayload);
|
|
87
|
+
const replaced = !transactionIdsAreEqual(previousTransactionId, newTransactionId);
|
|
88
|
+
const immediate = [
|
|
89
|
+
updateTransaction(getTransactionDetailKey(newTransactionId), transactionPayload),
|
|
90
|
+
initializeTransactionDetailLocalData({
|
|
91
|
+
transactionId: newTransactionId,
|
|
92
|
+
vendorId: vendorPayload?.[0]?.vendor_id,
|
|
93
|
+
}),
|
|
94
|
+
...buildSuccessSideEffects(sourceTransactionId),
|
|
95
|
+
];
|
|
96
|
+
if (replaced) {
|
|
97
|
+
immediate.push(createTransferEntryReplacedTransaction(previousTransactionId, newTransactionId));
|
|
98
|
+
}
|
|
99
|
+
return concat(from(immediate), delayedResetAndPreviousCleanup(replaced ? previousTransactionId : undefined));
|
|
100
|
+
}
|
|
101
|
+
// ─── Top-level epic ──────────────────────────────────────────────────────────
|
|
102
|
+
export const createTransferEntryEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(createTransferEntry.match), mergeMap((action) => {
|
|
103
|
+
const { transactionId, creditAccountIntegrationId, debitAccountIntegrationId, memo, transactionType, sourceTransactionType, } = action.payload;
|
|
104
|
+
const previousTransactionId = {
|
|
105
|
+
id: transactionId,
|
|
106
|
+
type: sourceTransactionType,
|
|
107
|
+
};
|
|
108
|
+
const payload = {
|
|
109
|
+
transaction_type: transactionType,
|
|
110
|
+
transaction_id: transactionId,
|
|
111
|
+
to_account_integration_id: creditAccountIntegrationId,
|
|
112
|
+
from_account_integration_id: debitAccountIntegrationId,
|
|
113
|
+
memo: memo ?? '',
|
|
114
|
+
};
|
|
115
|
+
return zeniAPI
|
|
116
|
+
.postAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/transactions/transaction-type-update`, payload)
|
|
117
|
+
.pipe(mergeMap((response) => {
|
|
118
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
119
|
+
const transaction = response.data?.transaction;
|
|
120
|
+
if (transaction != null) {
|
|
121
|
+
if (!isWellFormedTransactionPayload(transaction)) {
|
|
122
|
+
return from(buildFailureActions());
|
|
123
|
+
}
|
|
124
|
+
return handleSuccessWithTransactionBody(transaction, response.data?.vendors, previousTransactionId, transactionId);
|
|
125
|
+
}
|
|
126
|
+
return handleSuccessWithoutTransactionBody(transactionId);
|
|
127
|
+
}
|
|
128
|
+
return from(buildFailureActions());
|
|
129
|
+
}), catchError(() => from(buildFailureActions())));
|
|
130
|
+
}));
|
|
@@ -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 = `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/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
|
+
}))));
|
|
@@ -5,6 +5,7 @@ import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
|
5
5
|
import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
|
|
6
6
|
import { filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, shouldAutoSelectAndAdd, 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',
|
|
@@ -1040,7 +1041,18 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
1040
1041
|
};
|
|
1041
1042
|
},
|
|
1042
1043
|
},
|
|
1044
|
+
removeTransactionFromAllTabs: {
|
|
1045
|
+
prepare(transactionId) {
|
|
1046
|
+
return { payload: { transactionId } };
|
|
1047
|
+
},
|
|
1048
|
+
reducer(draft, action) {
|
|
1049
|
+
const { transactionId } = action.payload;
|
|
1050
|
+
for (const tab of TRANSACTIONS_TABS) {
|
|
1051
|
+
removeTransactionFromTabView(draft.transactionCategorizationView[tab], transactionId);
|
|
1052
|
+
}
|
|
1053
|
+
},
|
|
1054
|
+
},
|
|
1043
1055
|
},
|
|
1044
1056
|
});
|
|
1045
|
-
export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, updateTransactionFilters, 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;
|
|
1057
|
+
export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, updateTransactionFilters, 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, removeTransactionFromAllTabs, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
|
|
1046
1058
|
export default expenseAutomationTransactionsView.reducer;
|
|
@@ -8,6 +8,7 @@ import { getSupportedTransactionsByIds } from '../../../entity/transaction/trans
|
|
|
8
8
|
import { applyTransactionFilters, } from '../transactionFilterHelpers';
|
|
9
9
|
import { getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
|
|
10
10
|
import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
|
|
11
|
+
import { getResolvedTransferAccounts } from '../../createTransferEntry/createTransferEntrySelector';
|
|
11
12
|
import { getProjectList } from '../../projectList/projectListSelector';
|
|
12
13
|
import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
|
|
13
14
|
// Walks the tab's transactionReviewLocalDataById map and returns
|
|
@@ -81,8 +82,11 @@ export const toTransactionView = (transaction, transactionLocalData) => {
|
|
|
81
82
|
transactionLocalData,
|
|
82
83
|
};
|
|
83
84
|
};
|
|
85
|
+
// Re-exported here for back-compat. New code should import from
|
|
86
|
+
// `view/createTransferEntry/createTransferEntrySelector`.
|
|
87
|
+
export { getLastTransferEntryReplacement } from '../../createTransferEntry/createTransferEntrySelector';
|
|
84
88
|
export function getExpenseAutomationTransactionView(state) {
|
|
85
|
-
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
89
|
+
const { accountState, classState, classListState, accountListState, createTransferEntryState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
86
90
|
const { selectedTransactionCategorizationCompletedSubTab, selectedTransactionCategorizationTab, } = expenseAutomationTransactionsViewState;
|
|
87
91
|
const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, filters, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
|
|
88
92
|
const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
|
|
@@ -214,6 +218,7 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
214
218
|
: [];
|
|
215
219
|
const transactionCompletionStatus = allSteps.find((step) => step.step === 'transaction_categorization')?.completionStatus;
|
|
216
220
|
const completionStatus = transactionCompletionStatus ?? 'incomplete';
|
|
221
|
+
const { transferAccountsList, creditCardTransferAccountsList } = getResolvedTransferAccounts(state);
|
|
217
222
|
const inFlightCategoryClassRecommendationsByTransactionId = getCategorizationInFlightRecommendationsByTransactionId(state);
|
|
218
223
|
const hasInFlightCategoryClassRecommendations = getCategorizationHasInFlightRecommendations(state);
|
|
219
224
|
return {
|
|
@@ -238,6 +243,7 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
238
243
|
refreshStatus,
|
|
239
244
|
saveStatus,
|
|
240
245
|
markAsNotMiscategorizedStatus,
|
|
246
|
+
createTransferEntryStatus: createTransferEntryState.createTransferEntryStatus,
|
|
241
247
|
completionStatus,
|
|
242
248
|
totalCountByTab,
|
|
243
249
|
parentTabTotalCountByTab,
|
|
@@ -245,6 +251,8 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
245
251
|
uploadReceiptStatusById,
|
|
246
252
|
selectedTransactionId,
|
|
247
253
|
selectedTransactionLineId,
|
|
254
|
+
creditCardTransferAccountsList,
|
|
255
|
+
transferAccounts: transferAccountsList,
|
|
248
256
|
selectedTransactionCategorizationCompletedSubTab,
|
|
249
257
|
};
|
|
250
258
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { toAmount } from '../../../../commonStateTypes/amount';
|
|
2
|
+
import { getNextTransferDate } from '../../../../commonStateTypes/workingDayHelper';
|
|
3
|
+
import { toAutoSweepFrequency, } from './autoSweepFlowState';
|
|
4
|
+
export const toSaveAutoSweepSettingsBody = (state) => ({
|
|
5
|
+
first_transfer_date: getNextTransferDate(state.frequency).format('YYYY-MM-DD'),
|
|
6
|
+
frequency: state.frequency,
|
|
7
|
+
memo: state.memo,
|
|
8
|
+
minimum_buffer_usd: state.bufferAmount.amount,
|
|
9
|
+
});
|
|
10
|
+
/**
|
|
11
|
+
* Map a `GET /cash-management/settings` payload into the slice's writable
|
|
12
|
+
* fields. Defensive on the frequency (string-from-the-wire → union via
|
|
13
|
+
* `toAutoSweepFrequency`, falling back to `'weekly'` if the server returns
|
|
14
|
+
* something we don't model yet).
|
|
15
|
+
*/
|
|
16
|
+
export const toAutoSweepFlowStatePatch = (payload) => {
|
|
17
|
+
const currencyCode = payload.source_bank_account.balances?.currency_code;
|
|
18
|
+
const currencySymbol = payload.source_bank_account.balances?.currency_symbol;
|
|
19
|
+
return {
|
|
20
|
+
bufferAmount: toAmount(payload.minimum_buffer_usd, currencyCode ?? 'USD', currencySymbol ?? '$'),
|
|
21
|
+
frequency: toAutoSweepFrequency(payload.frequency) ?? 'weekly',
|
|
22
|
+
settingsId: payload.cash_management_settings_id ?? undefined,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
+
import { toAutoSweepFlowStatePatch, } from './autoSweepFlowPayload';
|
|
3
|
+
import { initialAutoSweepFlowState, } from './autoSweepFlowState';
|
|
4
|
+
export const initialState = initialAutoSweepFlowState;
|
|
5
|
+
const autoSweepFlow = createSlice({
|
|
6
|
+
name: 'autoSweepFlow',
|
|
7
|
+
initialState,
|
|
8
|
+
reducers: {
|
|
9
|
+
/**
|
|
10
|
+
* Kicks off `GET /cash-management/settings`. Flips the top-level
|
|
11
|
+
* fetchState to `In-Progress`; the fetch epic picks this up.
|
|
12
|
+
*/
|
|
13
|
+
fetchCashManagementSettings(draft) {
|
|
14
|
+
draft.fetchState = 'In-Progress';
|
|
15
|
+
draft.error = undefined;
|
|
16
|
+
},
|
|
17
|
+
/**
|
|
18
|
+
* Hydrates the form fields with the server-side settings (and stashes
|
|
19
|
+
* the settings id for subsequent saves). Marks the GET as `Completed`.
|
|
20
|
+
*/
|
|
21
|
+
updateCashManagementSettings(draft, action) {
|
|
22
|
+
const patch = toAutoSweepFlowStatePatch(action.payload.data);
|
|
23
|
+
draft.bufferAmount = patch.bufferAmount;
|
|
24
|
+
draft.frequency = patch.frequency;
|
|
25
|
+
draft.settingsId = patch.settingsId;
|
|
26
|
+
draft.fetchState = 'Completed';
|
|
27
|
+
draft.error = undefined;
|
|
28
|
+
},
|
|
29
|
+
updateCashManagementSettingsFetchStatus(draft, action) {
|
|
30
|
+
draft.fetchState = action.payload.fetchState;
|
|
31
|
+
draft.error = action.payload.error;
|
|
32
|
+
},
|
|
33
|
+
/**
|
|
34
|
+
* Persists the form values into state and flips `saveStatus` to
|
|
35
|
+
* `In-Progress`; the save epic picks this up and PUTs the settings.
|
|
36
|
+
*/
|
|
37
|
+
saveAutoSweepSettings(draft, action) {
|
|
38
|
+
draft.bufferAmount = action.payload.bufferAmount;
|
|
39
|
+
draft.frequency = action.payload.frequency;
|
|
40
|
+
draft.memo = action.payload.memo;
|
|
41
|
+
draft.saveStatus = { fetchState: 'In-Progress', error: undefined };
|
|
42
|
+
},
|
|
43
|
+
updateAutoSweepSettingsFetchStatus(draft, action) {
|
|
44
|
+
draft.saveStatus = {
|
|
45
|
+
fetchState: action.payload.fetchState,
|
|
46
|
+
error: action.payload.error,
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
clearAutoSweepFlow() {
|
|
50
|
+
return initialAutoSweepFlowState;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
export const { fetchCashManagementSettings, updateCashManagementSettings, updateCashManagementSettingsFetchStatus, saveAutoSweepSettings, updateAutoSweepSettingsFetchStatus, clearAutoSweepFlow, } = autoSweepFlow.actions;
|
|
55
|
+
export default autoSweepFlow.reducer;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { bufferAmountToRisk, } from './autoSweepFlowState';
|
|
2
|
+
export const getAutoSweepFlow = (state) => {
|
|
3
|
+
const { bufferAmount, frequency, memo, saveStatus, settingsId, fetchState, error, } = state.autoSweepFlowState;
|
|
4
|
+
return {
|
|
5
|
+
fetchState,
|
|
6
|
+
error,
|
|
7
|
+
bufferAmount,
|
|
8
|
+
frequency,
|
|
9
|
+
memo,
|
|
10
|
+
risk: bufferAmountToRisk(bufferAmount.amount),
|
|
11
|
+
saveStatus,
|
|
12
|
+
settingsId,
|
|
13
|
+
version: 0,
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { toAmount } from '../../../../commonStateTypes/amount';
|
|
2
|
+
import { stringToUnion } from '../../../../commonStateTypes/stringToUnion';
|
|
3
|
+
/**
|
|
4
|
+
* Cadence at which the auto-sweep transfer fires. Lowercase to match the
|
|
5
|
+
* cash-management settings API contract (and the `SweepFrequency` literal
|
|
6
|
+
* already used by `<AutoSweepSetupPage>` in web-components).
|
|
7
|
+
*/
|
|
8
|
+
export const ALL_AUTO_SWEEP_FREQUENCIES = [
|
|
9
|
+
'daily',
|
|
10
|
+
'weekly',
|
|
11
|
+
'biweekly',
|
|
12
|
+
'monthly',
|
|
13
|
+
];
|
|
14
|
+
export const toAutoSweepFrequency = (v) => stringToUnion(v ?? '', ALL_AUTO_SWEEP_FREQUENCIES);
|
|
15
|
+
/**
|
|
16
|
+
* Risk presets the auto-sweep setup form exposes. Each level maps to a
|
|
17
|
+
* canonical minimum-buffer amount (USD). Selecting a level in the UI is
|
|
18
|
+
* shorthand for choosing that buffer.
|
|
19
|
+
*/
|
|
20
|
+
export const ALL_AUTO_SWEEP_RISK_LEVELS = ['low', 'moderate', 'high'];
|
|
21
|
+
export const toAutoSweepRiskLevel = (v) => stringToUnion(v ?? '', ALL_AUTO_SWEEP_RISK_LEVELS);
|
|
22
|
+
export const RISK_BUFFER_AMOUNT = {
|
|
23
|
+
low: 15000,
|
|
24
|
+
moderate: 10000,
|
|
25
|
+
high: 5000,
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Maps a buffer amount back onto the closest preset risk level. Falls back
|
|
29
|
+
* to `'moderate'` when the amount doesn't match a known preset (e.g. the
|
|
30
|
+
* server returned a custom value). The dropdown in `<AutoSweepSetupPage>`
|
|
31
|
+
* uses this so it can pre-select the user's existing band on edit.
|
|
32
|
+
*/
|
|
33
|
+
export const bufferAmountToRisk = (amount) => {
|
|
34
|
+
const match = Object.entries(RISK_BUFFER_AMOUNT).find(([, presetAmount]) => presetAmount === amount);
|
|
35
|
+
return match != null ? match[0] : 'moderate';
|
|
36
|
+
};
|
|
37
|
+
export const initialAutoSweepFlowState = {
|
|
38
|
+
bufferAmount: toAmount(10000, 'USD'),
|
|
39
|
+
frequency: 'weekly',
|
|
40
|
+
memo: '',
|
|
41
|
+
saveStatus: { fetchState: 'Not-Started', error: undefined },
|
|
42
|
+
settingsId: undefined,
|
|
43
|
+
fetchState: 'Not-Started',
|
|
44
|
+
error: undefined,
|
|
45
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
|
|
3
|
+
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../../responsePayload';
|
|
4
|
+
import { fetchCashManagementSettings, updateCashManagementSettings, updateCashManagementSettingsFetchStatus, } from '../autoSweepFlowReducer';
|
|
5
|
+
/**
|
|
6
|
+
* Hits `GET /cash-management/settings` and hydrates the auto-sweep slice
|
|
7
|
+
* with the server's existing settings (frequency, minimum buffer, settings
|
|
8
|
+
* id). Called from the cash management overview page-fetch epic alongside
|
|
9
|
+
* the other top-level fetches.
|
|
10
|
+
*
|
|
11
|
+
* TODO: switch the endpoint to a base URL constant once the prod endpoint
|
|
12
|
+
* lands; right now this hits the dev environment directly.
|
|
13
|
+
*/
|
|
14
|
+
export const fetchCashManagementSettingsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchCashManagementSettings.match), switchMap(() => zeniAPI
|
|
15
|
+
.getJSON(`https://dev.api.zeni.ai/cash-management-agent/1.0/cash-management/settings`)
|
|
16
|
+
.pipe(mergeMap((response) => {
|
|
17
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
18
|
+
return of(updateCashManagementSettings({ data: response.data }));
|
|
19
|
+
}
|
|
20
|
+
return of(updateCashManagementSettingsFetchStatus({
|
|
21
|
+
fetchState: 'Error',
|
|
22
|
+
error: response.status,
|
|
23
|
+
}));
|
|
24
|
+
}), catchError((error) => of(updateCashManagementSettingsFetchStatus({
|
|
25
|
+
fetchState: 'Error',
|
|
26
|
+
error: createZeniAPIStatus('Unexpected Error', 'Fetch cash management settings REST API call errored out' +
|
|
27
|
+
JSON.stringify(error)),
|
|
28
|
+
}))))));
|