@zeniai/client-epic-state 4.19.84-betaVR6 → 4.19.84-betaVR8

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.
@@ -12,9 +12,13 @@ export const bulkUploadReceiptsEpic = (actions$, _state$, zeniAPI) => actions$.p
12
12
  .postFormData(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/receipts/bulk-upload`, formData)
13
13
  .pipe(mergeMap((response) => {
14
14
  if (isSuccessResponse(response) && response.data != null) {
15
- return of(bulkUploadReceiptsSuccess({
16
- batchId: response.data.batch_id,
17
- }));
15
+ const batchId = String(response.data.batch_id ?? '').trim();
16
+ if (batchId === '') {
17
+ return of(bulkUploadReceiptsFailure({
18
+ error: createZeniAPIStatus('Unexpected Error', 'Bulk upload succeeded but batch_id was missing or empty.'),
19
+ }));
20
+ }
21
+ return of(bulkUploadReceiptsSuccess({ batchId }));
18
22
  }
19
23
  return of(bulkUploadReceiptsFailure({ error: response.status }));
20
24
  }), catchError((error) => of(bulkUploadReceiptsFailure({
@@ -1,5 +1,6 @@
1
1
  import { EMPTY, from, merge, of, timer } from 'rxjs';
2
2
  import { catchError, debounceTime, filter, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
+ import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
3
4
  import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
4
5
  import { isSuccessResponse } from '../../../../responsePayload';
5
6
  import { extractTransactionPayloadsFromBatchFiles, isBatchDetailsApiStatusCompleted, toBatchDetails, } from '../../payload/missingReceiptsPayload';
@@ -18,8 +19,14 @@ export const pusherBatchStatusCompletionEpic = (actions$) => actions$.pipe(filte
18
19
  }),
19
20
  requestMissingReceiptsTabNavigation({ tab: 'unmatched' }),
20
21
  ])));
22
+ /**
23
+ * Fallback polling when Pusher is slow or unavailable. Network errors on a poll tick show one
24
+ * error snackbar per upload session (first failure only) so the user is not spammed every
25
+ * interval; Pusher or a later successful poll still completes the flow.
26
+ */
21
27
  export const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(bulkUploadReceiptsSuccess.match), mergeMap((action) => {
22
28
  const { batchId } = action.payload;
29
+ let pollErrorNotified = false;
23
30
  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
31
  .getJSON(`${zeniAPI.apiEndPoints.communicationAgentMicroServiceBaseUrl}/1.0/batches/${batchId}`)
25
32
  .pipe(mergeMap((response) => {
@@ -38,5 +45,15 @@ export const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => act
38
45
  }
39
46
  }
40
47
  return EMPTY;
41
- }), catchError(() => of()))));
48
+ }), catchError(() => {
49
+ if (!pollErrorNotified) {
50
+ pollErrorNotified = true;
51
+ return of(openSnackbar({
52
+ messageSection: 'receipts_upload',
53
+ messageText: 'failed',
54
+ type: 'error',
55
+ }));
56
+ }
57
+ return EMPTY;
58
+ }))));
42
59
  }));
@@ -1,6 +1,36 @@
1
1
  import { toURL } from '../../../commonPayloadTypes/urlPayload';
2
2
  import { getTransactionPayloadAmount, } from '../../../entity/transaction/payloadTypes/transactionPayload';
3
3
  // -- Converter functions --
4
+ /**
5
+ * Maps batch-details API `files[].status` strings to canonical {@link BatchFile} statuses.
6
+ * The service may send aliases (`unmatched`, `possible_match`) or different separators; the UI
7
+ * only treats `un_matched` / `no_match` as work for the Unmatched tab.
8
+ *
9
+ * **Unknown values:** Any string not matched below is returned as-is (typed as
10
+ * {@link BatchFile} status). Callers should not assume exhaustiveness—treat unknown statuses in
11
+ * UI as “other” (e.g. show a generic label or fall back to a safe bucket) until the API contract
12
+ * adds an explicit mapping.
13
+ */
14
+ export function normalizeBatchFileStatus(raw) {
15
+ const s = raw.trim().toLowerCase().replace(/-/g, '_');
16
+ switch (s) {
17
+ case 'un_matched':
18
+ case 'unmatched':
19
+ return 'un_matched';
20
+ case 'no_match':
21
+ case 'nomatch':
22
+ return 'no_match';
23
+ case 'matched':
24
+ return 'matched';
25
+ case 'failed':
26
+ return 'failed';
27
+ case 'possible_match':
28
+ case 'possiblematch':
29
+ return 'un_matched';
30
+ default:
31
+ return raw;
32
+ }
33
+ }
4
34
  export function toBatchFile(payload, filesEndPoint) {
5
35
  return {
6
36
  attachmentId: payload.attachment_id,
@@ -16,7 +46,7 @@ export function toBatchFile(payload, filesEndPoint) {
16
46
  filename: payload.filename,
17
47
  matchSource: payload.match_source,
18
48
  matchedTransactionId: payload.matched_transaction?.transaction_id,
19
- status: payload.status,
49
+ status: normalizeBatchFileStatus(payload.status),
20
50
  };
21
51
  }
22
52
  export function extractTransactionPayloadsFromBatchFiles(files) {
@@ -1,5 +1,6 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
3
+ import { MIN_MANUAL_TRANSACTION_SEARCH_LENGTH, } from '../types/missingReceiptsViewState';
3
4
  export const getCompletedTransactionsCacheKey = (periodId, sortKey, sortOrder, subTab) => `completed-${subTab}-${sortKey}-${sortOrder === 'ascending' ? 'asc' : 'desc'}-${periodId}`;
4
5
  export const initialBulkUploadState = {
5
6
  batchDetailsById: {},
@@ -386,13 +387,20 @@ const expenseAutomationMissingReceiptsView = createSlice({
386
387
  const isLoadMore = pageToken != null;
387
388
  const trimmed = query.trim();
388
389
  /**
389
- * Cleared or too-short query: no API (see epic). Reset fully (no In-Progress) so loading
390
- * UI never shows; bump key so inputs remount.
390
+ * Cleared or too-short query: no API (see epic). Reset fully (no In-Progress).
391
+ * Only bump `manualSearchUiResetKey` when clearing a real search — not when the UI
392
+ * dispatches `""` on every keystroke while length is below MIN (web-components always sends
393
+ * `onSearch("")` in that case), or ReceiptCard remounts and the input loses focus.
391
394
  */
392
395
  if (!isLoadMore &&
393
- trimmed.length < 2) {
396
+ trimmed.length < MIN_MANUAL_TRANSACTION_SEARCH_LENGTH) {
397
+ const hadActiveSearch = draft.bulkUpload.manualSearch.searchQuery.trim().length >=
398
+ MIN_MANUAL_TRANSACTION_SEARCH_LENGTH ||
399
+ draft.bulkUpload.manualSearch.results.length > 0;
394
400
  draft.bulkUpload.manualSearch = initialBulkUploadState.manualSearch;
395
- draft.bulkUpload.manualSearchUiResetKey += 1;
401
+ if (hadActiveSearch) {
402
+ draft.bulkUpload.manualSearchUiResetKey += 1;
403
+ }
396
404
  return;
397
405
  }
398
406
  draft.bulkUpload.manualSearch.searchQuery = query;
@@ -7,3 +7,9 @@ const MISSING_RECEIPTS_SORT_KEYS = [
7
7
  'amount',
8
8
  ];
9
9
  export const toMissingReceiptsSortKey = (v) => stringToUnion(v, MISSING_RECEIPTS_SORT_KEYS);
10
+ // -- Bulk Receipt Upload Types --
11
+ /**
12
+ * Minimum trimmed query length before manual-match search calls the expense-automation API.
13
+ * Kept in sync with `ManualSearchInput` in web-components (fires search only when length ≥ this).
14
+ */
15
+ export const MIN_MANUAL_TRANSACTION_SEARCH_LENGTH = 2;