@zeniai/client-epic-state 5.0.52 → 5.0.53-betaML4
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.
- package/lib/commonStateTypes/recommendationBase.d.ts +2 -1
- package/lib/commonStateTypes/viewAndReport/viewAndReport.d.ts +2 -2
- package/lib/commonStateTypes/viewAndReport/viewAndReport.js +1 -0
- package/lib/entity/account/accountState.d.ts +1 -1
- package/lib/entity/accountGroup/accountGroupState.d.ts +1 -1
- package/lib/entity/class/classReducer.d.ts +4 -4
- package/lib/entity/class/classState.d.ts +1 -1
- package/lib/entity/forecast/forecastState.d.ts +1 -1
- package/lib/entity/project/projectPayload.d.ts +8 -1
- package/lib/entity/project/projectPayload.js +15 -1
- package/lib/entity/project/projectState.d.ts +4 -2
- package/lib/entity/sectionAccountsView/sectionAccountsView.d.ts +1 -1
- package/lib/entity/sectionClassesViewV2/sectionClassesView.d.ts +1 -1
- package/lib/entity/sectionClassesViewV2/sectionClassesViewReducer.d.ts +2 -2
- package/lib/entity/sectionProjectView/sectionProjectView.d.ts +1 -1
- package/lib/entity/sectionProjectView/sectionProjectViewReducer.d.ts +2 -2
- package/lib/entity/tenant/clearAllEpic.d.ts +2 -1
- package/lib/entity/tenant/clearAllEpic.js +2 -0
- package/lib/entity/transaction/payloadTypes/transactionLinePayload.d.ts +2 -0
- package/lib/entity/transaction/payloadTypes/transactionLinePayload.js +9 -0
- package/lib/entity/transaction/payloadTypes/transactionPayload.d.ts +2 -0
- package/lib/entity/transaction/payloadTypes/transactionPayload.js +6 -0
- package/lib/entity/transaction/stateTypes/transaction.d.ts +2 -0
- package/lib/entity/transaction/stateTypes/transactionLine.d.ts +2 -0
- package/lib/epic.d.ts +4 -1
- package/lib/epic.js +5 -1
- package/lib/esm/commonStateTypes/viewAndReport/viewAndReport.js +1 -0
- package/lib/esm/entity/project/projectPayload.js +12 -0
- package/lib/esm/entity/tenant/clearAllEpic.js +2 -0
- package/lib/esm/entity/transaction/payloadTypes/transactionLinePayload.js +9 -0
- package/lib/esm/entity/transaction/payloadTypes/transactionPayload.js +6 -0
- package/lib/esm/epic.js +5 -1
- package/lib/esm/index.js +11 -7
- package/lib/esm/reducer.js +3 -0
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
- package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/updateTransactionCategorizationEpic.js +3 -0
- package/lib/esm/view/expenseAutomationView/helpers/saveTransactionHelper.js +1 -0
- package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +11 -2
- package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +127 -31
- package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +13 -2
- package/lib/esm/view/expenseAutomationView/types/transactionsViewState.js +1 -0
- package/lib/esm/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.js +44 -0
- package/lib/esm/view/projectList/fetchProjectListEpic.js +38 -0
- package/lib/esm/view/projectList/projectListPayload.js +1 -0
- package/lib/esm/view/projectList/projectListReducer.js +39 -0
- package/lib/esm/view/projectList/projectListSelector.js +9 -0
- package/lib/esm/view/projectList/projectListState.js +1 -0
- package/lib/esm/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.js +83 -0
- package/lib/esm/view/recommendation/recommendationReducer.js +52 -1
- package/lib/esm/view/transactionDetail/epics/saveTransactionHelper.js +1 -0
- package/lib/esm/view/transactionDetail/epics/transactionDetailEpic.js +6 -0
- package/lib/esm/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -0
- package/lib/esm/view/transactionDetail/transactionDetailSelector.js +14 -2
- package/lib/esm/view/transactionList/fetchTransactionListByProjectEpic.js +45 -0
- package/lib/esm/view/transactionList/parallelFetchProjectTransactionListEpic.js +32 -0
- package/lib/esm/view/transactionList/transactionListByProjectSelector.js +58 -0
- package/lib/esm/view/transactionList/transactionListReducer.js +83 -2
- package/lib/esm/view/transactionList/transactionListState.js +10 -0
- package/lib/index.d.ts +13 -9
- package/lib/index.js +42 -27
- package/lib/reducer.d.ts +3 -0
- package/lib/reducer.js +3 -0
- package/lib/tsconfig.typecheck.tsbuildinfo +1 -0
- package/lib/view/auditReportView/auditReportViewState.d.ts +1 -1
- package/lib/view/billPayCard/billPayCardSelector.d.ts +1 -1
- package/lib/view/cardBalance/cardBalanceSelector.d.ts +1 -1
- package/lib/view/cashBalance/cashBalanceSelector.d.ts +1 -1
- package/lib/view/cashInCashOut/cashInCashOutSelector.d.ts +1 -1
- package/lib/view/cashPosition/cashPositionSelector.d.ts +1 -1
- package/lib/view/dashboard/dashboardReducer.d.ts +2 -2
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.d.ts +2 -1
- package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationViewEpic.js +8 -0
- package/lib/view/expenseAutomationView/epics/transactionCategorization/updateTransactionCategorizationEpic.js +3 -0
- package/lib/view/expenseAutomationView/helpers/saveTransactionHelper.js +1 -0
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +2 -1
- package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js +11 -2
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.d.ts +3 -1
- package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +127 -31
- package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +4 -0
- package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +12 -1
- package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +5 -1
- package/lib/view/expenseAutomationView/types/transactionsViewState.js +1 -0
- package/lib/view/financeStatement/financeStatementReducer.d.ts +1 -1
- package/lib/view/globalMerchantView/globalMerchantViewReducer.d.ts +2 -2
- package/lib/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.d.ts +15 -1
- package/lib/view/profitAndLossClassesView/profitAndLossHorizontalEnrichment.js +45 -0
- package/lib/view/projectList/fetchProjectListEpic.d.ts +8 -0
- package/lib/view/projectList/fetchProjectListEpic.js +42 -0
- package/lib/view/projectList/projectListPayload.d.ts +7 -0
- package/lib/view/projectList/projectListPayload.js +2 -0
- package/lib/view/projectList/projectListReducer.d.ts +9 -0
- package/lib/view/projectList/projectListReducer.js +43 -0
- package/lib/view/projectList/projectListSelector.d.ts +8 -0
- package/lib/view/projectList/projectListSelector.js +13 -0
- package/lib/view/projectList/projectListState.d.ts +4 -0
- package/lib/view/projectList/projectListState.js +2 -0
- package/lib/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.d.ts +8 -0
- package/lib/view/recommendation/fetchEntityRecommendationsForCategorizationEpic.js +87 -0
- package/lib/view/recommendation/recommendationReducer.d.ts +21 -1
- package/lib/view/recommendation/recommendationReducer.js +53 -2
- package/lib/view/reimbursementCard/reimbursementCardSelector.d.ts +1 -1
- package/lib/view/reportsResync/reportsResyncReducer.d.ts +2 -2
- package/lib/view/tasksCard/tasksCardSelector.d.ts +1 -1
- package/lib/view/topEx/topExSelector.d.ts +1 -1
- package/lib/view/transactionDetail/epics/saveTransactionHelper.js +1 -0
- package/lib/view/transactionDetail/epics/transactionDetailEpic.d.ts +2 -1
- package/lib/view/transactionDetail/epics/transactionDetailEpic.js +6 -0
- package/lib/view/transactionDetail/epics/updateTransactionDetailEpic.js +3 -0
- package/lib/view/transactionDetail/transactionDetailSelector.d.ts +3 -0
- package/lib/view/transactionDetail/transactionDetailSelector.js +13 -1
- package/lib/view/transactionDetail/transactionDetailState.d.ts +2 -0
- package/lib/view/transactionDetail/transactionDetailTypes.d.ts +2 -0
- package/lib/view/transactionList/fetchTransactionListByProjectEpic.d.ts +8 -0
- package/lib/view/transactionList/fetchTransactionListByProjectEpic.js +52 -0
- package/lib/view/transactionList/parallelFetchProjectTransactionListEpic.d.ts +6 -0
- package/lib/view/transactionList/parallelFetchProjectTransactionListEpic.js +36 -0
- package/lib/view/transactionList/transactionListByProjectSelector.d.ts +22 -0
- package/lib/view/transactionList/transactionListByProjectSelector.js +66 -0
- package/lib/view/transactionList/transactionListPayload.d.ts +15 -0
- package/lib/view/transactionList/transactionListReducer.d.ts +25 -2
- package/lib/view/transactionList/transactionListReducer.js +83 -2
- package/lib/view/transactionList/transactionListState.d.ts +15 -0
- package/lib/view/transactionList/transactionListState.js +13 -1
- package/package.json +1 -1
package/lib/esm/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.js
CHANGED
|
@@ -183,6 +183,7 @@ const buildLineItemById = (transaction, uncategorizedIncomeExpense, recommendati
|
|
|
183
183
|
const lineId = line.id;
|
|
184
184
|
record.account = accountLine.account;
|
|
185
185
|
record.class = accountLine.class;
|
|
186
|
+
record.project = accountLine.project;
|
|
186
187
|
record.vendor =
|
|
187
188
|
accountLine.vendor != null
|
|
188
189
|
? {
|
|
@@ -305,7 +306,7 @@ const buildLineItemById = (transaction, uncategorizedIncomeExpense, recommendati
|
|
|
305
306
|
});
|
|
306
307
|
return lineItemData;
|
|
307
308
|
};
|
|
308
|
-
export const toSetAllLineItemsToCategoryClass = (draft, transaction, transactionId, transactionLineId, transactionDetailLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
|
|
309
|
+
export const toSetAllLineItemsToCategoryClass = (draft, transaction, transactionId, transactionLineId, transactionDetailLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) => {
|
|
309
310
|
let newLineItems = {};
|
|
310
311
|
let selectedCheckBoxTransactionIds = draft.selectedCheckBoxTransactionIds;
|
|
311
312
|
let similarTransactionUpdated = false;
|
|
@@ -325,7 +326,7 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
|
|
|
325
326
|
const record = {
|
|
326
327
|
...transactionDetailLocalData.lineItemById[lineId],
|
|
327
328
|
};
|
|
328
|
-
// if the line is the one that is being updated, then set the account and
|
|
329
|
+
// if the line is the one that is being updated, then set the account, class, and project
|
|
329
330
|
if (transactionId === record.transactionId &&
|
|
330
331
|
transactionLineId === lineId) {
|
|
331
332
|
if (selectedAccount != null) {
|
|
@@ -334,6 +335,9 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
|
|
|
334
335
|
if (selectedClassBase != null) {
|
|
335
336
|
record.class = selectedClassBase;
|
|
336
337
|
}
|
|
338
|
+
if (selectedProjectBase != null) {
|
|
339
|
+
record.project = selectedProjectBase;
|
|
340
|
+
}
|
|
337
341
|
const isClassSatisfied = isAccountingClassesEnabled === false || record.class != null;
|
|
338
342
|
if (isClassSatisfied && record.account != null) {
|
|
339
343
|
if (selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
|
|
@@ -385,6 +389,10 @@ export const toSetAllLineItemsToCategoryClass = (draft, transaction, transaction
|
|
|
385
389
|
selectedCheckBoxTransactionIds.push(transaction.id);
|
|
386
390
|
}
|
|
387
391
|
}
|
|
392
|
+
// Project intentionally does NOT propagate to other rows of
|
|
393
|
+
// the same vendor: each line can carry a different project,
|
|
394
|
+
// and auto-filling siblings (the way class does) leads to
|
|
395
|
+
// unexpected bulk selection.
|
|
388
396
|
}
|
|
389
397
|
}
|
|
390
398
|
if (line.type === 'transaction_with_product_or_service_line') {
|
|
@@ -514,6 +522,7 @@ export const setEntityRecommendationForLineIdInLocalData = (draft, transaction,
|
|
|
514
522
|
...newLineItem,
|
|
515
523
|
class: matchingRecord.class,
|
|
516
524
|
account: matchingRecord.account,
|
|
525
|
+
project: matchingRecord.project,
|
|
517
526
|
lineEntity: matchingRecord.lineEntity,
|
|
518
527
|
};
|
|
519
528
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSlice } from '@reduxjs/toolkit';
|
|
2
2
|
import recordGet from 'lodash/get';
|
|
3
3
|
import uniq from 'lodash/uniq';
|
|
4
|
+
import { fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, } from '../../recommendation/recommendationReducer';
|
|
4
5
|
import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
|
|
5
6
|
import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
|
|
6
7
|
import { MAX_SELECTION_LIMIT, checkIfAllLineItemsAreCategoryClassFilled, filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
|
|
@@ -282,7 +283,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
282
283
|
},
|
|
283
284
|
setAllItemsToCategoryClassInLocalDataForCategorization: {
|
|
284
285
|
reducer(draft, action) {
|
|
285
|
-
const { vendorId, customerId, selectedAccount, selectedClassBase, transactions, transactionId, lineId, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
286
|
+
const { vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, transactions, transactionId, lineId, selectedTab, uncategorizedAccounts, isAccountingClassesEnabled, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
286
287
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
287
288
|
.transactionReviewLocalDataById, transactionId, undefined);
|
|
288
289
|
const existingAccount = transactionLocalData?.transactionReviewLocalData.lineItemById[lineId]
|
|
@@ -296,6 +297,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
296
297
|
...transactionLocalData.transactionReviewLocalData.lineItemById[lineId],
|
|
297
298
|
account: selectedAccount,
|
|
298
299
|
class: selectedClassBase,
|
|
300
|
+
project: selectedProjectBase,
|
|
299
301
|
},
|
|
300
302
|
},
|
|
301
303
|
};
|
|
@@ -324,9 +326,16 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
324
326
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
325
327
|
.transactionReviewLocalDataById, transaction.id, undefined);
|
|
326
328
|
if (transactionLocalData != null) {
|
|
327
|
-
const
|
|
329
|
+
const existingClassId = transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.class?.classId;
|
|
330
|
+
const existingProjectId = transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.project?.projectId;
|
|
331
|
+
const updatedItem = existingAccount?.accountId !== selectedAccount?.accountId
|
|
328
332
|
? 'category'
|
|
329
|
-
:
|
|
333
|
+
: existingClassId !== selectedClassBase?.classId
|
|
334
|
+
? 'class'
|
|
335
|
+
: existingProjectId !== selectedProjectBase?.projectId
|
|
336
|
+
? 'project'
|
|
337
|
+
: 'class';
|
|
338
|
+
const { localData, similarTransactionUpdated } = toSetAllLineItemsToCategoryClass(draft.transactionCategorizationView[selectedTab], transaction, transactionId, lineId, transactionLocalData.transactionReviewLocalData, updatedItem, uncategorizedAccounts, selectedTab, vendorId, customerId, selectedAccount, selectedClassBase, selectedProjectBase, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
|
|
330
339
|
draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
|
|
331
340
|
transactionId: transaction.id,
|
|
332
341
|
transactionReviewLocalData: localData,
|
|
@@ -345,7 +354,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
345
354
|
...dirtyIds,
|
|
346
355
|
]);
|
|
347
356
|
},
|
|
348
|
-
prepare(selectedTab, transactionId, lineId, transactions, uncategorizedAccounts, vendorId, customerId, selectedAccount, selectedClassBase, lineEntity, recommendations, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) {
|
|
357
|
+
prepare(selectedTab, transactionId, lineId, transactions, uncategorizedAccounts, vendorId, customerId, selectedAccount, selectedClassBase, lineEntity, recommendations, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled, selectedProjectBase) {
|
|
349
358
|
return {
|
|
350
359
|
payload: {
|
|
351
360
|
selectedTab,
|
|
@@ -357,6 +366,7 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
357
366
|
lineEntity,
|
|
358
367
|
selectedAccount,
|
|
359
368
|
selectedClassBase,
|
|
369
|
+
selectedProjectBase,
|
|
360
370
|
recommendations,
|
|
361
371
|
uncategorizedAccounts,
|
|
362
372
|
isAccountingClassesEnabled,
|
|
@@ -387,10 +397,53 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
387
397
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
388
398
|
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
389
399
|
if (transactionLocalData != null) {
|
|
400
|
+
// Snapshot the pre-fetch selection. setEntityRecommendationForLineIdInLocalData
|
|
401
|
+
// mutates selectedCheckBoxTransactionIds internally (auto-add when
|
|
402
|
+
// all-filled, auto-remove otherwise) but does not enforce
|
|
403
|
+
// MAX_SELECTION_LIMIT. We restore from this snapshot below and let
|
|
404
|
+
// the reducer-level gate own the selection contract so the cap is
|
|
405
|
+
// honored and the gate mirrors the original auto-add gate from
|
|
406
|
+
// updateSelectedVendor/CustomerForTransaction.
|
|
407
|
+
const selectionBeforeFetch = [
|
|
408
|
+
...draft.transactionCategorizationView[selectedTab]
|
|
409
|
+
.selectedCheckBoxTransactionIds,
|
|
410
|
+
];
|
|
411
|
+
const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
|
|
412
|
+
// Flip the per-line "fetching" flag to Completed for the lines this
|
|
413
|
+
// fetch covered. Reads as the success arm of the
|
|
414
|
+
// fetchEntityRecommendationsForCategorization → epic → reducer cycle.
|
|
415
|
+
lineIds.forEach((lineId) => {
|
|
416
|
+
const lineItem = newLocalData.lineItemById[lineId];
|
|
417
|
+
if (lineItem != null) {
|
|
418
|
+
newLocalData.lineItemById[lineId] = {
|
|
419
|
+
...lineItem,
|
|
420
|
+
categoryClassRecommendationsFetchState: {
|
|
421
|
+
fetchState: 'Completed',
|
|
422
|
+
error: undefined,
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
});
|
|
390
427
|
draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
|
|
391
428
|
transactionId: transaction.id,
|
|
392
|
-
transactionReviewLocalData:
|
|
429
|
+
transactionReviewLocalData: newLocalData,
|
|
393
430
|
};
|
|
431
|
+
// Auto-add the transaction to the bulk-save selection now that
|
|
432
|
+
// fresh recs have landed and all required fields are filled. This
|
|
433
|
+
// is the success-arm of the deferral noted in
|
|
434
|
+
// updateSelectedVendor/CustomerForTransaction: their auto-add was
|
|
435
|
+
// suppressed because pre-fetch category/class were stale; we run
|
|
436
|
+
// the same gate here against newLocalData so the checkbox flips
|
|
437
|
+
// on against fresh recs.
|
|
438
|
+
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = selectionBeforeFetch;
|
|
439
|
+
if (checkIfAllLineItemsAreCategoryClassFilled(transaction, newLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true &&
|
|
440
|
+
selectionBeforeFetch.length < MAX_SELECTION_LIMIT &&
|
|
441
|
+
selectionBeforeFetch.includes(transaction.id) === false) {
|
|
442
|
+
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
443
|
+
...selectionBeforeFetch,
|
|
444
|
+
transaction.id,
|
|
445
|
+
];
|
|
446
|
+
}
|
|
394
447
|
// Mark transaction as dirty (autofill happened)
|
|
395
448
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
|
|
396
449
|
draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
|
|
@@ -444,7 +497,13 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
444
497
|
action.payload.status;
|
|
445
498
|
},
|
|
446
499
|
updateSelectedCustomerForTransaction(draft, action) {
|
|
447
|
-
const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab,
|
|
500
|
+
const { transactionId, lineId, updatedCustomer, selectedTransaction, selectedTab,
|
|
501
|
+
// uncategorizedAccounts, isAccountingClassesEnabled, and
|
|
502
|
+
// isUncategorizedExpenseCategoryEnabled remain in the payload type for
|
|
503
|
+
// backward compatibility but are no longer read here — the post-fetch
|
|
504
|
+
// success reducer (setEntityRecommendationForLineIdsForCategorization)
|
|
505
|
+
// owns the all-fields-filled check that drives auto-selection.
|
|
506
|
+
} = action.payload;
|
|
448
507
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
449
508
|
.transactionReviewLocalDataById, transactionId, undefined);
|
|
450
509
|
if (transactionLocalData != null) {
|
|
@@ -493,18 +552,13 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
493
552
|
transactionReviewLocalData: newTransactionLocalData,
|
|
494
553
|
transactionId,
|
|
495
554
|
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
504
|
-
...selectedCheckBoxTransactionIds,
|
|
505
|
-
transactionId,
|
|
506
|
-
];
|
|
507
|
-
}
|
|
555
|
+
// Auto-selection of the row checkbox is intentionally deferred to
|
|
556
|
+
// setEntityRecommendationForLineIdsForCategorization (success path of
|
|
557
|
+
// the recommendations fetch). On a payee change the existing
|
|
558
|
+
// category/class are stale, so auto-selecting against them would
|
|
559
|
+
// either trigger save flows on the wrong data or rely on a later
|
|
560
|
+
// recommendations write to fix it. Letting the success reducer do the
|
|
561
|
+
// auto-add keeps "checkbox flips on" aligned with "fresh recs are in".
|
|
508
562
|
// Mark transaction as dirty (customer updated)
|
|
509
563
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
|
|
510
564
|
draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
|
|
@@ -516,7 +570,11 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
516
570
|
}
|
|
517
571
|
},
|
|
518
572
|
updateSelectedVendorForTransaction(draft, action) {
|
|
519
|
-
const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab,
|
|
573
|
+
const { transactionId, lineId, updatedVendor, selectedTransaction, selectedTab,
|
|
574
|
+
// See updateSelectedCustomerForTransaction for why
|
|
575
|
+
// uncategorizedAccounts, isAccountingClassesEnabled, and
|
|
576
|
+
// isUncategorizedExpenseCategoryEnabled are no longer destructured.
|
|
577
|
+
} = action.payload;
|
|
520
578
|
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
521
579
|
.transactionReviewLocalDataById, transactionId, undefined);
|
|
522
580
|
if (transactionLocalData != null) {
|
|
@@ -569,18 +627,8 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
569
627
|
},
|
|
570
628
|
transactionId,
|
|
571
629
|
};
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
if (newTransactionLocalData.vendor != null &&
|
|
575
|
-
newTransactionLocalData.vendor.id != null &&
|
|
576
|
-
checkIfAllLineItemsAreCategoryClassFilled(selectedTransaction, newTransactionLocalData, uncategorizedAccounts, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled) === true && // check if all line items are filled
|
|
577
|
-
selectedCheckBoxTransactionIds.length < MAX_SELECTION_LIMIT &&
|
|
578
|
-
selectedCheckBoxTransactionIds.includes(transactionId) === false) {
|
|
579
|
-
draft.transactionCategorizationView[selectedTab].selectedCheckBoxTransactionIds = [
|
|
580
|
-
...selectedCheckBoxTransactionIds,
|
|
581
|
-
transactionId,
|
|
582
|
-
];
|
|
583
|
-
}
|
|
630
|
+
// See updateSelectedCustomerForTransaction above for why auto-select
|
|
631
|
+
// is deferred to setEntityRecommendationForLineIdsForCategorization.
|
|
584
632
|
// Mark transaction as dirty (vendor updated)
|
|
585
633
|
if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transactionId)) {
|
|
586
634
|
draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
|
|
@@ -793,6 +841,54 @@ const expenseAutomationTransactionsView = createSlice({
|
|
|
793
841
|
},
|
|
794
842
|
},
|
|
795
843
|
},
|
|
844
|
+
extraReducers: (builder) => {
|
|
845
|
+
// Start arm of the categorization-only recommendations fetch. Marks every
|
|
846
|
+
// affected line's per-line flag as In-Progress. The epic uses
|
|
847
|
+
// groupBy(transactionId+lineIds) + switchMap, so concurrent fetches for
|
|
848
|
+
// distinct (transaction, lineIds-set) targets do not cancel each other —
|
|
849
|
+
// there's no need for a global cleanup sweep here.
|
|
850
|
+
builder.addCase(fetchEntityRecommendationsForCategorization, (draft, action) => {
|
|
851
|
+
const { selectedTab, transactionDetails } = action.payload;
|
|
852
|
+
const { transactionId, lineIds } = transactionDetails;
|
|
853
|
+
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
854
|
+
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
855
|
+
if (transactionLocalData != null) {
|
|
856
|
+
lineIds.forEach((lineId) => {
|
|
857
|
+
const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
|
|
858
|
+
if (lineItem != null) {
|
|
859
|
+
transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
|
|
860
|
+
...lineItem,
|
|
861
|
+
categoryClassRecommendationsFetchState: {
|
|
862
|
+
fetchState: 'In-Progress',
|
|
863
|
+
error: undefined,
|
|
864
|
+
},
|
|
865
|
+
};
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
// Failure arm. Flips the affected lines' per-line flag to Error so the
|
|
871
|
+
// skeleton resolves and the queued checkbox re-evaluation drops the entry.
|
|
872
|
+
builder.addCase(setCategoryClassRecommendationsForCategorizationFailure, (draft, action) => {
|
|
873
|
+
const { selectedTab, transactionId, lineIds, error } = action.payload;
|
|
874
|
+
const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
|
|
875
|
+
.transactionReviewLocalDataById, transactionId.id, undefined);
|
|
876
|
+
if (transactionLocalData != null) {
|
|
877
|
+
lineIds.forEach((lineId) => {
|
|
878
|
+
const lineItem = transactionLocalData.transactionReviewLocalData.lineItemById[lineId];
|
|
879
|
+
if (lineItem != null) {
|
|
880
|
+
transactionLocalData.transactionReviewLocalData.lineItemById[lineId] = {
|
|
881
|
+
...lineItem,
|
|
882
|
+
categoryClassRecommendationsFetchState: {
|
|
883
|
+
fetchState: 'Error',
|
|
884
|
+
error,
|
|
885
|
+
},
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
});
|
|
891
|
+
},
|
|
796
892
|
});
|
|
797
893
|
export const { fetchTransactionCategorization, updateTransactionCategorizationUIState, initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, fetchTransactionCategorizationFailure, saveTransactionCategorization, updateTransactionCategorization, updateCurrentSelectedTransactionCategorizationTab, updateTransactionCategorizationSaveStatus, markTransactionAsNotMiscategorized, updateStatusForTransactionNotMiscategorizedUpdateForCategorization, updateSelectedVendorForTransaction, updateSelectedCustomerForTransaction, setAllItemsToCategoryClassInLocalDataForCategorization, updateTotalCountForTransactionCategorization, fetchTransactionCategorizationSuccess, resetOtherTabsFetchState, fetchTransactionCategorizationView, updateSelectedCheckboxTransactionIds, clearExpenseAutomationTransactionsViewPerTabView, clearExpenseAutomationTransactionsView, setEntityRecommendationForLineIdsForCategorization, updateSelectedTransactionId, syncTransactionCategorizationFromDetailSave, backgroundRefetchReviewTab, updateTransactionCategorizationUploadReceiptState, uploadTransactionCategorizationReceiptSuccess, } = expenseAutomationTransactionsView.actions;
|
|
798
894
|
export default expenseAutomationTransactionsView.reducer;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import omit from 'lodash/omit';
|
|
2
2
|
import { reduceAllFetchState, reduceAnyFetchState, } from '../../../commonStateTypes/reduceFetchState';
|
|
3
3
|
import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
|
|
4
|
-
import { getIsAccountingClassesEnabled,
|
|
4
|
+
import { getCurrentTenant, getIsAccountingClassesEnabled, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
|
|
5
5
|
import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
|
|
6
6
|
import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
|
|
7
7
|
import { getAccountList, getNestedAccountListHierarchy, getUncategorizedAccounts, } from '../../accountList/accountListSelector';
|
|
8
8
|
import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
|
|
9
|
+
import { getProjectList } from '../../projectList/projectListSelector';
|
|
9
10
|
import { getAllSteps } from '../selectorTypes/expenseAutomationViewSelectorTypes';
|
|
10
11
|
export function getExpenseAutomationTransactionView(state) {
|
|
11
|
-
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
12
|
+
const { accountState, classState, classListState, accountListState, expenseAutomationTransactionsViewState, expenseAutomationViewState, projectState, projectListState, transactionState, monthEndCloseChecksState, monthEndCloseChecksViewState, } = state;
|
|
12
13
|
const { selectedTransactionCategorizationTab } = expenseAutomationTransactionsViewState;
|
|
13
14
|
const { uiState, refreshStatus, saveStatus, markAsNotMiscategorizedStatus, transactionIdsBySelectedPeriod, transactionReviewLocalDataById, selectedCheckBoxTransactionIds, transactionIdsWithUnsavedData, uploadReceiptStatusById, selectedTransactionId, selectedTransactionLineId, } = expenseAutomationTransactionsViewState.transactionCategorizationView[selectedTransactionCategorizationTab];
|
|
14
15
|
const uncategorizedIncomeExpense = getUncategorizedAccounts(accountState, accountListState, 'accountList');
|
|
@@ -17,6 +18,8 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
17
18
|
const allClasses = classList.classes.map((classData) => {
|
|
18
19
|
return omit(classData, ['accountIds', 'key']);
|
|
19
20
|
});
|
|
21
|
+
const projectList = getProjectList(projectState, projectListState);
|
|
22
|
+
const isAccountingProjectsEnabled = getIsAccountingProjectsEnabled(state);
|
|
20
23
|
const accountsHierarchyList = getNestedAccountListHierarchy(accountListState, 'accountList');
|
|
21
24
|
const classHierarchyList = getNestedClassListHierarchy(classListState);
|
|
22
25
|
const currentTenant = getCurrentTenant(state);
|
|
@@ -39,6 +42,11 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
39
42
|
return null;
|
|
40
43
|
})
|
|
41
44
|
.filter((transaction) => transaction != null);
|
|
45
|
+
// True if any visible line is currently fetching category/class
|
|
46
|
+
// recommendations because the user changed the payee. Used to disable Save
|
|
47
|
+
// and to defer manual checkbox selection.
|
|
48
|
+
const hasInFlightCategoryClassRecommendations = transactionLocalData.some((transaction) => Object.values(transaction.transactionReviewLocalData.lineItemById).some((lineItem) => lineItem.categoryClassRecommendationsFetchState?.fetchState ===
|
|
49
|
+
'In-Progress'));
|
|
42
50
|
const transactions = getSupportedTransactionsByIds(transactionState, transactionIds);
|
|
43
51
|
const transactionsWithCOT = transactions.map((transaction) => getTransactionWithCOT(transaction));
|
|
44
52
|
const monthEndFetchState = monthYearPeriodId != null
|
|
@@ -96,6 +104,8 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
96
104
|
classList: allClasses,
|
|
97
105
|
accountsHierarchyList,
|
|
98
106
|
classHierarchyList,
|
|
107
|
+
isAccountingProjectsEnabled,
|
|
108
|
+
projectList,
|
|
99
109
|
transactionLocalData,
|
|
100
110
|
uiState,
|
|
101
111
|
refreshStatus,
|
|
@@ -107,5 +117,6 @@ export function getExpenseAutomationTransactionView(state) {
|
|
|
107
117
|
uploadReceiptStatusById,
|
|
108
118
|
selectedTransactionId,
|
|
109
119
|
selectedTransactionLineId,
|
|
120
|
+
hasInFlightCategoryClassRecommendations,
|
|
110
121
|
};
|
|
111
122
|
}
|
|
@@ -115,6 +115,50 @@ function buildHorizontalClassColumnBalancesSlice(report, classAmounts, totalAmou
|
|
|
115
115
|
},
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Sum balances across descendant accounts (recursive).
|
|
120
|
+
*
|
|
121
|
+
* Parent rows in the horizontal pivot only carry their own direct postings
|
|
122
|
+
* (zero for "container" accounts), so callers rendering "Total {parent}" rows
|
|
123
|
+
* must aggregate from descendants to get the real per-class totals.
|
|
124
|
+
*
|
|
125
|
+
* Returns one `COABalance` per class column + total (same shape as the
|
|
126
|
+
* enriched `balancesInTimeframe.balances`), with `amount` summed and `type`
|
|
127
|
+
* set to `default_with_null_balance` for zeros. Returns `[]` when no
|
|
128
|
+
* descendants have enriched data.
|
|
129
|
+
*/
|
|
130
|
+
export function aggregateHorizontalDescendantBalances(accounts, childIndices, childrenByParent) {
|
|
131
|
+
let template;
|
|
132
|
+
const sums = new Map();
|
|
133
|
+
const walk = (idx) => {
|
|
134
|
+
const childRow = accounts[idx];
|
|
135
|
+
const childBalances = childRow?.accountReportData?.balancesInTimeframe.balances;
|
|
136
|
+
if (childBalances != null) {
|
|
137
|
+
if (template == null) {
|
|
138
|
+
template = childBalances;
|
|
139
|
+
}
|
|
140
|
+
childBalances.forEach((b, i) => {
|
|
141
|
+
sums.set(i, (sums.get(i) ?? 0) + (b.balance.amount ?? 0));
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// Always recurse — a missing intermediate node should not prune its
|
|
145
|
+
// descendants from the aggregate.
|
|
146
|
+
const grandChildren = childrenByParent[idx] ?? [];
|
|
147
|
+
grandChildren.forEach(walk);
|
|
148
|
+
};
|
|
149
|
+
childIndices.forEach(walk);
|
|
150
|
+
if (template == null) {
|
|
151
|
+
return [];
|
|
152
|
+
}
|
|
153
|
+
return template.map((tpl, i) => {
|
|
154
|
+
const amount = sums.get(i) ?? 0;
|
|
155
|
+
return {
|
|
156
|
+
...tpl,
|
|
157
|
+
balance: { ...tpl.balance, amount },
|
|
158
|
+
type: amount === 0 ? 'default_with_null_balance' : 'default',
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
118
162
|
/**
|
|
119
163
|
* Parent index per DFS row — O(n). Matches the scan in the former
|
|
120
164
|
* `findHorizontalAccountParentIndex` (depth <= 2 rows have no parent).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, mergeMap, switchMap, withLatestFrom, } from 'rxjs/operators';
|
|
3
|
+
import { updateProjects } from '../../entity/project/projectReducer';
|
|
4
|
+
import { getIsAccountingProjectsEnabled } from '../../entity/tenant/tenantSelector';
|
|
5
|
+
import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
|
|
6
|
+
import { fetchProjectList, updateProjectList, updateProjectListFailure, } from './projectListReducer';
|
|
7
|
+
const buildProjectListQuery = (isIncludeDeleted) => {
|
|
8
|
+
const query = {
|
|
9
|
+
is_deleted: isIncludeDeleted,
|
|
10
|
+
sort_by: 'project_name',
|
|
11
|
+
sort_order: 'asc',
|
|
12
|
+
is_full: true,
|
|
13
|
+
};
|
|
14
|
+
return `?query=${encodeURIComponent(JSON.stringify(query))}`;
|
|
15
|
+
};
|
|
16
|
+
export const fetchProjectListEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchProjectList.match), withLatestFrom(state$), switchMap(([action, state]) => {
|
|
17
|
+
if (!getIsAccountingProjectsEnabled(state)) {
|
|
18
|
+
return of(updateProjectList({
|
|
19
|
+
status: createZeniAPIStatus('Success', ''),
|
|
20
|
+
data: { projects: [] },
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
const { isIncludeDeleted } = action.payload;
|
|
24
|
+
const queryParams = buildProjectListQuery(isIncludeDeleted);
|
|
25
|
+
return zeniAPI
|
|
26
|
+
.getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/projects${queryParams}`)
|
|
27
|
+
.pipe(mergeMap((response) => {
|
|
28
|
+
if (isSuccessResponse(response)) {
|
|
29
|
+
return of(updateProjects(response.data?.projects ?? []), updateProjectList(response));
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
return of(updateProjectListFailure(response.status));
|
|
33
|
+
}
|
|
34
|
+
}), catchError((error) => {
|
|
35
|
+
return of(updateProjectListFailure(createZeniAPIStatus('Unexpected Error', 'Fetch Projects REST API call errored out' +
|
|
36
|
+
JSON.stringify(error))));
|
|
37
|
+
}));
|
|
38
|
+
}));
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createSlice } from '@reduxjs/toolkit';
|
|
2
|
+
export const initialState = {
|
|
3
|
+
fetchState: 'Not-Started',
|
|
4
|
+
error: undefined,
|
|
5
|
+
projectIds: [],
|
|
6
|
+
hasValidState() {
|
|
7
|
+
return this.fetchState == 'Completed';
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const projectList = createSlice({
|
|
11
|
+
name: 'projectList',
|
|
12
|
+
initialState,
|
|
13
|
+
reducers: {
|
|
14
|
+
fetchProjectList: {
|
|
15
|
+
reducer(draft) {
|
|
16
|
+
draft.fetchState = 'In-Progress';
|
|
17
|
+
draft.error = undefined;
|
|
18
|
+
},
|
|
19
|
+
prepare(isIncludeDeleted) {
|
|
20
|
+
return { payload: { isIncludeDeleted: isIncludeDeleted ?? false } };
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
updateProjectList(draft, action) {
|
|
24
|
+
draft.projectIds =
|
|
25
|
+
action.payload.data?.projects?.map((project) => project.zeni_project_id) ?? [];
|
|
26
|
+
draft.fetchState = 'Completed';
|
|
27
|
+
draft.error = undefined;
|
|
28
|
+
},
|
|
29
|
+
updateProjectListFailure(draft, action) {
|
|
30
|
+
draft.fetchState = 'Error';
|
|
31
|
+
draft.error = action.payload;
|
|
32
|
+
},
|
|
33
|
+
clearProjectList(draft) {
|
|
34
|
+
Object.assign(draft, initialState);
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
export const { fetchProjectList, updateProjectList, updateProjectListFailure, clearProjectList, } = projectList.actions;
|
|
39
|
+
export default projectList.reducer;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getProjectsByIds } from '../../entity/project/projectSelector';
|
|
2
|
+
export const getProjectList = (projectState, projectListState) => {
|
|
3
|
+
return {
|
|
4
|
+
projects: getProjectsByIds(projectState, projectListState.projectIds),
|
|
5
|
+
fetchState: projectListState.fetchState,
|
|
6
|
+
error: projectListState.error,
|
|
7
|
+
version: 0,
|
|
8
|
+
};
|
|
9
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { from, of } from 'rxjs';
|
|
2
|
+
import { catchError, filter, groupBy, mergeMap, switchMap } from 'rxjs/operators';
|
|
3
|
+
import { getIsAccountingClassesEnabled } from '../../entity/tenant/tenantSelector';
|
|
4
|
+
import { getTransactionWithCOT, getTransactionWithCOTFromRecommendations, } from '../../entity/transaction/transactionHelper';
|
|
5
|
+
import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
|
|
6
|
+
import { setEntityRecommendationForLineIdsForCategorization } from '../../view/expenseAutomationView/reducers/transactionsViewReducer';
|
|
7
|
+
import { getUncategorizedAccounts } from '../accountList/accountListSelector';
|
|
8
|
+
import { getActualTransactionID } from '../transactionDetail/transactionDetailState';
|
|
9
|
+
import { getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT, getRecommendationFromRecommendationPayload, } from './recommendationHelper';
|
|
10
|
+
import { fetchEntityRecommendationsForCategorization, setCategoryClassRecommendationsForCategorizationFailure, updateRecommendationForEntity, } from './recommendationReducer';
|
|
11
|
+
// Categorization-list-only recommendations fetch.
|
|
12
|
+
//
|
|
13
|
+
// Concurrency model: groupBy on (transactionId.id, sorted lineIds) → switchMap
|
|
14
|
+
// inside each group → mergeMap to flatten groups. This means:
|
|
15
|
+
// • Same target (same transaction + same lineIds-set) — newer fetch cancels
|
|
16
|
+
// the prior in-flight fetch. Avoids the same-line stale-response race.
|
|
17
|
+
// • Different targets — run fully in parallel. Avoids the silent
|
|
18
|
+
// "stuck spinner" cancellation that the global-switchMap epic suffers
|
|
19
|
+
// from when the user moves between rows or JE lines mid-flight.
|
|
20
|
+
//
|
|
21
|
+
// The transactionsViewReducer's per-line categoryClassRecommendationsFetchState
|
|
22
|
+
// flag is set to In-Progress when the action enters this epic (start arm in
|
|
23
|
+
// extraReducers), Completed via setEntityRecommendationForLineIdsForCategorization,
|
|
24
|
+
// or Error via setCategoryClassRecommendationsForCategorizationFailure.
|
|
25
|
+
export const fetchEntityRecommendationsForCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchEntityRecommendationsForCategorization.match), groupBy((action) => {
|
|
26
|
+
const { transactionId, lineIds } = action.payload.transactionDetails;
|
|
27
|
+
return `${transactionId.id}|${[...lineIds].sort().join('|')}`;
|
|
28
|
+
}), mergeMap((group$) => group$.pipe(switchMap((action) => {
|
|
29
|
+
const { entity, amount, transactionDetails, isFetchCOT, selectedTab, isUncategorizedExpenseCategoryEnabled, } = action.payload;
|
|
30
|
+
const query = {
|
|
31
|
+
amount,
|
|
32
|
+
transaction_id: getActualTransactionID(transactionDetails.transactionId.id),
|
|
33
|
+
transaction_type: transactionDetails.transaction.type,
|
|
34
|
+
line_ids: transactionDetails.lineIds,
|
|
35
|
+
};
|
|
36
|
+
if (isFetchCOT === true) {
|
|
37
|
+
query.include_cot = true;
|
|
38
|
+
}
|
|
39
|
+
const entityQuery = {
|
|
40
|
+
entity_name: entity.name,
|
|
41
|
+
entity_type: entity.type,
|
|
42
|
+
};
|
|
43
|
+
const recommendationUrl = entity.qboId === entity.name // entity id null case
|
|
44
|
+
? `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/recommendations?query=${encodeURIComponent(JSON.stringify(entityQuery))}`
|
|
45
|
+
: `${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/${toEntityTypeURL(entity.type)}/${entity.qboId}/recommendations?query=${encodeURIComponent(JSON.stringify(query))}`;
|
|
46
|
+
return zeniAPI
|
|
47
|
+
.getJSON(recommendationUrl)
|
|
48
|
+
.pipe(mergeMap((response) => {
|
|
49
|
+
if (isSuccessResponse(response) && response.data != null) {
|
|
50
|
+
const actions = [];
|
|
51
|
+
const { transactionId, transaction, lineIds } = transactionDetails;
|
|
52
|
+
const recommendationWithCOTByLineId = getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT(response.data.recommendations, lineIds);
|
|
53
|
+
const recommendations = getRecommendationFromRecommendationPayload(response.data.recommendations);
|
|
54
|
+
const transactionWithCOT = isFetchCOT === true &&
|
|
55
|
+
recommendationWithCOTByLineId != null
|
|
56
|
+
? getTransactionWithCOTFromRecommendations(transaction, recommendationWithCOTByLineId)
|
|
57
|
+
: getTransactionWithCOT(transaction);
|
|
58
|
+
actions.push(updateRecommendationForEntity({
|
|
59
|
+
entityId: entity.id,
|
|
60
|
+
entityType: entity.type,
|
|
61
|
+
recommendations,
|
|
62
|
+
}));
|
|
63
|
+
if (recommendationWithCOTByLineId != null) {
|
|
64
|
+
const uncategorizedIncomeExpense = getUncategorizedAccounts(state$.value.accountState, state$.value.accountListState, 'accountList');
|
|
65
|
+
actions.push(setEntityRecommendationForLineIdsForCategorization(selectedTab, transactionId, transactionWithCOT, lineIds, entity, amount, recommendationWithCOTByLineId, uncategorizedIncomeExpense, isUncategorizedExpenseCategoryEnabled, getIsAccountingClassesEnabled(state$.value)));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Successful HTTP but no per-line recommendations — still
|
|
69
|
+
// need to clear the In-Progress flag, otherwise the
|
|
70
|
+
// skeleton sticks. Treat as a "no recommendations" success
|
|
71
|
+
// by firing the failure action with a benign error so the
|
|
72
|
+
// line flag flips out of In-Progress.
|
|
73
|
+
actions.push(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionId, lineIds, createZeniAPIStatus('Unexpected Error', 'Recommendations response missing per-line data')));
|
|
74
|
+
}
|
|
75
|
+
return from(actions);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
return of(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionDetails.transactionId, transactionDetails.lineIds, response.status));
|
|
79
|
+
}
|
|
80
|
+
}), catchError((error) => of(setCategoryClassRecommendationsForCategorizationFailure(selectedTab, transactionDetails.transactionId, transactionDetails.lineIds, createZeniAPIStatus('Unexpected Error', 'Fetch recommendations for categorization REST API call errored out' +
|
|
81
|
+
JSON.stringify(error))))));
|
|
82
|
+
}))));
|
|
83
|
+
const toEntityTypeURL = (entityType) => `${entityType}s`;
|