@zeniai/client-epic-state 4.19.37-betaAS2 → 4.19.37-betaVR1

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 (61) hide show
  1. package/lib/epic.d.ts +8 -1
  2. package/lib/epic.js +8 -1
  3. package/lib/esm/epic.js +8 -1
  4. package/lib/esm/index.js +8 -6
  5. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.js +28 -0
  6. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/confirmBulkUploadMatchEpic.js +25 -0
  7. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.js +31 -0
  8. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +16 -0
  9. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +48 -0
  10. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +27 -0
  11. package/lib/esm/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +41 -0
  12. package/lib/esm/view/expenseAutomationView/payload/missingReceiptsPayload.js +164 -1
  13. package/lib/esm/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +245 -1
  14. package/lib/esm/view/expenseAutomationView/selectors/missingReceiptsSelector.js +114 -1
  15. package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewReducer.js +1 -10
  16. package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewSelector.js +2 -4
  17. package/lib/esm/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.js +2 -5
  18. package/lib/esm/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js +1 -10
  19. package/lib/esm/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewSelector.js +2 -4
  20. package/lib/esm/zeniAPI.js +41 -3
  21. package/lib/index.d.ts +11 -9
  22. package/lib/index.js +57 -32
  23. package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
  24. package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.d.ts +18 -0
  25. package/lib/view/expenseAutomationView/epics/missingReceipts/bulkUploadReceiptsEpic.js +32 -0
  26. package/lib/view/expenseAutomationView/epics/missingReceipts/confirmBulkUploadMatchEpic.d.ts +6 -0
  27. package/lib/view/expenseAutomationView/epics/missingReceipts/confirmBulkUploadMatchEpic.js +29 -0
  28. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.d.ts +8 -0
  29. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchDetailsEpic.js +35 -0
  30. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.d.ts +6 -0
  31. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchBulkUploadBatchesEpic.js +20 -0
  32. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.d.ts +8 -0
  33. package/lib/view/expenseAutomationView/epics/missingReceipts/fetchCompletedTransactionsEpic.js +52 -0
  34. package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.d.ts +6 -0
  35. package/lib/view/expenseAutomationView/epics/missingReceipts/searchTransactionsForManualMatchEpic.js +31 -0
  36. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.d.ts +15 -0
  37. package/lib/view/expenseAutomationView/epics/missingReceipts/watchBulkUploadBatchStatusEpic.js +46 -0
  38. package/lib/view/expenseAutomationView/payload/missingReceiptsPayload.d.ts +168 -0
  39. package/lib/view/expenseAutomationView/payload/missingReceiptsPayload.js +172 -0
  40. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.d.ts +43 -3
  41. package/lib/view/expenseAutomationView/reducers/missingReceiptsViewReducer.js +246 -2
  42. package/lib/view/expenseAutomationView/selectorTypes/missingReceiptsSelectorTypes.d.ts +60 -1
  43. package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.d.ts +1 -1
  44. package/lib/view/expenseAutomationView/selectors/missingReceiptsSelector.js +113 -0
  45. package/lib/view/expenseAutomationView/types/missingReceiptsViewState.d.ts +102 -0
  46. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewReducer.d.ts +1 -1
  47. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewReducer.js +2 -11
  48. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewSelector.js +2 -4
  49. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewSelectorTypes.d.ts +0 -2
  50. package/lib/view/spendManagement/billPay/billPaySetupApproverView/billPaySetupApproverViewState.d.ts +0 -1
  51. package/lib/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.d.ts +0 -1
  52. package/lib/view/spendManagement/billPay/billPaySetupApproverView/types/commonPayload.js +2 -5
  53. package/lib/view/spendManagement/billPay/billPaySetupApproverView/types/commonState.d.ts +0 -2
  54. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.d.ts +1 -1
  55. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewReducer.js +2 -11
  56. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewSelector.js +2 -4
  57. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewSelectorTypes.d.ts +0 -2
  58. package/lib/view/spendManagement/reimbursement/remiSetupApproverView/remiSetupApproverViewState.d.ts +0 -1
  59. package/lib/zeniAPI.d.ts +9 -0
  60. package/lib/zeniAPI.js +40 -2
  61. package/package.json +1 -1
