@zeniai/client-epic-state 5.1.61 → 5.1.62

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 (88) hide show
  1. package/lib/coreEpics.js +3 -2
  2. package/lib/entity/chargeCard/chargeCard.d.ts +1 -1
  3. package/lib/entity/notificationRegistry/notificationRegistryPayload.d.ts +18 -0
  4. package/lib/entity/notificationRegistry/notificationRegistryPayload.js +28 -0
  5. package/lib/entity/notificationRegistry/notificationRegistryReducer.d.ts +10 -0
  6. package/lib/entity/notificationRegistry/notificationRegistryReducer.js +50 -0
  7. package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +2 -0
  8. package/lib/entity/notificationRegistry/notificationRegistrySelector.js +10 -0
  9. package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +32 -0
  10. package/lib/entity/notificationRegistry/notificationRegistryState.js +10 -0
  11. package/lib/entity/snackbar/snackbarTypes.d.ts +1 -1
  12. package/lib/entity/snackbar/snackbarTypes.js +3 -0
  13. package/lib/entity/tenant/clearAllEpic.d.ts +3 -1
  14. package/lib/entity/tenant/clearAllEpic.js +4 -0
  15. package/lib/entity/tenant/tenantReducer.d.ts +4 -1
  16. package/lib/entity/tenant/tenantReducer.js +8 -2
  17. package/lib/entity/tenant/tenantSelector.d.ts +1 -0
  18. package/lib/entity/tenant/tenantSelector.js +4 -0
  19. package/lib/epic.d.ts +5 -1
  20. package/lib/epic.js +5 -1
  21. package/lib/esm/coreEpics.js +3 -2
  22. package/lib/esm/entity/notificationRegistry/notificationRegistryPayload.js +24 -0
  23. package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +46 -0
  24. package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +6 -0
  25. package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +5 -0
  26. package/lib/esm/entity/snackbar/snackbarTypes.js +3 -0
  27. package/lib/esm/entity/tenant/clearAllEpic.js +4 -0
  28. package/lib/esm/entity/tenant/tenantReducer.js +7 -1
  29. package/lib/esm/entity/tenant/tenantSelector.js +3 -0
  30. package/lib/esm/epic.js +5 -1
  31. package/lib/esm/index.js +7 -2
  32. package/lib/esm/reducer.js +6 -0
  33. package/lib/esm/view/companyView/companyViewReducer.js +68 -1
  34. package/lib/esm/view/companyView/epic/companyPassport/dismissCapitalizationOnboardingEpic.js +2 -1
  35. package/lib/esm/view/companyView/epic/companyPassport/projects/disableAccountingProjectsEpic.js +56 -0
  36. package/lib/esm/view/companyView/epic/companyPassport/projects/enableAccountingProjectsEpic.js +37 -0
  37. package/lib/esm/view/companyView/epic/companyPassport/projects/resumeEnableAccountingProjectsEpic.js +73 -0
  38. package/lib/esm/view/companyView/epic/companyPassport/updateAccountingClassesEnabledEpic.js +2 -1
  39. package/lib/esm/view/companyView/epic/companyPassport/updateCapitalizationAccountThresholdEpic.js +2 -1
  40. package/lib/esm/view/companyView/types/companyPassport/companyPassportViewState.js +5 -0
  41. package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +116 -0
  42. package/lib/esm/view/notificationPreferencesView/notificationPreferencesEndpoint.js +5 -0
  43. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewPayload.js +36 -0
  44. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +222 -0
  45. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +25 -0
  46. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewState.js +4 -0
  47. package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +34 -2
  48. package/lib/esm/view/settingsView/epic/saveNotificationSettingsEpic.js +2 -1
  49. package/lib/esm/view/settingsView/settingsViewHelpers.js +3 -2
  50. package/lib/index.d.ts +8 -2
  51. package/lib/index.js +52 -32
  52. package/lib/reducer.d.ts +6 -0
  53. package/lib/reducer.js +6 -0
  54. package/lib/view/common/recurringViewHelper.d.ts +2 -2
  55. package/lib/view/companyView/companyViewReducer.d.ts +9 -1
  56. package/lib/view/companyView/companyViewReducer.js +70 -3
  57. package/lib/view/companyView/epic/companyPassport/dismissCapitalizationOnboardingEpic.js +2 -1
  58. package/lib/view/companyView/epic/companyPassport/projects/disableAccountingProjectsEpic.d.ts +10 -0
  59. package/lib/view/companyView/epic/companyPassport/projects/disableAccountingProjectsEpic.js +60 -0
  60. package/lib/view/companyView/epic/companyPassport/projects/enableAccountingProjectsEpic.d.ts +11 -0
  61. package/lib/view/companyView/epic/companyPassport/projects/enableAccountingProjectsEpic.js +41 -0
  62. package/lib/view/companyView/epic/companyPassport/projects/resumeEnableAccountingProjectsEpic.d.ts +13 -0
  63. package/lib/view/companyView/epic/companyPassport/projects/resumeEnableAccountingProjectsEpic.js +77 -0
  64. package/lib/view/companyView/epic/companyPassport/updateAccountingClassesEnabledEpic.js +2 -1
  65. package/lib/view/companyView/epic/companyPassport/updateCapitalizationAccountThresholdEpic.js +2 -1
  66. package/lib/view/companyView/types/companyPassport/companyPassportViewState.d.ts +2 -0
  67. package/lib/view/companyView/types/companyPassport/companyPassportViewState.js +5 -0
  68. package/lib/view/expenseAutomationView/helpers/transactionCategorizationLocalDataHelper.d.ts +1 -1
  69. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.d.ts +40 -0
  70. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +120 -0
  71. package/lib/view/notificationPreferencesView/notificationPreferencesEndpoint.d.ts +2 -0
  72. package/lib/view/notificationPreferencesView/notificationPreferencesEndpoint.js +9 -0
  73. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.d.ts +7 -0
  74. package/lib/view/notificationPreferencesView/notificationPreferencesViewPayload.js +41 -0
  75. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +30 -0
  76. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +226 -0
  77. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +7 -0
  78. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +32 -0
  79. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +14 -0
  80. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.js +8 -0
  81. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.d.ts +9 -1
  82. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +34 -2
  83. package/lib/view/settingsView/epic/saveNotificationSettingsEpic.js +2 -1
  84. package/lib/view/settingsView/settingsViewHelpers.d.ts +1 -1
  85. package/lib/view/settingsView/settingsViewHelpers.js +3 -2
  86. package/lib/view/settingsView/settingsViewPayload.d.ts +4 -0
  87. package/lib/view/settingsView/settingsViewState.d.ts +1 -1
  88. package/package.json +1 -1
