@zeniai/client-epic-state 5.2.70 → 5.2.71
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/entity/actualMatching/actualMatchingReducer.d.ts +3 -1
- package/lib/entity/actualMatching/actualMatchingReducer.js +11 -2
- package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +2 -0
- package/lib/entity/jeSchedules/jeSchedulesPayload.js +2 -0
- package/lib/entity/jeSchedules/jeSchedulesState.d.ts +2 -0
- package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
- package/lib/entity/snackbar/snackbarTypes.js +2 -0
- package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +2 -10
- package/lib/epic.d.ts +2 -1
- package/lib/epic.js +2 -0
- package/lib/esm/entity/actualMatching/actualMatchingReducer.js +9 -1
- package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +2 -0
- package/lib/esm/entity/snackbar/snackbarTypes.js +2 -0
- package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +2 -10
- package/lib/esm/epic.js +2 -0
- package/lib/esm/index.js +5 -5
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +4 -2
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +50 -0
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +26 -13
- package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +2 -23
- package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +1 -2
- package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +89 -38
- package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +68 -62
- package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +15 -0
- package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +18 -9
- package/lib/esm/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.js +11 -2
- package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +4 -30
- package/lib/index.d.ts +6 -6
- package/lib/index.js +57 -49
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.d.ts +2 -2
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +3 -1
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.d.ts +10 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +54 -0
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +24 -11
- package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +1 -22
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +1 -2
- package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +12 -1
- package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +19 -8
- package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +90 -39
- package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +24 -10
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +7 -4
- package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +71 -62
- package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +19 -4
- package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +18 -1
- package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +12 -3
- package/lib/view/invoicing/settingsView/settingsViewSelector.js +17 -8
- package/lib/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.js +11 -2
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +1 -1
- package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +4 -30
- package/package.json +1 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { from, of } from 'rxjs';
|
|
2
|
-
import { catchError, exhaustMap, filter, mergeMap, takeUntil } from 'rxjs/operators';
|
|
2
|
+
import { catchError, exhaustMap, filter, groupBy, mergeMap, takeUntil, } from 'rxjs/operators';
|
|
3
3
|
import { getActualMatchingMatchById } from '../../../../../entity/actualMatching/actualMatchingSelector';
|
|
4
4
|
import { openSnackbar } from '../../../../../entity/snackbar/snackbarReducer';
|
|
5
|
-
import { createZeniAPIStatus, isSuccessResponse } from '../../../../../responsePayload';
|
|
5
|
+
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../../responsePayload';
|
|
6
6
|
import { clearAll } from '../../../../../rootActions';
|
|
7
7
|
import { toMatchAndReverseResult, } from '../../../payload/actualMatchingPayload';
|
|
8
8
|
import { clearMatchNewModal, fetchActualMatchingMatch, matchAndReverseAccruals, matchAndReverseAccrualsFailure, matchAndReverseAccrualsSuccess, } from '../../../reducers/actualMatchingViewReducer';
|
|
9
9
|
import { isMatchAndReverseCompleteStatus, } from '../../../types/actualMatchingViewState';
|
|
10
10
|
import { refreshActualMatchingList } from './refreshActualMatchingList';
|
|
11
11
|
function isMatchFailedStatus(status) {
|
|
12
|
-
return
|
|
12
|
+
return status === 'conflict' || status === 'claim_lost' || status === 'error';
|
|
13
13
|
}
|
|
14
14
|
function matchAndReverseSnackbar(results, vendorName) {
|
|
15
15
|
const reversedCount = results.filter((result) => result.status === 'reversed').length;
|
|
@@ -56,12 +56,15 @@ function matchAndReverseHttpFailSnackbar(vendorName) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
function matchVendorName(state, matchId) {
|
|
59
|
-
return getActualMatchingMatchById(state.actualMatchingState, matchId)
|
|
59
|
+
return (getActualMatchingMatchById(state.actualMatchingState, matchId)
|
|
60
|
+
?.vendorName ?? '');
|
|
60
61
|
}
|
|
61
62
|
export const matchAndReverseAccrualsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(matchAndReverseAccruals.match),
|
|
62
|
-
//
|
|
63
|
-
exhaustMap
|
|
64
|
-
|
|
63
|
+
// Per-match exhaust: a second click on the same match drops while HTTP is in
|
|
64
|
+
// flight. A different match must still run — a global exhaustMap left the
|
|
65
|
+
// other match's reducer In-Progress with no request.
|
|
66
|
+
groupBy((action) => `${action.payload.transactionType.toLowerCase()}:${action.payload.transactionIntegrationId}`), mergeMap((perMatch$) => perMatch$.pipe(exhaustMap((action) => {
|
|
67
|
+
const { transactionId, transactionIntegrationId, transactionType, lineIds, accrualIds, transactionSyncToken, forMatchModal, } = action.payload;
|
|
65
68
|
const body = {
|
|
66
69
|
transaction_id: transactionId,
|
|
67
70
|
transaction_integration_id: transactionIntegrationId,
|
|
@@ -80,29 +83,39 @@ exhaustMap((action) => {
|
|
|
80
83
|
if (isSuccessResponse(response) && response.data != null) {
|
|
81
84
|
const results = response.data.results.map(toMatchAndReverseResult);
|
|
82
85
|
const successActions = [
|
|
83
|
-
matchAndReverseAccrualsSuccess({
|
|
86
|
+
matchAndReverseAccrualsSuccess({
|
|
87
|
+
matchId,
|
|
88
|
+
results,
|
|
89
|
+
...(forMatchModal === true ? { forMatchModal: true } : {}),
|
|
90
|
+
}),
|
|
84
91
|
];
|
|
85
92
|
const occupancyChanged = results.some((result) => isMatchAndReverseCompleteStatus(result.status));
|
|
86
93
|
const allComplete = results.length > 0 &&
|
|
87
94
|
results.every((result) => isMatchAndReverseCompleteStatus(result.status));
|
|
88
95
|
if (occupancyChanged) {
|
|
89
96
|
// List + detail: list can omit leftovers; detail GET always
|
|
90
|
-
// writes intended occupancy so
|
|
91
|
-
//
|
|
97
|
+
// writes intended occupancy so leftover retry can join C
|
|
98
|
+
// onto the occupied bill instead of a stale cached row.
|
|
92
99
|
successActions.push(...refreshActualMatchingList(state$.value), fetchActualMatchingMatch(matchId, true));
|
|
93
100
|
}
|
|
94
101
|
const snackbar = matchAndReverseSnackbar(results, vendorName);
|
|
95
102
|
if (snackbar != null) {
|
|
96
103
|
successActions.push(snackbar);
|
|
97
104
|
}
|
|
98
|
-
if (allComplete) {
|
|
105
|
+
if (allComplete && forMatchModal === true) {
|
|
99
106
|
successActions.push(clearMatchNewModal());
|
|
100
107
|
}
|
|
101
108
|
return from(successActions);
|
|
102
109
|
}
|
|
103
|
-
return of(matchAndReverseAccrualsFailure({
|
|
110
|
+
return of(matchAndReverseAccrualsFailure({
|
|
111
|
+
status: response.status,
|
|
112
|
+
matchId,
|
|
113
|
+
...(forMatchModal === true ? { forMatchModal: true } : {}),
|
|
114
|
+
}), matchAndReverseHttpFailSnackbar(vendorName));
|
|
104
115
|
}), catchError((error) => of(matchAndReverseAccrualsFailure({
|
|
116
|
+
matchId,
|
|
117
|
+
...(forMatchModal === true ? { forMatchModal: true } : {}),
|
|
105
118
|
status: createZeniAPIStatus('Unexpected Error', 'Match and reverse accruals REST API call errored out' +
|
|
106
119
|
JSON.stringify(error)),
|
|
107
120
|
}), matchAndReverseHttpFailSnackbar(vendorName))), takeUntil(actions$.pipe(filter(clearAll.match))));
|
|
108
|
-
}));
|
|
121
|
+
}))));
|
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
import { from, merge, of } from 'rxjs';
|
|
2
2
|
import { catchError, debounceTime, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
|
|
3
|
-
import { getActualMatchingMatchById } from '../../../../../entity/actualMatching/actualMatchingSelector';
|
|
4
3
|
import { createZeniAPIStatus, isSuccessResponse, } from '../../../../../responsePayload';
|
|
5
4
|
import { clearAll } from '../../../../../rootActions';
|
|
6
5
|
import { toActualMatchingTransactionSearchResult, } from '../../../payload/actualMatchingPayload';
|
|
7
6
|
import { openMatchNewModal, searchMatchAccruals, searchMatchTransactions, searchMatchTransactionsFailure, searchMatchTransactionsSuccess, } from '../../../reducers/actualMatchingViewReducer';
|
|
8
|
-
import { getTenantDisplayCurrency
|
|
7
|
+
import { getTenantDisplayCurrency } from '../../../selectors/actualMatchingViewSelector';
|
|
9
8
|
import { MATCH_SEARCH_DEBOUNCE_MS, MATCH_TRANSACTION_SEARCH_PAGE_SIZE, } from '../../../types/actualMatchingViewState';
|
|
10
9
|
export const searchMatchTransactionsEpic = (actions$, state$, zeniAPI) => {
|
|
11
|
-
const openModal$ = actions$.pipe(filter(openMatchNewModal.match), mergeMap((
|
|
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
|
-
}));
|
|
10
|
+
const openModal$ = actions$.pipe(filter(openMatchNewModal.match), mergeMap(() => from([searchMatchTransactions(''), searchMatchAccruals('')])));
|
|
32
11
|
const searchActions$ = actions$.pipe(filter(searchMatchTransactions.match));
|
|
33
12
|
const pagination$ = searchActions$.pipe(filter((action) => action.payload.pageToken != null));
|
|
34
13
|
const newSearchDebounced$ = searchActions$.pipe(filter((action) => action.payload.pageToken == null), debounceTime(MATCH_SEARCH_DEBOUNCE_MS));
|
package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js
CHANGED
|
@@ -304,8 +304,7 @@ const buildLineItemById = (transaction, recommendations) => {
|
|
|
304
304
|
isClassMiscategorized: false,
|
|
305
305
|
};
|
|
306
306
|
}
|
|
307
|
-
if (line.type === 'transaction_info_line'
|
|
308
|
-
line.type === 'transaction_line_base') {
|
|
307
|
+
if (line.type === 'transaction_info_line') {
|
|
309
308
|
const infoRecord = line;
|
|
310
309
|
lineItemData[line.id] = {
|
|
311
310
|
...infoRecord,
|
|
@@ -22,9 +22,6 @@ export const initialAccrualSearchState = {
|
|
|
22
22
|
};
|
|
23
23
|
export const initialMatchModalState = {
|
|
24
24
|
isOpen: false,
|
|
25
|
-
openedFromMatchId: undefined,
|
|
26
|
-
didPinPrefillTransactions: false,
|
|
27
|
-
pinnedTransactionResults: [],
|
|
28
25
|
transactionSearch: initialTransactionSearchState,
|
|
29
26
|
accrualSearch: initialAccrualSearchState,
|
|
30
27
|
confirmStatus: { fetchState: 'Not-Started', error: undefined },
|
|
@@ -44,16 +41,28 @@ export const initialState = {
|
|
|
44
41
|
sortOrder: 'descending',
|
|
45
42
|
searchString: '',
|
|
46
43
|
scrollPosition: { scrollTop: 0, scrollLeft: undefined },
|
|
44
|
+
statusFilter: 'all',
|
|
47
45
|
},
|
|
48
46
|
matchModal: initialMatchModalState,
|
|
49
47
|
undoReversalStatusByMatchId: {},
|
|
50
|
-
|
|
48
|
+
ignoreStatusByMatchId: {},
|
|
49
|
+
confirmStatusByMatchId: {},
|
|
50
|
+
closeMatchAfterUndoMatchIds: [],
|
|
51
|
+
closeMatchAfterIgnoreMatchIds: [],
|
|
51
52
|
lastMatchResultsByMatchId: {},
|
|
52
53
|
lastUndoResultsByMatchId: {},
|
|
53
54
|
hasValidState() {
|
|
54
55
|
return this.fetchState == 'Completed';
|
|
55
56
|
},
|
|
56
57
|
};
|
|
58
|
+
function enqueueCloseMatchId(ids, matchId) {
|
|
59
|
+
if (!ids.includes(matchId)) {
|
|
60
|
+
ids.push(matchId);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function dropCloseMatchId(ids, matchId) {
|
|
64
|
+
return ids.filter((id) => id !== matchId);
|
|
65
|
+
}
|
|
57
66
|
function pruneLastMatchResultsForUndone(draft, matchId, results) {
|
|
58
67
|
const undoneIds = new Set(results
|
|
59
68
|
.filter((result) => result.status === 'undone')
|
|
@@ -96,9 +105,6 @@ function resetMatchModalSearches(draft) {
|
|
|
96
105
|
error: undefined,
|
|
97
106
|
};
|
|
98
107
|
draft.matchModal.lastBatchResults = undefined;
|
|
99
|
-
draft.matchModal.openedFromMatchId = undefined;
|
|
100
|
-
draft.matchModal.didPinPrefillTransactions = false;
|
|
101
|
-
draft.matchModal.pinnedTransactionResults = [];
|
|
102
108
|
}
|
|
103
109
|
const expenseAutomationActualMatchingView = createSlice({
|
|
104
110
|
name: 'expenseAutomationActualMatchingView',
|
|
@@ -239,21 +245,13 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
239
245
|
if (action.payload.scrollPosition != null) {
|
|
240
246
|
draft.uiState.scrollPosition = action.payload.scrollPosition;
|
|
241
247
|
}
|
|
248
|
+
if (action.payload.statusFilter != null) {
|
|
249
|
+
draft.uiState.statusFilter = action.payload.statusFilter;
|
|
250
|
+
}
|
|
242
251
|
},
|
|
243
|
-
openMatchNewModal
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
reducer(draft, action) {
|
|
248
|
-
resetMatchModalSearches(draft);
|
|
249
|
-
draft.matchModal.isOpen = true;
|
|
250
|
-
const matchId = action.payload.matchId;
|
|
251
|
-
draft.matchModal.openedFromMatchId =
|
|
252
|
-
matchId != null && matchId !== '' ? matchId : undefined;
|
|
253
|
-
if (draft.matchModal.openedFromMatchId != null) {
|
|
254
|
-
draft.matchModal.accrualSearch.fetchState = 'Completed';
|
|
255
|
-
}
|
|
256
|
-
},
|
|
252
|
+
openMatchNewModal(draft) {
|
|
253
|
+
resetMatchModalSearches(draft);
|
|
254
|
+
draft.matchModal.isOpen = true;
|
|
257
255
|
},
|
|
258
256
|
// Dismiss (Cancel / overlay). Store reset matches `clearMatchNewModal`;
|
|
259
257
|
// the actions stay distinct so confirm-success vs user-dismiss stay observable.
|
|
@@ -308,11 +306,6 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
308
306
|
}
|
|
309
307
|
else {
|
|
310
308
|
draft.matchModal.transactionSearch.results = results;
|
|
311
|
-
if (draft.matchModal.openedFromMatchId != null &&
|
|
312
|
-
!draft.matchModal.didPinPrefillTransactions) {
|
|
313
|
-
draft.matchModal.didPinPrefillTransactions = true;
|
|
314
|
-
draft.matchModal.pinnedTransactionResults = results;
|
|
315
|
-
}
|
|
316
309
|
}
|
|
317
310
|
draft.matchModal.transactionSearch.nextPageToken = nextPageToken;
|
|
318
311
|
draft.matchModal.transactionSearch.isLoadingMore = false;
|
|
@@ -365,21 +358,27 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
365
358
|
draft.matchModal.accrualSearch.error = action.payload.error;
|
|
366
359
|
},
|
|
367
360
|
matchAndReverseAccruals: {
|
|
368
|
-
reducer(draft) {
|
|
369
|
-
|
|
361
|
+
reducer(draft, action) {
|
|
362
|
+
const inProgress = {
|
|
370
363
|
fetchState: 'In-Progress',
|
|
371
364
|
error: undefined,
|
|
372
365
|
};
|
|
366
|
+
if (action.payload.forMatchModal === true) {
|
|
367
|
+
draft.matchModal.confirmStatus = inProgress;
|
|
368
|
+
}
|
|
369
|
+
const matchId = `${action.payload.transactionType.toLowerCase()}:${action.payload.transactionIntegrationId}`;
|
|
370
|
+
draft.confirmStatusByMatchId[matchId] = inProgress;
|
|
373
371
|
},
|
|
374
|
-
prepare(transactionId, transactionIntegrationId, transactionType, lineIds, transactionSyncToken, accrualIds) {
|
|
372
|
+
prepare(transactionId, transactionIntegrationId, transactionType, lineIds, transactionSyncToken, accrualIds, forMatchModal) {
|
|
375
373
|
return {
|
|
376
374
|
payload: {
|
|
375
|
+
accrualIds,
|
|
376
|
+
lineIds,
|
|
377
377
|
transactionId,
|
|
378
378
|
transactionIntegrationId,
|
|
379
|
-
transactionType,
|
|
380
|
-
lineIds,
|
|
381
379
|
transactionSyncToken,
|
|
382
|
-
|
|
380
|
+
transactionType,
|
|
381
|
+
forMatchModal,
|
|
383
382
|
},
|
|
384
383
|
};
|
|
385
384
|
},
|
|
@@ -388,23 +387,33 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
388
387
|
const { results } = action.payload;
|
|
389
388
|
const allComplete = results.length > 0 &&
|
|
390
389
|
results.every((result) => isMatchAndReverseCompleteStatus(result.status));
|
|
391
|
-
|
|
390
|
+
const confirmStatus = allComplete
|
|
392
391
|
? { fetchState: 'Completed', error: undefined }
|
|
393
392
|
: {
|
|
394
393
|
fetchState: 'Error',
|
|
395
394
|
error: createZeniAPIStatus('Partial match-and-reverse', 'Some accruals were not reversed'),
|
|
396
395
|
};
|
|
397
|
-
|
|
396
|
+
if (action.payload.forMatchModal === true) {
|
|
397
|
+
draft.matchModal.confirmStatus = confirmStatus;
|
|
398
|
+
draft.matchModal.lastBatchResults = results;
|
|
399
|
+
}
|
|
398
400
|
if (action.payload.matchId != null) {
|
|
401
|
+
draft.confirmStatusByMatchId[action.payload.matchId] = confirmStatus;
|
|
399
402
|
draft.lastMatchResultsByMatchId[action.payload.matchId] = results;
|
|
400
403
|
pruneLastUndoResultsForMatched(draft, action.payload.matchId, action.payload.results);
|
|
401
404
|
}
|
|
402
405
|
},
|
|
403
406
|
matchAndReverseAccrualsFailure(draft, action) {
|
|
404
|
-
|
|
407
|
+
const failed = {
|
|
405
408
|
fetchState: 'Error',
|
|
406
409
|
error: action.payload.status,
|
|
407
410
|
};
|
|
411
|
+
if (action.payload.forMatchModal === true) {
|
|
412
|
+
draft.matchModal.confirmStatus = failed;
|
|
413
|
+
}
|
|
414
|
+
if (action.payload.matchId != null) {
|
|
415
|
+
draft.confirmStatusByMatchId[action.payload.matchId] = failed;
|
|
416
|
+
}
|
|
408
417
|
},
|
|
409
418
|
undoReversalAccruals: {
|
|
410
419
|
reducer(draft, action) {
|
|
@@ -426,7 +435,12 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
426
435
|
};
|
|
427
436
|
const allUndone = results.length > 0 &&
|
|
428
437
|
results.every((result) => result.status === 'undone');
|
|
429
|
-
|
|
438
|
+
if (allUndone) {
|
|
439
|
+
enqueueCloseMatchId(draft.closeMatchAfterUndoMatchIds, matchId);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
draft.closeMatchAfterUndoMatchIds = dropCloseMatchId(draft.closeMatchAfterUndoMatchIds, matchId);
|
|
443
|
+
}
|
|
430
444
|
pruneLastMatchResultsForUndone(draft, matchId, results);
|
|
431
445
|
if (allUndone) {
|
|
432
446
|
delete draft.lastUndoResultsByMatchId[matchId];
|
|
@@ -440,7 +454,44 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
440
454
|
},
|
|
441
455
|
},
|
|
442
456
|
ackCloseMatchAfterUndo(draft) {
|
|
443
|
-
draft.
|
|
457
|
+
draft.closeMatchAfterUndoMatchIds.shift();
|
|
458
|
+
},
|
|
459
|
+
ignoreActualMatchingSuggestion: {
|
|
460
|
+
prepare(matchId, reason, note) {
|
|
461
|
+
return { payload: { matchId, reason, note } };
|
|
462
|
+
},
|
|
463
|
+
reducer(draft, action) {
|
|
464
|
+
draft.ignoreStatusByMatchId[action.payload.matchId] = {
|
|
465
|
+
fetchState: 'In-Progress',
|
|
466
|
+
error: undefined,
|
|
467
|
+
};
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
ignoreActualMatchingSuggestionSuccess(draft, action) {
|
|
471
|
+
const { matchId } = action.payload;
|
|
472
|
+
draft.ignoreStatusByMatchId[matchId] = {
|
|
473
|
+
fetchState: 'Completed',
|
|
474
|
+
error: undefined,
|
|
475
|
+
};
|
|
476
|
+
enqueueCloseMatchId(draft.closeMatchAfterIgnoreMatchIds, matchId);
|
|
477
|
+
delete draft.lastMatchResultsByMatchId[matchId];
|
|
478
|
+
delete draft.lastUndoResultsByMatchId[matchId];
|
|
479
|
+
delete draft.detailFetchStateByMatchId[matchId];
|
|
480
|
+
delete draft.detailRefreshStatusByMatchId[matchId];
|
|
481
|
+
delete draft.undoReversalStatusByMatchId[matchId];
|
|
482
|
+
delete draft.confirmStatusByMatchId[matchId];
|
|
483
|
+
Object.keys(draft.matchIdsByPeriod).forEach((periodId) => {
|
|
484
|
+
draft.matchIdsByPeriod[periodId] = draft.matchIdsByPeriod[periodId].filter((id) => id !== matchId);
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
ignoreActualMatchingSuggestionFailure(draft, action) {
|
|
488
|
+
draft.ignoreStatusByMatchId[action.payload.matchId] = {
|
|
489
|
+
fetchState: 'Error',
|
|
490
|
+
error: action.payload.status,
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
ackCloseMatchAfterIgnore(draft) {
|
|
494
|
+
draft.closeMatchAfterIgnoreMatchIds.shift();
|
|
444
495
|
},
|
|
445
496
|
undoReversalAccrualsFailure(draft, action) {
|
|
446
497
|
draft.undoReversalStatusByMatchId[action.payload.matchId] = {
|
|
@@ -458,5 +509,5 @@ const expenseAutomationActualMatchingView = createSlice({
|
|
|
458
509
|
});
|
|
459
510
|
},
|
|
460
511
|
});
|
|
461
|
-
export const { ackCloseMatchAfterUndo, clearActualMatchingView, clearMatchNewModal, closeMatchNewModal, fetchActualMatching, fetchActualMatchingFailure, fetchActualMatchingMatch, fetchActualMatchingMatchFailure, fetchActualMatchingMatchSuccess, fetchActualMatchingPage, fetchActualMatchingSuccess, matchAndReverseAccruals, matchAndReverseAccrualsFailure, matchAndReverseAccrualsSuccess, openMatchNewModal, searchMatchAccruals, searchMatchAccrualsFailure, searchMatchAccrualsSuccess, searchMatchTransactions, searchMatchTransactionsFailure, searchMatchTransactionsSuccess, undoReversalAccruals, undoReversalAccrualsFailure, undoReversalAccrualsSuccess, updateActualMatchingUIState, } = expenseAutomationActualMatchingView.actions;
|
|
512
|
+
export const { ackCloseMatchAfterIgnore, ackCloseMatchAfterUndo, clearActualMatchingView, clearMatchNewModal, closeMatchNewModal, fetchActualMatching, fetchActualMatchingFailure, fetchActualMatchingMatch, fetchActualMatchingMatchFailure, fetchActualMatchingMatchSuccess, fetchActualMatchingPage, fetchActualMatchingSuccess, ignoreActualMatchingSuggestion, ignoreActualMatchingSuggestionFailure, ignoreActualMatchingSuggestionSuccess, matchAndReverseAccruals, matchAndReverseAccrualsFailure, matchAndReverseAccrualsSuccess, openMatchNewModal, searchMatchAccruals, searchMatchAccrualsFailure, searchMatchAccrualsSuccess, searchMatchTransactions, searchMatchTransactionsFailure, searchMatchTransactionsSuccess, undoReversalAccruals, undoReversalAccrualsFailure, undoReversalAccrualsSuccess, updateActualMatchingUIState, } = expenseAutomationActualMatchingView.actions;
|
|
462
513
|
export default expenseAutomationActualMatchingView.reducer;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createSelector } from '@reduxjs/toolkit';
|
|
1
2
|
import orderBy from 'lodash/orderBy';
|
|
2
3
|
import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
3
4
|
import { getActualMatchingMatchById } from '../../../entity/actualMatching/actualMatchingSelector';
|
|
@@ -5,7 +6,6 @@ import { generateJEOneTimeAccrualKey } from '../../../entity/jeSchedules/jeSched
|
|
|
5
6
|
import { getJEOneTimeAccrualByKey, } from '../../../entity/jeSchedules/jeSchedulesSelector';
|
|
6
7
|
import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
|
|
7
8
|
import { date } from '../../../zeniDayJS';
|
|
8
|
-
import { actualMatchingTransactionLineKey, } from '../types/actualMatchingViewState';
|
|
9
9
|
export function getSelectedMonthYearForCurrentTenant(state) {
|
|
10
10
|
const tenantId = getCurrentTenant(state)?.tenantId;
|
|
11
11
|
if (tenantId == null) {
|
|
@@ -34,27 +34,6 @@ function toMatchModalAccrual(member) {
|
|
|
34
34
|
jeOneTimeAccrualKey: generateJEOneTimeAccrualKey(member.accrualId),
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
function uniqueMatchModalAccruals(pinned, searched) {
|
|
38
|
-
const byId = new Map();
|
|
39
|
-
pinned.forEach((row) => byId.set(row.accrualId, row));
|
|
40
|
-
searched.forEach((row) => {
|
|
41
|
-
if (!byId.has(row.accrualId)) {
|
|
42
|
-
byId.set(row.accrualId, row);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
return Array.from(byId.values());
|
|
46
|
-
}
|
|
47
|
-
function uniqueTransactionSearchResults(pinned, searched) {
|
|
48
|
-
const byKey = new Map();
|
|
49
|
-
pinned.forEach((row) => byKey.set(actualMatchingTransactionLineKey(row.transactionType, row.transactionIntegrationId, row.lineId), row));
|
|
50
|
-
searched.forEach((row) => {
|
|
51
|
-
const key = actualMatchingTransactionLineKey(row.transactionType, row.transactionIntegrationId, row.lineId);
|
|
52
|
-
if (!byKey.has(key)) {
|
|
53
|
-
byKey.set(key, row);
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
return Array.from(byKey.values());
|
|
57
|
-
}
|
|
58
37
|
export function parseActualMatchingMatchId(matchId) {
|
|
59
38
|
const separator = matchId.indexOf(':');
|
|
60
39
|
if (separator <= 0 || separator === matchId.length - 1) {
|
|
@@ -115,13 +94,18 @@ function collectGroupedMembers(members) {
|
|
|
115
94
|
estimatedAmount: member.estimatedAmount,
|
|
116
95
|
jeDebitAccountName: member.jeDebitAccountName,
|
|
117
96
|
memo: member.memo,
|
|
97
|
+
reversalTransactionId: member.reversalTransactionId,
|
|
98
|
+
reversalTransactionIntegrationId: member.reversalTransactionIntegrationId,
|
|
118
99
|
serviceMonth: member.serviceMonth,
|
|
100
|
+
transactionId: member.transactionId,
|
|
101
|
+
transactionIntegrationId: member.transactionIntegrationId,
|
|
119
102
|
}));
|
|
120
103
|
}
|
|
121
104
|
function collectLeftoverMembers(leftovers) {
|
|
122
105
|
if (leftovers == null || leftovers.length === 0) {
|
|
123
106
|
return undefined;
|
|
124
107
|
}
|
|
108
|
+
// Leftover accruals did not post, so they have no journal-entry ids to open.
|
|
125
109
|
return leftovers.map((member) => ({
|
|
126
110
|
accrualId: member.accrualId,
|
|
127
111
|
estimatedAmount: member.estimatedAmount,
|
|
@@ -227,6 +211,7 @@ function emptyKpiSummary() {
|
|
|
227
211
|
actualsReceived: { count: 0, expectedCount: 0, vendorsPending: 0 },
|
|
228
212
|
autoReversed: { count: 0, percent: 0, clearedAmount: 0 },
|
|
229
213
|
pendingReview: { count: 0, amount: 0, estimatedMinutesToClear: 0 },
|
|
214
|
+
reversed: { count: 0 },
|
|
230
215
|
variance: { percent: 0, averagePercentPerMatch: 0, netAmount: 0 },
|
|
231
216
|
};
|
|
232
217
|
}
|
|
@@ -282,6 +267,7 @@ export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
|
|
|
282
267
|
}
|
|
283
268
|
const autoReversed = kpiRows.filter((row) => row.kind === 'auto_reversed');
|
|
284
269
|
const pendingReview = kpiRows.filter((row) => row.kind === 'pending_review');
|
|
270
|
+
const reversed = kpiRows.filter((row) => isReversedMatchKind(row.kind));
|
|
285
271
|
const receivedCount = kpiRows.length;
|
|
286
272
|
const rowsWithActual = kpiRows.filter((row) => row.actualAmount?.amount != null);
|
|
287
273
|
const accruedTotal = rowsWithActual.reduce((sum, row) => sum + (row.accruedAmount?.amount ?? row.estimatedAmount.amount), 0);
|
|
@@ -307,6 +293,9 @@ export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
|
|
|
307
293
|
amount: sumRowAmounts(pendingReview, displayCurrencyCode),
|
|
308
294
|
estimatedMinutesToClear: pendingReview.length * ESTIMATED_MINUTES_PER_PENDING_MATCH,
|
|
309
295
|
},
|
|
296
|
+
reversed: {
|
|
297
|
+
count: reversed.length,
|
|
298
|
+
},
|
|
310
299
|
variance: {
|
|
311
300
|
percent: accruedTotal === 0
|
|
312
301
|
? 0
|
|
@@ -319,23 +308,66 @@ export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
|
|
|
319
308
|
},
|
|
320
309
|
};
|
|
321
310
|
}
|
|
322
|
-
|
|
323
|
-
const view = state.expenseAutomationActualMatchingViewState;
|
|
311
|
+
function toActualMatchingMatchView(match) {
|
|
324
312
|
return {
|
|
325
|
-
match
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
lastUndoResults: view.lastUndoResultsByMatchId?.[matchId],
|
|
313
|
+
...match,
|
|
314
|
+
members: match.members.map(toMatchModalAccrual),
|
|
315
|
+
leftovers: match.leftovers == null
|
|
316
|
+
? undefined
|
|
317
|
+
: match.leftovers.map(toMatchModalAccrual),
|
|
331
318
|
};
|
|
332
319
|
}
|
|
320
|
+
const matchViewSelectorById = new Map();
|
|
321
|
+
export function getExpenseAutomationActualMatchingMatchView(state, matchId) {
|
|
322
|
+
let selector = matchViewSelectorById.get(matchId);
|
|
323
|
+
if (selector == null) {
|
|
324
|
+
selector = createSelector([
|
|
325
|
+
(root) => root.expenseAutomationActualMatchingViewState
|
|
326
|
+
.detailFetchStateByMatchId[matchId] ?? NOT_STARTED,
|
|
327
|
+
(root) => root.expenseAutomationActualMatchingViewState.ignoreStatusByMatchId[matchId] ?? NOT_STARTED,
|
|
328
|
+
(root) => root.expenseAutomationActualMatchingViewState
|
|
329
|
+
.lastMatchResultsByMatchId?.[matchId],
|
|
330
|
+
(root) => root.expenseAutomationActualMatchingViewState
|
|
331
|
+
.lastUndoResultsByMatchId?.[matchId],
|
|
332
|
+
(root) => root.expenseAutomationActualMatchingViewState
|
|
333
|
+
.detailRefreshStatusByMatchId[matchId] ?? NOT_STARTED,
|
|
334
|
+
(root) => root.expenseAutomationActualMatchingViewState
|
|
335
|
+
.undoReversalStatusByMatchId[matchId] ?? NOT_STARTED,
|
|
336
|
+
(root) => getActualMatchingMatchById(root.actualMatchingState, matchId),
|
|
337
|
+
], (detailFetchState, ignoreStatus, lastMatchResults, lastUndoResults, refreshStatus, undoReversalStatus, match) => ({
|
|
338
|
+
detailFetchState,
|
|
339
|
+
ignoreStatus,
|
|
340
|
+
lastMatchResults,
|
|
341
|
+
lastUndoResults,
|
|
342
|
+
match: match == null ? undefined : toActualMatchingMatchView(match),
|
|
343
|
+
refreshStatus,
|
|
344
|
+
undoReversalStatus,
|
|
345
|
+
}));
|
|
346
|
+
matchViewSelectorById.set(matchId, selector);
|
|
347
|
+
}
|
|
348
|
+
return selector(state);
|
|
349
|
+
}
|
|
333
350
|
export function getCloseMatchAfterUndo(state) {
|
|
334
|
-
return state.expenseAutomationActualMatchingViewState
|
|
351
|
+
return state.expenseAutomationActualMatchingViewState
|
|
352
|
+
.closeMatchAfterUndoMatchIds[0];
|
|
353
|
+
}
|
|
354
|
+
export function getCloseMatchAfterUndoMatchId(state) {
|
|
355
|
+
return getCloseMatchAfterUndo(state);
|
|
356
|
+
}
|
|
357
|
+
export function getCloseMatchAfterIgnoreMatchId(state) {
|
|
358
|
+
return state.expenseAutomationActualMatchingViewState
|
|
359
|
+
.closeMatchAfterIgnoreMatchIds[0];
|
|
360
|
+
}
|
|
361
|
+
export function getActualMatchingMatchModalConfirmStatus(state, matchId) {
|
|
362
|
+
if (matchId != null && matchId !== '') {
|
|
363
|
+
return (state.expenseAutomationActualMatchingViewState.confirmStatusByMatchId[matchId] ?? NOT_STARTED);
|
|
364
|
+
}
|
|
365
|
+
return state.expenseAutomationActualMatchingViewState.matchModal
|
|
366
|
+
.confirmStatus;
|
|
335
367
|
}
|
|
336
368
|
export function getExpenseAutomationActualMatchingView(state) {
|
|
337
369
|
const { expenseAutomationActualMatchingViewState } = state;
|
|
338
|
-
const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, } = expenseAutomationActualMatchingViewState;
|
|
370
|
+
const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, ignoreStatusByMatchId, undoReversalStatusByMatchId, } = expenseAutomationActualMatchingViewState;
|
|
339
371
|
const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
|
|
340
372
|
const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : undefined;
|
|
341
373
|
const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
|
|
@@ -351,32 +383,6 @@ export function getExpenseAutomationActualMatchingView(state) {
|
|
|
351
383
|
const searchedAccruals = matchModal.accrualSearch.results
|
|
352
384
|
.map((key) => getJEOneTimeAccrualByKey(key, state.jeSchedulesState))
|
|
353
385
|
.filter((accrual) => accrual != null);
|
|
354
|
-
const openedMatch = matchModal.openedFromMatchId != null
|
|
355
|
-
? getActualMatchingMatchById(state.actualMatchingState, matchModal.openedFromMatchId)
|
|
356
|
-
: undefined;
|
|
357
|
-
const pinnedAccruals = [
|
|
358
|
-
...(openedMatch?.members ?? []),
|
|
359
|
-
...(openedMatch?.leftovers ?? []),
|
|
360
|
-
].map(toMatchModalAccrual);
|
|
361
|
-
const firstMember = openedMatch?.members[0];
|
|
362
|
-
const cleared = firstMember?.cleared;
|
|
363
|
-
const suggestion = firstMember?.matchSuggestion;
|
|
364
|
-
const stamp = cleared ?? suggestion;
|
|
365
|
-
const consumedLineIds = cleared?.consumedLineIds;
|
|
366
|
-
const prefill = openedMatch == null
|
|
367
|
-
? undefined
|
|
368
|
-
: {
|
|
369
|
-
selectedAccrualIds: [
|
|
370
|
-
...openedMatch.members.map((member) => member.accrualId),
|
|
371
|
-
...(openedMatch.leftovers ?? []).map((leftover) => leftover.accrualId),
|
|
372
|
-
],
|
|
373
|
-
actualAmount: openedMatch.actualAmount.amount,
|
|
374
|
-
matchedTransactionId: stamp?.matchedTransactionId,
|
|
375
|
-
matchedLineId: cleared == null ? suggestion?.matchedLineId : undefined,
|
|
376
|
-
...(consumedLineIds != null && consumedLineIds.length > 0
|
|
377
|
-
? { consumedLineIds }
|
|
378
|
-
: {}),
|
|
379
|
-
};
|
|
380
386
|
// `fetchState` is a flat slice field that survives a period switch. Until this
|
|
381
387
|
// month's ids land, leftover Completed/Error is the previous month. Report
|
|
382
388
|
// Not-Started so chrome matches Review's "cache slot missing" skeleton, not
|
|
@@ -394,16 +400,16 @@ export function getExpenseAutomationActualMatchingView(state) {
|
|
|
394
400
|
kpiSummary,
|
|
395
401
|
uiState,
|
|
396
402
|
refreshStatus,
|
|
403
|
+
ignoreStatusByMatchId,
|
|
404
|
+
undoReversalStatusByMatchId,
|
|
397
405
|
hasLoadedForSelectedPeriod: matchIds != null,
|
|
398
406
|
matchModal: {
|
|
399
407
|
isOpen: matchModal.isOpen,
|
|
400
|
-
openedFromMatchId: matchModal.openedFromMatchId,
|
|
401
|
-
prefill,
|
|
402
408
|
confirmStatus: matchModal.confirmStatus,
|
|
403
409
|
lastBatchResults: matchModal.lastBatchResults,
|
|
404
410
|
transactionSearch: {
|
|
405
411
|
query: matchModal.transactionSearch.query,
|
|
406
|
-
results:
|
|
412
|
+
results: matchModal.transactionSearch.results,
|
|
407
413
|
fetchState: {
|
|
408
414
|
fetchState: matchModal.transactionSearch.fetchState,
|
|
409
415
|
error: matchModal.transactionSearch.error,
|
|
@@ -413,7 +419,7 @@ export function getExpenseAutomationActualMatchingView(state) {
|
|
|
413
419
|
},
|
|
414
420
|
accrualSearch: {
|
|
415
421
|
query: matchModal.accrualSearch.query,
|
|
416
|
-
results:
|
|
422
|
+
results: searchedAccruals,
|
|
417
423
|
fetchState: {
|
|
418
424
|
fetchState: matchModal.accrualSearch.fetchState,
|
|
419
425
|
error: matchModal.accrualSearch.error,
|
|
@@ -13,6 +13,12 @@ const ACTUAL_MATCHING_SORT_KEYS = [
|
|
|
13
13
|
'variance',
|
|
14
14
|
];
|
|
15
15
|
export const toActualMatchingSortKey = (v) => stringToUnion(v, ACTUAL_MATCHING_SORT_KEYS);
|
|
16
|
+
const ACTUAL_MATCHING_STATUS_FILTERS = [
|
|
17
|
+
'all',
|
|
18
|
+
'pending_review',
|
|
19
|
+
'reversed',
|
|
20
|
+
];
|
|
21
|
+
export const toActualMatchingStatusFilter = (v) => stringToUnion(v, ACTUAL_MATCHING_STATUS_FILTERS);
|
|
16
22
|
export const MATCH_TRANSACTION_SEARCH_PAGE_SIZE = 50;
|
|
17
23
|
export const MATCH_ACCRUAL_SEARCH_LIMIT = 50;
|
|
18
24
|
export const MATCH_SEARCH_DEBOUNCE_MS = 300;
|
|
@@ -33,6 +39,15 @@ const UNDO_REVERSAL_RESULT_STATUSES = [
|
|
|
33
39
|
'error',
|
|
34
40
|
];
|
|
35
41
|
export const toUndoReversalResultStatus = (v) => stringToUnion(v, UNDO_REVERSAL_RESULT_STATUSES);
|
|
42
|
+
const ACTUAL_MATCHING_IGNORE_REASONS = [
|
|
43
|
+
'wrong_bill',
|
|
44
|
+
'wrong_period',
|
|
45
|
+
'amount_off',
|
|
46
|
+
'already_handled',
|
|
47
|
+
'other_ignore',
|
|
48
|
+
];
|
|
49
|
+
export const toActualMatchingIgnoreReasonType = (v) => stringToUnion(v, ACTUAL_MATCHING_IGNORE_REASONS);
|
|
50
|
+
export const ALL_ACTUAL_MATCHING_IGNORE_REASONS = ACTUAL_MATCHING_IGNORE_REASONS;
|
|
36
51
|
export function actualMatchingTransactionLineKey(transactionType, transactionIntegrationId, lineId) {
|
|
37
52
|
return `${transactionType}:${transactionIntegrationId}:${lineId}`;
|
|
38
53
|
}
|