@@ -0,0 +1,48 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { convertToPeriod } from '../../../../commonStateTypes/timePeriod';
4
+ import { updateTransactions } from '../../../../entity/transaction/transactionReducer';
5
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
6
+ import { completedTransactionPayloadToSupportedTransactionPayload, } from '../../payload/missingReceiptsPayload';
7
+ import { fetchCompletedTransactions, fetchCompletedTransactionsFailure, fetchCompletedTransactionsSuccess, } from '../../reducers/missingReceiptsViewReducer';
8
+ export const fetchCompletedTransactionsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchCompletedTransactions.match), switchMap((action) => {
9
+ const state = state$.value;
10
+ const { expenseAutomationMissingReceiptsViewState: { bulkUpload }, } = state;
11
+ const { selectedPeriod } = action.payload;
12
+ const queryPayload = {};
13
+ if (bulkUpload.completedSubTab !== 'all') {
14
+ queryPayload.match_type = bulkUpload.completedSubTab;
15
+ }
16
+ const period = convertToPeriod(selectedPeriod);
17
+ queryPayload.start_date = period.start;
18
+ queryPayload.end_date = period.end;
19
+ if (action.payload.pageToken != null) {
20
+ queryPayload.page_token = action.payload.pageToken;
21
+ }
22
+ const encodedQuery = encodeURIComponent(JSON.stringify(queryPayload));
23
+ return zeniAPI
24
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/receipt-agent/transactions/completed?query=${encodedQuery}`)
25
+ .pipe(mergeMap((response) => {
26
+ if (isSuccessResponse(response) && response.data != null) {
27
+ const transactionPayloads = response.data.transactions.map(completedTransactionPayloadToSupportedTransactionPayload);
28
+ const transactionIds = response.data.transactions.map((t) => t.transaction_id);
29
+ const updateActions = [];
30
+ updateActions.push(updateTransactions(transactionPayloads, (value) => value.transaction_id));
31
+ updateActions.push(fetchCompletedTransactionsSuccess({
32
+ transactionIds,
33
+ nextPageToken: response.data.next_page_token,
34
+ totalCount: response.data.total_count,
35
+ selectedPeriod,
36
+ }));
37
+ return from(updateActions);
38
+ }
39
+ return of(fetchCompletedTransactionsFailure({
40
+ error: response.status,
41
+ selectedPeriod,
42
+ }));
43
+ }), catchError((error) => of(fetchCompletedTransactionsFailure({
44
+ error: createZeniAPIStatus('Unexpected Error', 'Fetch completed transactions errored: ' +
45
+ JSON.stringify(error)),
46
+ selectedPeriod,
47
+ }))));
48
+ }));
@@ -0,0 +1,27 @@
1
+ import { of } from 'rxjs';
2
+ import { catchError, debounceTime, filter, switchMap } from 'rxjs/operators';
3
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
+ import { toManualSearchResult, } from '../../payload/missingReceiptsPayload';
5
+ import { searchTransactionsForManualMatch, searchTransactionsForManualMatchFailure, searchTransactionsForManualMatchSuccess, } from '../../reducers/missingReceiptsViewReducer';
6
+ export const searchTransactionsForManualMatchEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(searchTransactionsForManualMatch.match), debounceTime(300), switchMap((action) => {
7
+ const { query } = action.payload;
8
+ const searchQuery = JSON.stringify({
9
+ search_text: query,
10
+ search_fields: ['date', 'amount', 'vendor_name'],
11
+ });
12
+ const encodedQuery = encodeURIComponent(searchQuery);
13
+ return zeniAPI
14
+ .getJSON(`${zeniAPI.apiEndPoints.searchMicroServiceBaseUrl}/1.0/transaction-search?query=${encodedQuery}`)
15
+ .pipe(switchMap((response) => {
16
+ if (isSuccessResponse(response) && response.data != null) {
17
+ const results = response.data.transactions.map(toManualSearchResult);
18
+ return of(searchTransactionsForManualMatchSuccess(results));
19
+ }
20
+ return of(searchTransactionsForManualMatchFailure({
21
+ error: response.status,
22
+ }));
23
+ }), catchError((error) => of(searchTransactionsForManualMatchFailure({
24
+ error: createZeniAPIStatus('Unexpected Error', 'Search transactions for manual match errored: ' +
25
+ JSON.stringify(error)),
26
+ }))));
27
+ }));
@@ -0,0 +1,41 @@
1
+ import { of, timer } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap, takeUntil, takeWhile, } from 'rxjs/operators';
3
+ import { isSuccessResponse } from '../../../../responsePayload';
4
+ import { toBatchStatus, } from '../../payload/missingReceiptsPayload';
5
+ import { bulkUploadReceiptsSuccess, clearBulkUpload, fetchBulkUploadBatchDetails, pusherBatchStatusUpdate, updateBulkUploadBatchStatus, } from '../../reducers/missingReceiptsViewReducer';
6
+ /**
7
+ * Watches batch status using two strategies:
8
+ * 1. Pusher (primary): Reacts to `pusherBatchStatusUpdate` dispatched by the UI layer
9
+ * when a Pusher event arrives. Immediately fetches full details on completion.
10
+ * 2. Polling (fallback): Starts a relaxed 10s poll after upload success as a safety net
11
+ * in case the Pusher event is missed. Stops on completion or `clearBulkUpload`.
12
+ */
13
+ // Pusher-driven: when Pusher delivers a completed status, fetch details
14
+ export const pusherBatchStatusCompletionEpic = (actions$) => actions$.pipe(filter(pusherBatchStatusUpdate.match), filter((action) => action.payload.status === 'completed'), mergeMap(() => of(fetchBulkUploadBatchDetails())));
15
+ // Polling fallback: relaxed interval as safety net
16
+ const FALLBACK_POLL_INTERVAL_MS = 10000;
17
+ export const pollBulkUploadBatchStatusEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(bulkUploadReceiptsSuccess.match), switchMap((action) => {
18
+ const { batchId } = action.payload;
19
+ return timer(0, FALLBACK_POLL_INTERVAL_MS).pipe(takeUntil(actions$.pipe(filter((a) => clearBulkUpload.match(a) ||
20
+ (pusherBatchStatusUpdate.match(a) &&
21
+ a.payload.status === 'completed')))), switchMap(() => zeniAPI
22
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/receipts/batches/${batchId}/status`)
23
+ .pipe(mergeMap((response) => {
24
+ if (isSuccessResponse(response) && response.data != null) {
25
+ const batchStatus = toBatchStatus(response.data);
26
+ const actions = [
27
+ updateBulkUploadBatchStatus(batchStatus),
28
+ ];
29
+ if (batchStatus.status === 'completed') {
30
+ actions.push(fetchBulkUploadBatchDetails());
31
+ }
32
+ return actions;
33
+ }
34
+ return [];
35
+ }), catchError(() => of()))), takeWhile((action) => {
36
+ if (fetchBulkUploadBatchDetails.match(action)) {
37
+ return false;
38
+ }
39
+ return true;
40
+ }, true));
41
+ }));
@@ -1 +1,164 @@
1
- export {};
1
+ // -- Converter functions --
2
+ export function toBatchFile(payload) {
3
+ return {
4
+ attachmentId: payload.attachment_id,
5
+ candidates: payload.candidates?.map((c) => ({
6
+ transactionId: c.transaction_id,
7
+ matchScore: c.match_score,
8
+ })),
9
+ fileId: payload.file_id,
10
+ filePreviewUrl: payload.file_preview_url,
11
+ filename: payload.filename,
12
+ matchedTransactionId: payload.matched_transaction?.transaction_id,
13
+ status: payload.status,
14
+ };
15
+ }
16
+ export function extractTransactionPayloadsFromBatchFiles(files) {
17
+ const seen = new Set();
18
+ const payloads = [];
19
+ for (const file of files) {
20
+ if (file.matched_transaction != null) {
21
+ const id = file.matched_transaction.transaction_id;
22
+ if (!seen.has(id)) {
23
+ seen.add(id);
24
+ payloads.push(matchedTransactionPayloadToSupportedTransactionPayload(file.matched_transaction));
25
+ }
26
+ }
27
+ if (file.candidates != null) {
28
+ for (const candidate of file.candidates) {
29
+ const id = candidate.transaction_id;
30
+ if (!seen.has(id)) {
31
+ seen.add(id);
32
+ payloads.push(matchedTransactionPayloadToSupportedTransactionPayload(candidate));
33
+ }
34
+ }
35
+ }
36
+ }
37
+ return payloads;
38
+ }
39
+ function matchedTransactionPayloadToSupportedTransactionPayload(payload) {
40
+ return {
41
+ transaction_id: payload.transaction_id,
42
+ transaction_date: payload.transaction_date,
43
+ currency_code: payload.currency_code,
44
+ total_amount: payload.amount,
45
+ vendor_name: payload.vendor_name,
46
+ };
47
+ }
48
+ export function toBatchSummary(payload) {
49
+ return {
50
+ failed: payload.failed,
51
+ matched: payload.matched,
52
+ noMatch: payload.no_match,
53
+ possibleMatches: payload.possible_matches,
54
+ total: payload.total,
55
+ };
56
+ }
57
+ export function toBatchStatus(payload) {
58
+ return {
59
+ batchId: payload.batch_id,
60
+ progress: payload.progress,
61
+ status: payload.status,
62
+ };
63
+ }
64
+ export function toBatchDetails(payload) {
65
+ return {
66
+ batchId: payload.batch_id,
67
+ createdAt: payload.created_at,
68
+ files: payload.files.map(toBatchFile),
69
+ status: payload.status,
70
+ summary: toBatchSummary(payload.summary),
71
+ };
72
+ }
73
+ export function toBatchListItem(payload) {
74
+ return {
75
+ batchId: payload.batch_id,
76
+ createdAt: payload.created_at,
77
+ failedCount: payload.failed_count,
78
+ matchedCount: payload.matched_count,
79
+ noMatchCount: payload.no_match_count,
80
+ possibleMatchesCount: payload.possible_matches_count,
81
+ status: payload.status,
82
+ totalFiles: payload.total_files,
83
+ };
84
+ }
85
+ export function toManualSearchResult(payload) {
86
+ return {
87
+ amount: payload.amount,
88
+ currency: payload.currency,
89
+ entityId: payload.entity_id,
90
+ memo: payload.memo,
91
+ transactionDate: payload.transaction_date,
92
+ transactionId: payload.transaction_id,
93
+ transactionType: payload.transaction_type,
94
+ vendorName: payload.vendor_name,
95
+ };
96
+ }
97
+ function completedTransactionAttachmentToAttachmentPayload(payload) {
98
+ return {
99
+ attachment_create_time: payload.attachment_create_time ?? '',
100
+ attachment_id: payload.attachment_id,
101
+ attachment_update_time: payload.attachment_update_time ?? '',
102
+ content_type: payload.content_type,
103
+ file_checksum: null,
104
+ file_download_url: payload.file_download_url,
105
+ file_name: payload.file_name,
106
+ file_size: payload.file_size,
107
+ };
108
+ }
109
+ function completedTransactionLineToLinePayload(payload) {
110
+ return {
111
+ amount: payload.amount,
112
+ line_description: payload.line_description,
113
+ line_detail_type: payload.line_detail_type,
114
+ line_id: payload.line_id,
115
+ categorization_status: payload.categorization_status,
116
+ line_detail: {
117
+ account: {
118
+ account_id: payload.line_detail.account?.account_id ?? '',
119
+ account_name: payload.line_detail.account?.account_name ?? '',
120
+ labels: [],
121
+ coa_id: payload.line_detail.account?.coa_id,
122
+ qbo_id: payload.line_detail.account?.qbo_id,
123
+ },
124
+ class: {
125
+ accounting_class_id: payload.line_detail.class?.accounting_class_id ?? '',
126
+ accounting_class_name: payload.line_detail.class?.accounting_class_name ?? '',
127
+ accounting_class_number: payload.line_detail.class?.accounting_class_number,
128
+ fully_qualified_accounting_class_name: payload.line_detail.class?.fully_qualified_accounting_class_name,
129
+ qbo_id: payload.line_detail.class?.qbo_id,
130
+ },
131
+ line_detail_type: payload.line_detail_type,
132
+ customer: payload.line_detail.customer != null
133
+ ? {
134
+ customer_id: payload.line_detail.customer.customer_id,
135
+ customer_name: payload.line_detail.customer.customer_name,
136
+ qbo_id: payload.line_detail.customer.qbo_id,
137
+ }
138
+ : undefined,
139
+ vendor: payload.line_detail.vendor != null
140
+ ? {
141
+ vendor_id: payload.line_detail.vendor.vendor_id,
142
+ vendor_name: payload.line_detail.vendor.vendor_name,
143
+ }
144
+ : undefined,
145
+ },
146
+ };
147
+ }
148
+ export function completedTransactionPayloadToSupportedTransactionPayload(payload) {
149
+ return {
150
+ transaction_id: payload.transaction_id,
151
+ transaction_type: payload.transaction_type,
152
+ transaction_date: payload.transaction_date,
153
+ transaction_type_name: payload.transaction_type_name,
154
+ currency_code: payload.currency_code,
155
+ currency_symbol: payload.currency_symbol,
156
+ total_amount: payload.total_amount,
157
+ vendor_id: payload.vendor_id,
158
+ vendor_name: payload.vendor_name,
159
+ account_id: payload.account_id,
160
+ account_name: payload.account_name,
161
+ attachments: payload.attachments.map(completedTransactionAttachmentToAttachmentPayload),
162
+ lines: payload.lines.map(completedTransactionLineToLinePayload),
163
+ };
164
+ }
@@ -1,6 +1,28 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
3
+ export const initialBulkUploadState = {
4
+ batchDetailsById: {},
5
+ batchList: [],
6
+ batchListFetchState: { fetchState: 'Not-Started', error: undefined },
7
+ batchStatusById: {},
8
+ completedSubTab: 'all',
9
+ completedTransactionsByPeriod: {},
10
+ confirmMatchStatus: { fetchState: 'Not-Started', error: undefined },
11
+ currentBatchId: undefined,
12
+ currentResultsTab: 'unmatched',
13
+ manualSearch: {
14
+ fetchState: { fetchState: 'Not-Started', error: undefined },
15
+ results: [],
16
+ searchQuery: '',
17
+ },
18
+ phase: 'idle',
19
+ sortKey: 'date',
20
+ sortOrder: 'descending',
21
+ uploadProgress: 0,
22
+ uploadStatus: { fetchState: 'Not-Started', error: undefined },
23
+ };
3
24
  export const initialState = {
25
+ bulkUpload: initialBulkUploadState,
4
26
  uploadReceiptStatusById: {},
5
27
  refreshStatus: {
6
28
  fetchState: 'Not-Started',
@@ -120,7 +142,229 @@ const expenseAutomationMissingReceiptsView = createSlice({
120
142
  clearExpenseAutomationMissingReceiptsView(draft) {
121
143
  Object.assign(draft, initialState);
122
144
  },
145
+ // -- Bulk Upload Actions --
146
+ bulkUploadReceipts: {
147
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
148
+ reducer(draft, _action) {
149
+ draft.bulkUpload.phase = 'uploading';
150
+ draft.bulkUpload.uploadProgress = 0;
151
+ draft.bulkUpload.uploadStatus = {
152
+ fetchState: 'In-Progress',
153
+ error: undefined,
154
+ };
155
+ },
156
+ prepare(files) {
157
+ return { payload: { files } };
158
+ },
159
+ },
160
+ updateBulkUploadProgress(draft, action) {
161
+ draft.bulkUpload.uploadProgress = action.payload;
162
+ },
163
+ bulkUploadReceiptsSuccess(draft, action) {
164
+ draft.bulkUpload.currentBatchId = action.payload.batchId;
165
+ draft.bulkUpload.uploadStatus = {
166
+ fetchState: 'Completed',
167
+ error: undefined,
168
+ };
169
+ draft.bulkUpload.phase = 'matching';
170
+ },
171
+ bulkUploadReceiptsFailure(draft, action) {
172
+ draft.bulkUpload.uploadStatus = {
173
+ fetchState: 'Error',
174
+ error: action.payload.error,
175
+ };
176
+ draft.bulkUpload.phase = 'idle';
177
+ },
178
+ updateBulkUploadBatchStatus(draft, action) {
179
+ const batchStatus = action.payload;
180
+ draft.bulkUpload.batchStatusById[batchStatus.batchId] = batchStatus;
181
+ },
182
+ pusherBatchStatusUpdate(draft, action) {
183
+ const batchStatus = action.payload;
184
+ draft.bulkUpload.batchStatusById[batchStatus.batchId] = batchStatus;
185
+ },
186
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
187
+ fetchBulkUploadBatchDetails(_draft) {
188
+ // trigger epic, no state change needed
189
+ },
190
+ fetchBulkUploadBatchDetailsSuccess(draft, action) {
191
+ const details = action.payload;
192
+ draft.bulkUpload.batchDetailsById[details.batchId] = details;
193
+ draft.bulkUpload.phase = 'completed';
194
+ },
195
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
196
+ fetchBulkUploadBatchDetailsFailure(draft, _action) {
197
+ draft.bulkUpload.phase = 'completed';
198
+ },
199
+ fetchBulkUploadBatches(draft) {
200
+ draft.bulkUpload.batchListFetchState = {
201
+ fetchState: 'In-Progress',
202
+ error: undefined,
203
+ };
204
+ },
205
+ fetchBulkUploadBatchesSuccess(draft, action) {
206
+ draft.bulkUpload.batchList = action.payload;
207
+ draft.bulkUpload.batchListFetchState = {
208
+ fetchState: 'Completed',
209
+ error: undefined,
210
+ };
211
+ },
212
+ fetchBulkUploadBatchesFailure(draft, action) {
213
+ draft.bulkUpload.batchListFetchState = {
214
+ fetchState: 'Error',
215
+ error: action.payload.error,
216
+ };
217
+ },
218
+ confirmBulkUploadMatch: {
219
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
220
+ reducer(draft, _action) {
221
+ draft.bulkUpload.confirmMatchStatus = {
222
+ fetchState: 'In-Progress',
223
+ error: undefined,
224
+ };
225
+ },
226
+ prepare(batchId, attachmentId, transactionId) {
227
+ return { payload: { batchId, attachmentId, transactionId } };
228
+ },
229
+ },
230
+ confirmBulkUploadMatchSuccess(draft, action) {
231
+ const { batchId, attachmentId, transactionId } = action.payload;
232
+ draft.bulkUpload.confirmMatchStatus = {
233
+ fetchState: 'Completed',
234
+ error: undefined,
235
+ };
236
+ const details = draft.bulkUpload.batchDetailsById[batchId];
237
+ if (details != null) {
238
+ const file = details.files.find((f) => f.attachmentId === attachmentId);
239
+ if (file != null) {
240
+ file.status = 'matched';
241
+ file.matchedTransactionId = transactionId;
242
+ file.candidates = undefined;
243
+ }
244
+ details.summary.matched += 1;
245
+ details.summary.possibleMatches = Math.max(0, details.summary.possibleMatches - 1);
246
+ }
247
+ },
248
+ confirmBulkUploadMatchFailure(draft, action) {
249
+ draft.bulkUpload.confirmMatchStatus = {
250
+ fetchState: 'Error',
251
+ error: action.payload.error,
252
+ };
253
+ },
254
+ setBulkUploadResultsTab(draft, action) {
255
+ draft.bulkUpload.currentResultsTab = action.payload;
256
+ },
257
+ setBulkUploadCompletedSubTab(draft, action) {
258
+ draft.bulkUpload.completedSubTab = action.payload;
259
+ draft.bulkUpload.completedTransactionsByPeriod = {};
260
+ },
261
+ setBulkUploadSortConfig(draft, action) {
262
+ draft.bulkUpload.sortKey = action.payload.sortKey;
263
+ draft.bulkUpload.sortOrder = action.payload.sortOrder;
264
+ },
265
+ clearBulkUpload(draft) {
266
+ Object.assign(draft.bulkUpload, initialBulkUploadState);
267
+ },
268
+ searchTransactionsForManualMatch: {
269
+ reducer(draft, action) {
270
+ draft.bulkUpload.manualSearch.searchQuery = action.payload.query;
271
+ draft.bulkUpload.manualSearch.fetchState = {
272
+ fetchState: 'In-Progress',
273
+ error: undefined,
274
+ };
275
+ draft.bulkUpload.manualSearch.results = [];
276
+ },
277
+ prepare(query) {
278
+ return { payload: { query } };
279
+ },
280
+ },
281
+ searchTransactionsForManualMatchSuccess(draft, action) {
282
+ draft.bulkUpload.manualSearch.results = action.payload;
283
+ draft.bulkUpload.manualSearch.fetchState = {
284
+ fetchState: 'Completed',
285
+ error: undefined,
286
+ };
287
+ },
288
+ searchTransactionsForManualMatchFailure(draft, action) {
289
+ draft.bulkUpload.manualSearch.fetchState = {
290
+ fetchState: 'Error',
291
+ error: action.payload.error,
292
+ };
293
+ },
294
+ clearManualSearchResults(draft) {
295
+ draft.bulkUpload.manualSearch = initialBulkUploadState.manualSearch;
296
+ },
297
+ // -- Completed Transactions Actions --
298
+ fetchCompletedTransactions: {
299
+ reducer(draft, action) {
300
+ const periodId = toMonthYearPeriodId(action.payload.selectedPeriod);
301
+ const existing = draft.bulkUpload.completedTransactionsByPeriod[periodId];
302
+ if (action.payload.pageToken == null) {
303
+ draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
304
+ fetchState: { fetchState: 'In-Progress', error: undefined },
305
+ nextPageToken: null,
306
+ totalCount: 0,
307
+ transactionIds: [],
308
+ };
309
+ }
310
+ else if (existing != null) {
311
+ existing.fetchState = { fetchState: 'In-Progress', error: undefined };
312
+ }
313
+ else {
314
+ draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
315
+ fetchState: { fetchState: 'In-Progress', error: undefined },
316
+ nextPageToken: null,
317
+ totalCount: 0,
318
+ transactionIds: [],
319
+ };
320
+ }
321
+ },
322
+ prepare(selectedPeriod, pageToken) {
323
+ return { payload: { selectedPeriod, pageToken } };
324
+ },
325
+ },
326
+ fetchCompletedTransactionsSuccess(draft, action) {
327
+ const { transactionIds, nextPageToken, totalCount, selectedPeriod } = action.payload;
328
+ const periodId = toMonthYearPeriodId(selectedPeriod);
329
+ const existing = draft.bulkUpload.completedTransactionsByPeriod[periodId];
330
+ if (existing != null) {
331
+ existing.transactionIds.push(...transactionIds);
332
+ existing.nextPageToken = nextPageToken;
333
+ existing.totalCount = totalCount;
334
+ existing.fetchState = { fetchState: 'Completed', error: undefined };
335
+ }
336
+ else {
337
+ draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
338
+ transactionIds,
339
+ nextPageToken,
340
+ totalCount,
341
+ fetchState: { fetchState: 'Completed', error: undefined },
342
+ };
343
+ }
344
+ },
345
+ fetchCompletedTransactionsFailure(draft, action) {
346
+ const periodId = toMonthYearPeriodId(action.payload.selectedPeriod);
347
+ const existing = draft.bulkUpload.completedTransactionsByPeriod[periodId];
348
+ if (existing != null) {
349
+ existing.fetchState = {
350
+ fetchState: 'Error',
351
+ error: action.payload.error,
352
+ };
353
+ }
354
+ else {
355
+ draft.bulkUpload.completedTransactionsByPeriod[periodId] = {
356
+ fetchState: { fetchState: 'Error', error: action.payload.error },
357
+ nextPageToken: null,
358
+ totalCount: 0,
359
+ transactionIds: [],
360
+ };
361
+ }
362
+ },
123
363
  },