package/lib/esm/index.js CHANGED
@@ -61,6 +61,8 @@ import { ALL_SCHEDULES_TYPES, getJEScheduleTransactionKey, toScheduleTypesType,
61
61
  import { ALL_MONTH_END_CLOSE_CHECKS_FREQUENCY, } from './entity/monthEndCloseChecks/monthEndCloseChecksState';
62
62
  import { updateCommentsNotifications, updateCommentsNotificationsStatuses, } from './entity/notification/notificationReducer';
63
63
  import { toNotificationModeStrict, } from './entity/notification/types/notificationTypes';
64
+ import { getNotificationRegistry } from './entity/notificationRegistry/notificationRegistrySelector';
65
+ import { toNotificationChannel, toNotificationFrequency, } from './entity/notificationRegistry/notificationRegistryState';
64
66
  import { getPlaidPaymentAccounts } from './entity/paymentAccount/paymentAccountSelector';
65
67
  import { VERIFIED_PAYMENT_ACCOUNT_PROVIDER_VERIFICATION_STATUS, } from './entity/paymentAccount/paymentAccountState';
66
68
  import { closeSnackbar, openSnackbar } from './entity/snackbar/snackbarReducer';
@@ -150,7 +152,7 @@ import { fetchCompanyMonthEndReportHistoricData, fetchCompanyMonthEndReportHisto
150
152
  import { getCompanyMonthEndReportHistoricData, getCompanyMonthEndReportSelectorView, } from './view/companyMonthEndReportView/companyMonthEndReportViewSelector';
151
153
  import { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, updateCompanyTaskManagerViewFilters, } from './view/companyTaskManagerView/companyTaskManagerViewReducer';
152
154
  import { getCompanyTaskManagerView, } from './view/companyTaskManagerView/companyTaskManagerViewSelector';
153
- import { clearCompanyView, companyManagementSaveUpdates, companyPassportClearDataInLocalStore, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, dismissCapitalizationOnboarding, fetchAllCockpitViews, fetchCompanyManagementView, fetchCompanyPassportView, fetchCompanyPortfolioView, fetchManagementView, fetchOnboardingView, fetchParentSubsidiaryManagementView, fetchPortfolioView, fetchSubscriptionView, fetchZeniUsers, saveCompanyPassportDetails, saveIndustryAndIncDateInCompanyPassportLocalStore, updateAccountingClassesEnabled, updateCapitalizationAccountThreshold, updateCompanyDownloadState, updateCompanyManagementUIState, updateCompanyPassportLocalStoreData, updateCompanyPortfolioUIState, } from './view/companyView/companyViewReducer';
155
+ import { clearCompanyView, companyManagementSaveUpdates, companyPassportClearDataInLocalStore, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, dismissCapitalizationOnboarding, fetchAllCockpitViews, fetchCompanyManagementView, fetchCompanyPassportView, fetchCompanyPortfolioView, fetchManagementView, fetchOnboardingView, fetchParentSubsidiaryManagementView, fetchPortfolioView, fetchSubscriptionView, fetchZeniUsers, saveCompanyPassportDetails, saveIndustryAndIncDateInCompanyPassportLocalStore, updateAccountingClassesEnabled, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, updateCompanyDownloadState, updateCompanyManagementUIState, updateCompanyPassportLocalStoreData, updateCompanyPortfolioUIState, } from './view/companyView/companyViewReducer';
154
156
  import { canSendMonthEndEmailReport, shouldEnableCalendarPickerForLastReportSent, } from './view/companyView/helpers/cockpitHelpers';
155
157
  import { getParentSubsidiaryManagementView } from './view/companyView/parentSubsidiaryView/parentSubsidiaryViewSelector';
156
158
  import { getAddonListZeniSku, getCompanyManagementView, getPlanListZeniSku, } from './view/companyView/selector/companyManagementViewSelector';
@@ -210,6 +212,8 @@ import { isNetBurnOrIncomeClassesViewCalculatedSectionID, isNetBurnOrIncomeClass
210
212
  import { toAverageMonthsCount } from './view/netBurnOrIncomeStoryCard/epic/updateNetBurnOrIncomeStoryCardSettingsEpic';
211
213
  import { fetchNetBurnOrIncomeStoryCard, updateNetBurnOrIncomeStoryCardSettings, } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardReducer';
212
214
  import { getNetBurnOrIncomeStoryCardReport } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardSelector';
215
+ import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, setGroupFrequency, toggleEventChannel, } from './view/notificationPreferencesView/notificationPreferencesViewReducer';
216
+ import { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, hasUnsavedNotificationPreferences, } from './view/notificationPreferencesView/notificationPreferencesViewSelector';
213
217
  import { fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, fetchNotificationView, updateNotificationViewAllNotificationsStatus, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewNotificationStatus, updateNotificationViewSubTab, updateNotificationViewTabState, updateNotificationViewUIState, } from './view/notificationView/notificationViewReducer';
214
218
  import { getExternalNotificationsForSelectedSubTab, getNotificationView, getNotificationsForSelectedSubTab, } from './view/notificationView/notificationViewSelector';
215
219
  import { toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, } from './view/notificationView/notificationViewState';
@@ -550,7 +554,7 @@ export { newAddressInLocalStore, getAddress, getNewAddress, getAllNewAddresses,
550
554
  export { toManagementSortKeyType, toOnboardingSortKeyType, toPortfolioSortKeyType, toHealthSortKeyType, toTaskManagerSortKeyType, updateCompanyTaskManagerViewFilters, };
551
555
  export { ALL_COCKPIT_TABS_FILE_TYPES, ALL_COCKPIT_TABS_IDS, toCockpitTabsIDStrict, toCockpitTabsFileTypeStrict, };
552
556
  export { isAllowedValueWithCode, isAllowedValueWithID, };
553
- export { fetchCompanyPassportView, getCompanyPassportView, getCompanyPassportLocalStoreData, isCompanyPassportDataToBeSaved, companyPassportSaveDataInLocalStore, saveIndustryAndIncDateInCompanyPassportLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, updateCompanyPassportLocalStoreData, saveCompanyPassportDetails, updateAccountingClassesEnabled, updateCapitalizationAccountThreshold, dismissCapitalizationOnboarding, clearCompanyView, };
557
+ export { fetchCompanyPassportView, getCompanyPassportView, getCompanyPassportLocalStoreData, isCompanyPassportDataToBeSaved, companyPassportSaveDataInLocalStore, saveIndustryAndIncDateInCompanyPassportLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, updateCompanyPassportLocalStoreData, saveCompanyPassportDetails, updateAccountingClassesEnabled, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, dismissCapitalizationOnboarding, clearCompanyView, };
554
558
  export { toCompanyPassportLocalData, };
555
559
  export { COMPANY_ONBOARDING_INDUSTRY_TYPE_CODES, COMPANY_ONBOARDING_SUB_INDUSTRY_CODES_BY_INDUSTRY, COMPANY_PURPOSE_OF_ACCOUNT_CODES, COMPANY_SOURCE_OF_FUNDS_CODES, COMPANY_TRANSACTION_VOLUME_CODES, COMPANY_US_NEXUS_TYPE_CODES, getCompanyOnboardingSubIndustryCodesForIndustry, };
556
560
  export { getUncategorizedAccounts, getNestedAccountListHierarchy, };
@@ -642,6 +646,7 @@ export { toNotificationModeStrict, updateCommentsNotifications, updateCommentsNo
642
646
  export { toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, fetchNotificationView, fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, updateNotificationViewAllNotificationsStatus, updateNotificationViewNotificationStatus, updateNotificationViewTabState, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewSubTab, updateNotificationViewUIState, getNotificationView, getExternalNotificationsForSelectedSubTab, getNotificationsForSelectedSubTab, };
643
647
  export { clearFeatureNotificationView, fetchRegisteredInterests, notifyMeForFeature, getFeatureNotificationView, getRegisteredInterests, getRegisteredInterestsByFeature, isFeatureInterestRegistered, };
644
648
  export { pushToastNotification, getLastNotificationTime, getNotifications, };
649
+ export { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, getNotificationRegistry, hasUnsavedNotificationPreferences, clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, setGroupFrequency, toggleEventChannel, toNotificationChannel, toNotificationFrequency, };
645
650
  export { getReferralListView, getInviteFormView, toReferralListViewSortKeyType, StatusTypes, AmountStatusTypes, DEFAULT_REFERRER_AMOUNT, fetchReferrals, sendReferralInvite, clearReferrals, saveReferralFormDataInLocalStore, updateReferralListSortUiState, resendReferralInvite, fetchRewardsPlan, getRewardsPlanCard, updateReferViewed, };
646
651
  export { ALL_WEEK_DAYS, SEMI_WEEKLY_REQUIRED_DAYS_COUNT, getMinAllowedEndDate, getRecurringEndDateFromCount, toDayOfWeek, toRecurringFrequency, };
647
652
  export { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, getCompanyTaskManagerView, createTaskFromTaskGroupTemplate, };
@@ -46,6 +46,7 @@ import jeSchedules, { initialState as initialJESchedulesState, } from './entity/
46
46
  import merchant, { initialState as initialMerchantState, } from './entity/merchant/merchantReducer';
47
47
  import monthEndCloseChecks, { initialState as initialMonthEndCloseChecksState, } from './entity/monthEndCloseChecks/monthEndCloseChecksReducer';
48
48
  import notification, { initialState as initialNotificationState, } from './entity/notification/notificationReducer';
49
+ import notificationRegistry, { initialState as initialNotificationRegistryState, } from './entity/notificationRegistry/notificationRegistryReducer';
49
50
  import onboardingCustomer, { initialState as initialOnboardingCustomerState, } from './entity/onboardingCustomer/onboardingCustomerReducer';
50
51
  import paymentAccount, { initialState as initialPaymentAccountState, } from './entity/paymentAccount/paymentAccountReducer';
51
52
  import paymentInstrument, { initialState as initialPaymentInstrumentState, } from './entity/paymentInstrument/paymentInstrumentReducer';
@@ -168,6 +169,7 @@ import monthEndCloseChecksView, { initialState as initialMonthEndCloseChecksView
168
169
  import netBurnOrIncome, { initialState as initialNetBurnOrIncomeWithForecastState, } from './view/netBurnOrIncome/netBurnOrIncomeReducer';
169
170
  import netBurnOrIncomeClassesView, { initialState as initialNetBurnOrIncomeClassesViewState, } from './view/netBurnOrIncomeClassesView/netBurnOrIncomeClassesViewReducer';
170
171
  import netBurnOrIncomeStoryCard, { initialState as initialNetBurnOrIncomeStoryCardState, } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardReducer';
172
+ import notificationPreferencesView, { initialState as initialNotificationPreferencesViewState, } from './view/notificationPreferencesView/notificationPreferencesViewReducer';
171
173
  import notificationView, { initialState as initialNotificationViewState, } from './view/notificationView/notificationViewReducer';
172
174
  import onboardingCockpitView, { initialState as initialOnboardingCockpitViewState, } from './view/onboardingView/cockpitView/onboardingCockpitViewReducer';
173
175
  import onboardingCustomerView, { initialState as initialOnboardingCustomerViewState, } from './view/onboardingView/customerView/onboardingCustomerViewReducer';
@@ -481,6 +483,8 @@ const initialViewsState = {
481
483
  netBurnOrIncomeClassesViewState: initialNetBurnOrIncomeClassesViewState,
482
484
  netBurnOrIncomeState: initialNetBurnOrIncomeWithForecastState,
483
485
  netBurnOrIncomeStoryCardState: initialNetBurnOrIncomeStoryCardState,
486
+ notificationPreferencesViewState: initialNotificationPreferencesViewState,
487
+ notificationRegistryState: initialNotificationRegistryState,
484
488
  notificationViewState: initialNotificationViewState,
485
489
  onboardingCockpitViewState: initialOnboardingCockpitViewState,
486
490
  onboardingCustomerViewState: initialOnboardingCustomerViewState,
@@ -768,6 +772,8 @@ const viewReducers = {
768
772
  netBurnOrIncomeClassesViewState: netBurnOrIncomeClassesView,
769
773
  netBurnOrIncomeState: netBurnOrIncome,
770
774
  netBurnOrIncomeStoryCardState: netBurnOrIncomeStoryCard,
775
+ notificationPreferencesViewState: notificationPreferencesView,
776
+ notificationRegistryState: notificationRegistry,
771
777
  notificationViewState: notificationView,
772
778
  onboardingCockpitViewState: onboardingCockpitView,
773
779
  onboardingCustomerViewState: onboardingCustomerView,
@@ -309,6 +309,73 @@ const companyView = createSlice({
309
309
  error: action.payload.status,
310
310
  };
311
311
  },
312
+ enableAccountingProjects: {
313
+ reducer(draft,
314
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
315
+ _action) {
316
+ draft.passportView.accountingProjectsUpdateStatus = {
317
+ fetchState: 'In-Progress',
318
+ error: undefined,
319
+ };
320
+ draft.passportView.needsQboProjectsReconnect = false;
321
+ },
322
+ prepare(companyId) {
323
+ return { payload: { companyId } };
324
+ },
325
+ },
326
+ resumeEnableAccountingProjects: {
327
+ reducer(draft,
328
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
329
+ _action) {
330
+ draft.passportView.accountingProjectsUpdateStatus = {
331
+ fetchState: 'In-Progress',
332
+ error: undefined,
333
+ };
334
+ draft.passportView.needsQboProjectsReconnect = false;
335
+ },
336
+ prepare(companyId) {
337
+ return { payload: { companyId } };
338
+ },
339
+ },
340
+ disableAccountingProjects: {
341
+ reducer(draft,
342
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
343
+ _action) {
344
+ draft.passportView.accountingProjectsUpdateStatus = {
345
+ fetchState: 'In-Progress',
346
+ error: undefined,
347
+ };
348
+ draft.passportView.needsQboProjectsReconnect = false;
349
+ },
350
+ prepare(companyId) {
351
+ return { payload: { companyId } };
352
+ },
353
+ },
354
+ updateAccountingProjectsEnabledSuccess(draft) {
355
+ draft.passportView.accountingProjectsUpdateStatus = {
356
+ fetchState: 'Completed',
357
+ error: undefined,
358
+ };
359
+ draft.passportView.needsQboProjectsReconnect = false;
360
+ },
361
+ updateAccountingProjectsEnabledFailure(draft, action) {
362
+ draft.passportView.accountingProjectsUpdateStatus = {
363
+ fetchState: 'Error',
364
+ error: action.payload.status,
365
+ };
366
+ draft.passportView.needsQboProjectsReconnect = false;
367
+ },
368
+ requestQboProjectsReconnect(draft) {
369
+ draft.passportView.needsQboProjectsReconnect = true;
370
+ // Keep In-Progress so the Projects toggle stays locked through OAuth.
371
+ draft.passportView.accountingProjectsUpdateStatus = {
372
+ fetchState: 'In-Progress',
373
+ error: undefined,
374
+ };
375
+ },
376
+ clearQboProjectsReconnectRequest(draft) {
377
+ draft.passportView.needsQboProjectsReconnect = false;
378
+ },
312
379
  updateCapitalizationAccountThreshold: {
313
380
  prepare(companyId, accountId, threshold, dismissOnboardingForAccount, allOverrides) {
314
381
  return {
@@ -627,5 +694,5 @@ const companyView = createSlice({
627
694
  },
628
695
  },
629
696
  });
630
- export const { fetchCompanyPassportView, fetchAllCockpitViews, fetchOnboardingView, fetchManagementView, fetchSubscriptionView, fetchPortfolioView, fetchCompanyManagementView, companyManagementSaveUpdates, companyManagementSavePendingUpdates, companyManagementDiscardUpdates, companyManagementSaveUpdatesSuccess, companyManagementSaveUpdatesFailure, updateCompanyViewOnSuccess, updateCompanyViewOnFailure, fetchCompanyPortfolioView, fetchZeniUsers, updateZeniUsersOnSuccess, updateZeniUsersOnFailure, clearCompanyView, updateCompanyPortfolioUIState, updateCompanyManagementUIState, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, companyPassportUpdateCompanyDetails, companyPassportUpdateCompanyDetailsSuccess, companyPassportUpdateCompanyDetailsFailure, updateAccountingClassesEnabled, updateAccountingClassesEnabledSuccess, updateAccountingClassesEnabledFailure, updateCapitalizationAccountThreshold, updateCapitalizationAccountThresholdSuccess, updateCapitalizationAccountThresholdFailure, dismissCapitalizationOnboarding, companyPassportUpdatePrimaryContact, companyPassportUpdatePrimaryContactSuccess, companyPassportUpdatePrimaryContactFailure, companyPassportUpdateCompanyOfficer, companyPassportUpdateCompanyOfficerSuccess, companyPassportUpdateCompanyOfficerFailure, companyPassportCreateCompanyOfficers, companyPassportCreateCompanyOfficersSuccess, companyPassportCreateCompanyOfficersFailure, updateCompanyPassportLocalStoreData, saveIndustryAndIncDateInCompanyPassportLocalStore, saveCompanyPassportDetails, fetchParentSubsidiaryManagementView, updateParentSubsidiaryView, updateParentSubsidiaryViewOnFailure, updateCompanyDownloadState, fetchCompanyMetaData, updateCompanyMetaDataOnSuccess, updateCompanyMetaDataOnFailure, updateMetaDataOnSendMonthEndReport, } = companyView.actions;
697
+ export const { fetchCompanyPassportView, fetchAllCockpitViews, fetchOnboardingView, fetchManagementView, fetchSubscriptionView, fetchPortfolioView, fetchCompanyManagementView, companyManagementSaveUpdates, companyManagementSavePendingUpdates, companyManagementDiscardUpdates, companyManagementSaveUpdatesSuccess, companyManagementSaveUpdatesFailure, updateCompanyViewOnSuccess, updateCompanyViewOnFailure, fetchCompanyPortfolioView, fetchZeniUsers, updateZeniUsersOnSuccess, updateZeniUsersOnFailure, clearCompanyView, updateCompanyPortfolioUIState, updateCompanyManagementUIState, companyPassportSaveDataInLocalStore, deleteCompanyOfficerInLocalStore, companyPassportClearDataInLocalStore, companyPassportUpdateCompanyDetails, companyPassportUpdateCompanyDetailsSuccess, companyPassportUpdateCompanyDetailsFailure, updateAccountingClassesEnabled, updateAccountingClassesEnabledSuccess, updateAccountingClassesEnabledFailure, enableAccountingProjects, resumeEnableAccountingProjects, disableAccountingProjects, updateAccountingProjectsEnabledSuccess, updateAccountingProjectsEnabledFailure, requestQboProjectsReconnect, clearQboProjectsReconnectRequest, updateCapitalizationAccountThreshold, updateCapitalizationAccountThresholdSuccess, updateCapitalizationAccountThresholdFailure, dismissCapitalizationOnboarding, companyPassportUpdatePrimaryContact, companyPassportUpdatePrimaryContactSuccess, companyPassportUpdatePrimaryContactFailure, companyPassportUpdateCompanyOfficer, companyPassportUpdateCompanyOfficerSuccess, companyPassportUpdateCompanyOfficerFailure, companyPassportCreateCompanyOfficers, companyPassportCreateCompanyOfficersSuccess, companyPassportCreateCompanyOfficersFailure, updateCompanyPassportLocalStoreData, saveIndustryAndIncDateInCompanyPassportLocalStore, saveCompanyPassportDetails, fetchParentSubsidiaryManagementView, updateParentSubsidiaryView, updateParentSubsidiaryViewOnFailure, updateCompanyDownloadState, fetchCompanyMetaData, updateCompanyMetaDataOnSuccess, updateCompanyMetaDataOnFailure, updateMetaDataOnSendMonthEndReport, } = companyView.actions;
631
698
  export default companyView.reducer;
@@ -1,11 +1,12 @@
1
1
  import { from } from 'rxjs';
2
2
  import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { updateTenantCapitalizationOnboardingDismissed } from '../../../../entity/tenant/tenantReducer';
4
+ import { getTenantIdByCompanyId } from '../../../../entity/tenant/tenantSelector';
4
5
  import { isSuccessResponse } from '../../../../responsePayload';
5
6
  import { dismissCapitalizationOnboarding } from '../../companyViewReducer';
6
7
  export const dismissCapitalizationOnboardingEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(dismissCapitalizationOnboarding.match), switchMap((action) => {
7
8
  const { companyId, accountId } = action.payload;
8
- const tenantId = Object.values(state$.value.tenantState.tenantsById).find((t) => t.companyId === companyId)?.tenantId;
9
+ const tenantId = getTenantIdByCompanyId(state$.value.tenantState, companyId);
9
10
  const body = {
10
11
  capitalization_onboarding_dismissed_accounts: { [accountId]: true },
11
12
  };
@@ -0,0 +1,56 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { updateCompanies } from '../../../../../entity/company/companyReducer';
4
+ import { openSnackbar } from '../../../../../entity/snackbar/snackbarReducer';
5
+ import { updateTenantAccountingProjectsEnabled } from '../../../../../entity/tenant/tenantReducer';
6
+ import { getTenantIdByCompanyId } from '../../../../../entity/tenant/tenantSelector';
7
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../../responsePayload';
8
+ import { disableAccountingProjects, updateAccountingProjectsEnabledFailure, updateAccountingProjectsEnabledSuccess, } from '../../../companyViewReducer';
9
+ export const disableAccountingProjectsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(disableAccountingProjects.match), switchMap((action) => {
10
+ const { companyId } = action.payload;
11
+ const tenantId = getTenantIdByCompanyId(state$.value.tenantState, companyId);
12
+ return zeniAPI
13
+ .putAndGetJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/companies/${companyId}`, { is_accounting_projects_enabled: false })
14
+ .pipe(mergeMap((response) => {
15
+ const companies = response.data?.companies ?? [];
16
+ if (isSuccessResponse(response) && companies.length > 0) {
17
+ const actions = [
18
+ updateCompanies({
19
+ payload: companies,
20
+ schema: {},
21
+ }),
22
+ ];
23
+ if (tenantId != null) {
24
+ actions.push(updateTenantAccountingProjectsEnabled({
25
+ enabled: false,
26
+ tenantId,
27
+ }));
28
+ }
29
+ actions.push(updateAccountingProjectsEnabledSuccess());
30
+ actions.push(openSnackbar({
31
+ messageSection: 'accounting_projects_enabled_update',
32
+ messageText: 'success',
33
+ type: 'success',
34
+ }));
35
+ return from(actions);
36
+ }
37
+ return from([
38
+ updateAccountingProjectsEnabledFailure({
39
+ status: response.status,
40
+ }),
41
+ openSnackbar({
42
+ messageSection: 'accounting_projects_enabled_update',
43
+ messageText: 'failed',
44
+ type: 'error',
45
+ }),
46
+ ]);
47
+ }), catchError((error) => of(updateAccountingProjectsEnabledFailure({
48
+ status: createZeniAPIStatus('Unexpected error', `Update Accounting Projects Enabled errored out - ${error instanceof Error
49
+ ? error.message
50
+ : JSON.stringify(error)}`),
51
+ }), openSnackbar({
52
+ messageSection: 'accounting_projects_enabled_update',
53
+ messageText: 'failed',
54
+ type: 'error',
55
+ }))));
56
+ }));
@@ -0,0 +1,37 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { openSnackbar } from '../../../../../entity/snackbar/snackbarReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../../responsePayload';
5
+ import { enableAccountingProjects, requestQboProjectsReconnect, resumeEnableAccountingProjects, updateAccountingProjectsEnabledFailure, } from '../../../companyViewReducer';
6
+ export const enableAccountingProjectsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(enableAccountingProjects.match), switchMap((action) => {
7
+ const { companyId } = action.payload;
8
+ return zeniAPI
9
+ .getJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accounting_provider_project_authorization_status`)
10
+ .pipe(mergeMap((response) => {
11
+ if (!isSuccessResponse(response) || response.data == null) {
12
+ return from([
13
+ updateAccountingProjectsEnabledFailure({
14
+ status: response.status,
15
+ }),
16
+ openSnackbar({
17
+ messageSection: 'accounting_projects_enabled_update',
18
+ messageText: 'failed',
19
+ type: 'error',
20
+ }),
21
+ ]);
22
+ }
23
+ if (response.data.is_accounting_provider_project_apis_authorized !==
24
+ true) {
25
+ return from([requestQboProjectsReconnect()]);
26
+ }
27
+ return from([resumeEnableAccountingProjects(companyId)]);
28
+ }), catchError((error) => of(updateAccountingProjectsEnabledFailure({
29
+ status: createZeniAPIStatus('Unexpected error', `Accounting provider project authorization status errored out - ${error instanceof Error
30
+ ? error.message
31
+ : JSON.stringify(error)}`),
32
+ }), openSnackbar({
33
+ messageSection: 'accounting_projects_enabled_update',
34
+ messageText: 'failed',
35
+ type: 'error',
36
+ }))));
37
+ }));
@@ -0,0 +1,73 @@
1
+ import { from, of } from 'rxjs';
2
+ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
+ import { openSnackbar } from '../../../../../entity/snackbar/snackbarReducer';
4
+ import { updateTenantAccountingProjectsEnabled } from '../../../../../entity/tenant/tenantReducer';
5
+ import { getTenantIdByCompanyId } from '../../../../../entity/tenant/tenantSelector';
6
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../../../responsePayload';
7
+ import { resumeEnableAccountingProjects, updateAccountingProjectsEnabledFailure, updateAccountingProjectsEnabledSuccess, } from '../../../companyViewReducer';
8
+ export const resumeEnableAccountingProjectsEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(resumeEnableAccountingProjects.match), switchMap((action) => {
9
+ const { companyId } = action.payload;
10
+ const tenantId = getTenantIdByCompanyId(state$.value.tenantState, companyId);
11
+ if (tenantId == null) {
12
+ return from([
13
+ updateAccountingProjectsEnabledFailure({
14
+ status: createZeniAPIStatus('Unexpected error', 'Tenant not found for company'),
15
+ }),
16
+ openSnackbar({
17
+ messageSection: 'accounting_projects_enabled_update',
18
+ messageText: 'failed',
19
+ type: 'error',
20
+ }),
21
+ ]);
22
+ }
23
+ return zeniAPI
24
+ .putAndGetJSON(`${zeniAPI.apiEndPoints.accountMicroServiceBaseUrl}/1.0/accounting_provider_project_enablement_status`)
25
+ .pipe(mergeMap((response) => {
26
+ if (!isSuccessResponse(response) || response.data == null) {
27
+ return from([
28
+ updateAccountingProjectsEnabledFailure({
29
+ status: response.status,
30
+ }),
31
+ openSnackbar({
32
+ messageSection: 'accounting_projects_enabled_update',
33
+ messageText: 'failed',
34
+ type: 'error',
35
+ }),
36
+ ]);
37
+ }
38
+ if (response.data.is_accounting_provider_project_apis_enabled !==
39
+ true ||
40
+ response.data.is_accounting_projects_enabled !== true) {
41
+ return from([
42
+ updateAccountingProjectsEnabledFailure({
43
+ status: createZeniAPIStatus('Projects not enabled in QBO', 'Please enable projects in QBO before enabling in the Zeni dashboard!'),
44
+ }),
45
+ openSnackbar({
46
+ messageSection: 'accounting_projects_qbo_not_enabled',
47
+ messageText: 'failed',
48
+ type: 'error',
49
+ }),
50
+ ]);
51
+ }
52
+ return from([
53
+ updateTenantAccountingProjectsEnabled({
54
+ enabled: true,
55
+ tenantId,
56
+ }),
57
+ updateAccountingProjectsEnabledSuccess(),
58
+ openSnackbar({
59
+ messageSection: 'accounting_projects_enabled_update',
60
+ messageText: 'success',
61
+ type: 'success',
62
+ }),
63
+ ]);
64
+ }), catchError((error) => of(updateAccountingProjectsEnabledFailure({
65
+ status: createZeniAPIStatus('Unexpected error', `Accounting provider project enablement status errored out - ${error instanceof Error
66
+ ? error.message
67
+ : JSON.stringify(error)}`),
68
+ }), openSnackbar({
69
+ messageSection: 'accounting_projects_enabled_update',
70
+ messageText: 'failed',
71
+ type: 'error',
72
+ }))));
73
+ }));
@@ -3,11 +3,12 @@ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { updateCompanies } from '../../../../entity/company/companyReducer';
4
4
  import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
5
  import { updateTenantAccountingClassesEnabled } from '../../../../entity/tenant/tenantReducer';
6
+ import { getTenantIdByCompanyId } from '../../../../entity/tenant/tenantSelector';
6
7
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
7
8
  import { updateAccountingClassesEnabled, updateAccountingClassesEnabledFailure, updateAccountingClassesEnabledSuccess, } from '../../companyViewReducer';
8
9
  export const updateAccountingClassesEnabledEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(updateAccountingClassesEnabled.match), switchMap((action) => {
9
10
  const { companyId, enabled } = action.payload;
10
- const tenantId = Object.values(state$.value.tenantState.tenantsById).find((t) => t.companyId === companyId)?.tenantId;
11
+ const tenantId = getTenantIdByCompanyId(state$.value.tenantState, companyId);
11
12
  return zeniAPI
12
13
  .putAndGetJSON(`${zeniAPI.apiEndPoints.tenantMicroServiceBaseUrl}/1.0/companies/${companyId}`, { is_accounting_classes_enabled: enabled })
13
14
  .pipe(mergeMap((response) => {
@@ -3,11 +3,12 @@ import { catchError, filter, mergeMap, switchMap } from 'rxjs/operators';
3
3
  import { updateCompanies } from '../../../../entity/company/companyReducer';
4
4
  import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
5
5
  import { updateTenantCapitalizationAccountOverride, updateTenantCapitalizationOnboardingDismissed, } from '../../../../entity/tenant/tenantReducer';
6
+ import { getTenantIdByCompanyId } from '../../../../entity/tenant/tenantSelector';
6
7
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
7
8
  import { updateCapitalizationAccountThreshold, updateCapitalizationAccountThresholdFailure, updateCapitalizationAccountThresholdSuccess, } from '../../companyViewReducer';
8
9
  export const updateCapitalizationAccountThresholdEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(updateCapitalizationAccountThreshold.match), switchMap((action) => {
9
10
  const { companyId, accountId, threshold, dismissOnboardingForAccount, allOverrides, } = action.payload;
10
- const tenantId = Object.values(state$.value.tenantState.tenantsById).find((t) => t.companyId === companyId)?.tenantId;
11
+ const tenantId = getTenantIdByCompanyId(state$.value.tenantState, companyId);
11
12
  let finalOverrides;
12
13
  if (allOverrides !== undefined) {
13
14
  finalOverrides = allOverrides ?? {};
@@ -5,10 +5,15 @@ export const initialCompanyPassportView = {
5
5
  fetchState: 'Not-Started',
6
6
  error: undefined,
7
7
  },
8
+ accountingProjectsUpdateStatus: {
9
+ fetchState: 'Not-Started',
10
+ error: undefined,
11
+ },
8
12
  capitalizationThresholdUpdateStatus: {
9
13
  fetchState: 'Not-Started',
10
14
  error: undefined,
11
15
  },
16
+ needsQboProjectsReconnect: false,
12
17
  companyDetails: {
13
18
  localData: undefined,
14
19
  updateStatus: {
@@ -0,0 +1,116 @@
1
+ import { EMPTY, concat, of } from 'rxjs';
2
+ import { catchError, debounceTime, filter, finalize, mergeMap, switchMap, takeUntil, withLatestFrom, } from 'rxjs/operators';
3
+ import { openSnackbar } from '../../../entity/snackbar/snackbarReducer';
4
+ import { createZeniAPIStatus, isSuccessResponse, } from '../../../responsePayload';
5
+ import { notificationPreferencesUrl } from '../notificationPreferencesEndpoint';
6
+ import { mapPreferencesToPayload, } from '../notificationPreferencesViewPayload';
7
+ import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, saveNotificationPreferencesFailure, saveNotificationPreferencesSuccess, setGroupFrequency, toggleEventChannel, } from '../notificationPreferencesViewReducer';
8
+ import { getNotificationLocalOverrides } from '../notificationPreferencesViewSelector';
9
+ const DEBOUNCE_MS = 300;
10
+ export const saveNotificationPreferencesEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter((action) => toggleEventChannel.match(action) || setGroupFrequency.match(action)),
11
+ // Coalesce bursts of toggles into a single PUT. Trade-off: rapid clickers
12
+ // defer saves until they pause. Consider a max-wait wrapper if this
13
+ // becomes a UX problem.
14
+ debounceTime(DEBOUNCE_MS), withLatestFrom(state$),
15
+ // `switchMap` unsubscribes from any in-flight PUT observable when a new
16
+ // debounced batch fires. The prior HTTP request has already left the
17
+ // client — the network call is NOT cancelled — but its response is
18
+ // ignored, so stale success/failure actions cannot clobber the newer
19
+ // batch's state. Server-side ordering is arrival-order LWW.
20
+ switchMap(([, state]) => {
21
+ const overrides = getNotificationLocalOverrides(state);
22
+ const preferencesPatch = mapPreferencesToPayload(overrides);
23
+ if (Object.keys(preferencesPatch).length === 0) {
24
+ // No diff → do not dispatch a synthetic success (that would wipe
25
+ // stored preferences via the reducer). Simply no-op.
26
+ return EMPTY;
27
+ }
28
+ // Snapshot the Cancel epoch at PUT-dispatch time. Every Cancel bumps
29
+ // `cancelEpoch`; the reducer compares the captured value on Success/
30
+ // Failure and skips the merge when the value has advanced (user
31
+ // pressed Cancel since this save started). See Cursor Bugbot 4aa021f7
32
+ // — the earlier counter model failed when `switchMap` abandoned an
33
+ // in-flight save's response (Success/Failure never dispatched, so
34
+ // the counter never drained and future saves got their merge dropped).
35
+ const cancelEpochAtDispatch = state.notificationPreferencesViewState.cancelEpoch;
36
+ const body = { task_checklist_preferences: preferencesPatch };
37
+ // Suppress the error snackbar when the response lands AFTER the user
38
+ // pressed Cancel — the Failure reducer already swallows the state
39
+ // update, so the toast would be a stale error for a save the user
40
+ // abandoned. `StateObservable#value` reads the current cancelEpoch at
41
+ // response time (not at dispatch time — that's what
42
+ // `cancelEpochAtDispatch` captures). Cursor Bugbot da2f4fe2 + Nikunj.
43
+ const errorSnackbar = openSnackbar({
44
+ messageSection: 'notification_settings_saved',
45
+ messageText: 'failed',
46
+ type: 'error',
47
+ });
48
+ const isCancelledSince = () => cancelEpochAtDispatch <
49
+ state$.value.notificationPreferencesViewState.cancelEpoch;
50
+ // Abort the in-flight HTTP request when `switchMap` disposes this
51
+ // observable (new debounced batch fires, tenant switch, unmount).
52
+ // Without an AbortSignal, `switchMap` would drop the response
53
+ // client-side but the older PUT would still land on the server —
54
+ // under patch-semantic LWW, a slower older PUT arriving after a
55
+ // newer one silently reverts the user's latest choice on the
56
+ // server. Cursor Bugbot e3861a76. `AbortController.abort()` is
57
+ // idempotent so it's safe if the request already completed.
58
+ const controller = new AbortController();
59
+ const request$ = zeniAPI
60
+ .putAndGetJSON(notificationPreferencesUrl(zeniAPI), body, undefined, controller.signal)
61
+ .pipe(mergeMap((response) => {
62
+ if (isSuccessResponse(response)) {
63
+ return of(saveNotificationPreferencesSuccess({
64
+ cancelEpochAtDispatch,
65
+ savedOverrides: overrides,
66
+ serverEcho: response.data?.task_checklist_preferences,
67
+ }));
68
+ }
69
+ const actions = [
70
+ saveNotificationPreferencesFailure({
71
+ cancelEpochAtDispatch,
72
+ error: response.status,
73
+ }),
74
+ ];
75
+ if (!isCancelledSince()) {
76
+ // Surface a user-visible error — without it the UI reverts to
77
+ // server truth after debounce and the failed toggle looks like
78
+ // a mystery UI bug.
79
+ actions.push(errorSnackbar);
80
+ }
81
+ return of(...actions);
82
+ }), catchError((error) => {
83
+ const actions = [
84
+ saveNotificationPreferencesFailure({
85
+ cancelEpochAtDispatch,
86
+ error: createZeniAPIStatus('Unexpected Error', 'Save notification preferences failed: ' +
87
+ JSON.stringify(error)),
88
+ }),
89
+ ];
90
+ if (!isCancelledSince()) {
91
+ actions.push(errorSnackbar);
92
+ }
93
+ return of(...actions);
94
+ }), finalize(() => {
95
+ // switchMap disposal (new batch fires, unmount) OR the takeUntil
96
+ // below (Cancel fires) → abort the still-in-flight HTTP request.
97
+ // No-op if the request has already completed successfully or
98
+ // errored. Without this, the pre-Cancel PUT would still land on
99
+ // the server; under patch-semantic LWW, a slower older PUT
100
+ // arriving after Cancel silently reverts the user's just-cleared
101
+ // overrides on the server.
102
+ controller.abort();
103
+ }),
104
+ // Cancel and tenant-switch/logout do not flow through the outer
105
+ // `filter`, so switchMap would never dispose this observable on
106
+ // either. `takeUntil` completes request$ on Cancel OR clear-all,
107
+ // which triggers finalize above (aborts the in-flight HTTP
108
+ // request). Without matching clear-all, a tenant switch during
109
+ // an in-flight PUT would let the older PUT land on the server
110
+ // under patch-semantic LWW. Cursor Bugbot 3637969823 + 3641372002.
111
+ takeUntil(actions$.pipe(filter((action) => clearNotificationPreferencesLocalOverrides.match(action) ||
112
+ clearAllNotificationPreferencesView.match(action)))));
113
+ // Flip savePreferencesState to In-Progress BEFORE the network call so
114
+ // any "saving…" UI can render.
115
+ return concat(of(saveNotificationPreferences()), request$);
116
+ }));
@@ -0,0 +1,5 @@
1
+ // Shared endpoint used by both the GET (envelope refresh in
2
+ // `fetchNotificationSettingsEpic`) and the PUT (patch in
3
+ // `saveNotificationPreferencesEpic`). Kept as a helper so an endpoint rename
4
+ // can't drift between the two callers.
5
+ export const notificationPreferencesUrl = (zeniAPI) => `${zeniAPI.apiEndPoints.notificationMicroServiceBaseUrl}/1.0/notification-preferences`;
@@ -0,0 +1,36 @@
1
+ import { toNotificationChannel, toNotificationFrequency, } from '../../entity/notificationRegistry/notificationRegistryState';
2
+ const mapEventEnabledByChannel = (raw) => {
3
+ if (raw == null) {
4
+ return {};
5
+ }
6
+ const out = {};
7
+ Object.entries(raw).forEach(([eventId, channelMap]) => {
8
+ const mapped = {};
9
+ Object.entries(channelMap).forEach(([channel, enabled]) => {
10
+ mapped[toNotificationChannel(channel)] = enabled;
11
+ });
12
+ out[eventId] = mapped;
13
+ });
14
+ return out;
15
+ };
16
+ export const mapPayloadToPreferences = (payload) => {
17
+ const groupFrequencyRaw = payload?.group_frequency_by_group_id ?? {};
18
+ const groupFrequencyByGroupId = Object.fromEntries(Object.entries(groupFrequencyRaw).map(([groupId, frequency]) => [
19
+ groupId,
20
+ toNotificationFrequency(frequency),
21
+ ]));
22
+ return {
23
+ eventEnabledByChannel: mapEventEnabledByChannel(payload?.event_enabled_by_channel),
24
+ groupFrequencyByGroupId,
25
+ };
26
+ };
27
+ export const mapPreferencesToPayload = (preferences) => {
28
+ const payload = {};
29
+ if (Object.keys(preferences.eventEnabledByChannel).length > 0) {
30
+ payload.event_enabled_by_channel = preferences.eventEnabledByChannel;
31
+ }
32
+ if (Object.keys(preferences.groupFrequencyByGroupId).length > 0) {
33
+ payload.group_frequency_by_group_id = preferences.groupFrequencyByGroupId;
34
+ }
35
+ return payload;
36
+ };