@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.
Files changed (51) 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 +2 -0
  4. package/lib/entity/jeSchedules/jeSchedulesPayload.js +2 -0
  5. package/lib/entity/jeSchedules/jeSchedulesState.d.ts +2 -0
  6. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  7. package/lib/entity/snackbar/snackbarTypes.js +2 -0
  8. package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +2 -10
  9. package/lib/epic.d.ts +2 -1
  10. package/lib/epic.js +2 -0
  11. package/lib/esm/entity/actualMatching/actualMatchingReducer.js +9 -1
  12. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +2 -0
  13. package/lib/esm/entity/snackbar/snackbarTypes.js +2 -0
  14. package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +2 -10
  15. package/lib/esm/epic.js +2 -0
  16. package/lib/esm/index.js +5 -5
  17. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +4 -2
  18. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +50 -0
  19. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +26 -13
  20. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +2 -23
  21. package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +1 -2
  22. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +89 -38
  23. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +68 -62
  24. package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +15 -0
  25. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +18 -9
  26. package/lib/esm/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.js +11 -2
  27. package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +4 -30
  28. package/lib/index.d.ts +6 -6
  29. package/lib/index.js +57 -49
  30. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.d.ts +2 -2
  31. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +3 -1
  32. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.d.ts +10 -0
  33. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +54 -0
  34. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +24 -11
  35. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +1 -22
  36. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  37. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +1 -2
  38. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +12 -1
  39. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +19 -8
  40. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +90 -39
  41. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +24 -10
  42. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +7 -4
  43. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +71 -62
  44. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +19 -4
  45. package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +18 -1
  46. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +12 -3
  47. package/lib/view/invoicing/settingsView/settingsViewSelector.js +17 -8
  48. package/lib/view/invoicing/settingsView/submitInvoicingBrandingFormEpic.js +11 -2
  49. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +1 -1
  50. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +4 -30
  51. 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 (status === 'conflict' || status === 'claim_lost' || status === 'error');
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)?.vendorName ?? '';
59
+ return (getActualMatchingMatchById(state.actualMatchingState, matchId)
60
+ ?.vendorName ?? '');
60
61
  }
61
62
  export const matchAndReverseAccrualsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(matchAndReverseAccruals.match),
62
- // Posts reversing JEs; drop duplicate clicks while a request is in flight.
63
- exhaustMap((action) => {
64
- const { transactionId, transactionIntegrationId, transactionType, lineIds, accrualIds, transactionSyncToken, } = action.payload;
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({ matchId, results }),
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 Continue match prefills the
91
- // remaining leftover set instead of a stale cached row.
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({ status: response.status }), matchAndReverseHttpFailSnackbar(vendorName));
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, 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));
@@ -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
- closeMatchAfterUndo: false,
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
- 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
- },
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
- draft.matchModal.confirmStatus = {
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
- accrualIds,
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
- draft.matchModal.confirmStatus = allComplete
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
- draft.matchModal.lastBatchResults = results;
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
- draft.matchModal.confirmStatus = {
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
- draft.closeMatchAfterUndo = allUndone;
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.closeMatchAfterUndo = false;
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
- export function getExpenseAutomationActualMatchingMatchView(state, matchId) {
323
- const view = state.expenseAutomationActualMatchingViewState;
311
+ function toActualMatchingMatchView(match) {
324
312
  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],
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.closeMatchAfterUndo;
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: uniqueTransactionSearchResults(matchModal.pinnedTransactionResults, matchModal.transactionSearch.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: uniqueMatchModalAccruals(pinnedAccruals, searchedAccruals),
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
  }