@zeniai/client-epic-state 5.0.95-beta1ND → 5.0.95-betaML1

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 (95) hide show
  1. package/lib/commonStateTypes/animations.d.ts +1 -1
  2. package/lib/commonStateTypes/animations.js +0 -1
  3. package/lib/entity/company/companyPayload.d.ts +0 -4
  4. package/lib/entity/company/companyPayload.js +0 -4
  5. package/lib/entity/company/companyStateTypes.d.ts +0 -4
  6. package/lib/entity/paymentAccount/paymentAccountSelector.d.ts +0 -1
  7. package/lib/entity/paymentAccount/paymentAccountSelector.js +0 -4
  8. package/lib/entity/tenant/epic/deleteConnectionEpic.d.ts +1 -1
  9. package/lib/entity/tenant/epic/saveAPIKeyConnectionEpic.d.ts +1 -1
  10. package/lib/entity/tenant/epic/saveConnectorCredentialsEpic.d.ts +3 -3
  11. package/lib/entity/tenant/epic/saveOAuthConnectionEpic.d.ts +1 -1
  12. package/lib/entity/tenant/tenantPayload.d.ts +0 -1
  13. package/lib/entity/tenant/tenantReducer.d.ts +25 -38
  14. package/lib/entity/tenant/tenantReducer.js +5 -65
  15. package/lib/entity/tenant/tenantState.d.ts +0 -1
  16. package/lib/epic.d.ts +1 -6
  17. package/lib/epic.js +1 -7
  18. package/lib/esm/commonStateTypes/animations.js +0 -1
  19. package/lib/esm/entity/company/companyPayload.js +0 -4
  20. package/lib/esm/entity/paymentAccount/paymentAccountSelector.js +0 -3
  21. package/lib/esm/entity/tenant/tenantReducer.js +3 -63
  22. package/lib/esm/epic.js +1 -7
  23. package/lib/esm/index.js +8 -11
  24. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/initializeTransactionCategorizationLocalDataEpic.js +15 -0
  25. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +63 -5
  26. package/lib/esm/view/fileView/epic/deleteFileEpic.js +2 -2
  27. package/lib/esm/view/fileView/epic/deleteFileListEpic.js +2 -2
  28. package/lib/esm/view/fileView/epic/updateFileNameEpic.js +2 -2
  29. package/lib/esm/view/onboardingView/customerView/onboardingCustomerViewReducer.js +50 -217
  30. package/lib/esm/view/onboardingView/customerView/onboardingCustomerViewSelector.js +1 -7
  31. package/lib/esm/view/onboardingView/customerView/onboardingCustomerViewState.js +0 -12
  32. package/lib/esm/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +12 -3
  33. package/lib/esm/view/recommendation/recommendationReducer.js +18 -1
  34. package/lib/esm/view/recommendation/recommendationStaleResponseHelper.js +26 -0
  35. package/lib/esm/view/spendManagement/commonSetup/setupViewReducer.js +52 -105
  36. package/lib/esm/view/spendManagement/commonSetup/setupViewSelector.js +1 -6
  37. package/lib/esm/view/transactionDetail/epics/initializeTransactionDetailLocalDataEpic.js +7 -0
  38. package/lib/esm/view/transactionDetail/transactionDetailReducer.js +44 -3
  39. package/lib/index.d.ts +9 -13
  40. package/lib/index.js +35 -54
  41. package/lib/view/expenseAutomationView/epics/transactionCategorization/initializeTransactionCategorizationLocalDataEpic.d.ts +2 -1
  42. package/lib/view/expenseAutomationView/epics/transactionCategorization/initializeTransactionCategorizationLocalDataEpic.js +15 -0
  43. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +63 -5
  44. package/lib/view/fileView/epic/deleteFileEpic.js +1 -1
  45. package/lib/view/fileView/epic/deleteFileListEpic.js +1 -1
  46. package/lib/view/fileView/epic/updateFileNameEpic.js +1 -1
  47. package/lib/view/onboardingView/customerView/onboardingCustomerViewPayload.d.ts +0 -3
  48. package/lib/view/onboardingView/customerView/onboardingCustomerViewReducer.d.ts +4 -41
  49. package/lib/view/onboardingView/customerView/onboardingCustomerViewReducer.js +51 -219
  50. package/lib/view/onboardingView/customerView/onboardingCustomerViewSelector.d.ts +1 -5
  51. package/lib/view/onboardingView/customerView/onboardingCustomerViewSelector.js +2 -11
  52. package/lib/view/onboardingView/customerView/onboardingCustomerViewState.d.ts +0 -40
  53. package/lib/view/onboardingView/customerView/onboardingCustomerViewState.js +1 -13
  54. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.d.ts +2 -2
  55. package/lib/view/recommendation/fetchEntityRecommendationsForLineUpdateEpic.js +10 -1
  56. package/lib/view/recommendation/recommendationReducer.d.ts +3 -1
  57. package/lib/view/recommendation/recommendationReducer.js +19 -2
  58. package/lib/view/recommendation/recommendationStaleResponseHelper.d.ts +7 -0
  59. package/lib/view/recommendation/recommendationStaleResponseHelper.js +30 -0
  60. package/lib/view/spendManagement/commonSetup/setupViewReducer.d.ts +2 -28
  61. package/lib/view/spendManagement/commonSetup/setupViewReducer.js +53 -106
  62. package/lib/view/spendManagement/commonSetup/setupViewSelector.d.ts +2 -7
  63. package/lib/view/spendManagement/commonSetup/setupViewSelector.js +1 -6
  64. package/lib/view/spendManagement/commonSetup/setupViewState.d.ts +0 -19
  65. package/lib/view/transactionDetail/epics/initializeTransactionDetailLocalDataEpic.d.ts +2 -1
  66. package/lib/view/transactionDetail/epics/initializeTransactionDetailLocalDataEpic.js +7 -0
  67. package/lib/view/transactionDetail/transactionDetailReducer.js +44 -3
  68. package/package.json +1 -1
  69. package/lib/entity/tenant/epic/initEmailConnectOAuthEpic.d.ts +0 -21
  70. package/lib/entity/tenant/epic/initEmailConnectOAuthEpic.js +0 -54
  71. package/lib/esm/entity/tenant/epic/initEmailConnectOAuthEpic.js +0 -50
  72. package/lib/esm/view/onboardingView/customerView/epic/aiAgentsActivation/fetchAiAgentsActivationStatusEpic.js +0 -42
  73. package/lib/esm/view/onboardingView/customerView/epic/customerDetails/acknowledgeOnboardingAiActivationViewedEpic.js +0 -29
  74. package/lib/esm/view/onboardingView/customerView/epic/customerDetails/acknowledgeOnboardingAiFinanceTeamEpic.js +0 -40
  75. package/lib/esm/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.js +0 -58
  76. package/lib/esm/view/spendManagement/commonSetup/epic/setup/parseUploadedKycDocumentEpic.js +0 -77
  77. package/lib/esm/view/spendManagement/commonSetup/kycKybAutofillActions.js +0 -14
  78. package/lib/esm/view/spendManagement/commonSetup/kycKybParseMapper.js +0 -210
  79. package/lib/esm/view/spendManagement/commonSetup/types/kycKybAutofill.js +0 -50
  80. package/lib/view/onboardingView/customerView/epic/aiAgentsActivation/fetchAiAgentsActivationStatusEpic.d.ts +0 -7
  81. package/lib/view/onboardingView/customerView/epic/aiAgentsActivation/fetchAiAgentsActivationStatusEpic.js +0 -46
  82. package/lib/view/onboardingView/customerView/epic/customerDetails/acknowledgeOnboardingAiActivationViewedEpic.d.ts +0 -8
  83. package/lib/view/onboardingView/customerView/epic/customerDetails/acknowledgeOnboardingAiActivationViewedEpic.js +0 -33
  84. package/lib/view/onboardingView/customerView/epic/customerDetails/acknowledgeOnboardingAiFinanceTeamEpic.d.ts +0 -9
  85. package/lib/view/onboardingView/customerView/epic/customerDetails/acknowledgeOnboardingAiFinanceTeamEpic.js +0 -44
  86. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.d.ts +0 -28
  87. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKybDocumentEpic.js +0 -62
  88. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKycDocumentEpic.d.ts +0 -9
  89. package/lib/view/spendManagement/commonSetup/epic/setup/parseUploadedKycDocumentEpic.js +0 -81
  90. package/lib/view/spendManagement/commonSetup/kycKybAutofillActions.d.ts +0 -33
  91. package/lib/view/spendManagement/commonSetup/kycKybAutofillActions.js +0 -17
  92. package/lib/view/spendManagement/commonSetup/kycKybParseMapper.d.ts +0 -36
  93. package/lib/view/spendManagement/commonSetup/kycKybParseMapper.js +0 -218
  94. package/lib/view/spendManagement/commonSetup/types/kycKybAutofill.d.ts +0 -154
  95. package/lib/view/spendManagement/commonSetup/types/kycKybAutofill.js +0 -55
