@zeniai/client-epic-state 4.19.37-betaVR21 → 4.19.37-betaVR23

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.
@@ -1,30 +1,39 @@
1
- import { from, of } from 'rxjs';
1
+ import { EMPTY, from, of } from 'rxjs';
2
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
3
3
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
5
- import { extractTransactionPayloadsFromBatchFiles, toBatchDetails, } from '../../payload/missingReceiptsPayload';
5
+ import { extractTransactionPayloadsFromBatchFiles, isBatchDetailsApiStatusCompleted, shouldFetchBatchDetailsForBatchId, toBatchDetails, } from '../../payload/missingReceiptsPayload';
6
6
  import { fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsFailure, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatches, } from '../../reducers/missingReceiptsViewReducer';
7
7
  export const fetchBulkUploadBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchBulkUploadBatchDetails.match), mergeMap(() => {
8
- const { currentBatchId } = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
8
+ const bulkUpload = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
9
+ const { currentBatchId } = bulkUpload;
9
10
  if (currentBatchId == null) {
10
11
  return of(fetchBulkUploadBatchDetailsFailure({
11
12
  error: createZeniAPIStatus('Unexpected Error', 'No current batch ID available'),
12
13
  }));
13
14
  }
15
+ if (!shouldFetchBatchDetailsForBatchId(bulkUpload.batchStatusById, bulkUpload.batchListByPeriod, currentBatchId)) {
16
+ return EMPTY;
17
+ }
14
18
  return zeniAPI
15
19
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${currentBatchId}`)
16
20
  .pipe(mergeMap((response) => {
17
- if (isSuccessResponse(response) && response.data != null) {
18
- const actions = [];
19
- const transactionPayloads = extractTransactionPayloadsFromBatchFiles(response.data.files);
20
- if (transactionPayloads.length > 0) {
21
- actions.push(updateTransactions(transactionPayloads, (value) => value.transaction_id));
21
+ if (!isSuccessResponse(response) ||
22
+ response.data == null ||
23
+ !isBatchDetailsApiStatusCompleted(response.data.status)) {
24
+ if (isSuccessResponse(response) && response.data != null) {
25
+ return EMPTY;
22
26
  }
23
- actions.push(fetchBulkUploadBatchDetailsSuccess(toBatchDetails(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
24
- actions.push(fetchBulkUploadBatches(true));
25
- return from(actions);
27
+ return of(fetchBulkUploadBatchDetailsFailure({ error: response.status }));
28
+ }
29
+ const actions = [];
30
+ const transactionPayloads = extractTransactionPayloadsFromBatchFiles(response.data.files);
31
+ if (transactionPayloads.length > 0) {
32
+ actions.push(updateTransactions(transactionPayloads, (value) => value.transaction_id));
26
33
  }
27
- return of(fetchBulkUploadBatchDetailsFailure({ error: response.status }));
34
+ actions.push(fetchBulkUploadBatchDetailsSuccess(toBatchDetails(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
35
+ actions.push(fetchBulkUploadBatches(true));
36
+ return from(actions);
28
37
  }), catchError((error) => of(fetchBulkUploadBatchDetailsFailure({
29
38
  error: createZeniAPIStatus('Unexpected Error', 'Fetch batch details errored out: ' + JSON.stringify(error)),
30
39
  }))));
@@ -29,6 +29,7 @@ export const fetchBulkUploadBatchesEpic = (actions$, state$, zeniAPI) => actions
29
29
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
30
30
  .pipe(mergeMap((response) => {
31
31
  if (isSuccessResponse(response) && response.data != null) {
32
+ /** Full list (all statuses) for UI; batch-details epics filter to `completed` only. */
32
33
  return of(fetchBulkUploadBatchesSuccess({
33
34
  batchList: response.data.batches.map(toBatchListItem),
34
35
  selectedPeriod,
@@ -1,10 +1,9 @@
1
- import orderBy from 'lodash/orderBy';
2
1
  import { concat, of } from 'rxjs';
3
2
  import { catchError, exhaustMap, filter } from 'rxjs/operators';
4
3
  import { toMonthYearPeriodId } from '../../../../commonStateTypes/timePeriod';
5
4
  import { getCurrentTenant } from '../../../../entity/tenant/tenantSelector';
6
5
  import { createZeniAPIStatus } from '../../../../responsePayload';
7
- import { isBatchListStatusCompleted } from '../../payload/missingReceiptsPayload';
6
+ import { filterBatchListItemsForBatchDetailsFetch } from '../../payload/missingReceiptsPayload';
8
7
  import { fetchMoreBatchDetails, fetchMoreBatchDetailsComplete, fetchMoreBatchDetailsFailure, } from '../../reducers/missingReceiptsViewReducer';
9
8
  import { fetchBatchDetailsByIds } from './fetchMultipleBatchDetailsEpic';
10
9
  const MORE_BATCH_PAGE_SIZE = 4;
@@ -21,10 +20,9 @@ exhaustMap(() => {
21
20
  const periodId = toMonthYearPeriodId(selectedPeriod);
22
21
  const batchList = bulkUpload.batchListByPeriod[periodId] ?? [];
23
22
  const failedIds = new Set(bulkUpload.failedBatchDetailIds);
24
- const unfetchedBatches = batchList.filter((batch) => isBatchListStatusCompleted(batch.status) &&
25
- bulkUpload.batchDetailsById[batch.batchId] == null &&
23
+ const unfetchedBatches = filterBatchListItemsForBatchDetailsFetch(batchList).filter((batch) => bulkUpload.batchDetailsById[batch.batchId] == null &&
26
24
  !failedIds.has(batch.batchId));
27
- const unfetchedBatchIds = orderBy(unfetchedBatches, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
25
+ const unfetchedBatchIds = unfetchedBatches.map((batch) => batch.batchId);
28
26
  if (unfetchedBatchIds.length === 0) {
29
27
  return of(fetchMoreBatchDetailsComplete());
30
28
  }
@@ -1,9 +1,8 @@
1
- import orderBy from 'lodash/orderBy';
2
1
  import { EMPTY, concat, from, of } from 'rxjs';
3
2
  import { catchError, filter, mergeMap } from 'rxjs/operators';
4
3
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
5
4
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
- import { extractTransactionPayloadsFromBatchFiles, isBatchListStatusCompleted, toBatchDetails, } from '../../payload/missingReceiptsPayload';
5
+ import { extractTransactionPayloadsFromBatchFiles, filterBatchListItemsForBatchDetailsFetch, isBatchDetailsApiStatusCompleted, toBatchDetails, } from '../../payload/missingReceiptsPayload';
7
6
  import { batchDetailFetchFailed, fetchBulkUploadBatchesSuccess, fetchMoreBatchDetailsComplete, setInitialBatchDetailsLoading, storeBatchDetails, } from '../../reducers/missingReceiptsViewReducer';
8
7
  const INITIAL_BATCH_PAGE_SIZE = 4;
9
8
  export function fetchBatchDetailsByIds(batchIds, zeniAPI) {
@@ -13,16 +12,18 @@ export function fetchBatchDetailsByIds(batchIds, zeniAPI) {
13
12
  return from(batchIds).pipe(mergeMap((batchId) => zeniAPI
14
13
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${batchId}`)
15
14
  .pipe(mergeMap((response) => {
16
- if (isSuccessResponse(response) && response.data != null) {
17
- const actions = [];
18
- const transactionPayloads = extractTransactionPayloadsFromBatchFiles(response.data.files);
19
- if (transactionPayloads.length > 0) {
20
- actions.push(updateTransactions(transactionPayloads, (value) => value.transaction_id));
21
- }
22
- actions.push(storeBatchDetails(toBatchDetails(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
23
- return from(actions);
15
+ if (!isSuccessResponse(response) ||
16
+ response.data == null ||
17
+ !isBatchDetailsApiStatusCompleted(response.data.status)) {
18
+ return EMPTY;
24
19
  }
25
- return EMPTY;
20
+ const actions = [];
21
+ const transactionPayloads = extractTransactionPayloadsFromBatchFiles(response.data.files);
22
+ if (transactionPayloads.length > 0) {
23
+ actions.push(updateTransactions(transactionPayloads, (value) => value.transaction_id));
24
+ }
25
+ actions.push(storeBatchDetails(toBatchDetails(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
26
+ return from(actions);
26
27
  }), catchError((error) => {
27
28
  console.error(`Failed to fetch batch details for ${batchId}:`, createZeniAPIStatus('Unexpected Error', 'Fetch batch details errored out: ' +
28
29
  JSON.stringify(error)));
@@ -32,10 +33,9 @@ export function fetchBatchDetailsByIds(batchIds, zeniAPI) {
32
33
  export const fetchMultipleBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchBulkUploadBatchesSuccess.match), mergeMap((action) => {
33
34
  const batchList = action.payload.batchList;
34
35
  const { batchDetailsById } = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
35
- /** Oldest first matches primary-batch selection in missingReceiptsSelector (chronological). */
36
- const completedBatchesMissingDetails = batchList.filter((batch) => isBatchListStatusCompleted(batch.status) &&
37
- batchDetailsById[batch.batchId] == null);
38
- const completedBatchIds = orderBy(completedBatchesMissingDetails, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
36
+ /** Only completed list rows may trigger batch-details API; preserve platform order. */
37
+ const completedBatchesMissingDetails = filterBatchListItemsForBatchDetailsFetch(batchList).filter((batch) => batchDetailsById[batch.batchId] == null);
38
+ const completedBatchIds = completedBatchesMissingDetails.map((batch) => batch.batchId);
39
39
  const batchIdsToFetch = completedBatchIds.slice(0, INITIAL_BATCH_PAGE_SIZE);
40
40
  if (batchIdsToFetch.length === 0) {
41
41
  return EMPTY;
@@ -2,14 +2,11 @@ import { EMPTY, from, merge, of, timer } from 'rxjs';
2
2
  import { catchError, debounceTime, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
3
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
4
4
  import { isSuccessResponse } from '../../../../responsePayload';
5
- import { extractTransactionPayloadsFromBatchFiles, toBatchDetails, } from '../../payload/missingReceiptsPayload';
5
+ import { extractTransactionPayloadsFromBatchFiles, isBatchDetailsApiStatusCompleted, toBatchDetails, } from '../../payload/missingReceiptsPayload';
6
6
  import { bulkUploadReceiptsSuccess, clearBulkUpload, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatches, pusherBatchStatusUpdate, requestMissingReceiptsTabNavigation, } from '../../reducers/missingReceiptsViewReducer';
7
7
  /** First poll after upload; then interval while batch not resolved via Pusher or API. */
8
8
  const FALLBACK_FIRST_DELAY_MS = 30000;
9
9
  const FALLBACK_POLL_INTERVAL_MS = 10000;
10
- function isBatchDetailsCompletedStatus(status) {
11
- return status.toLowerCase() === 'completed';
12
- }
13
10
  /**
14
11
  * On Pusher batch completion: refresh batch list (then fetchMultipleBatchDetailsEpic runs),
15
12
  * and request switching to the Unmatched tab. Debounced to reduce duplicate refreshes.
@@ -23,8 +20,9 @@ export const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => act
23
20
  return timer(FALLBACK_FIRST_DELAY_MS, FALLBACK_POLL_INTERVAL_MS).pipe(takeUntil(merge(actions$.pipe(filter(clearBulkUpload.match)), actions$.pipe(filter(pusherBatchStatusUpdate.match), filter((a) => a.payload.batchId === batchId)), actions$.pipe(filter(fetchBulkUploadBatchDetailsSuccess.match), filter((a) => a.payload.batchId === batchId)))), switchMap(() => zeniAPI
24
21
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${batchId}`)
25
22
  .pipe(mergeMap((response) => {
23
+ /** Same batch-details URL; polls until `status` is completed (Pusher may win first). */
26
24
  if (isSuccessResponse(response) && response.data != null) {
27
- if (isBatchDetailsCompletedStatus(response.data.status)) {
25
+ if (isBatchDetailsApiStatusCompleted(response.data.status)) {
28
26
  const actions = [];
29
27
  const transactionPayloads = extractTransactionPayloadsFromBatchFiles(response.data.files);
30
28
  if (transactionPayloads.length > 0) {
@@ -75,6 +75,49 @@ export function toBatchDetails(payload, filesEndPoint) {
75
75
  export function isBatchListStatusCompleted(status) {
76
76
  return status.toLowerCase() === 'completed';
77
77
  }
78
+ /**
79
+ * Subset of the get-all-batches (`/batches?query=...`) list that is eligible for
80
+ * `GET /batches/:batchId` details fetches. The stored list may include pending, failed, or other
81
+ * statuses for UI; only `completed` rows should trigger batch-details requests.
82
+ */
83
+ export function filterBatchListItemsForBatchDetailsFetch(batches) {
84
+ return batches.filter((b) => isBatchListStatusCompleted(b.status));
85
+ }
86
+ /**
87
+ * Batch details API response `status` field — only completed payloads should be stored from
88
+ * multi-batch fetch paths (list can be briefly stale vs. live processing state).
89
+ */
90
+ export function isBatchDetailsApiStatusCompleted(status) {
91
+ return status.toLowerCase() === 'completed';
92
+ }
93
+ export function findBatchListItemForBatchId(batchListByPeriod, batchId) {
94
+ for (const list of Object.values(batchListByPeriod)) {
95
+ const found = list.find((b) => b.batchId === batchId);
96
+ if (found != null) {
97
+ return found;
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ /**
103
+ * Whether it is appropriate to call `GET .../batches/:id` for details.
104
+ * Skips when live batch status or batch list row indicates the batch is not completed yet.
105
+ * When status is unknown in both sources, allows the request (e.g. dispatch before list refresh).
106
+ */
107
+ export function shouldFetchBatchDetailsForBatchId(batchStatusById, batchListByPeriod, batchId) {
108
+ const live = batchStatusById[batchId]?.status;
109
+ if (live === 'completed') {
110
+ return true;
111
+ }
112
+ if (live === 'pending' || live === 'processing') {
113
+ return false;
114
+ }
115
+ const listItem = findBatchListItemForBatchId(batchListByPeriod, batchId);
116
+ if (listItem != null && !isBatchListStatusCompleted(listItem.status)) {
117
+ return false;
118
+ }
119
+ return true;
120
+ }
78
121
  export function toBatchListItem(payload) {
79
122
  return {
80
123
  batchId: payload.batch_id,
@@ -56,15 +56,13 @@ export function getExpenseAutomationMissingReceiptsView(state) {
56
56
  const batchList = monthYearPeriodId != null
57
57
  ? bulkUpload.batchListByPeriod[monthYearPeriodId] ?? []
58
58
  : [];
59
- /** API returns newest-first (`sort_order: desc`); primary batch uses oldest-first order. */
60
- const batchListChronological = orderBy(batchList, (b) => new Date(b.createdAt).valueOf(), 'asc');
61
59
  /**
62
- * Primary "Unmatched" batch = first completed batch in chronological order, preferring the
60
+ * Primary "Unmatched" batch = first completed batch in platform list order, preferring the
63
61
  * first batch that still has un_matched/no_match work when any completed batch has such work.
64
62
  * Batches without details yet: wait if the list row still reports unmatched work; otherwise skip
65
- * so we do not block the whole tab while older rows are not fetched yet.
63
+ * so we do not block the whole tab while other rows are not fetched yet.
66
64
  */
67
- const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchListChronological, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
65
+ const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchList, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
68
66
  /** During upload/matching, the in-flight batch may not appear in batchList yet — prefer currentBatchId. */
69
67
  const unmatchedSectionBatchId = bulkUpload.phase === 'matching' || bulkUpload.phase === 'uploading'
70
68
  ? bulkUpload.currentBatchId ?? primaryBatchIdFromDetailsOrder
@@ -208,17 +206,17 @@ function batchListItemHasUnmatchedWork(batch) {
208
206
  return batch.noMatchCount > 0 || batch.possibleMatchesCount > 0;
209
207
  }
210
208
  /**
211
- * `batchList` must be in chronological order (oldest first). Picks the primary batch for the
212
- * top "Unmatched" section. Skips failed detail fetches.
209
+ * `batchList` must match platform order (same sequence as the bulk-upload batches API). Picks the
210
+ * primary batch for the top "Unmatched" section. Skips failed detail fetches.
213
211
  *
214
- * If details are not loaded for an older batch but the list row reports no unmatched work, we
215
- * skip that batch so newer batches with loaded details can be primary (avoids an empty tab when
216
- * the first fetch page only loaded newer batches). If the list reports unmatched work but details
217
- * are still loading, we wait (undefined) so primary order stays correct once details arrive.
212
+ * If details are not loaded for an earlier list row but that row reports no unmatched work, we
213
+ * skip it so later rows with loaded details can be primary (avoids an empty tab when the first
214
+ * fetch page only loaded later batches). If the list reports unmatched work but details are still
215
+ * loading, we wait (undefined) so primary order stays correct once details arrive.
218
216
  */
219
- function getPrimaryBatchIdFromBatchListOrder(batchListChronological, batchDetailsById, failedBatchDetailIds) {
217
+ function getPrimaryBatchIdFromBatchListOrder(batchListInPlatformOrder, batchDetailsById, failedBatchDetailIds) {
220
218
  const failedIds = new Set(failedBatchDetailIds);
221
- const anyCompletedBatchHasUnmatchedWork = batchListChronological.some((b) => {
219
+ const anyCompletedBatchHasUnmatchedWork = batchListInPlatformOrder.some((b) => {
222
220
  if (failedIds.has(b.batchId)) {
223
221
  return false;
224
222
  }
@@ -230,7 +228,7 @@ function getPrimaryBatchIdFromBatchListOrder(batchListChronological, batchDetail
230
228
  }
231
229
  return d == null && batchListItemHasUnmatchedWork(b);
232
230
  });
233
- for (const batch of batchListChronological) {
231
+ for (const batch of batchListInPlatformOrder) {
234
232
  if (failedIds.has(batch.batchId)) {
235
233
  continue;
236
234
  }
@@ -8,26 +8,35 @@ const responsePayload_1 = require("../../../../responsePayload");
8
8
  const missingReceiptsPayload_1 = require("../../payload/missingReceiptsPayload");
9
9
  const missingReceiptsViewReducer_1 = require("../../reducers/missingReceiptsViewReducer");
10
10
  const fetchBulkUploadBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.fetchBulkUploadBatchDetails.match), (0, operators_1.mergeMap)(() => {
11
- const { currentBatchId } = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
11
+ const bulkUpload = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
12
+ const { currentBatchId } = bulkUpload;
12
13
  if (currentBatchId == null) {
13
14
  return (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsFailure)({
14
15
  error: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'No current batch ID available'),
15
16
  }));
16
17
  }
18
+ if (!(0, missingReceiptsPayload_1.shouldFetchBatchDetailsForBatchId)(bulkUpload.batchStatusById, bulkUpload.batchListByPeriod, currentBatchId)) {
19
+ return rxjs_1.EMPTY;
20
+ }
17
21
  return zeniAPI
18
22
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${currentBatchId}`)
19
23
  .pipe((0, operators_1.mergeMap)((response) => {
20
- if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
21
- const actions = [];
22
- const transactionPayloads = (0, missingReceiptsPayload_1.extractTransactionPayloadsFromBatchFiles)(response.data.files);
23
- if (transactionPayloads.length > 0) {
24
- actions.push((0, transactionReducer_1.updateTransactions)(transactionPayloads, (value) => value.transaction_id));
24
+ if (!(0, responsePayload_1.isSuccessResponse)(response) ||
25
+ response.data == null ||
26
+ !(0, missingReceiptsPayload_1.isBatchDetailsApiStatusCompleted)(response.data.status)) {
27
+ if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
28
+ return rxjs_1.EMPTY;
25
29
  }
26
- actions.push((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsSuccess)((0, missingReceiptsPayload_1.toBatchDetails)(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
27
- actions.push((0, missingReceiptsViewReducer_1.fetchBulkUploadBatches)(true));
28
- return (0, rxjs_1.from)(actions);
30
+ return (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsFailure)({ error: response.status }));
31
+ }
32
+ const actions = [];
33
+ const transactionPayloads = (0, missingReceiptsPayload_1.extractTransactionPayloadsFromBatchFiles)(response.data.files);
34
+ if (transactionPayloads.length > 0) {
35
+ actions.push((0, transactionReducer_1.updateTransactions)(transactionPayloads, (value) => value.transaction_id));
29
36
  }
30
- return (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsFailure)({ error: response.status }));
37
+ actions.push((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsSuccess)((0, missingReceiptsPayload_1.toBatchDetails)(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
38
+ actions.push((0, missingReceiptsViewReducer_1.fetchBulkUploadBatches)(true));
39
+ return (0, rxjs_1.from)(actions);
31
40
  }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsFailure)({
32
41
  error: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch batch details errored out: ' + JSON.stringify(error)),
33
42
  }))));
@@ -32,6 +32,7 @@ const fetchBulkUploadBatchesEpic = (actions$, state$, zeniAPI) => actions$.pipe(
32
32
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches?query=${encodeURIComponent(JSON.stringify(queryParam))}`)
33
33
  .pipe((0, operators_1.mergeMap)((response) => {
34
34
  if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
35
+ /** Full list (all statuses) for UI; batch-details epics filter to `completed` only. */
35
36
  return (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchBulkUploadBatchesSuccess)({
36
37
  batchList: response.data.batches.map(missingReceiptsPayload_1.toBatchListItem),
37
38
  selectedPeriod,
@@ -1,10 +1,6 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.fetchMoreBatchDetailsEpic = void 0;
7
- const orderBy_1 = __importDefault(require("lodash/orderBy"));
8
4
  const rxjs_1 = require("rxjs");
9
5
  const operators_1 = require("rxjs/operators");
10
6
  const timePeriod_1 = require("../../../../commonStateTypes/timePeriod");
@@ -27,10 +23,9 @@ const fetchMoreBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((
27
23
  const periodId = (0, timePeriod_1.toMonthYearPeriodId)(selectedPeriod);
28
24
  const batchList = bulkUpload.batchListByPeriod[periodId] ?? [];
29
25
  const failedIds = new Set(bulkUpload.failedBatchDetailIds);
30
- const unfetchedBatches = batchList.filter((batch) => (0, missingReceiptsPayload_1.isBatchListStatusCompleted)(batch.status) &&
31
- bulkUpload.batchDetailsById[batch.batchId] == null &&
26
+ const unfetchedBatches = (0, missingReceiptsPayload_1.filterBatchListItemsForBatchDetailsFetch)(batchList).filter((batch) => bulkUpload.batchDetailsById[batch.batchId] == null &&
32
27
  !failedIds.has(batch.batchId));
33
- const unfetchedBatchIds = (0, orderBy_1.default)(unfetchedBatches, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
28
+ const unfetchedBatchIds = unfetchedBatches.map((batch) => batch.batchId);
34
29
  if (unfetchedBatchIds.length === 0) {
35
30
  return (0, rxjs_1.of)((0, missingReceiptsViewReducer_1.fetchMoreBatchDetailsComplete)());
36
31
  }
@@ -1,11 +1,7 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.fetchMultipleBatchDetailsEpic = void 0;
7
4
  exports.fetchBatchDetailsByIds = fetchBatchDetailsByIds;
8
- const orderBy_1 = __importDefault(require("lodash/orderBy"));
9
5
  const rxjs_1 = require("rxjs");
10
6
  const operators_1 = require("rxjs/operators");
11
7
  const transactionReducer_1 = require("../../../../entity/transaction/transactionReducer");
@@ -20,16 +16,18 @@ function fetchBatchDetailsByIds(batchIds, zeniAPI) {
20
16
  return (0, rxjs_1.from)(batchIds).pipe((0, operators_1.mergeMap)((batchId) => zeniAPI
21
17
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${batchId}`)
22
18
  .pipe((0, operators_1.mergeMap)((response) => {
23
- if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
24
- const actions = [];
25
- const transactionPayloads = (0, missingReceiptsPayload_1.extractTransactionPayloadsFromBatchFiles)(response.data.files);
26
- if (transactionPayloads.length > 0) {
27
- actions.push((0, transactionReducer_1.updateTransactions)(transactionPayloads, (value) => value.transaction_id));
28
- }
29
- actions.push((0, missingReceiptsViewReducer_1.storeBatchDetails)((0, missingReceiptsPayload_1.toBatchDetails)(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
30
- return (0, rxjs_1.from)(actions);
19
+ if (!(0, responsePayload_1.isSuccessResponse)(response) ||
20
+ response.data == null ||
21
+ !(0, missingReceiptsPayload_1.isBatchDetailsApiStatusCompleted)(response.data.status)) {
22
+ return rxjs_1.EMPTY;
31
23
  }
32
- return rxjs_1.EMPTY;
24
+ const actions = [];
25
+ const transactionPayloads = (0, missingReceiptsPayload_1.extractTransactionPayloadsFromBatchFiles)(response.data.files);
26
+ if (transactionPayloads.length > 0) {
27
+ actions.push((0, transactionReducer_1.updateTransactions)(transactionPayloads, (value) => value.transaction_id));
28
+ }
29
+ actions.push((0, missingReceiptsViewReducer_1.storeBatchDetails)((0, missingReceiptsPayload_1.toBatchDetails)(response.data, zeniAPI.apiEndPoints.fileMicroServiceBaseUrl)));
30
+ return (0, rxjs_1.from)(actions);
33
31
  }), (0, operators_1.catchError)((error) => {
34
32
  console.error(`Failed to fetch batch details for ${batchId}:`, (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch batch details errored out: ' +
35
33
  JSON.stringify(error)));
@@ -39,10 +37,9 @@ function fetchBatchDetailsByIds(batchIds, zeniAPI) {
39
37
  const fetchMultipleBatchDetailsEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.fetchBulkUploadBatchesSuccess.match), (0, operators_1.mergeMap)((action) => {
40
38
  const batchList = action.payload.batchList;
41
39
  const { batchDetailsById } = state$.value.expenseAutomationMissingReceiptsViewState.bulkUpload;
42
- /** Oldest first matches primary-batch selection in missingReceiptsSelector (chronological). */
43
- const completedBatchesMissingDetails = batchList.filter((batch) => (0, missingReceiptsPayload_1.isBatchListStatusCompleted)(batch.status) &&
44
- batchDetailsById[batch.batchId] == null);
45
- const completedBatchIds = (0, orderBy_1.default)(completedBatchesMissingDetails, (b) => new Date(b.createdAt).valueOf(), 'asc').map((batch) => batch.batchId);
40
+ /** Only completed list rows may trigger batch-details API; preserve platform order. */
41
+ const completedBatchesMissingDetails = (0, missingReceiptsPayload_1.filterBatchListItemsForBatchDetailsFetch)(batchList).filter((batch) => batchDetailsById[batch.batchId] == null);
42
+ const completedBatchIds = completedBatchesMissingDetails.map((batch) => batch.batchId);
46
43
  const batchIdsToFetch = completedBatchIds.slice(0, INITIAL_BATCH_PAGE_SIZE);
47
44
  if (batchIdsToFetch.length === 0) {
48
45
  return rxjs_1.EMPTY;
@@ -10,9 +10,6 @@ const missingReceiptsViewReducer_1 = require("../../reducers/missingReceiptsView
10
10
  /** First poll after upload; then interval while batch not resolved via Pusher or API. */
11
11
  const FALLBACK_FIRST_DELAY_MS = 30000;
12
12
  const FALLBACK_POLL_INTERVAL_MS = 10000;
13
- function isBatchDetailsCompletedStatus(status) {
14
- return status.toLowerCase() === 'completed';
15
- }
16
13
  /**
17
14
  * On Pusher batch completion: refresh batch list (then fetchMultipleBatchDetailsEpic runs),
18
15
  * and request switching to the Unmatched tab. Debounced to reduce duplicate refreshes.
@@ -27,8 +24,9 @@ const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => actions$.p
27
24
  return (0, rxjs_1.timer)(FALLBACK_FIRST_DELAY_MS, FALLBACK_POLL_INTERVAL_MS).pipe((0, operators_1.takeUntil)((0, rxjs_1.merge)(actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.clearBulkUpload.match)), actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.pusherBatchStatusUpdate.match), (0, operators_1.filter)((a) => a.payload.batchId === batchId)), actions$.pipe((0, operators_1.filter)(missingReceiptsViewReducer_1.fetchBulkUploadBatchDetailsSuccess.match), (0, operators_1.filter)((a) => a.payload.batchId === batchId)))), (0, operators_1.switchMap)(() => zeniAPI
28
25
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${batchId}`)
29
26
  .pipe((0, operators_1.mergeMap)((response) => {
27
+ /** Same batch-details URL; polls until `status` is completed (Pusher may win first). */
30
28
  if ((0, responsePayload_1.isSuccessResponse)(response) && response.data != null) {
31
- if (isBatchDetailsCompletedStatus(response.data.status)) {
29
+ if ((0, missingReceiptsPayload_1.isBatchDetailsApiStatusCompleted)(response.data.status)) {
32
30
  const actions = [];
33
31
  const transactionPayloads = (0, missingReceiptsPayload_1.extractTransactionPayloadsFromBatchFiles)(response.data.files);
34
32
  if (transactionPayloads.length > 0) {
@@ -2,7 +2,8 @@ import { URLPayload } from '../../../commonPayloadTypes/urlPayload';
2
2
  import { TransactionPayload } from '../../../entity/transaction/payloadTypes/transactionPayload';
3
3
  import { SupportedTransactionPayload } from '../../../entity/transaction/transactionState';
4
4
  import { ZeniAPIResponse } from '../../../responsePayload';
5
- import type { BatchDetails, BatchFile, BatchListItem, BatchSummary, ManualSearchResult } from '../types/missingReceiptsViewState';
5
+ import type { MonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
6
+ import type { BatchDetails, BatchFile, BatchListItem, BatchStatus, BatchSummary, ManualSearchResult } from '../types/missingReceiptsViewState';
6
7
  export interface MissingReceiptsQueryPayload {
7
8
  end_date: string;
8
9
  is_attachment_missing_only: boolean;
@@ -83,6 +84,24 @@ export declare function toBatchSummary(payload: BatchDetailsResponsePayload['sum
83
84
  export declare function toBatchDetails(payload: BatchDetailsResponsePayload, filesEndPoint?: string): BatchDetails;
84
85
  /** Batch list `status` is a string from the API — compare case-insensitively. */
85
86
  export declare function isBatchListStatusCompleted(status: string): boolean;
87
+ /**
88
+ * Subset of the get-all-batches (`/batches?query=...`) list that is eligible for
89
+ * `GET /batches/:batchId` details fetches. The stored list may include pending, failed, or other
90
+ * statuses for UI; only `completed` rows should trigger batch-details requests.
91
+ */
92
+ export declare function filterBatchListItemsForBatchDetailsFetch(batches: BatchListItem[]): BatchListItem[];
93
+ /**
94
+ * Batch details API response `status` field — only completed payloads should be stored from
95
+ * multi-batch fetch paths (list can be briefly stale vs. live processing state).
96
+ */
97
+ export declare function isBatchDetailsApiStatusCompleted(status: string): boolean;
98
+ export declare function findBatchListItemForBatchId(batchListByPeriod: Record<MonthYearPeriodId, BatchListItem[]>, batchId: string): BatchListItem | undefined;
99
+ /**
100
+ * Whether it is appropriate to call `GET .../batches/:id` for details.
101
+ * Skips when live batch status or batch list row indicates the batch is not completed yet.
102
+ * When status is unknown in both sources, allows the request (e.g. dispatch before list refresh).
103
+ */
104
+ export declare function shouldFetchBatchDetailsForBatchId(batchStatusById: Record<string, BatchStatus>, batchListByPeriod: Record<MonthYearPeriodId, BatchListItem[]>, batchId: string): boolean;
86
105
  export declare function toBatchListItem(payload: BatchListItemPayload): BatchListItem;
87
106
  /** Maps expense-automation transaction rows to manual-search list rows (Unmatched tab). */
88
107
  export declare function supportedTransactionPayloadToManualSearchResult(payload: SupportedTransactionPayload): ManualSearchResult;
@@ -5,6 +5,10 @@ exports.extractTransactionPayloadsFromBatchFiles = extractTransactionPayloadsFro
5
5
  exports.toBatchSummary = toBatchSummary;
6
6
  exports.toBatchDetails = toBatchDetails;
7
7
  exports.isBatchListStatusCompleted = isBatchListStatusCompleted;
8
+ exports.filterBatchListItemsForBatchDetailsFetch = filterBatchListItemsForBatchDetailsFetch;
9
+ exports.isBatchDetailsApiStatusCompleted = isBatchDetailsApiStatusCompleted;
10
+ exports.findBatchListItemForBatchId = findBatchListItemForBatchId;
11
+ exports.shouldFetchBatchDetailsForBatchId = shouldFetchBatchDetailsForBatchId;
8
12
  exports.toBatchListItem = toBatchListItem;
9
13
  exports.supportedTransactionPayloadToManualSearchResult = supportedTransactionPayloadToManualSearchResult;
10
14
  const urlPayload_1 = require("../../../commonPayloadTypes/urlPayload");
@@ -84,6 +88,49 @@ function toBatchDetails(payload, filesEndPoint) {
84
88
  function isBatchListStatusCompleted(status) {
85
89
  return status.toLowerCase() === 'completed';
86
90
  }
91
+ /**
92
+ * Subset of the get-all-batches (`/batches?query=...`) list that is eligible for
93
+ * `GET /batches/:batchId` details fetches. The stored list may include pending, failed, or other
94
+ * statuses for UI; only `completed` rows should trigger batch-details requests.
95
+ */
96
+ function filterBatchListItemsForBatchDetailsFetch(batches) {
97
+ return batches.filter((b) => isBatchListStatusCompleted(b.status));
98
+ }
99
+ /**
100
+ * Batch details API response `status` field — only completed payloads should be stored from
101
+ * multi-batch fetch paths (list can be briefly stale vs. live processing state).
102
+ */
103
+ function isBatchDetailsApiStatusCompleted(status) {
104
+ return status.toLowerCase() === 'completed';
105
+ }
106
+ function findBatchListItemForBatchId(batchListByPeriod, batchId) {
107
+ for (const list of Object.values(batchListByPeriod)) {
108
+ const found = list.find((b) => b.batchId === batchId);
109
+ if (found != null) {
110
+ return found;
111
+ }
112
+ }
113
+ return undefined;
114
+ }
115
+ /**
116
+ * Whether it is appropriate to call `GET .../batches/:id` for details.
117
+ * Skips when live batch status or batch list row indicates the batch is not completed yet.
118
+ * When status is unknown in both sources, allows the request (e.g. dispatch before list refresh).
119
+ */
120
+ function shouldFetchBatchDetailsForBatchId(batchStatusById, batchListByPeriod, batchId) {
121
+ const live = batchStatusById[batchId]?.status;
122
+ if (live === 'completed') {
123
+ return true;
124
+ }
125
+ if (live === 'pending' || live === 'processing') {
126
+ return false;
127
+ }
128
+ const listItem = findBatchListItemForBatchId(batchListByPeriod, batchId);
129
+ if (listItem != null && !isBatchListStatusCompleted(listItem.status)) {
130
+ return false;
131
+ }
132
+ return true;
133
+ }
87
134
  function toBatchListItem(payload) {
88
135
  return {
89
136
  batchId: payload.batch_id,
@@ -62,15 +62,13 @@ function getExpenseAutomationMissingReceiptsView(state) {
62
62
  const batchList = monthYearPeriodId != null
63
63
  ? bulkUpload.batchListByPeriod[monthYearPeriodId] ?? []
64
64
  : [];
65
- /** API returns newest-first (`sort_order: desc`); primary batch uses oldest-first order. */
66
- const batchListChronological = (0, orderBy_1.default)(batchList, (b) => new Date(b.createdAt).valueOf(), 'asc');
67
65
  /**
68
- * Primary "Unmatched" batch = first completed batch in chronological order, preferring the
66
+ * Primary "Unmatched" batch = first completed batch in platform list order, preferring the
69
67
  * first batch that still has un_matched/no_match work when any completed batch has such work.
70
68
  * Batches without details yet: wait if the list row still reports unmatched work; otherwise skip
71
- * so we do not block the whole tab while older rows are not fetched yet.
69
+ * so we do not block the whole tab while other rows are not fetched yet.
72
70
  */
73
- const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchListChronological, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
71
+ const primaryBatchIdFromDetailsOrder = getPrimaryBatchIdFromBatchListOrder(batchList, bulkUpload.batchDetailsById, bulkUpload.failedBatchDetailIds);
74
72
  /** During upload/matching, the in-flight batch may not appear in batchList yet — prefer currentBatchId. */
75
73
  const unmatchedSectionBatchId = bulkUpload.phase === 'matching' || bulkUpload.phase === 'uploading'
76
74
  ? bulkUpload.currentBatchId ?? primaryBatchIdFromDetailsOrder
@@ -214,17 +212,17 @@ function batchListItemHasUnmatchedWork(batch) {
214
212
  return batch.noMatchCount > 0 || batch.possibleMatchesCount > 0;
215
213
  }
216
214
  /**
217
- * `batchList` must be in chronological order (oldest first). Picks the primary batch for the
218
- * top "Unmatched" section. Skips failed detail fetches.
215
+ * `batchList` must match platform order (same sequence as the bulk-upload batches API). Picks the
216
+ * primary batch for the top "Unmatched" section. Skips failed detail fetches.
219
217
  *
220
- * If details are not loaded for an older batch but the list row reports no unmatched work, we
221
- * skip that batch so newer batches with loaded details can be primary (avoids an empty tab when
222
- * the first fetch page only loaded newer batches). If the list reports unmatched work but details
223
- * are still loading, we wait (undefined) so primary order stays correct once details arrive.
218
+ * If details are not loaded for an earlier list row but that row reports no unmatched work, we
219
+ * skip it so later rows with loaded details can be primary (avoids an empty tab when the first
220
+ * fetch page only loaded later batches). If the list reports unmatched work but details are still
221
+ * loading, we wait (undefined) so primary order stays correct once details arrive.
224
222
  */
225
- function getPrimaryBatchIdFromBatchListOrder(batchListChronological, batchDetailsById, failedBatchDetailIds) {
223
+ function getPrimaryBatchIdFromBatchListOrder(batchListInPlatformOrder, batchDetailsById, failedBatchDetailIds) {
226
224
  const failedIds = new Set(failedBatchDetailIds);
227
- const anyCompletedBatchHasUnmatchedWork = batchListChronological.some((b) => {
225
+ const anyCompletedBatchHasUnmatchedWork = batchListInPlatformOrder.some((b) => {
228
226
  if (failedIds.has(b.batchId)) {
229
227
  return false;
230
228
  }
@@ -236,7 +234,7 @@ function getPrimaryBatchIdFromBatchListOrder(batchListChronological, batchDetail
236
234
  }
237
235
  return d == null && batchListItemHasUnmatchedWork(b);
238
236
  });
239
- for (const batch of batchListChronological) {
237
+ for (const batch of batchListInPlatformOrder) {
240
238
  if (failedIds.has(batch.batchId)) {
241
239
  continue;
242
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "4.19.37-betaVR21",
3
+ "version": "4.19.37-betaVR23",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",