@zeniai/client-epic-state 5.2.62 → 5.2.64

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 (42) 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 -0
  8. package/lib/epic.d.ts +2 -1
  9. package/lib/epic.js +2 -0
  10. package/lib/esm/entity/actualMatching/actualMatchingReducer.js +9 -1
  11. package/lib/esm/entity/jeSchedules/jeSchedulesPayload.js +1 -0
  12. package/lib/esm/entity/snackbar/snackbarTypes.js +1 -0
  13. package/lib/esm/epic.js +2 -0
  14. package/lib/esm/index.js +5 -5
  15. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +4 -2
  16. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +50 -0
  17. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +26 -13
  18. package/lib/esm/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +2 -23
  19. package/lib/esm/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +89 -38
  20. package/lib/esm/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +66 -64
  21. package/lib/esm/view/expenseAutomationView/types/actualMatchingViewState.js +15 -0
  22. package/lib/esm/view/spendManagement/billPay/editBillView/editBillViewSelector.js +4 -30
  23. package/lib/index.d.ts +6 -6
  24. package/lib/index.js +57 -49
  25. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.d.ts +2 -2
  26. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/fetchActualMatchingMatchEpic.js +3 -1
  27. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.d.ts +10 -0
  28. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/ignoreActualMatchingSuggestionEpic.js +54 -0
  29. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/matchAndReverseAccrualsEpic.js +24 -11
  30. package/lib/view/expenseAutomationView/epics/jeSchedule/actualMatching/searchMatchTransactionsEpic.js +1 -22
  31. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  32. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +12 -1
  33. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.d.ts +19 -8
  34. package/lib/view/expenseAutomationView/reducers/actualMatchingViewReducer.js +90 -39
  35. package/lib/view/expenseAutomationView/selectorTypes/actualMatchingViewSelectorTypes.d.ts +20 -8
  36. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.d.ts +7 -4
  37. package/lib/view/expenseAutomationView/selectors/actualMatchingViewSelector.js +69 -64
  38. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +20 -4
  39. package/lib/view/expenseAutomationView/types/actualMatchingViewState.js +18 -1
  40. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.d.ts +1 -1
  41. package/lib/view/spendManagement/billPay/editBillView/editBillViewSelector.js +4 -30
  42. package/package.json +1 -1
@@ -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,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,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,61 @@ 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.detailFetchStateByMatchId[matchId] ?? NOT_STARTED,
322
+ (root) => root.expenseAutomationActualMatchingViewState.ignoreStatusByMatchId[matchId] ?? NOT_STARTED,
323
+ (root) => root.expenseAutomationActualMatchingViewState.lastMatchResultsByMatchId?.[matchId],
324
+ (root) => root.expenseAutomationActualMatchingViewState.lastUndoResultsByMatchId?.[matchId],
325
+ (root) => root.expenseAutomationActualMatchingViewState.detailRefreshStatusByMatchId[matchId] ?? NOT_STARTED,
326
+ (root) => root.expenseAutomationActualMatchingViewState.undoReversalStatusByMatchId[matchId] ?? NOT_STARTED,
327
+ (root) => getActualMatchingMatchById(root.actualMatchingState, matchId),
328
+ ], (detailFetchState, ignoreStatus, lastMatchResults, lastUndoResults, refreshStatus, undoReversalStatus, match) => ({
329
+ detailFetchState,
330
+ ignoreStatus,
331
+ lastMatchResults,
332
+ lastUndoResults,
333
+ match: match == null ? undefined : toActualMatchingMatchView(match),
334
+ refreshStatus,
335
+ undoReversalStatus,
336
+ }));
337
+ matchViewSelectorById.set(matchId, selector);
338
+ }
339
+ return selector(state);
340
+ }
333
341
  export function getCloseMatchAfterUndo(state) {
334
- return state.expenseAutomationActualMatchingViewState.closeMatchAfterUndo;
342
+ return state.expenseAutomationActualMatchingViewState
343
+ .closeMatchAfterUndoMatchIds[0];
344
+ }
345
+ export function getCloseMatchAfterUndoMatchId(state) {
346
+ return getCloseMatchAfterUndo(state);
347
+ }
348
+ export function getCloseMatchAfterIgnoreMatchId(state) {
349
+ return state.expenseAutomationActualMatchingViewState
350
+ .closeMatchAfterIgnoreMatchIds[0];
351
+ }
352
+ export function getActualMatchingMatchModalConfirmStatus(state, matchId) {
353
+ if (matchId != null && matchId !== '') {
354
+ return (state.expenseAutomationActualMatchingViewState.confirmStatusByMatchId[matchId] ?? NOT_STARTED);
355
+ }
356
+ return state.expenseAutomationActualMatchingViewState.matchModal.confirmStatus;
335
357
  }