124
364
  });
125
- export const { fetchMissingReceipts, fetchMissingReceiptsSuccess, fetchMissingReceiptsFailure, uploadMissingReceiptSuccess, updateMissingReceiptUploadState, updateMissingReceiptsUIState, markMissingReceiptAsDone, clearExpenseAutomationMissingReceiptsView, } = expenseAutomationMissingReceiptsView.actions;
365
+ export const { fetchMissingReceipts, fetchMissingReceiptsSuccess, fetchMissingReceiptsFailure, uploadMissingReceiptSuccess, updateMissingReceiptUploadState, updateMissingReceiptsUIState, markMissingReceiptAsDone, clearExpenseAutomationMissingReceiptsView,
366
+ // Bulk Upload
367
+ bulkUploadReceipts, updateBulkUploadProgress, bulkUploadReceiptsSuccess, bulkUploadReceiptsFailure, updateBulkUploadBatchStatus, pusherBatchStatusUpdate, fetchBulkUploadBatchDetails, fetchBulkUploadBatchDetailsSuccess, fetchBulkUploadBatchDetailsFailure, fetchBulkUploadBatches, fetchBulkUploadBatchesSuccess, fetchBulkUploadBatchesFailure, confirmBulkUploadMatch, confirmBulkUploadMatchSuccess, confirmBulkUploadMatchFailure, setBulkUploadResultsTab, setBulkUploadCompletedSubTab, setBulkUploadSortConfig, clearBulkUpload, searchTransactionsForManualMatch, searchTransactionsForManualMatchSuccess, searchTransactionsForManualMatchFailure, clearManualSearchResults,
368
+ // Completed Transactions
369
+ fetchCompletedTransactions, fetchCompletedTransactionsSuccess, fetchCompletedTransactionsFailure, } = expenseAutomationMissingReceiptsView.actions;
126
370
  export default expenseAutomationMissingReceiptsView.reducer;
