@zeniai/client-epic-state 5.2.62-beta0KB → 5.2.62-beta0MM

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 (70) hide show
  1. package/lib/entity/actualMatching/actualMatchingReducer.d.ts +3 -1
  2. package/lib/entity/actualMatching/actualMatchingReducer.js +11 -2
  3. package/lib/entity/jeSchedules/jeSchedulesPayload.d.ts +1 -0
  4. package/lib/entity/jeSchedules/jeSchedulesPayload.js +1 -0
  5. package/lib/entity/jeSchedules/jeSchedulesState.d.ts +1 -0
  6. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  7. package/lib/entity/snackbar/snackbarTypes.js +1 -1
  8. package/lib/entity/transaction/stateTypes/paymentType.d.ts +2 -2
  9. package/lib/entity/transaction/stateTypes/paymentType.js +1 -9
  10. package/lib/epic.d.ts +2 -4
  11. package/lib/epic.js +2 -6
  12. package/lib/esm/entity/actualMatching/actualMatchingReducer.js +9 -1
  13. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +1 -0
  14. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -1
  15. package/lib/esm/entity/transaction/stateTypes/paymentType.js +1 -9
  16. package/lib/esm/epic.js +2 -6
  17. package/lib/esm/index.js +7 -10
  18. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +50 -0
  19. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +7 -3
  20. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +2 -23
  21. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +66 -35
  22. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +60 -64
  23. package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +15 -0
  24. package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +30 -4
  25. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +1 -174
  26. package/lib/index.d.ts +8 -12
  27. package/lib/index.js +57 -70
  28. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.d.ts +10 -0
  29. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +54 -0
  30. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +7 -3
  31. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +1 -22
  32. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  33. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +12 -1
  34. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +16 -8
  35. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +67 -36
  36. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +20 -8
  37. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +7 -4
  38. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +63 -64
  39. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +20 -4
  40. package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +18 -1
  41. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +1 -1
  42. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +30 -4
  43. package/lib/view/transactionDetail/transactionDetailReducer.d.ts +1 -37
  44. package/lib/view/transactionDetail/transactionDetailReducer.js +2 -176
  45. package/lib/view/transactionDetail/transactionDetailState.d.ts +0 -12
  46. package/package.json +1 -1
  47. package/lib/entity/transaction/stateTypes/printStatus.d.ts +0 -4
  48. package/lib/entity/transaction/stateTypes/printStatus.js +0 -15
  49. package/lib/esm/entity/transaction/stateTypes/printStatus.js +0 -11
  50. package/lib/esm/view/transactionDetail/createTransaction/createTransactionLocalDataHelper.js +0 -133
  51. package/lib/esm/view/transactionDetail/createTransaction/createTransactionPayload.js +0 -114
  52. package/lib/esm/view/transactionDetail/createTransaction/createTransactionSelector.js +0 -41
  53. package/lib/esm/view/transactionDetail/createTransaction/createTransactionState.js +0 -11
  54. package/lib/esm/view/transactionDetail/createTransaction/epics/createTransactionEpic.js +0 -58
  55. package/lib/esm/view/transactionDetail/createTransaction/epics/fetchVendorRecommendationsForCreateTransactionEpic.js +0 -55
  56. package/lib/esm/view/transactionDetail/createTransaction/epics/initializeCreateTransactionLocalDataEpic.js +0 -14
  57. package/lib/view/transactionDetail/createTransaction/createTransactionLocalDataHelper.d.ts +0 -51
  58. package/lib/view/transactionDetail/createTransaction/createTransactionLocalDataHelper.js +0 -148
  59. package/lib/view/transactionDetail/createTransaction/createTransactionPayload.d.ts +0 -82
  60. package/lib/view/transactionDetail/createTransaction/createTransactionPayload.js +0 -118
  61. package/lib/view/transactionDetail/createTransaction/createTransactionSelector.d.ts +0 -40
  62. package/lib/view/transactionDetail/createTransaction/createTransactionSelector.js +0 -48
  63. package/lib/view/transactionDetail/createTransaction/createTransactionState.d.ts +0 -81
  64. package/lib/view/transactionDetail/createTransaction/createTransactionState.js +0 -14
  65. package/lib/view/transactionDetail/createTransaction/epics/createTransactionEpic.d.ts +0 -9
  66. package/lib/view/transactionDetail/createTransaction/epics/createTransactionEpic.js +0 -62
  67. package/lib/view/transactionDetail/createTransaction/epics/fetchVendorRecommendationsForCreateTransactionEpic.d.ts +0 -20
  68. package/lib/view/transactionDetail/createTransaction/epics/fetchVendorRecommendationsForCreateTransactionEpic.js +0 -59
  69. package/lib/view/transactionDetail/createTransaction/epics/initializeCreateTransactionLocalDataEpic.d.ts +0 -12
  70. package/lib/view/transactionDetail/createTransaction/epics/initializeCreateTransactionLocalDataEpic.js +0 -18
