@zeniai/client-epic-state 5.2.62-beta0ND → 5.2.62-beta1MM

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 +61 -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 +64 -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 +26 -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,56 @@ 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
338
+ .closeMatchAfterUndoMatchIds[0];
339
+ }
340
+ export function getCloseMatchAfterUndoMatchId(state) {
341
+ return getCloseMatchAfterUndo(state);
342
+ }
343
+ export function getCloseMatchAfterIgnoreMatchId(state) {
344
+ return state.expenseAutomationActualMatchingViewState
345
+ .closeMatchAfterIgnoreMatchIds[0];
346
+ }
347
+ export function getActualMatchingMatchModalConfirmStatus(state, matchId) {
348
+ if (matchId != null && matchId !== '') {
349
+ return (state.expenseAutomationActualMatchingViewState.confirmStatusByMatchId[matchId] ?? NOT_STARTED);
350
+ }
351
+ return state.expenseAutomationActualMatchingViewState.matchModal.confirmStatus;
335
352
  }
336
353
  export function getExpenseAutomationActualMatchingView(state) {
337
354
  const { expenseAutomationActualMatchingViewState } = state;
338
- const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, } = expenseAutomationActualMatchingViewState;
355
+ const { matchIdsByPeriod, fetchState, error, refreshStatus, uiState, matchModal, ignoreStatusByMatchId, undoReversalStatusByMatchId, } = expenseAutomationActualMatchingViewState;
339
356
  const selectedPeriod = getSelectedMonthYearForCurrentTenant(state);
340
357
  const monthYearPeriodId = selectedPeriod != null ? toMonthYearPeriodId(selectedPeriod) : undefined;
341
358
  const matchIds = monthYearPeriodId != null ? matchIdsByPeriod[monthYearPeriodId] : undefined;
@@ -345,38 +362,18 @@ export function getExpenseAutomationActualMatchingView(state) {
345
362
  .filter((match) => match.members.length > 0)
346
363
  .map(toActualMatchingRow);
347
364
  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);
365
+ const includePendingReview = uiState.statusFilter !== 'reversed';
366
+ const includeReversed = uiState.statusFilter !== 'pending_review';
367
+ const reversed = includeReversed
368
+ ? sortActualMatchingRows(rows.filter((row) => isReversedMatchKind(row.kind)), uiState)
369
+ : EMPTY_ACTUAL_MATCHING_ROWS;
370
+ const pendingReview = includePendingReview
371
+ ? sortActualMatchingRows(rows.filter((row) => row.kind === 'pending_review'), uiState)
372
+ : EMPTY_ACTUAL_MATCHING_ROWS;
350
373
  const kpiSummary = buildActualMatchingKpiSummary(allRows, getTenantDisplayCurrency(state)?.currencyCode);
351
374
  const searchedAccruals = matchModal.accrualSearch.results
352
375
  .map((key) => getJEOneTimeAccrualByKey(key, state.jeSchedulesState))
353
376
  .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
377
  // `fetchState` is a flat slice field that survives a period switch. Until this
381
378
  // month's ids land, leftover Completed/Error is the previous month. Report
382
379
  // Not-Started so chrome matches Review's "cache slot missing" skeleton, not
@@ -394,16 +391,16 @@ export function getExpenseAutomationActualMatchingView(state) {
394
391
  kpiSummary,
395
392
  uiState,
396
393
  refreshStatus,
394
+ ignoreStatusByMatchId,
395
+ undoReversalStatusByMatchId,
397
396
  hasLoadedForSelectedPeriod: matchIds != null,
398
397
  matchModal: {
399
398
  isOpen: matchModal.isOpen,
400
- openedFromMatchId: matchModal.openedFromMatchId,
401
- prefill,
402
399
  confirmStatus: matchModal.confirmStatus,
403
400
  lastBatchResults: matchModal.lastBatchResults,
404
401
  transactionSearch: {
405
402
  query: matchModal.transactionSearch.query,
406
- results: uniqueTransactionSearchResults(matchModal.pinnedTransactionResults, matchModal.transactionSearch.results),
403
+ results: matchModal.transactionSearch.results,
407
404
  fetchState: {
408
405
  fetchState: matchModal.transactionSearch.fetchState,
409
406
  error: matchModal.transactionSearch.error,
@@ -413,7 +410,7 @@ export function getExpenseAutomationActualMatchingView(state) {
413
410
  },
414
411
  accrualSearch: {
415
412
  query: matchModal.accrualSearch.query,
416
- results: uniqueMatchModalAccruals(pinnedAccruals, searchedAccruals),
413
+ results: searchedAccruals,
417
414
  fetchState: {
418
415
  fetchState: matchModal.accrualSearch.fetchState,
419
416
  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);