@@ -1,7 +1,7 @@
1
1
  import orderBy from 'lodash/orderBy';
2
2
  import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
3
3
  import { getCurrentTenant } from '../../../entity/tenant/tenantSelector';
4
- import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
4
+ import { getSupportedTransactionById, getSupportedTransactionsByIds, } from '../../../entity/transaction/transactionSelector';
5
5
  import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
6
6
  export function getExpenseAutomationMissingReceiptsView(state) {
7
7
  const { expenseAutomationMissingReceiptsViewState, expenseAutomationViewState, transactionState, monthEndCloseChecksState, } = state;
@@ -47,6 +47,66 @@ export function getExpenseAutomationMissingReceiptsView(state) {
47
47
  const completionStatus = missingReceiptsCompletionStatus != null
48
48
  ? missingReceiptsCompletionStatus
49
49
  : 'incomplete';
50
+ const { bulkUpload } = expenseAutomationMissingReceiptsViewState;
51
+ const currentBatchStatus = bulkUpload.currentBatchId != null
52
+ ? bulkUpload.batchStatusById[bulkUpload.currentBatchId]
53
+ : undefined;
54
+ const currentBatchDetails = bulkUpload.currentBatchId != null
55
+ ? bulkUpload.batchDetailsById[bulkUpload.currentBatchId]
56
+ : undefined;
57
+ const resolvedFiles = currentBatchDetails?.files.map((file) => resolveBatchFile(file, transactionState));
58
+ const getSortValue = (file) => {
59
+ const t = file.matchedTransaction;
60
+ if (t == null) {
61
+ return null;
62
+ }
63
+ switch (bulkUpload.sortKey) {
64
+ case 'vendor':
65
+ return (t.vendorName ?? t.description)?.toLowerCase() ?? null;
66
+ case 'category': {
67
+ const line = t.lines?.[0];
68
+ return line?.account?.accountName?.toLowerCase() ?? null;
69
+ }
70
+ case 'class': {
71
+ const line = t.lines?.[0];
72
+ return line?.class?.className?.toLowerCase() ?? null;
73
+ }
74
+ case 'date':
75
+ return t.date.valueOf();
76
+ case 'amount':
77
+ return t.amount.amount;
78
+ default:
79
+ return null;
80
+ }
81
+ };
82
+ const bulkUploadSortDirection = bulkUpload.sortOrder === 'ascending' ? 'asc' : 'desc';
83
+ const matchedFiles = orderBy(resolvedFiles?.filter((f) => f.status === 'matched') ?? [], getSortValue, bulkUploadSortDirection);
84
+ const unmatchedFiles = orderBy(resolvedFiles?.filter((f) => f.status === 'un_matched' || f.status === 'no_match') ?? [], getSortValue, bulkUploadSortDirection);
85
+ const progress = currentBatchStatus != null
86
+ ? {
87
+ ...currentBatchStatus.progress,
88
+ percentage: currentBatchStatus.progress.total > 0
89
+ ? Math.round((currentBatchStatus.progress.processed /
90
+ currentBatchStatus.progress.total) *
91
+ 100)
92
+ : 0,
93
+ }
94
+ : { total: 0, processed: 0, percentage: 0 };
95
+ const pastBatches = bulkUpload.batchList.filter((b) => b.batchId !== bulkUpload.currentBatchId);
96
+ const completedTransactionsForPeriod = monthYearPeriodId != null
97
+ ? bulkUpload.completedTransactionsByPeriod[monthYearPeriodId]
98
+ : undefined;
99
+ const completedTransactionIds = completedTransactionsForPeriod?.transactionIds ?? [];
100
+ const resolvedCompletedTransactions = getSupportedTransactionsByIds(transactionState, completedTransactionIds);
101
+ const resolvedBatchDetails = currentBatchDetails != null && resolvedFiles != null
102
+ ? {
103
+ batchId: currentBatchDetails.batchId,
104
+ createdAt: currentBatchDetails.createdAt,
105
+ files: resolvedFiles,
106
+ status: currentBatchDetails.status,
107
+ summary: currentBatchDetails.summary,
108
+ }
109
+ : undefined;
50
110
  return {
51
111
  fetchState,
52
112
  error,
@@ -55,5 +115,58 @@ export function getExpenseAutomationMissingReceiptsView(state) {
55
115
  refreshStatus,
56
116
  transactions: sortedTransactions,
57
117
  completionStatus,
118
+ bulkUpload: {
119
+ phase: bulkUpload.phase,
120
+ uploadStatus: bulkUpload.uploadStatus,
121
+ batchStatus: currentBatchStatus,
122
+ batchDetails: resolvedBatchDetails,
123
+ completedSubTab: bulkUpload.completedSubTab,
124
+ completedTransactions: {
125
+ fetchState: completedTransactionsForPeriod?.fetchState ?? {
126
+ fetchState: 'Not-Started',
127
+ error: undefined,
128
+ },
129
+ nextPageToken: completedTransactionsForPeriod?.nextPageToken ?? null,
130
+ totalCount: completedTransactionsForPeriod?.totalCount ?? 0,
131
+ transactions: resolvedCompletedTransactions,
132
+ },
133
+ matchedFiles,
134
+ unmatchedFiles,
135
+ progress,
136
+ currentResultsTab: bulkUpload.currentResultsTab,
137
+ sortKey: bulkUpload.sortKey,
138
+ sortOrder: bulkUpload.sortOrder,
139
+ batchList: bulkUpload.batchList,
140
+ batchListFetchState: bulkUpload.batchListFetchState,
141
+ pastBatches,
142
+ confirmMatchStatus: bulkUpload.confirmMatchStatus,
143
+ manualSearch: bulkUpload.manualSearch,
144
+ uploadProgress: bulkUpload.uploadProgress,
145
+ },
146
+ };
147
+ }
148
+ function resolveBatchFile(file, transactionState) {
149
+ const matchedTransaction = file.matchedTransactionId != null
150
+ ? getSupportedTransactionById(transactionState, file.matchedTransactionId)
151
+ : undefined;
152
+ const candidates = file.candidates
153
+ ?.map((ref) => {
154
+ const transaction = getSupportedTransactionById(transactionState, ref.transactionId);
155
+ return transaction != null
156
+ ? { transaction, matchScore: ref.matchScore }
157
+ : undefined;
158
+ })
159
+ .filter((c) => c != null);
160
+ return {
161
+ attachmentId: file.attachmentId,
162
+ fileId: file.fileId,
163
+ filename: file.filename,
164
+ filePreviewUrl: file.filePreviewUrl,
165
+ status: file.status,
166
+ matchedTransaction,
167
+ candidates: candidates != null && candidates.length > 0
168
+ ? candidates
169
+ : undefined,
170
+ matchSource: file.matchSource,
58
171
  };
59
172
  }