@@ -3,6 +3,7 @@ import { filter, mergeMap } from 'rxjs/operators';
3
3
  import { getTransactionWithCOT } from '../../../../entity/transaction/transactionHelper';
4
4
  import { getSupportedTransactionsByIds } from '../../../../entity/transaction/transactionSelector';
5
5
  import { getUncategorizedAccounts } from '../../../accountList/accountListSelector';
6
+ import { cancelEntityRecommendationsForLineUpdate } from '../../../recommendation/recommendationReducer';
6
7
  import { mergeTabSpecificLineItems, toTransactionDetailLocalData, } from '../../helpers/transactionCategorizationLocalDataHelper';
7
8
  import { initializeTransactionCategorizationViewLocalData, saveTransactionCategorizationLocalData, } from '../../reducers/transactionsViewReducer';
8
9
  import { initialSupportedTransactionCategorization, } from '../../types/transactionsViewState';
@@ -51,6 +52,20 @@ export const initializeTransactionCategorizationViewLocalDataEpic = (actions$, s
51
52
  };
52
53
  }
53
54
  });
55
+ // Cancel any in-flight line-update recommendations fetch for the
56
+ // transactions whose local data is about to be wiped. Only runs
57
+ // for the destructive `keepExistingListItems === false` branch
58
+ // (list-page Cancel footer, modal confirm-reset, and the silent
59
+ // `backgroundRefetchReviewTab` path); the merge branch keeps
60
+ // existing per-line state intact and races no fetches.
61
+ if (keepExistingListItems === false) {
62
+ transactions.forEach((transaction) => {
63
+ fetchActions.push(cancelEntityRecommendationsForLineUpdate({
64
+ id: transaction.id,
65
+ type: transaction.type,
66
+ }));
67
+ });
68
+ }
54
69
  fetchActions.push(saveTransactionCategorizationLocalData({
55
70
  selectedTab,
56
71
  transactionIds,
@@ -3,6 +3,7 @@ import recordGet from 'lodash/get';
3
3
  import uniq from 'lodash/uniq';
4
4
  import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
5
5
  import { isVendorTransaction } from '../../../entity/transaction/stateTypes/vendorTransaction';
6
+ import { getLineIdsWhereEntityStillMatches } from '../../recommendation/recommendationStaleResponseHelper';
6
7
  import { filterAutoTabLineItems, mergeTabSpecificLineItems, removeTransactionFromTabView, setEntityRecommendationForLineIdInLocalData, shouldAutoSelectAndAdd, toSetAllLineItemsToCategoryClass, toTransactionDetailLocalData, } from '../helpers/transactionCategorizationLocalDataHelper';
7
8
  import { DEFAULT_COMPLETED_SUB_TAB, } from '../types/completedSubTab';
8
9
  import { TRANSACTIONS_TABS, } from '../types/transactionsViewState';
@@ -516,11 +517,66 @@ const expenseAutomationTransactionsView = createSlice({
516
517
  const transactionLocalData = recordGet(draft.transactionCategorizationView[selectedTab]
517
518
  .transactionReviewLocalDataById, transactionId.id, undefined);
518
519
  if (transactionLocalData != null) {
519
- const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, lineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
520
+ // Stale-response guard. Filter `lineIds` down to lines whose
521
+ // currently selected entity still matches the payload
522
+ // entity, so a response that lands after the user (or the
523
+ // silent backgroundRefetchReviewTab path) wiped local data
524
+ // can't overwrite the restored state. Lines that fail the
525
+ // guard are still released to Completed below so their
526
+ // inline skeletons resolve. Complements the epic-level
527
+ // `takeUntil`.
528
+ //
529
+ // List flow: ONLY vendor uses page-level `selectedEntity`;
530
+ // customer + JE + deposit all use per-line
531
+ // `selectedEntityLine`. This differs from the detail flow
532
+ // (where customer is page-level too) and matches the
533
+ // dichotomy in `setEntityRecommendationForLineIdInLocalData`.
534
+ const usesPageLevelEntity = isVendorTransaction(transaction);
535
+ const matchedLineIds = getLineIdsWhereEntityStillMatches({
536
+ lineIds,
537
+ payloadEntity: entity,
538
+ getCurrentEntityForLine: (lineId) => usesPageLevelEntity
539
+ ? transactionLocalData.transactionReviewLocalData.selectedEntity
540
+ : transactionLocalData.transactionReviewLocalData.lineItemById[lineId]?.selectedEntityLine,
541
+ });
542
+ // When every line failed the guard, skip the helper entirely.
543
+ // The list-flow helper's `else` branch writes
544
+ // recommendations + account/class onto vendor-txn lines even
545
+ // when they're not in `recommendationLineIds` (so callers
546
+ // can fold full-transaction recs into untouched lines), and
547
+ // we don't want any of those writes when the whole response
548
+ // is stale. We still flip the per-line fetch flag so
549
+ // skeletons resolve.
550
+ if (matchedLineIds.length === 0) {
551
+ const existingLocalData = transactionLocalData.transactionReviewLocalData;
552
+ const releasedLineItems = { ...existingLocalData.lineItemById };
553
+ lineIds.forEach((lineId) => {
554
+ const lineItem = releasedLineItems[lineId];
555
+ if (lineItem != null) {
556
+ releasedLineItems[lineId] = {
557
+ ...lineItem,
558
+ categoryClassRecommendationsFetchState: {
559
+ fetchState: 'Completed',
560
+ error: undefined,
561
+ },
562
+ };
563
+ }
564
+ });
565
+ draft.transactionCategorizationView[selectedTab].transactionReviewLocalDataById[transaction.id] = {
566
+ transactionId: transaction.id,
567
+ transactionReviewLocalData: {
568
+ ...existingLocalData,
569
+ lineItemById: releasedLineItems,
570
+ },
571
+ };
572
+ return;
573
+ }
574
+ const newLocalData = setEntityRecommendationForLineIdInLocalData(draft.transactionCategorizationView[selectedTab], transaction, transactionLocalData.transactionReviewLocalData, entity, matchedLineIds, uncategorizedAccounts, action.payload.recommendationWithCOTByLineId, isUncategorizedExpenseCategoryEnabled, isAccountingClassesEnabled);
520
575
  // Release the per-line fetch flag for every line this fetch
521
- // covered. Done in-place on the freshly returned local data
522
- // (the helper produces a new mutable object), before we write
523
- // it back to the draft.
576
+ // covered (matched AND unmatched) so the inline skeleton
577
+ // resolves on stale lines too. Done in-place on the freshly
578
+ // returned local data (the helper produces a new mutable
579
+ // object), before we write it back to the draft.
524
580
  lineIds.forEach((lineId) => {
525
581
  const lineItem = newLocalData.lineItemById[lineId];
526
582
  if (lineItem != null) {
@@ -537,7 +593,9 @@ const expenseAutomationTransactionsView = createSlice({
537
593
  transactionId: transaction.id,
538
594
  transactionReviewLocalData: newLocalData,
539
595
  };
540
- // Mark transaction as dirty (autofill happened)
596
+ // Mark transaction as dirty (autofill happened). We only
597
+ // reach this point with at least one matched line, so
598
+ // recommendations were applied.
541
599
  if (!draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData.includes(transaction.id)) {
542
600
  draft.transactionCategorizationView[selectedTab].transactionIdsWithUnsavedData = [
543
601
  ...draft.transactionCategorizationView[selectedTab]
@@ -1,9 +1,9 @@
1
1
  import { of } from 'rxjs';
2
- import { catchError, filter, mergeMap } from 'rxjs/operators';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { removeFiles } from '../../../entity/file/fileReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
5
  import { deleteFile, deleteFileFailure, deleteFileSuccess, } from '../fileViewReducer';
6
- export const deleteFileEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(deleteFile.match), mergeMap((action) => {
6
+ export const deleteFileEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(deleteFile.match), switchMap((action) => {
7
7
  /**
8
8
  * tenantId is required for deleting onboarding files.
9
9
  */
@@ -1,9 +1,9 @@
1
1
  import { of } from 'rxjs';
2
- import { catchError, filter, mergeMap } from 'rxjs/operators';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { removeFiles } from '../../../entity/file/fileReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
5
  import { deleteFileList, deleteFileListFailure, deleteFileListSuccess, } from '../fileViewReducer';
6
- export const deleteFileListEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(deleteFileList.match), mergeMap((action) => {
6
+ export const deleteFileListEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(deleteFileList.match), switchMap((action) => {
7
7
  const { fileIds } = action.payload;
8
8
  return zeniAPI
9
9
  .deleteAndGetJSON(`${zeniAPI.apiEndPoints.fileMicroServiceBaseUrl}/1.0/files?query=${encodeURIComponent(JSON.stringify({ file_ids: fileIds }))}`)
@@ -1,9 +1,9 @@
1
1
  import { of } from 'rxjs';
2
- import { catchError, filter, mergeMap } from 'rxjs/operators';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { updateFiles } from '../../../entity/file/fileReducer';
4
4
  import { createZeniAPIStatus, isSuccessResponse } from '../../../responsePayload';
5
5
  import { updateFileName, updateFileNameFailure, updateFileNameSuccess, } from '../fileViewReducer';
6
- export const updateFileNameEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(updateFileName.match), mergeMap((action) => {
6
+ export const updateFileNameEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(updateFileName.match), switchMap((action) => {
7
7
  /**
8
8
  * tenantId is required for updating onboarding file names.
9
9
  */
@@ -1,7 +1,5 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { date } from '../../../zeniDayJS';
3
- import { parseUploadedKybDocument, parseUploadedKybDocumentFailure, parseUploadedKycDocument, parseUploadedKycDocumentFailure, } from '../../spendManagement/commonSetup/kycKybAutofillActions';
4
- import { emptyAiAgentsActivationSubview, } from './onboardingCustomerViewState';
5
3
  const initialStatus = {
6
4
  fetchState: 'Not-Started',
7
5
  error: undefined,
@@ -11,31 +9,61 @@ export const initialState = {
11
9
  saveStatus: initialStatus,
12
10
  dashboardLoadedStatus: initialStatus,
13
11
  currentStep: 'link_payment_account',
14
- parsingDocumentFileIds: [],
15
12
  companyDetails: {
16
- autoFilledFields: [],
17
13
  localData: undefined,
18
14
  },
19
- companyOfficerUpdateStatus: (() => {
20
- const emptyOfficerSlot = {
21
- autoFilledFields: [],
15
+ companyOfficerUpdateStatus: {
16
+ Officer_1: {
22
17
  localData: undefined,
23
18
  sendOtpStatus: initialStatus,
24
19
  otpverificationStatus: initialStatus,
25
- };
26
- return {
27
- Officer_1: { ...emptyOfficerSlot },
28
- Officer_2: { ...emptyOfficerSlot },
29
- Officer_3: { ...emptyOfficerSlot },
30
- Officer_4: { ...emptyOfficerSlot },
31
- Officer_5: { ...emptyOfficerSlot },
32
- Officer_6: { ...emptyOfficerSlot },
33
- Officer_7: { ...emptyOfficerSlot },
34
- Officer_8: { ...emptyOfficerSlot },
35
- Officer_9: { ...emptyOfficerSlot },
36
- Officer_10: { ...emptyOfficerSlot },
37
- };
38
- })(),
20
+ },
21
+ Officer_2: {
22
+ localData: undefined,
23
+ sendOtpStatus: initialStatus,
24
+ otpverificationStatus: initialStatus,
25
+ },
26
+ Officer_3: {
27
+ localData: undefined,
28
+ sendOtpStatus: initialStatus,
29
+ otpverificationStatus: initialStatus,
30
+ },
31
+ Officer_4: {
32
+ localData: undefined,
33
+ sendOtpStatus: initialStatus,
34
+ otpverificationStatus: initialStatus,
35
+ },
36
+ Officer_5: {
37
+ localData: undefined,
38
+ sendOtpStatus: initialStatus,
39
+ otpverificationStatus: initialStatus,
40
+ },
41
+ Officer_6: {
42
+ localData: undefined,
43
+ sendOtpStatus: initialStatus,
44
+ otpverificationStatus: initialStatus,
45
+ },
46
+ Officer_7: {
47
+ localData: undefined,
48
+ sendOtpStatus: initialStatus,
49
+ otpverificationStatus: initialStatus,
50
+ },
51
+ Officer_8: {
52
+ localData: undefined,
53
+ sendOtpStatus: initialStatus,
54
+ otpverificationStatus: initialStatus,
55
+ },
56
+ Officer_9: {
57
+ localData: undefined,
58
+ sendOtpStatus: initialStatus,
59
+ otpverificationStatus: initialStatus,
60
+ },
61
+ Officer_10: {
62
+ localData: undefined,
63
+ sendOtpStatus: initialStatus,
64
+ otpverificationStatus: initialStatus,
65
+ },
66
+ },
39
67
  primaryContactDetails: {},
40
68
  paymentAccountDetails: {
41
69
  fetchStatus: initialStatus,
@@ -63,7 +91,6 @@ export const initialState = {
63
91
  },
64
92
  fetchState: 'Not-Started',
65
93
  error: undefined,
66
- aiAgentsActivation: { ...emptyAiAgentsActivationSubview },
67
94
  uiState: {
68
95
  isQBOPopupOpen: false,
69
96
  isCardPaymentPopupOpen: false,
@@ -153,126 +180,6 @@ const onboardingCustomerView = createSlice({
153
180
  };
154
181
  },
155
182
  },
156
- acknowledgeOnboardingAiFinanceTeam: {
157
- reducer(draft) {
158
- draft.aiFinanceTeamAcknowledgementStatus = {
159
- fetchState: 'In-Progress',
160
- error: undefined,
161
- };
162
- },
163
- prepare(companyId) {
164
- return {
165
- payload: { companyId },
166
- };
167
- },
168
- },
169
- acknowledgeOnboardingAiFinanceTeamSuccess(draft) {
170
- draft.aiFinanceTeamAcknowledgementStatus = {
171
- fetchState: 'Completed',
172
- error: undefined,
173
- };
174
- },
175
- acknowledgeOnboardingAiFinanceTeamFailure: {
176
- reducer(draft, action) {
177
- draft.aiFinanceTeamAcknowledgementStatus = {
178
- fetchState: 'Error',
179
- error: action.payload,
180
- };
181
- },
182
- prepare(error) {
183
- return {
184
- payload: error,
185
- };
186
- },
187
- },
188
- acknowledgeOnboardingAiActivationViewed: {
189
- reducer(draft) {
190
- draft.aiActivationViewedAcknowledgementStatus = {
191
- fetchState: 'In-Progress',
192
- error: undefined,
193
- };
194
- },
195
- prepare(companyId) {
196
- return {
197
- payload: { companyId },
198
- };
199
- },
200
- },
201
- acknowledgeOnboardingAiActivationViewedSuccess(draft) {
202
- draft.aiActivationViewedAcknowledgementStatus = {
203
- fetchState: 'Completed',
204
- error: undefined,
205
- };
206
- },
207
- acknowledgeOnboardingAiActivationViewedFailure: {
208
- reducer(draft, action) {
209
- draft.aiActivationViewedAcknowledgementStatus = {
210
- fetchState: 'Error',
211
- error: action.payload,
212
- };
213
- },
214
- prepare(error) {
215
- return {
216
- payload: error,
217
- };
218
- },
219
- },
220
- fetchAiAgentsActivationStatus(draft) {
221
- draft.aiAgentsActivation.fetchStatus = {
222
- fetchState: 'In-Progress',
223
- error: undefined,
224
- };
225
- },
226
- fetchAiAgentsActivationStatusSuccess: {
227
- reducer(draft, action) {
228
- draft.aiAgentsActivation.fetchStatus = {
229
- fetchState: 'Completed',
230
- error: undefined,
231
- };
232
- draft.aiAgentsActivation.phases = action.payload.phases;
233
- draft.aiAgentsActivation.counts = action.payload.counts;
234
- },
235
- prepare(payload) {
236
- return { payload };
237
- },
238
- },
239
- fetchAiAgentsActivationStatusFailure: {
240
- reducer(draft, action) {
241
- draft.aiAgentsActivation.fetchStatus = {
242
- fetchState: 'Error',
243
- error: action.payload,
244
- };
245
- },
246
- prepare(error) {
247
- return { payload: error };
248
- },
249
- },
250
- aiAgentsActivationCountsUpdated: {
251
- reducer(draft, action) {
252
- draft.aiAgentsActivation.counts = {
253
- ...draft.aiAgentsActivation.counts,
254
- ...action.payload,
255
- };
256
- },
257
- prepare(payload) {
258
- return { payload };
259
- },
260
- },
261
- aiAgentsActivationPhaseChanged: {
262
- reducer(draft, action) {
263
- const target = draft.aiAgentsActivation.phases;
264
- const index = target.findIndex((phase) => phase.key === action.payload.key);
265
- if (index === -1) {
266
- target.push(action.payload);
267
- }
268
- else {
269
- target[index] = { ...target[index], ...action.payload };
270
- }
271
- },
272
- prepare(payload) {
273
- return { payload };
274
- },
275
- },
276
183
  updateOnboardingCustomerViewDashboardLoaded: {
277
184
  reducer(draft) {
278
185
  draft.dashboardLoadedStatus = {
@@ -685,81 +592,7 @@ const onboardingCustomerView = createSlice({
685
592
  clearOnboardingCustomerView(draft) {
686
593
  Object.assign(draft, initialState);
687
594
  },
688
- /** Merge AI-parsed officer fields into the officer's onboarding localData. */
689
- applyKycDocumentAutofillForOnboarding: {
690
- reducer(draft, action) {
691
- const { officerType, values, autoFilledFieldNames, fileId } = action.payload;
692
- const slot = draft.companyOfficerUpdateStatus[officerType];
693
- slot.localData = Object.assign({}, slot.localData, values);
694
- slot.autoFilledFields = Array.from(new Set([...slot.autoFilledFields, ...autoFilledFieldNames]));
695
- draft.parsingDocumentFileIds = draft.parsingDocumentFileIds.filter((id) => id !== fileId);
696
- },
697
- prepare(payload) {
698
- return { payload };
699
- },
700
- },
701
- /** Merge AI-parsed company fields into companyDetails onboarding localData. */
702
- applyKybDocumentAutofillForOnboarding: {
703
- reducer(draft, action) {
704
- const { values, autoFilledFieldNames, fileId } = action.payload;
705
- draft.companyDetails.localData = Object.assign({}, draft.companyDetails.localData, values);
706
- const prev = draft.companyDetails.autoFilledFields ?? [];
707
- draft.companyDetails.autoFilledFields = Array.from(new Set([...prev, ...autoFilledFieldNames]));
708
- draft.parsingDocumentFileIds = draft.parsingDocumentFileIds.filter((id) => id !== fileId);
709
- },
710
- prepare(payload) {
711
- return { payload };
712
- },
713
- },
714
- /** Wipe AI-autofill badges during onboarding. */
715
- clearKycKybAutofillForOnboarding: {
716
- reducer(draft, action) {
717
- const { officerType } = action.payload;
718
- if (officerType != null) {
719
- draft.companyOfficerUpdateStatus[officerType].autoFilledFields = [];
720
- }
721
- else {
722
- draft.companyDetails.autoFilledFields = [];
723
- Object.keys(draft.companyOfficerUpdateStatus).forEach((key) => {
724
- draft.companyOfficerUpdateStatus[key].autoFilledFields = [];
725
- });
726
- }
727
- },
728
- prepare(payload = {}) {
729
- return { payload };
730
- },
731
- },
732
- },
733
- extraReducers: (builder) => {
734
- const addParsingFileId = (draft, fileId) => {
735
- if (!draft.parsingDocumentFileIds.includes(fileId)) {
736
- draft.parsingDocumentFileIds.push(fileId);
737
- }
738
- };
739
- const removeParsingFileId = (draft, fileId) => {
740
- draft.parsingDocumentFileIds = draft.parsingDocumentFileIds.filter((id) => id !== fileId);
741
- };
742
- builder.addCase(parseUploadedKycDocument, (draft, action) => {
743
- if (action.payload.target === 'onboarding') {
744
- addParsingFileId(draft, action.payload.fileId);
745
- }
746
- });
747
- builder.addCase(parseUploadedKybDocument, (draft, action) => {
748
- if (action.payload.target === 'onboarding') {
749
- addParsingFileId(draft, action.payload.fileId);
750
- }
751
- });
752
- builder.addCase(parseUploadedKycDocumentFailure, (draft, action) => {
753
- if (action.payload.target === 'onboarding') {
754
- removeParsingFileId(draft, action.payload.fileId);
755
- }
756
- });
757
- builder.addCase(parseUploadedKybDocumentFailure, (draft, action) => {
758
- if (action.payload.target === 'onboarding') {
759
- removeParsingFileId(draft, action.payload.fileId);
760
- }
761
- });
762
595
  },
763
596
  });
764
- export const { fetchOnboardingCustomerView, fetchOnboardingCustomerSetupView, fetchOnboardingCustomerSetupViewSuccess, fetchOnboardingCustomerSetupViewFailure, updateOnboardingCustomerViewCompleteStatus, updateOnboardingCustomerViewCompleteStatusSuccess, updateOnboardingCustomerViewCompleteStatusFailure, acknowledgeOnboardingAiFinanceTeam, acknowledgeOnboardingAiFinanceTeamSuccess, acknowledgeOnboardingAiFinanceTeamFailure, acknowledgeOnboardingAiActivationViewed, acknowledgeOnboardingAiActivationViewedSuccess, acknowledgeOnboardingAiActivationViewedFailure, fetchAiAgentsActivationStatus, fetchAiAgentsActivationStatusSuccess, fetchAiAgentsActivationStatusFailure, aiAgentsActivationCountsUpdated, aiAgentsActivationPhaseChanged, updateOnboardingCustomerViewDashboardLoaded, updateOnboardingCustomerViewDashboardLoadedSuccess, updateOnboardingCustomerViewDashboardLoadedFailure, updateOnboardingCustomerView, updateOnboardingCustomerViewSuccess, updateOnboardingCustomerViewFailure, updateOnboardingPaymentAccountStatus, updateOnboardingPaymentAccountStatusSuccess, updateOnboardingPaymentAccountStatusFailure, updateOnboardingPaymentAccountLoginStatus, updateOnboardingPaymentAccountLoginStatusSuccess, updateOnboardingPaymentAccountLoginStatusFailure, establishOnboardingPlaidConnection, establishOnboardingPlaidConnectionSuccess, establishOnboardingPlaidConnectionFailure, getOnboardingPlaidLinkToken, getOnboardingPlaidLinkTokenSuccess, getOnboardingPlaidLinkTokenFailure, updateCurrentStep, updateOnboardingCustomerViewAccountDetails, updateOnboardingCustomerViewLocalStoreData, saveOnboardingCustomerViewDataInLocalStore, saveOnboardingCompnayOfficerPhoneInLocalStore, updateOnboardingCustomerViewUIState, sendOnboardingOfficerOtp, sendOnboardingOfficerOtpSuccess, sendOnboardingOfficerOtpFailure, resendOnboardingOfficerOtp, verifyOnboardingOfficerOtp, verifyOnboardingOfficerOtpSuccess, verifyOnboardingOfficerOtpFailure, clearOnboardingCustomerViewDataInLocalStore, clearOnboardingCustomerView, applyKycDocumentAutofillForOnboarding, applyKybDocumentAutofillForOnboarding, clearKycKybAutofillForOnboarding, } = onboardingCustomerView.actions;
597
+ export const { fetchOnboardingCustomerView, fetchOnboardingCustomerSetupView, fetchOnboardingCustomerSetupViewSuccess, fetchOnboardingCustomerSetupViewFailure, updateOnboardingCustomerViewCompleteStatus, updateOnboardingCustomerViewCompleteStatusSuccess, updateOnboardingCustomerViewCompleteStatusFailure, updateOnboardingCustomerViewDashboardLoaded, updateOnboardingCustomerViewDashboardLoadedSuccess, updateOnboardingCustomerViewDashboardLoadedFailure, updateOnboardingCustomerView, updateOnboardingCustomerViewSuccess, updateOnboardingCustomerViewFailure, updateOnboardingPaymentAccountStatus, updateOnboardingPaymentAccountStatusSuccess, updateOnboardingPaymentAccountStatusFailure, updateOnboardingPaymentAccountLoginStatus, updateOnboardingPaymentAccountLoginStatusSuccess, updateOnboardingPaymentAccountLoginStatusFailure, establishOnboardingPlaidConnection, establishOnboardingPlaidConnectionSuccess, establishOnboardingPlaidConnectionFailure, getOnboardingPlaidLinkToken, getOnboardingPlaidLinkTokenSuccess, getOnboardingPlaidLinkTokenFailure, updateCurrentStep, updateOnboardingCustomerViewAccountDetails, updateOnboardingCustomerViewLocalStoreData, saveOnboardingCustomerViewDataInLocalStore, saveOnboardingCompnayOfficerPhoneInLocalStore, updateOnboardingCustomerViewUIState, sendOnboardingOfficerOtp, sendOnboardingOfficerOtpSuccess, sendOnboardingOfficerOtpFailure, resendOnboardingOfficerOtp, verifyOnboardingOfficerOtp, verifyOnboardingOfficerOtpSuccess, verifyOnboardingOfficerOtpFailure, clearOnboardingCustomerViewDataInLocalStore, clearOnboardingCustomerView, } = onboardingCustomerView.actions;
765
598
  export default onboardingCustomerView.reducer;
@@ -11,7 +11,7 @@ import { getCompanyAndIdentityDetails, } from '../../spendManagement/commonSetup
11
11
  import { ALL_ONBOARDING_STEPS, ALL_ZENI_USER_ONBOARDING_STEPS, REQUIRED_ONBOARDING_STEPS, ZENI_USER_REQUIRED_ONBOARDING_STEPS, toOnboardingStepType, } from './onboardingCustomerViewState';
12
12
  export function getOnboardingCustomerView(state, companyId) {
13
13
  const { fileState, paymentAccountState, onboardingCustomerViewState, userState, userRoleState, subscriptionPaymentAccountState, companyConfigState, cardPaymentViewState, countryListState, addressViewState, } = state;
14
- const { companyDetails, companyOfficerUpdateStatus, primaryContactDetails, subscriptionPaymentAccountIds, paymentAccountDetails, plaidConnectionDetails, parsingDocumentFileIds, updateStatus, saveStatus, currentStep, uiState, fetchState, error, } = onboardingCustomerViewState;
14
+ const { companyDetails, companyOfficerUpdateStatus, primaryContactDetails, subscriptionPaymentAccountIds, paymentAccountDetails, plaidConnectionDetails, updateStatus, saveStatus, currentStep, uiState, fetchState, error, } = onboardingCustomerViewState;
15
15
  const { paymentAccountIds } = paymentAccountDetails;
16
16
  let subscriptionPaymentAccount;
17
17
  let paymentAccount;
@@ -54,7 +54,6 @@ export function getOnboardingCustomerView(state, companyId) {
54
54
  companyDetails,
55
55
  companyOfficerUpdateStatus,
56
56
  primaryContactDetails,
57
- parsingDocumentFileIds,
58
57
  });
59
58
  const companyAddress = addressViewState.newAddressState?.company_address;
60
59
  if (companyAddress == null &&
@@ -120,7 +119,6 @@ export function getOnboardingCustomerView(state, companyId) {
120
119
  return {
121
120
  companyDetailsLocalData,
122
121
  companyOfficerLocalData,
123
- parsingDocumentFileIds,
124
122
  subscriptionBillingAddress: subscriptionBillingAddress?.addressToCreate,
125
123
  companyView,
126
124
  allNationalityCountries,
@@ -325,7 +323,3 @@ export const getProductSettingsString = (productSettings) => {
325
323
  .map((key) => productKeyMap[key])
326
324
  .join(',');
327
325
  };
328
- const fromActivationView = (state) => state.onboardingCustomerViewState.aiAgentsActivation;
329
- export const getAiAgentsActivationSubview = (state) => fromActivationView(state);
330
- export const getAiAgentsActivationPhases = (state) => fromActivationView(state).phases;
331
- export const getAiAgentsActivationCounts = (state) => fromActivationView(state).counts;
@@ -26,15 +26,3 @@ export const ALL_ONBOARDING_STEPS = [
26
26
  ...ALL_ZENI_USER_ONBOARDING_STEPS_WITHOUT_ACCOUNT,
27
27
  ];
28
28
  export const toOnboardingStepType = (v) => stringToUnion(v, ALL_ONBOARDING_STEPS);
29
- export const emptyAiAgentsActivationCounts = {
30
- customerCount: 0,
31
- glAccountCount: 0,
32
- targetTransactionCount: 0,
33
- transactionCount: 0,
34
- vendorCount: 0,
35
- };
36
- export const emptyAiAgentsActivationSubview = {
37
- counts: emptyAiAgentsActivationCounts,
38
- fetchStatus: { fetchState: 'Not-Started', error: undefined },
39
- phases: [],
40
- };
@@ -1,5 +1,5 @@
1
1
  import { concat, of, timer } from 'rxjs';
2
- import { catchError, debounce, filter, groupBy, mergeMap, switchMap, } from 'rxjs/operators';
2
+ import { catchError, debounce, filter, groupBy, mergeMap, switchMap, takeUntil, } from 'rxjs/operators';
3
3
  import { getIsAccountingClassesEnabled } from '../../entity/tenant/tenantSelector';
4
4
  import { getTransactionWithCOT, getTransactionWithCOTFromRecommendations, } from '../../entity/transaction/transactionHelper';
5
5
  import { createZeniAPIStatus, isSuccessResponse } from '../../responsePayload';
@@ -7,7 +7,7 @@ import { getUncategorizedAccounts } from '../accountList/accountListSelector';
7
7
  import { markCategoryClassRecommendationsCompletedForCategorization, markCategoryClassRecommendationsFailureForCategorization, markCategoryClassRecommendationsInProgressForCategorization, setEntityRecommendationForLineIdsForCategorization, } from '../expenseAutomationView/reducers/transactionsViewReducer';
8
8
  import { markCategoryClassRecommendationsCompletedForTransactionDetail, markCategoryClassRecommendationsFailureForTransactionDetail, markCategoryClassRecommendationsInProgressForTransactionDetail, setEntityRecommendationForLineIdsForTransactionDetail, } from '../transactionDetail/transactionDetailReducer';
9
9
  import { buildRecommendationUrl, getRecommendationByLineIdWithCOTFromRecommendationPayloadWithCOT, } from './recommendationHelper';
10
- import { fetchEntityRecommendationsForLineUpdate } from './recommendationReducer';
10
+ import { cancelEntityRecommendationsForLineUpdate, fetchEntityRecommendationsForLineUpdate, } from './recommendationReducer';
11
11
  // Close the per-target groupBy bucket after this much idle time so inactive
12
12
  // groups don't accumulate forever (each group holds a Subject + a chain of
13
13
  // active subscribers; without a duration selector groupBy retains them for
@@ -83,5 +83,14 @@ export const fetchEntityRecommendationsForLineUpdateEpic = (actions$, state$, ze
83
83
  ? markCategoryClassRecommendationsFailureForCategorization(payload.selectedTab, transactionId, lineIds, status)
84
84
  : markCategoryClassRecommendationsFailureForTransactionDetail(transactionId, lineIds, status));
85
85
  }));
86
- return concat(of(startAction), httpStream$);
86
+ // takeUntil kills the in-flight HTTP and suppresses every
87
+ // downstream success/failure/markCompleted emission once a
88
+ // cancel action lands for the same transactionId. groupBy +
89
+ // switchMap already handle re-fetch-on-same-target; takeUntil
90
+ // covers the user-initiated Cancel (detail footer + list
91
+ // footer) and the silent backgroundRefetchReviewTab path,
92
+ // which both wipe local data without dispatching another
93
+ // fetch on the same target.
94
+ const cancel$ = actions$.pipe(filter(cancelEntityRecommendationsForLineUpdate.match), filter((a) => a.payload.transactionId.id === transactionId.id));
95
+ return concat(of(startAction), httpStream$.pipe(takeUntil(cancel$)));
87
96
  }))));
@@ -58,6 +58,23 @@ const recommendation = createSlice({
58
58
  return { payload: args };
59
59
  },
60
60
  },
61
+ // No-op reducer; the action exists purely so the line-update epic
62
+ // can observe it via `takeUntil` and kill an in-flight HTTP request
63
+ // when the user (or a background refresh) wipes local data before
64
+ // the response lands. Without this, a stale response races the reset
65
+ // and overwrites the user's intent. Cancel is keyed by
66
+ // `transactionId` only: per-page Cancel buttons cancel everything
67
+ // under that transaction (including JE sibling line sets), which
68
+ // matches the UI semantics on both detail and categorization pages.
69
+ cancelEntityRecommendationsForLineUpdate: {
70
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
71
+ reducer(_draft, _action) {
72
+ // intentionally empty
73
+ },
74
+ prepare(transactionId) {
75
+ return { payload: { transactionId } };
76
+ },
77
+ },
61
78
  fetchRecommendationByEntityName: {
62
79
  reducer(draft, action) {
63
80
  const { entityName, entityType } = action.payload;
@@ -136,7 +153,7 @@ const recommendation = createSlice({
136
153
  },
137
154
  },
138
155
  });
139
- export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
156
+ export const { fetchEntityRecommendationsByTransactionId, fetchEntityRecommendationsForLineUpdate, cancelEntityRecommendationsForLineUpdate, fetchRecommendationByEntityName, fetchRecommendationByEntityId, updateRecommendationForEntity, updateRecommendationForEntityFailure, indicateReadyToInitializeRecommendation, clearRecommendation, } = recommendation.actions;
140
157
  export default recommendation.reducer;
141
158
  const doUpdateRecommendationForEntity = (draft, entityId, entityType, payload) => {
142
159
  const key = getEntityRecommendationKey(entityType, entityId);
@@ -0,0 +1,26 @@
1
+ // Two entities refer to the same real-world thing iff they share both
2
+ // type and id. We deliberately compare on these fields only — qboId /
3
+ // zeniId can lag during entity-create flows where the persisted id
4
+ // hasn't been roundtripped yet, while `id` is the local-source-of-truth
5
+ // the line carries.
6
+ const entitiesMatch = (a, b) => a != null && b != null && a.type === b.type && a.id === b.id;
7
+ // Filters `lineIds` down to those whose currently selected entity (as
8
+ // reported by the caller) still matches the payload entity at the
9
+ // moment the success reducer fires. Used by the two
10
+ // `setEntityRecommendationForLineIds…` reducers as a defensive guard
11
+ // against stale responses landing after the user (or a background
12
+ // refresh) has changed or wiped their selection.
13
+ //
14
+ // Each flow decides separately where the "current entity" lives,
15
+ // because the rules diverge:
16
+ // - Detail flow: vendor + customer transactions read from page-level
17
+ // `selectedEntity`; JE / deposit reads per-line `lineEntity`.
18
+ // - List flow: ONLY vendor reads from page-level `selectedEntity`;
19
+ // customer, JE, deposit all read per-line `selectedEntityLine`.
20
+ // Pushing the lookup into the caller keeps this helper a pure
21
+ // comparator and avoids the helper having to know which flow / page-
22
+ // level rule applies.
23
+ export const getLineIdsWhereEntityStillMatches = (args) => {
24
+ const { lineIds, payloadEntity, getCurrentEntityForLine } = args;
25
+ return lineIds.filter((lineId) => entitiesMatch(payloadEntity, getCurrentEntityForLine(lineId)));
26
+ };