336
358
  export function getExpenseAutomationActualMatchingView(state) {
337
359
  const { expenseAutomationActualMatchingViewState } = state;
338
- const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, } = expenseAutomationActualMatchingViewState;
360
+ const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, ignoreStatusByMatchId, undoReversalStatusByMatchId, } = expenseAutomationActualMatchingViewState;
339
361
  const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
340
362
  const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : undefined;
341
363
  const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
@@ -345,38 +367,18 @@ export function getExpenseAutomationActualMatchingView(state) {
345
367
  .filter((match) => match.members.length > 0)
346
368
  .map(toActualMatchingRow);
347
369
  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);
370
+ const includePendingReview = uiState.statusFilter !== 'reversed';
371
+ const includeReversed = uiState.statusFilter !== 'pending_review';
372
+ const reversed = includeReversed
373
+ ? sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState)
374
+ : EMPTY_ACTUAL_MATCHING_ROWS;
375
+ const pendingReview = includePendingReview
376
+ ? sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState)
377
+ : EMPTY_ACTUAL_MATCHING_ROWS;
350
378
  const kpiSummary = buildActualMatchingKpiSummary(allRows, getTenantDisplayCurrency(state)?.currencyCode);
351
379
  const searchedAccruals = matchModal.accrualSearch.results
352
380
  .map((key) => getJEOneTimeAccrualByKey(key, state.jeSchedulesState))
353
381
  .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
382
  // `fetchState` is a flat slice field that survives a period switch. Until this
381
383
  // month's ids land, leftover Completed/Error is the previous month. Report
382
384
  // Not-Started so chrome matches Review's "cache slot missing" skeleton, not
@@ -394,16 +396,16 @@ export function getExpenseAutomationActualMatchingView(state) {
394
396
  kpiSummary,
395
397
  uiState,
396
398
  refreshStatus,
399
+ ignoreStatusByMatchId,
400
+ undoReversalStatusByMatchId,
397
401
  hasLoadedForSelectedPeriod: matchIds != null,
398
402
  matchModal: {
399
403
  isOpen: matchModal.isOpen,
400
- openedFromMatchId: matchModal.openedFromMatchId,
401
- prefill,
402
404
  confirmStatus: matchModal.confirmStatus,
403
405
  lastBatchResults: matchModal.lastBatchResults,
404
406
  transactionSearch: {
405
407
  query: matchModal.transactionSearch.query,
406
- results: uniqueTransactionSearchResults(matchModal.pinnedTransactionResults, matchModal.transactionSearch.results),
408
+ results: matchModal.transactionSearch.results,
407
409
  fetchState: {
408
410
  fetchState: matchModal.transactionSearch.fetchState,
409
411
  error: matchModal.transactionSearch.error,
@@ -413,7 +415,7 @@ export function getExpenseAutomationActualMatchingView(state) {
413
415
  },
414
416
  accrualSearch: {
415
417
  query: matchModal.accrualSearch.query,
416
- results: uniqueMatchModalAccruals(pinnedAccruals, searchedAccruals),
418
+ results: searchedAccruals,
417
419
  fetchState: {
418
420
  fetchState: matchModal.accrualSearch.fetchState,
419
421
  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, depositAccountListState, paymentAccountListState, } = state;
628
+ const { paymentAccountState, depositAccountState, paymentAccountListState } = state;
629
629
  if (fromAccountId != null) {
630
630
  const withdrawFromAsDepositAcc = getDepositAccountByDepositAccountId(depositAccountState, fromAccountId);
631
631
  const withdrawFromAsPaymentAcc = getPaymentAccountByPaymentAccountId(paymentAccountState, fromAccountId);
@@ -639,41 +639,15 @@ export const getWithdrawFrom = (state, fromAccountId, fromAccountType, companyId
639
639
  }
640
640
  }
641
641
  else {
642
- return getDefaultWithdrawFromAccount(state, depositAccountListState.accountIds, companyId ?? '', lastSelectedFromAccount);
642
+ return getDefaultWithdrawFromAccount(state, companyId ?? '', lastSelectedFromAccount);
643
643
  }
644
644
  return undefined;
645
645
  };
646
- export const getDefaultWithdrawFromAccount = (state, depositAccounts, companyId, lastSelectedFromAccount) => {
646
+ export const getDefaultWithdrawFromAccount = (state, 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
- 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
- }
650
+ const defaultAccount = primaryAccount ?? lastSelectedFromAccount;
677
651
  if (defaultAccount != null) {
678
652
  const defaultAccAsDepositAcc = getDepositAccountByDepositAccountId(depositAccountState, defaultAccount?.accountId);
679
653
  const defaultAccAsPaymentAcc = getPaymentAccountByPaymentAccountId(paymentAccountState, defaultAccount?.accountId);