@@ -0,0 +1,50 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, exhaustMap, filter, groupBy, mergeMap, takeUntil, } from 'rxjs/operators';
3
+ import { removeActualMatchingMatch } from '../../../../../entity/actualMatching/actualMatchingReducer';
4
+ import { getActualMatchingMatchById } from '../../../../../entity/actualMatching/actualMatchingSelector';
5
+ import { openSnackbar } from '../../../../../entity/snackbar/snackbarReducer';
6
+ import { createZeniAPIStatus, isSuccessResponse } from '../../../../../responsePayload';
7
+ import { clearAll } from '../../../../../rootActions';
8
+ import { ignoreActualMatchingSuggestion, ignoreActualMatchingSuggestionFailure, ignoreActualMatchingSuggestionSuccess, } from '../../../reducers/actualMatchingViewReducer';
9
+ import { refreshActualMatchingList } from './refreshActualMatchingList';
10
+ function matchVendorName(state, matchId) {
11
+ return (getActualMatchingMatchById(state.actualMatchingState, matchId)?.vendorName ??
12
+ '');
13
+ }
14
+ function ignoreSuggestionSnackbar(vendorName, succeeded) {
15
+ return openSnackbar({
16
+ messageSection: 'actual_matching_ignore_suggestion',
17
+ messageText: succeeded ? 'success' : 'failed',
18
+ type: succeeded ? 'success' : 'error',
19
+ variables: [{ variableName: '_vendor_name_', variableValue: vendorName }],
20
+ });
21
+ }
22
+ export const ignoreActualMatchingSuggestionEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(ignoreActualMatchingSuggestion.match), groupBy((action) => action.payload.matchId), mergeMap((perMatch$) => perMatch$.pipe(exhaustMap((action) => {
23
+ const { matchId, reason, note } = action.payload;
24
+ const body = {
25
+ reason,
26
+ ...(note != null && note !== '' ? { note } : {}),
27
+ };
28
+ const vendorName = matchVendorName(state$.value, matchId);
29
+ return zeniAPI
30
+ .putAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accruals/actual-matching/${encodeURIComponent(matchId)}/ignore`, body)
31
+ .pipe(mergeMap((response) => {
32
+ if (isSuccessResponse(response) && response.data != null) {
33
+ const successActions = [
34
+ removeActualMatchingMatch(matchId),
35
+ ignoreActualMatchingSuggestionSuccess({ matchId }),
36
+ ...refreshActualMatchingList(state$.value),
37
+ ignoreSuggestionSnackbar(vendorName, true),
38
+ ];
39
+ return from(successActions);
40
+ }
41
+ return of(ignoreActualMatchingSuggestionFailure({
42
+ matchId,
43
+ status: response.status,
44
+ }), ignoreSuggestionSnackbar(vendorName, false));
45
+ }), catchError((error) => of(ignoreActualMatchingSuggestionFailure({
46
+ matchId,
47
+ status: createZeniAPIStatus('Unexpected Error', 'Ignore actual matching suggestion REST API call errored out' +
48
+ JSON.stringify(error)),
49
+ }), ignoreSuggestionSnackbar(vendorName, false))), takeUntil(actions$.pipe(filter(clearAll.match))));
50
+ }))));
@@ -87,8 +87,8 @@ exhaustMap((action) => {
87
87
  results.every((result) => isMatchAndReverseCompleteStatus(result.status));
88
88
  if (occupancyChanged) {
89
89
  // List + detail: list can omit leftovers; detail GET always
90
- // writes intended occupancy so Continue match prefills the
91
- // remaining leftover set instead of a stale cached row.
90
+ // writes intended occupancy so leftover retry can join C
91
+ // onto the occupied bill instead of a stale cached row.
92
92
  successActions.push(...refreshActualMatchingList(state$.value), fetchActualMatchingMatch(matchId, true));
93
93
  }
94
94
  const snackbar = matchAndReverseSnackbar(results, vendorName);
@@ -100,8 +100,12 @@ exhaustMap((action) => {
100
100
  }
101
101
  return from(successActions);
102
102
  }
103
- return of(matchAndReverseAccrualsFailure({ status: response.status }), matchAndReverseHttpFailSnackbar(vendorName));
103
+ return of(matchAndReverseAccrualsFailure({
104
+ status: response.status,
105
+ matchId,
106
+ }), matchAndReverseHttpFailSnackbar(vendorName));
104
107
  }), catchError((error) => of(matchAndReverseAccrualsFailure({
108
+ matchId,
105
109
  status: createZeniAPIStatus('Unexpected Error', 'Match and reverse accruals REST API call errored out' +
106
110
  JSON.stringify(error)),
107
111
  }), matchAndReverseHttpFailSnackbar(vendorName))), takeUntil(actions$.pipe(filter(clearAll.match))));
@@ -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, parseActualMatchingMatchId, } from '../../../selectors/actualMatchingViewSelector';
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((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
- }));
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));
@@ -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,10 +41,14 @@ 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
- closeMatchAfterUndo: false,
48
+ ignoreStatusByMatchId: {},
49
+ confirmStatusByMatchId: {},
50
+ closeMatchAfterUndoMatchId: undefined,
51
+ closeMatchAfterIgnoreMatchId: undefined,
51
52
  lastMatchResultsByMatchId: {},
52
53
  lastUndoResultsByMatchId: {},
53
54
  hasValidState() {
@@ -96,9 +97,6 @@ function resetMatchModalSearches(draft) {
96
97
  error: undefined,
97
98
  };
98
99
  draft.matchModal.lastBatchResults = undefined;
99
- draft.matchModal.openedFromMatchId = undefined;
100
- draft.matchModal.didPinPrefillTransactions = false;
101
- draft.matchModal.pinnedTransactionResults = [];
102
100
  }
103
101
  const expenseAutomationActualMatchingView = createSlice({
104
102
  name: 'expenseAutomationActualMatchingView',
@@ -239,21 +237,13 @@ const expenseAutomationActualMatchingView = createSlice({
239
237
  if (action.payload.scrollPosition != null) {
240
238
  draft.uiState.scrollPosition = action.payload.scrollPosition;
241
239
  }
240
+ if (action.payload.statusFilter != null) {
241
+ draft.uiState.statusFilter = action.payload.statusFilter;
242
+ }
242
243
  },
243
- openMatchNewModal: {
244
- prepare(matchId) {
245
- return { payload: { matchId } };
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
- },
244
+ openMatchNewModal(draft) {
245
+ resetMatchModalSearches(draft);
246
+ draft.matchModal.isOpen = true;
257
247
  },
258
248
  // Dismiss (Cancel / overlay). Store reset matches `clearMatchNewModal`;
259
249
  // the actions stay distinct so confirm-success vs user-dismiss stay observable.
@@ -308,11 +298,6 @@ const expenseAutomationActualMatchingView = createSlice({
308
298
  }
309
299
  else {
310
300
  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
301
  }
317
302
  draft.matchModal.transactionSearch.nextPageToken = nextPageToken;
318
303
  draft.matchModal.transactionSearch.isLoadingMore = false;
@@ -365,21 +350,24 @@ const expenseAutomationActualMatchingView = createSlice({
365
350
  draft.matchModal.accrualSearch.error = action.payload.error;
366
351
  },
367
352
  matchAndReverseAccruals: {
368
- reducer(draft) {
369
- draft.matchModal.confirmStatus = {
353
+ reducer(draft, action) {
354
+ const inProgress = {
370
355
  fetchState: 'In-Progress',
371
356
  error: undefined,
372
357
  };
358
+ draft.matchModal.confirmStatus = inProgress;
359
+ const matchId = `${action.payload.transactionType.toLowerCase()}:${action.payload.transactionIntegrationId}`;
360
+ draft.confirmStatusByMatchId[matchId] = inProgress;
373
361
  },
374
362
  prepare(transactionId, transactionIntegrationId, transactionType, lineIds, transactionSyncToken, accrualIds) {
375
363
  return {
376
364
  payload: {
365
+ accrualIds,
366
+ lineIds,
377
367
  transactionId,
378
368
  transactionIntegrationId,
379
- transactionType,
380
- lineIds,
381
369
  transactionSyncToken,
382
- accrualIds,
370
+ transactionType,
383
371
  },
384
372
  };
385
373
  },
@@ -396,15 +384,21 @@ const expenseAutomationActualMatchingView = createSlice({
396
384
  };
397
385
  draft.matchModal.lastBatchResults = results;
398
386
  if (action.payload.matchId != null) {
387
+ draft.confirmStatusByMatchId[action.payload.matchId] =
388
+ draft.matchModal.confirmStatus;
399
389
  draft.lastMatchResultsByMatchId[action.payload.matchId] = results;
400
390
  pruneLastUndoResultsForMatched(draft, action.payload.matchId, action.payload.results);
401
391
  }
402
392
  },
403
393
  matchAndReverseAccrualsFailure(draft, action) {
404
- draft.matchModal.confirmStatus = {
394
+ const failed = {
405
395
  fetchState: 'Error',
406
396
  error: action.payload.status,
407
397
  };
398
+ draft.matchModal.confirmStatus = failed;
399
+ if (action.payload.matchId != null) {
400
+ draft.confirmStatusByMatchId[action.payload.matchId] = failed;
401
+ }
408
402
  },
409
403
  undoReversalAccruals: {
410
404
  reducer(draft, action) {
@@ -426,7 +420,7 @@ const expenseAutomationActualMatchingView = createSlice({
426
420
  };
427
421
  const allUndone = results.length > 0 &&
428
422
  results.every((result) => result.status === 'undone');
429
- draft.closeMatchAfterUndo = allUndone;
423
+ draft.closeMatchAfterUndoMatchId = allUndone ? matchId : undefined;
430
424
  pruneLastMatchResultsForUndone(draft, matchId, results);
431
425
  if (allUndone) {
432
426
  delete draft.lastUndoResultsByMatchId[matchId];
@@ -440,7 +434,44 @@ const expenseAutomationActualMatchingView = createSlice({
440
434
  },
441
435
  },
442
436
  ackCloseMatchAfterUndo(draft) {
443
- draft.closeMatchAfterUndo = false;
437
+ draft.closeMatchAfterUndoMatchId = undefined;
438
+ },
439
+ ignoreActualMatchingSuggestion: {
440
+ prepare(matchId, reason, note) {
441
+ return { payload: { matchId, reason, note } };
442
+ },
443
+ reducer(draft, action) {
444
+ draft.ignoreStatusByMatchId[action.payload.matchId] = {
445
+ fetchState: 'In-Progress',
446
+ error: undefined,
447
+ };
448
+ },
449
+ },
450
+ ignoreActualMatchingSuggestionSuccess(draft, action) {
451
+ const { matchId } = action.payload;
452
+ draft.ignoreStatusByMatchId[matchId] = {
453
+ fetchState: 'Completed',
454
+ error: undefined,
455
+ };
456
+ draft.closeMatchAfterIgnoreMatchId = matchId;
457
+ delete draft.lastMatchResultsByMatchId[matchId];
458
+ delete draft.lastUndoResultsByMatchId[matchId];
459
+ delete draft.detailFetchStateByMatchId[matchId];
460
+ delete draft.detailRefreshStatusByMatchId[matchId];
461
+ delete draft.undoReversalStatusByMatchId[matchId];
462
+ delete draft.confirmStatusByMatchId[matchId];
463
+ Object.keys(draft.matchIdsByPeriod).forEach((periodId) => {
464
+ draft.matchIdsByPeriod[periodId] = draft.matchIdsByPeriod[periodId].filter((id) => id !== matchId);
465
+ });
466
+ },
467
+ ignoreActualMatchingSuggestionFailure(draft, action) {
468
+ draft.ignoreStatusByMatchId[action.payload.matchId] = {
469
+ fetchState: 'Error',
470
+ error: action.payload.status,
471
+ };
472
+ },
473
+ ackCloseMatchAfterIgnore(draft) {
474
+ draft.closeMatchAfterIgnoreMatchId = undefined;
444
475
  },
445
476
  undoReversalAccrualsFailure(draft, action) {
446
477
  draft.undoReversalStatusByMatchId[action.payload.matchId] = {
@@ -458,5 +489,5 @@ const expenseAutomationActualMatchingView = createSlice({
458
489
  });
459
490
  },
460
491
  });
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;
492
+ 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
493
  export default expenseAutomationActualMatchingView.reducer;
@@ -1,11 +1,11 @@
1
1
  import orderBy from 'lodash/orderBy';
2
+ import { createSelector } from '@reduxjs/toolkit';
2
3
  import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
3
4
  import { getActualMatchingMatchById } from '../../../entity/actualMatching/actualMatchingSelector';
4
5
  import { generateJEOneTimeAccrualKey } from '../../../entity/jeSchedules/jeScheduleHelper';
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) {
@@ -227,6 +206,7 @@ function emptyKpiSummary() {
227
206
  actualsReceived: { count: 0, expectedCount: 0, vendorsPending: 0 },
228
207
  autoReversed: { count: 0, percent: 0, clearedAmount: 0 },
229
208
  pendingReview: { count: 0, amount: 0, estimatedMinutesToClear: 0 },
209
+ reversed: { count: 0 },
230
210
  variance: { percent: 0, averagePercentPerMatch: 0, netAmount: 0 },
231
211
  };
232
212
  }
@@ -282,6 +262,7 @@ export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
282
262
  }
283
263
  const autoReversed = kpiRows.filter((row) => row.kind === 'auto_reversed');
284
264
  const pendingReview = kpiRows.filter((row) => row.kind === 'pending_review');
265
+ const reversed = kpiRows.filter((row) => isReversedMatchKind(row.kind));
285
266
  const receivedCount = kpiRows.length;
286
267
  const rowsWithActual = kpiRows.filter((row) => row.actualAmount?.amount != null);
287
268
  const accruedTotal = rowsWithActual.reduce((sum, row) => sum + (row.accruedAmount?.amount ?? row.estimatedAmount.amount), 0);
@@ -307,6 +288,9 @@ export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
307
288
  amount: sumRowAmounts(pendingReview, displayCurrencyCode),
308
289
  estimatedMinutesToClear: pendingReview.length * ESTIMATED_MINUTES_PER_PENDING_MATCH,
309
290
  },
291
+ reversed: {
292
+ count: reversed.length,
293
+ },
310
294
  variance: {
311
295
  percent: accruedTotal === 0
312
296
  ? 0
@@ -319,23 +303,55 @@ export function buildActualMatchingKpiSummary(rows, displayCurrencyCode) {
319
303
  },
320
304
  };
321
305
  }
322
- export function getExpenseAutomationActualMatchingMatchView(state, matchId) {
323
- const view = state.expenseAutomationActualMatchingViewState;
306
+ function toActualMatchingMatchView(match) {
324
307
  return {
325
- match: getActualMatchingMatchById(state.actualMatchingState, matchId),
326
- detailFetchState: view.detailFetchStateByMatchId[matchId] ?? NOT_STARTED,
327
- refreshStatus: view.detailRefreshStatusByMatchId[matchId] ?? NOT_STARTED,
328
- undoReversalStatus: view.undoReversalStatusByMatchId[matchId] ?? NOT_STARTED,
329
- lastMatchResults: view.lastMatchResultsByMatchId?.[matchId],
330
- lastUndoResults: view.lastUndoResultsByMatchId?.[matchId],
308
+ ...match,
309
+ members: match.members.map(toMatchModalAccrual),
310
+ leftovers: match.leftovers == null
311
+ ? undefined
312
+ : match.leftovers.map(toMatchModalAccrual),
331
313
  };
332
314
  }
315
+ const EMPTY_ACTUAL_MATCHING_ROWS = [];
316
+ const matchViewSelectorById = new Map();
317
+ export function getExpenseAutomationActualMatchingMatchView(state, matchId) {
318
+ let selector = matchViewSelectorById.get(matchId);
319
+ if (selector == null) {
320
+ selector = createSelector([
321
+ (root) => root.expenseAutomationActualMatchingViewState,
322
+ (root) => getActualMatchingMatchById(root.actualMatchingState, matchId),
323
+ ], (view, match) => ({
324
+ detailFetchState: view.detailFetchStateByMatchId[matchId] ?? NOT_STARTED,
325
+ ignoreStatus: view.ignoreStatusByMatchId[matchId] ?? NOT_STARTED,
326
+ lastMatchResults: view.lastMatchResultsByMatchId?.[matchId],
327
+ lastUndoResults: view.lastUndoResultsByMatchId?.[matchId],
328
+ match: match == null ? undefined : toActualMatchingMatchView(match),
329
+ refreshStatus: view.detailRefreshStatusByMatchId[matchId] ?? NOT_STARTED,
330
+ undoReversalStatus: view.undoReversalStatusByMatchId[matchId] ?? NOT_STARTED,
331
+ }));
332
+ matchViewSelectorById.set(matchId, selector);
333
+ }
334
+ return selector(state);
335
+ }
333
336
  export function getCloseMatchAfterUndo(state) {
334
- return state.expenseAutomationActualMatchingViewState.closeMatchAfterUndo;
337
+ return state.expenseAutomationActualMatchingViewState.closeMatchAfterUndoMatchId;
338
+ }
339
+ export function getCloseMatchAfterUndoMatchId(state) {
340
+ return getCloseMatchAfterUndo(state);
341
+ }
342
+ export function getCloseMatchAfterIgnoreMatchId(state) {
343
+ return state.expenseAutomationActualMatchingViewState
344
+ .closeMatchAfterIgnoreMatchId;
345
+ }
346
+ export function getActualMatchingMatchModalConfirmStatus(state, matchId) {
347
+ if (matchId != null && matchId !== '') {
348
+ return (state.expenseAutomationActualMatchingViewState.confirmStatusByMatchId[matchId] ?? NOT_STARTED);
349
+ }
350
+ return state.expenseAutomationActualMatchingViewState.matchModal.confirmStatus;
335
351
  }
336
352
  export function getExpenseAutomationActualMatchingView(state) {
337
353
  const { expenseAutomationActualMatchingViewState } = state;
338
- const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, } = expenseAutomationActualMatchingViewState;
354
+ const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, ignoreStatusByMatchId, undoReversalStatusByMatchId, } = expenseAutomationActualMatchingViewState;
339
355
  const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
340
356
  const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : undefined;
341
357
  const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
@@ -345,38 +361,18 @@ export function getExpenseAutomationActualMatchingView(state) {
345
361
  .filter((match) => match.members.length > 0)
346
362
  .map(toActualMatchingRow);
347
363
  const rows = allRows.filter((row) => matchesSearch(row, uiState.searchString));
348
- const reversed = sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState);
349
- const pendingReview = sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState);
364
+ const includePendingReview = uiState.statusFilter !== 'reversed';
365
+ const includeReversed = uiState.statusFilter !== 'pending_review';
366
+ const reversed = includeReversed
367
+ ? sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState)
368
+ : EMPTY_ACTUAL_MATCHING_ROWS;
369
+ const pendingReview = includePendingReview
370
+ ? sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState)
371
+ : EMPTY_ACTUAL_MATCHING_ROWS;
350
372
  const kpiSummary = buildActualMatchingKpiSummary(allRows, getTenantDisplayCurrency(state)?.currencyCode);
351
373
  const searchedAccruals = matchModal.accrualSearch.results
352
374
  .map((key) => getJEOneTimeAccrualByKey(key, state.jeSchedulesState))
353
375
  .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
376
  // `fetchState` is a flat slice field that survives a period switch. Until this
381
377
  // month's ids land, leftover Completed/Error is the previous month. Report
382
378
  // Not-Started so chrome matches Review's "cache slot missing" skeleton, not
@@ -394,16 +390,16 @@ export function getExpenseAutomationActualMatchingView(state) {
394
390
  kpiSummary,
395
391
  uiState,
396
392
  refreshStatus,
393
+ ignoreStatusByMatchId,
394
+ undoReversalStatusByMatchId,
397
395
  hasLoadedForSelectedPeriod: matchIds != null,
398
396
  matchModal: {
399
397
  isOpen: matchModal.isOpen,
400
- openedFromMatchId: matchModal.openedFromMatchId,
401
- prefill,
402
398
  confirmStatus: matchModal.confirmStatus,
403
399
  lastBatchResults: matchModal.lastBatchResults,
404
400
  transactionSearch: {
405
401
  query: matchModal.transactionSearch.query,
406
- results: uniqueTransactionSearchResults(matchModal.pinnedTransactionResults, matchModal.transactionSearch.results),
402
+ results: matchModal.transactionSearch.results,
407
403
  fetchState: {
408
404
  fetchState: matchModal.transactionSearch.fetchState,
409
405
  error: matchModal.transactionSearch.error,
@@ -413,7 +409,7 @@ export function getExpenseAutomationActualMatchingView(state) {
413
409
  },
414
410
  accrualSearch: {
415
411
  query: matchModal.accrualSearch.query,
416
- results: uniqueMatchModalAccruals(pinnedAccruals, searchedAccruals),
412
+ results: searchedAccruals,
417
413
  fetchState: {
418
414
  fetchState: matchModal.accrualSearch.fetchState,
419
415
  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
  }
@@ -625,7 +625,7 @@ export const getDueDate = (billStageCode, dueDate, invoiceDate, bookCloseDate) =
625
625
  }
626
626
  };
627
627
  export const getWithdrawFrom = (state, fromAccountId, fromAccountType, companyId, lastSelectedFromAccount) => {
628
- const { paymentAccountState, depositAccountState, paymentAccountListState } = state;
628
+ const { paymentAccountState, depositAccountState, depositAccountListState, paymentAccountListState, } = state;
629
629
  if (fromAccountId != null) {
630
630
  const withdrawFromAsDepositAcc = getDepositAccountByDepositAccountId(depositAccountState, fromAccountId);
631
631
  const withdrawFromAsPaymentAcc = getPaymentAccountByPaymentAccountId(paymentAccountState, fromAccountId);
@@ -639,15 +639,41 @@ export const getWithdrawFrom = (state, fromAccountId, fromAccountType, companyId
639
639
  }
640
640
  }
641
641
  else {
642
- return getDefaultWithdrawFromAccount(state, companyId ?? '', lastSelectedFromAccount);
642
+ return getDefaultWithdrawFromAccount(state, depositAccountListState.accountIds, companyId ?? '', lastSelectedFromAccount);
643
643
  }
644
644
  return undefined;
645
645
  };
646
- export const getDefaultWithdrawFromAccount = (state, companyId, lastSelectedFromAccount) => {
646
+ export const getDefaultWithdrawFromAccount = (state, depositAccounts, companyId, lastSelectedFromAccount) => {
647
647
  const { depositAccountState, paymentAccountState, paymentAccountListState } = state;
648
648
  const companyView = getCompanyView(state, companyId, []);
649
649
  const primaryAccount = companyView?.company.company.companyBillPayInfo.primaryFundingAccount;
650
- const defaultAccount = primaryAccount ?? lastSelectedFromAccount;
650
+ let defaultAccount = undefined;
651
+ // User paying bill for first time or previously paid with external account
652
+ if (lastSelectedFromAccount == null ||
653
+ lastSelectedFromAccount?.accountType === 'paymentAccount') {
654
+ if (depositAccounts.length === 0) {
655
+ defaultAccount = primaryAccount;
656
+ }
657
+ else {
658
+ if (primaryAccount?.accountType === 'depositAccount') {
659
+ defaultAccount = primaryAccount;
660
+ }
661
+ }
662
+ }
663
+ else {
664
+ // User paid with Zeni account previously
665
+ if (depositAccounts.length === 1) {
666
+ defaultAccount = lastSelectedFromAccount;
667
+ }
668
+ else {
669
+ if (primaryAccount?.accountType === 'depositAccount') {
670
+ defaultAccount = primaryAccount;
671
+ }
672
+ else {
673
+ defaultAccount = lastSelectedFromAccount;
674
+ }
675
+ }
676
+ }
651
677
  if (defaultAccount != null) {
652
678
  const defaultAccAsDepositAcc = getDepositAccountByDepositAccountId(depositAccountState, defaultAccount?.accountId);
653
679
  const defaultAccAsPaymentAcc = getPaymentAccountByPaymentAccountId(paymentAccountState, defaultAccount?.accountId);