@zeniai/client-epic-state 5.2.52 → 5.2.53-beta0ND

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 (52) hide show
  1. package/lib/entity/notificationRegistry/notificationRegistryReducer.d.ts +2 -1
  2. package/lib/entity/notificationRegistry/notificationRegistryReducer.js +20 -9
  3. package/lib/entity/notificationRegistry/notificationRegistrySelector.d.ts +2 -1
  4. package/lib/entity/notificationRegistry/notificationRegistrySelector.js +7 -1
  5. package/lib/entity/notificationRegistry/notificationRegistryState.d.ts +5 -0
  6. package/lib/entity/notificationRegistry/notificationRegistryState.js +7 -1
  7. package/lib/entity/sectionProjectView/sectionProjectViewReducer.js +3 -1
  8. package/lib/epic.js +4 -4
  9. package/lib/esm/entity/notificationRegistry/notificationRegistryReducer.js +20 -9
  10. package/lib/esm/entity/notificationRegistry/notificationRegistrySelector.js +5 -0
  11. package/lib/esm/entity/notificationRegistry/notificationRegistryState.js +5 -0
  12. package/lib/esm/entity/sectionProjectView/sectionProjectViewReducer.js +3 -1
  13. package/lib/esm/epic.js +4 -4
  14. package/lib/esm/index.js +4 -4
  15. package/lib/esm/view/aiCfoView/aiCfoViewReducer.js +4 -2
  16. package/lib/esm/view/aiCfoView/epics/fetchRoutineRunsEpic.js +3 -1
  17. package/lib/esm/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
  18. package/lib/esm/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
  19. package/lib/esm/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +28 -11
  20. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +19 -3
  21. package/lib/esm/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +21 -21
  22. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +57 -9
  23. package/lib/esm/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +20 -7
  24. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewReducer.js +43 -17
  25. package/lib/esm/view/notificationPreferencesView/notificationPreferencesViewSelector.js +24 -4
  26. package/lib/esm/view/settingsView/epic/fetchNotificationSettingsEpic.js +12 -2
  27. package/lib/index.d.ts +4 -4
  28. package/lib/index.js +21 -14
  29. package/lib/view/aiCfoView/aiCfoViewReducer.js +4 -2
  30. package/lib/view/aiCfoView/aiCfoViewSelector.d.ts +1 -1
  31. package/lib/view/aiCfoView/epics/fetchRoutineRunsEpic.js +3 -1
  32. package/lib/view/expenseAutomationView/epics/transactionCategorization/fetchTransactionCategorizationEpic.js +15 -4
  33. package/lib/view/expenseAutomationView/payload/transactionCategorizationPayload.d.ts +18 -7
  34. package/lib/view/expenseAutomationView/reducers/transactionsViewReducer.js +10 -0
  35. package/lib/view/expenseAutomationView/selectorTypes/transactionsViewSelectorTypes.d.ts +8 -4
  36. package/lib/view/expenseAutomationView/selectors/transactionCategorizationSelector.js +27 -10
  37. package/lib/view/expenseAutomationView/types/transactionsViewState.d.ts +8 -5
  38. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +7 -0
  39. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +18 -2
  40. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.d.ts +3 -2
  41. package/lib/view/invoicing/editInvoicingCouponDetailView/editInvoicingCouponDetailViewSelector.js +19 -19
  42. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +49 -0
  43. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +59 -10
  44. package/lib/view/notificationPreferencesView/epics/saveNotificationPreferencesEpic.js +19 -6
  45. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.d.ts +8 -5
  46. package/lib/view/notificationPreferencesView/notificationPreferencesViewReducer.js +43 -17
  47. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.d.ts +4 -0
  48. package/lib/view/notificationPreferencesView/notificationPreferencesViewSelector.js +30 -5
  49. package/lib/view/notificationPreferencesView/notificationPreferencesViewState.d.ts +4 -3
  50. package/lib/view/settingsView/epic/fetchNotificationSettingsEpic.js +12 -2
  51. package/lib/view/settingsView/settingsViewPayload.d.ts +2 -0
  52. package/package.json +1 -1
@@ -1,9 +1,10 @@
1
1
  import { NotificationRegistryPayload } from './notificationRegistryPayload';
2
- import { NotificationRegistryState } from './notificationRegistryState';
2
+ import { NotificationDomain, NotificationRegistryState } from './notificationRegistryState';
3
3
  export declare const initialState: NotificationRegistryState;
4
4
  export interface UpdateNotificationRegistryPayload {
5
5
  registry: NotificationRegistryPayload | undefined;
6
6
  registryEpochAtDispatch: number;
7
+ registriesByDomain?: Partial<Record<NotificationDomain, NotificationRegistryPayload | undefined>>;
7
8
  }
8
9
  export declare const updateNotificationRegistry: import("@reduxjs/toolkit").ActionCreatorWithPayload<UpdateNotificationRegistryPayload, "notificationRegistry/updateNotificationRegistry">;
9
10
  declare const _default: import("redux").Reducer<NotificationRegistryState>;
@@ -4,6 +4,7 @@ exports.updateNotificationRegistry = exports.initialState = void 0;
4
4
  const toolkit_1 = require("@reduxjs/toolkit");
5
5
  const rootActions_1 = require("../../rootActions");
6
6
  const notificationRegistryPayload_1 = require("./notificationRegistryPayload");
7
+ const notificationRegistryState_1 = require("./notificationRegistryState");
7
8
  exports.initialState = { registryEpoch: 0 };
8
9
  const notificationRegistry = (0, toolkit_1.createSlice)({
9
10
  name: 'notificationRegistry',
@@ -22,16 +23,24 @@ const notificationRegistry = (0, toolkit_1.createSlice)({
22
23
  // `task_checklist_registry` CLEARS so tenant B doesn't inherit A's
23
24
  // Task Checklist section. Guarded by the epoch check above so a stale
24
25
  // absent-response can't wipe a fresh registry.
25
- if (action.payload.registry == null) {
26
- draft.registry = undefined;
27
- return;
26
+ const { registriesByDomain } = action.payload;
27
+ // Absent `registriesByDomain` means a pre-domain caller: treat `registry`
28
+ // as the task_checklist snapshot so their behaviour is unchanged.
29
+ const snapshots = registriesByDomain ?? { task_checklist: action.payload.registry };
30
+ draft.registryByDomain = {};
31
+ for (const domain of notificationRegistryState_1.NOTIFICATION_DOMAINS) {
32
+ const payload = snapshots[domain];
33
+ // Full-replace per domain, and an absent snapshot CLEARS that domain so
34
+ // tenant B never inherits A's section. The gates are independent
35
+ // server-side, so one domain clearing must not touch the other.
36
+ if (payload != null) {
37
+ draft.registryByDomain[domain] =
38
+ (0, notificationRegistryPayload_1.mapNotificationRegistryPayloadToRegistry)(payload);
39
+ }
28
40
  }
29
- // Full-replace contract: the registry payload is atomic — every fetch
30
- // returns the complete group/event tree. If the backend ever ships a
31
- // partial registry response, this write MUST switch to a merge (mirror
32
- // the preferences slice's `mergePreferences`). Do not assume the
33
- // current payload is partial without also updating this reducer.
34
- draft.registry = (0, notificationRegistryPayload_1.mapNotificationRegistryPayloadToRegistry)(action.payload.registry);
41
+ // Mirrored for consumers that predate the domain-keyed map.
42
+ draft.registry = draft.registryByDomain.task_checklist;
43
+ return;
35
44
  },
36
45
  },
37
46
  extraReducers: (builder) => {
@@ -44,6 +53,8 @@ const notificationRegistry = (0, toolkit_1.createSlice)({
44
53
  // — Cursor Bugbot 67d6918e. Returning `initialState` verbatim would
45
54
  // rewind the counter and let a stale in-flight GET restore A's
46
55
  // registry over B's.
56
+ // Drops `registry` and `registryByDomain` together: a tenant switch must
57
+ // not leave one domain's section behind.
47
58
  return { registryEpoch: draft.registryEpoch + 1 };
48
59
  });
49
60
  },
@@ -1,2 +1,3 @@
1
- import { NotificationRegistry, NotificationRegistryState } from './notificationRegistryState';
1
+ import { NotificationDomain, NotificationRegistry, NotificationRegistryState } from './notificationRegistryState';
2
2
  export declare const getNotificationRegistry: (slice: NotificationRegistryState) => NotificationRegistry | undefined;
3
+ export declare const getNotificationRegistryForDomain: (slice: NotificationRegistryState, domain: NotificationDomain) => NotificationRegistry | undefined;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getNotificationRegistry = void 0;
3
+ exports.getNotificationRegistryForDomain = exports.getNotificationRegistry = void 0;
4
4
  // Current behavior: a null / undefined registry means the whole notification
5
5
  // section is hidden by consumers. This conflates two distinct states —
6
6
  // "user has no saved preferences yet" vs "the Task Checklist feature gate is
@@ -8,3 +8,9 @@ exports.getNotificationRegistry = void 0;
8
8
  // them (e.g. an explicit `gate_off` signal from the server) is a follow-up.
9
9
  const getNotificationRegistry = (slice) => slice.registry;
10
10
  exports.getNotificationRegistry = getNotificationRegistry;
11
+ // `undefined` conflates gate-off with no-preferences-yet; consumers hide the
12
+ // section either way. Falls back to `registry` for task_checklist so state built
13
+ // in the pre-domain shape (consumer mocks, older callers) still resolves.
14
+ const getNotificationRegistryForDomain = (slice, domain) => slice.registryByDomain?.[domain] ??
15
+ (domain === 'task_checklist' ? slice.registry : undefined);
16
+ exports.getNotificationRegistryForDomain = getNotificationRegistryForDomain;
@@ -1,3 +1,7 @@
1
+ export declare const NOTIFICATION_DOMAINS: readonly ["task_checklist", "commenting"];
2
+ export type NotificationDomain = (typeof NOTIFICATION_DOMAINS)[number];
3
+ export declare const DEFAULT_NOTIFICATION_DOMAIN: NotificationDomain;
4
+ export declare const toNotificationDomain: (raw: string) => NotificationDomain;
1
5
  export declare const NOTIFICATION_CHANNELS: readonly ["email", "slack", "in_app"];
2
6
  export type NotificationChannel = (typeof NOTIFICATION_CHANNELS)[number];
3
7
  export declare const toNotificationChannel: (raw: string) => NotificationChannel;
@@ -30,4 +34,5 @@ export interface NotificationRegistry {
30
34
  export interface NotificationRegistryState {
31
35
  registryEpoch: number;
32
36
  registry?: NotificationRegistry;
37
+ registryByDomain?: Partial<Record<NotificationDomain, NotificationRegistry>>;
33
38
  }
@@ -1,7 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.toNotificationFrequency = exports.NOTIFICATION_FREQUENCIES = exports.toNotificationChannel = exports.NOTIFICATION_CHANNELS = void 0;
3
+ exports.toNotificationFrequency = exports.NOTIFICATION_FREQUENCIES = exports.toNotificationChannel = exports.NOTIFICATION_CHANNELS = exports.toNotificationDomain = exports.DEFAULT_NOTIFICATION_DOMAIN = exports.NOTIFICATION_DOMAINS = void 0;
4
4
  const stringToUnion_1 = require("../../commonStateTypes/stringToUnion");
5
+ // Wire vocabulary owned by the notifications service, not by this slice.
6
+ exports.NOTIFICATION_DOMAINS = ['task_checklist', 'commenting'];
7
+ // Callers that predate the domain split mean Task Checklist.
8
+ exports.DEFAULT_NOTIFICATION_DOMAIN = 'task_checklist';
9
+ const toNotificationDomain = (raw) => (0, stringToUnion_1.stringToUnion)(raw, exports.NOTIFICATION_DOMAINS);
10
+ exports.toNotificationDomain = toNotificationDomain;
5
11
  exports.NOTIFICATION_CHANNELS = ['email', 'slack', 'in_app'];
6
12
  const toNotificationChannel = (raw) => (0, stringToUnion_1.stringToUnion)(raw, exports.NOTIFICATION_CHANNELS);
7
13
  exports.toNotificationChannel = toNotificationChannel;
@@ -35,7 +35,9 @@ const sectionProjectView = (0, toolkit_1.createSlice)({
35
35
  Object.keys(sectionPayload.projects).forEach((projectId) => {
36
36
  const projectPayload = sectionPayload.projects[projectId];
37
37
  // A calculated section is grouped by project with no accounts under it.
38
- const projectAccounts = 'accounts' in projectPayload ? projectPayload.accounts : undefined;
38
+ const projectAccounts = 'accounts' in projectPayload
39
+ ? projectPayload.accounts
40
+ : undefined;
39
41
  const nestedAccountIds = projectAccounts != null
40
42
  ? (0, getNestedAccountIDHierarchyForSection_1.getNestedAccountIDHierarchyForAccounts)(Object.keys(projectAccounts), accountsMetadata)
41
43
  : [];
package/lib/epic.js CHANGED
@@ -53,21 +53,21 @@ const acceptMasterTOSEpic_1 = require("./view/aiCfoView/epics/acceptMasterTOSEpi
53
53
  const createSessionAndSubmitEpic_1 = require("./view/aiCfoView/epics/createSessionAndSubmitEpic");
54
54
  const createSessionEpic_1 = require("./view/aiCfoView/epics/createSessionEpic");
55
55
  const deleteChatSessionEpic_1 = require("./view/aiCfoView/epics/deleteChatSessionEpic");
56
+ const deleteRoutineEpic_1 = require("./view/aiCfoView/epics/deleteRoutineEpic");
56
57
  const deleteSkillEpic_1 = require("./view/aiCfoView/epics/deleteSkillEpic");
57
58
  const fetchChatHistoryEpic_1 = require("./view/aiCfoView/epics/fetchChatHistoryEpic");
58
59
  const fetchChatSessionsForUserEpic_1 = require("./view/aiCfoView/epics/fetchChatSessionsForUserEpic");
60
+ const fetchRoutineRunsEpic_1 = require("./view/aiCfoView/epics/fetchRoutineRunsEpic");
61
+ const fetchRoutinesEpic_1 = require("./view/aiCfoView/epics/fetchRoutinesEpic");
59
62
  const fetchSkillDetailEpic_1 = require("./view/aiCfoView/epics/fetchSkillDetailEpic");
60
63
  const fetchSkillVersionBodyEpic_1 = require("./view/aiCfoView/epics/fetchSkillVersionBodyEpic");
61
64
  const fetchSkillVersionsEpic_1 = require("./view/aiCfoView/epics/fetchSkillVersionsEpic");
62
65
  const fetchSkillsEpic_1 = require("./view/aiCfoView/epics/fetchSkillsEpic");
63
66
  const fetchSuggestedQuestionsEpic_1 = require("./view/aiCfoView/epics/fetchSuggestedQuestionsEpic");
64
- const deleteRoutineEpic_1 = require("./view/aiCfoView/epics/deleteRoutineEpic");
65
- const fetchRoutineRunsEpic_1 = require("./view/aiCfoView/epics/fetchRoutineRunsEpic");
66
- const fetchRoutinesEpic_1 = require("./view/aiCfoView/epics/fetchRoutinesEpic");
67
67
  const runRoutineEpic_1 = require("./view/aiCfoView/epics/runRoutineEpic");
68
68
  const saveRoutineEpic_1 = require("./view/aiCfoView/epics/saveRoutineEpic");
69
- const setRoutineEnabledEpic_1 = require("./view/aiCfoView/epics/setRoutineEnabledEpic");
70
69
  const saveSkillEpic_1 = require("./view/aiCfoView/epics/saveSkillEpic");
70
+ const setRoutineEnabledEpic_1 = require("./view/aiCfoView/epics/setRoutineEnabledEpic");
71
71
  const setSkillVisibilityEpic_1 = require("./view/aiCfoView/epics/setSkillVisibilityEpic");
72
72
  const stopSubmitEpic_1 = require("./view/aiCfoView/epics/stopSubmitEpic");
73
73
  const stopSubmitQuestionEpic_1 = require("./view/aiCfoView/epics/stopSubmitQuestionEpic");
@@ -1,6 +1,7 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../rootActions';
3
3
  import { mapNotificationRegistryPayloadToRegistry, } from './notificationRegistryPayload';
4
+ import { NOTIFICATION_DOMAINS, } from './notificationRegistryState';
4
5
  export const initialState = { registryEpoch: 0 };
5
6
  const notificationRegistry = createSlice({
6
7
  name: 'notificationRegistry',
@@ -19,16 +20,24 @@ const notificationRegistry = createSlice({
19
20
  // `task_checklist_registry` CLEARS so tenant B doesn't inherit A's
20
21
  // Task Checklist section. Guarded by the epoch check above so a stale
21
22
  // absent-response can't wipe a fresh registry.
22
- if (action.payload.registry == null) {
23
- draft.registry = undefined;
24
- return;
23
+ const { registriesByDomain } = action.payload;
24
+ // Absent `registriesByDomain` means a pre-domain caller: treat `registry`
25
+ // as the task_checklist snapshot so their behaviour is unchanged.
26
+ const snapshots = registriesByDomain ?? { task_checklist: action.payload.registry };
27
+ draft.registryByDomain = {};
28
+ for (const domain of NOTIFICATION_DOMAINS) {
29
+ const payload = snapshots[domain];
30
+ // Full-replace per domain, and an absent snapshot CLEARS that domain so
31
+ // tenant B never inherits A's section. The gates are independent
32
+ // server-side, so one domain clearing must not touch the other.
33
+ if (payload != null) {
34
+ draft.registryByDomain[domain] =
35
+ mapNotificationRegistryPayloadToRegistry(payload);
36
+ }
25
37
  }
26
- // Full-replace contract: the registry payload is atomic — every fetch
27
- // returns the complete group/event tree. If the backend ever ships a
28
- // partial registry response, this write MUST switch to a merge (mirror
29
- // the preferences slice's `mergePreferences`). Do not assume the
30
- // current payload is partial without also updating this reducer.
31
- draft.registry = mapNotificationRegistryPayloadToRegistry(action.payload.registry);
38
+ // Mirrored for consumers that predate the domain-keyed map.
39
+ draft.registry = draft.registryByDomain.task_checklist;
40
+ return;
32
41
  },
33
42
  },
34
43
  extraReducers: (builder) => {
@@ -41,6 +50,8 @@ const notificationRegistry = createSlice({
41
50
  // — Cursor Bugbot 67d6918e. Returning `initialState` verbatim would
42
51
  // rewind the counter and let a stale in-flight GET restore A's
43
52
  // registry over B's.
53
+ // Drops `registry` and `registryByDomain` together: a tenant switch must
54
+ // not leave one domain's section behind.
44
55
  return { registryEpoch: draft.registryEpoch + 1 };
45
56
  });
46
57
  },
@@ -4,3 +4,8 @@
4
4
  // off for this tenant". Both currently surface as `undefined`. Distinguishing
5
5
  // them (e.g. an explicit `gate_off` signal from the server) is a follow-up.
6
6
  export const getNotificationRegistry = (slice) => slice.registry;
7
+ // `undefined` conflates gate-off with no-preferences-yet; consumers hide the
8
+ // section either way. Falls back to `registry` for task_checklist so state built
9
+ // in the pre-domain shape (consumer mocks, older callers) still resolves.
10
+ export const getNotificationRegistryForDomain = (slice, domain) => slice.registryByDomain?.[domain] ??
11
+ (domain === 'task_checklist' ? slice.registry : undefined);
@@ -1,4 +1,9 @@
1
1
  import { stringToUnion } from '../../commonStateTypes/stringToUnion';
2
+ // Wire vocabulary owned by the notifications service, not by this slice.
3
+ export const NOTIFICATION_DOMAINS = ['task_checklist', 'commenting'];
4
+ // Callers that predate the domain split mean Task Checklist.
5
+ export const DEFAULT_NOTIFICATION_DOMAIN = 'task_checklist';
6
+ export const toNotificationDomain = (raw) => stringToUnion(raw, NOTIFICATION_DOMAINS);
2
7
  export const NOTIFICATION_CHANNELS = ['email', 'slack', 'in_app'];
3
8
  export const toNotificationChannel = (raw) => stringToUnion(raw, NOTIFICATION_CHANNELS);
4
9
  export const NOTIFICATION_FREQUENCIES = ['instant', 'daily'];
@@ -32,7 +32,9 @@ const sectionProjectView = createSlice({
32
32
  Object.keys(sectionPayload.projects).forEach((projectId) => {
33
33
  const projectPayload = sectionPayload.projects[projectId];
34
34
  // A calculated section is grouped by project with no accounts under it.
35
- const projectAccounts = 'accounts' in projectPayload ? projectPayload.accounts : undefined;
35
+ const projectAccounts = 'accounts' in projectPayload
36
+ ? projectPayload.accounts
37
+ : undefined;
36
38
  const nestedAccountIds = projectAccounts != null
37
39
  ? getNestedAccountIDHierarchyForAccounts(Object.keys(projectAccounts), accountsMetadata)
38
40
  : [];
package/lib/esm/epic.js CHANGED
@@ -50,21 +50,21 @@ import { acceptMasterTOSEpic, } from './view/aiCfoView/epics/acceptMasterTOSEpic
50
50
  import { createSessionAndSubmitEpic, } from './view/aiCfoView/epics/createSessionAndSubmitEpic';
51
51
  import { createSessionEpic, } from './view/aiCfoView/epics/createSessionEpic';
52
52
  import { deleteChatSessionEpic, } from './view/aiCfoView/epics/deleteChatSessionEpic';
53
+ import { deleteRoutineEpic } from './view/aiCfoView/epics/deleteRoutineEpic';
53
54
  import { deleteSkillEpic } from './view/aiCfoView/epics/deleteSkillEpic';
54
55
  import { fetchChatHistoryEpic, } from './view/aiCfoView/epics/fetchChatHistoryEpic';
55
56
  import { fetchChatSessionsForUserEpic, } from './view/aiCfoView/epics/fetchChatSessionsForUserEpic';
57
+ import { fetchRoutineRunsEpic } from './view/aiCfoView/epics/fetchRoutineRunsEpic';
58
+ import { fetchRoutinesEpic } from './view/aiCfoView/epics/fetchRoutinesEpic';
56
59
  import { fetchSkillDetailEpic } from './view/aiCfoView/epics/fetchSkillDetailEpic';
57
60
  import { fetchSkillVersionBodyEpic } from './view/aiCfoView/epics/fetchSkillVersionBodyEpic';
58
61
  import { fetchSkillVersionsEpic } from './view/aiCfoView/epics/fetchSkillVersionsEpic';
59
62
  import { fetchSkillsEpic, } from './view/aiCfoView/epics/fetchSkillsEpic';
60
63
  import { fetchSuggestedQuestionsEpic, } from './view/aiCfoView/epics/fetchSuggestedQuestionsEpic';
61
- import { deleteRoutineEpic } from './view/aiCfoView/epics/deleteRoutineEpic';
62
- import { fetchRoutineRunsEpic } from './view/aiCfoView/epics/fetchRoutineRunsEpic';
63
- import { fetchRoutinesEpic } from './view/aiCfoView/epics/fetchRoutinesEpic';
64
64
  import { runRoutineEpic } from './view/aiCfoView/epics/runRoutineEpic';
65
65
  import { saveRoutineEpic } from './view/aiCfoView/epics/saveRoutineEpic';
66
- import { setRoutineEnabledEpic } from './view/aiCfoView/epics/setRoutineEnabledEpic';
67
66
  import { saveSkillEpic } from './view/aiCfoView/epics/saveSkillEpic';
67
+ import { setRoutineEnabledEpic } from './view/aiCfoView/epics/setRoutineEnabledEpic';
68
68
  import { setSkillVisibilityEpic } from './view/aiCfoView/epics/setSkillVisibilityEpic';
69
69
  import { stopSubmitEpic, } from './view/aiCfoView/epics/stopSubmitEpic';
70
70
  import { stopSubmitQuestionEpic, } from './view/aiCfoView/epics/stopSubmitQuestionEpic';
package/lib/esm/index.js CHANGED
@@ -64,8 +64,8 @@ import { ALL_JE_SCHEDULE_IGNORE_REASONS, ALL_JE_SCHEDULE_REVERT_REASONS, ALL_ONE
64
64
  import { ALL_MONTH_END_CLOSE_CHECKS_FREQUENCY, } from './entity/monthEndCloseChecks/monthEndCloseChecksState';
65
65
  import { updateCommentsNotifications, updateCommentsNotificationsStatuses, } from './entity/notification/notificationReducer';
66
66
  import { toNotificationModeStrict, } from './entity/notification/types/notificationTypes';
67
- import { getNotificationRegistry } from './entity/notificationRegistry/notificationRegistrySelector';
68
- import { toNotificationChannel, toNotificationFrequency, } from './entity/notificationRegistry/notificationRegistryState';
67
+ import { getNotificationRegistry, getNotificationRegistryForDomain, } from './entity/notificationRegistry/notificationRegistrySelector';
68
+ import { DEFAULT_NOTIFICATION_DOMAIN, NOTIFICATION_DOMAINS, toNotificationChannel, toNotificationDomain, toNotificationFrequency, } from './entity/notificationRegistry/notificationRegistryState';
69
69
  import { getPlaidPaymentAccounts } from './entity/paymentAccount/paymentAccountSelector';
70
70
  import { VERIFIED_PAYMENT_ACCOUNT_PROVIDER_VERIFICATION_STATUS, } from './entity/paymentAccount/paymentAccountState';
71
71
  import { closeSnackbar, openSnackbar } from './entity/snackbar/snackbarReducer';
@@ -236,7 +236,7 @@ import { toAverageMonthsCount } from './view/netBurnOrIncomeStoryCard/epic/updat
236
236
  import { fetchNetBurnOrIncomeStoryCard, updateNetBurnOrIncomeStoryCardSettings, } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardReducer';
237
237
  import { getNetBurnOrIncomeStoryCardReport } from './view/netBurnOrIncomeStoryCard/netBurnOrIncomeStoryCardSelector';
238
238
  import { clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, setGroupChannelMaster, setGroupFrequency, toggleEventChannel, } from './view/notificationPreferencesView/notificationPreferencesViewReducer';
239
- import { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, hasUnsavedNotificationPreferences, } from './view/notificationPreferencesView/notificationPreferencesViewSelector';
239
+ import { getEffectiveNotificationPreferences, getEffectiveNotificationPreferencesForDomain, getNotificationLocalOverrides, getNotificationLocalOverridesForDomain, getNotificationPreferencesSaveState, hasUnsavedNotificationPreferences, hasUnsavedNotificationPreferencesForDomain, } from './view/notificationPreferencesView/notificationPreferencesViewSelector';
240
240
  import { fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, fetchNotificationView, updateNotificationViewAllNotificationsStatus, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewNotificationStatus, updateNotificationViewSubTab, updateNotificationViewTabState, updateNotificationViewUIState, } from './view/notificationView/notificationViewReducer';
241
241
  import { getExternalNotificationsForSelectedSubTab, getNotificationView, getNotificationsForSelectedSubTab, } from './view/notificationView/notificationViewSelector';
242
242
  import { toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, } from './view/notificationView/notificationViewState';
@@ -680,7 +680,7 @@ export { toNotificationModeStrict, updateCommentsNotifications, updateCommentsNo
680
680
  export { toNotificationSubTabTypeStrict, toNotificationTabTypeStrict, fetchNotificationView, fetchNotificationUnreadCount, fetchNotificationUnreadCountSuccess, updateNotificationViewAllNotificationsStatus, updateNotificationViewNotificationStatus, updateNotificationViewTabState, updateNotificationViewCurrentTabAndSubTab, updateNotificationViewSubTab, updateNotificationViewUIState, getNotificationView, getExternalNotificationsForSelectedSubTab, getNotificationsForSelectedSubTab, };
681
681
  export { fetchRegisteredInterests, notifyMeForFeature, getFeatureNotificationView, getRegisteredInterests, getRegisteredInterestsByFeature, isFeatureInterestRegistered, };
682
682
  export { pushToastNotification, getLastNotificationTime, getNotifications, };
683
- export { getEffectiveNotificationPreferences, getNotificationLocalOverrides, getNotificationPreferencesSaveState, getNotificationRegistry, hasUnsavedNotificationPreferences, clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, setGroupChannelMaster, setGroupFrequency, toggleEventChannel, toNotificationChannel, toNotificationFrequency, };
683
+ export { getEffectiveNotificationPreferences, getEffectiveNotificationPreferencesForDomain, getNotificationLocalOverrides, getNotificationLocalOverridesForDomain, getNotificationPreferencesSaveState, getNotificationRegistry, getNotificationRegistryForDomain, hasUnsavedNotificationPreferences, hasUnsavedNotificationPreferencesForDomain, NOTIFICATION_DOMAINS, DEFAULT_NOTIFICATION_DOMAIN, clearAllNotificationPreferencesView, clearNotificationPreferencesLocalOverrides, saveNotificationPreferences, setGroupChannelMaster, setGroupFrequency, toggleEventChannel, toNotificationChannel, toNotificationDomain, toNotificationFrequency, };
684
684
  export { getReferralListView, getInviteFormView, toReferralListViewSortKeyType, StatusTypes, AmountStatusTypes, DEFAULT_REFERRER_AMOUNT, fetchReferrals, sendReferralInvite, clearReferrals, saveReferralFormDataInLocalStore, updateReferralListSortUiState, resendReferralInvite, fetchRewardsPlan, getRewardsPlanCard, updateReferViewed, };
685
685
  export { ALL_WEEK_DAYS, SEMI_WEEKLY_REQUIRED_DAYS_COUNT, getMinAllowedEndDate, getRecurringEndDateFromCount, toDayOfWeek, toRecurringFrequency, };
686
686
  export { fetchCockpitContext, fetchCompanyTaskManagerView, fetchTaskManagerMetrics, getCompanyTaskManagerView, createTaskFromTaskGroupTemplate, };
@@ -835,7 +835,8 @@ const aiCfoView = createSlice({
835
835
  // The server has it. Nothing it sends from here on will contain this row,
836
836
  // so the tombstone has done its job and holding it any longer is what
837
837
  // makes a routine recreated under the same id invisible forever.
838
- draft.routines.deletedScheduleIds = draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
838
+ draft.routines.deletedScheduleIds =
839
+ draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
839
840
  draft.routineMutation = {
840
841
  fetchState: 'Completed',
841
842
  error: undefined,
@@ -847,7 +848,8 @@ const aiCfoView = createSlice({
847
848
  // is what lets the refetch put the row back — leaving it hidden would
848
849
  // tell the user they had stopped something they had not, which is the
849
850
  // worst outcome this list can produce.
850
- draft.routines.deletedScheduleIds = draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
851
+ draft.routines.deletedScheduleIds =
852
+ draft.routines.deletedScheduleIds.filter((id) => id !== action.payload.scheduleId);
851
853
  // Its own slice: the refetch that puts the row back would otherwise wipe
852
854
  // this error, leaving the row silently reappearing with no explanation.
853
855
  draft.routineMutation = {
@@ -27,7 +27,9 @@ export const fetchRoutineRunsEpic = (actions$, _state$, zeniAPI) => actions$.pip
27
27
  // Filtered, not trusted: one malformed row must not take the
28
28
  // whole history down, and a run without an id is not addressable
29
29
  // by anything the UI does with it.
30
- runs: (data.runs ?? []).filter(isRoutineRunPayload).map(toRoutineRun),
30
+ runs: (data.runs ?? [])
31
+ .filter(isRoutineRunPayload)
32
+ .map(toRoutineRun),
31
33
  scheduleId,
32
34
  sessionId: data.session_id ?? null,
33
35
  });
@@ -6,7 +6,6 @@ import { updateTransactions } from '../../../../entity/transaction/transactionRe
6
6
  import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
7
7
  import { getLineItemsByTransactionsIdsFromResponse } from '../../helpers/transactionCategorizationLocalDataHelper';
8
8
  import { fetchTransactionCategorization, fetchTransactionCategorizationFailure, fetchTransactionCategorizationSuccess, initializeTransactionCategorizationViewLocalData, resetOtherTabsFetchState, updateParentTotalCountForTab, updateTotalCountForTransactionCategorization, updateTransactionCategorizationUIState, } from '../../reducers/transactionsViewReducer';
9
- import { DEFAULT_COMPLETED_SUB_TAB } from '../../types/completedSubTab';
10
9
  import { TRANSACTIONS_TABS, toTransactionsSortKey, } from '../../types/transactionsViewState';
11
10
  export const fetchTransactionCategorizationEpic = (actions$, state$, zeniAPI) => actions$.pipe(filter(fetchTransactionCategorization.match),
12
11
  // Per-tab switchMap: cancels in-flight requests for the same tab on a new
@@ -33,9 +32,10 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
33
32
  // cross-status search (omit or true) is reserved for Receipts
34
33
  // manual-match.
35
34
  search_all_statuses: false,
36
- sub_tab: selectedTab === 'autoCategorized'
37
- ? expenseAutomationTransactionsViewState.selectedTransactionCategorizationCompletedSubTab
38
- : DEFAULT_COMPLETED_SUB_TAB,
35
+ // Sent on both tabs: the backend ignores it for Review's own rows but uses
36
+ // it to scope the Completed badge it returns alongside them.
37
+ sub_tab: expenseAutomationTransactionsViewState
38
+ .selectedTransactionCategorizationCompletedSubTab,
39
39
  transaction_group: expenseAutomationTransactionsViewState.selectedTransactionGroup,
40
40
  };
41
41
  return zeniAPI
@@ -87,6 +87,17 @@ groupBy((action) => action.payload.selectedTab), mergeMap((group$) => group$.pip
87
87
  parentTotalCount: response.data.parent_tab_total_count ??
88
88
  response.data.total_count,
89
89
  }));
90
+ // The inactive tab's badge, counted under the same filters. Absent on
91
+ // pages after the first and on the cross-tab search path, where the
92
+ // last known value stays correct.
93
+ const otherTabTotalCount = response.data.other_tab_total_count;
94
+ if (otherTabTotalCount != null) {
95
+ updateActions.push(updateParentTotalCountForTab({
96
+ selectedPeriod,
97
+ selectedTab: selectedTab === 'review' ? 'autoCategorized' : 'review',
98
+ parentTotalCount: otherTabTotalCount,
99
+ }));
100
+ }
90
101
  updateActions.push(resetOtherTabsFetchState({
91
102
  refreshViewInBackground,
92
103
  tabs: TRANSACTIONS_TABS.filter((tab) => selectedTab !== tab),
@@ -373,6 +373,16 @@ const expenseAutomationTransactionsView = createSlice({
373
373
  if (cached != null) {
374
374
  autoCat.uiState.pageToken = cached.pageToken;
375
375
  autoCat.uiState.totalCount = { ...cached.totalCount };
376
+ // Keep the stored badge in step with the restored sub-tab: the selector
377
+ // falls back to it for any period this sub-tab has not loaded. Merged,
378
+ // not replaced -- the snapshot only holds periods this sub-tab actually
379
+ // loaded, while the badge map also carries periods filled from a Review
380
+ // response's `other_tab_total_count`. Overwriting would drop those and
381
+ // leave the badge at 0 for any period this sub-tab never visited.
382
+ draft.parentTotalCountByTab.autoCategorized = {
383
+ ...draft.parentTotalCountByTab.autoCategorized,
384
+ ...cached.totalCount,
385
+ };
376
386
  autoCat.uiState.scrollPosition = cached.scrollPosition;
377
387
  autoCat.transactionIdsBySelectedPeriod = {
378
388
  ...cached.transactionIdsBySelectedPeriod,
@@ -1,6 +1,6 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import omit from 'lodash/omit';
3
- import { toMonthYearPeriodId } from '../../../commonStateTypes/timePeriod';
3
+ import { toMonthYearPeriodId, } from '../../../commonStateTypes/timePeriod';
4
4
  import { getCurrentTenant, getIsAccountingProjectsEnabled, } from '../../../entity/tenant/tenantSelector';
5
5
  import { getTransactionWithCOT } from '../../../entity/transaction/transactionHelper';
6
6
  import { getSupportedTransactionsByIds } from '../../../entity/transaction/transactionSelector';
@@ -172,18 +172,35 @@ export function getExpenseAutomationTransactionView(state) {
172
172
  review: monthYearPeriodId != null ? totalCountByReview[monthYearPeriodId] : 0,
173
173
  autoCategorized: monthYearPeriodId != null ? totalCountByAutoCat[monthYearPeriodId] : 0,
174
174
  };
175
- // Parent-tab badge counts. Sourced from `parent_tab_total_count` in the
176
- // listing response and used exclusively by the navbar / tab strip badges.
177
- // Independent of which Completed sub-tab is active so the badge stays
178
- // stable across sub-tab switches.
175
+ // Tab badge counts, used exclusively by the navbar / tab strip badges. Both
176
+ // tabs are counted under the filters applied to the list, so a badge always
177
+ // equals the number of rows that tab shows.
178
+ //
179
+ // The ACTIVE tab prefers its own list count: the two are the same number (the
180
+ // backend's parent_tab_total_count mirrors total_count), but `uiState.totalCount`
181
+ // is restored by every cache path that skips a fetch -- switching back to an
182
+ // already-loaded period, or to a cached Completed sub-tab -- whereas the stored
183
+ // badge is only written when a response lands, so on those paths it would keep
184
+ // showing whatever was fetched last.
185
+ //
186
+ // The `in` check is load-bearing, not defensive: `totalCount` is a per-period
187
+ // map typed as `Record<MonthYearPeriodId, number>`, so indexing it is typed
188
+ // `number` but is `undefined` for any period the active (sub-)tab has not
189
+ // loaded -- the window between switching to a fresh sub-tab and its response
190
+ // landing. Falling back to the stored badge there keeps a real number in the
191
+ // label instead of blanking it.
179
192
  const parentTotalCountByTab = expenseAutomationTransactionsViewState.parentTotalCountByTab;
193
+ const storedBadgeByTab = (tab) => monthYearPeriodId != null
194
+ ? (parentTotalCountByTab[tab][monthYearPeriodId] ?? 0)
195
+ : 0;
196
+ const badgeForTab = (tab, listCountByPeriod) => selectedTransactionCategorizationTab === tab &&
197
+ monthYearPeriodId != null &&
198
+ monthYearPeriodId in listCountByPeriod
199
+ ? listCountByPeriod[monthYearPeriodId]
200
+ : storedBadgeByTab(tab);
180
201
  const parentTabTotalCountByTab = {
181
- review: monthYearPeriodId != null
182
- ? (parentTotalCountByTab.review[monthYearPeriodId] ?? 0)
183
- : 0,
184
- autoCategorized: monthYearPeriodId != null
185
- ? (parentTotalCountByTab.autoCategorized[monthYearPeriodId] ?? 0)
186
- : 0,
202
+ review: badgeForTab('review', totalCountByReview),
203
+ autoCategorized: badgeForTab('autoCategorized', totalCountByAutoCat),
187
204
  };
188
205
  const fetchStateByTab = {
189
206
  review: {
@@ -3,7 +3,7 @@ import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState'
3
3
  import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
4
4
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
5
5
  import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
6
- import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
6
+ import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, NO_INVOICING_CLASS_HIERARCHY, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
7
7
  import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, initialInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormHelper';
8
8
  export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
9
9
  fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
@@ -44,6 +44,21 @@ const NOT_STARTED = {
44
44
  error: undefined,
45
45
  fetchState: 'Not-Started',
46
46
  };
47
+ /**
48
+ * The QuickBooks ids a price point's income account may be picked from.
49
+ *
50
+ * Memoized, and collapsing to one shared empty array while QBO is disconnected:
51
+ * the pages turn this into their picker options and re-seed their form defaults
52
+ * when those change, so a fresh array on every store change would reset the form
53
+ * under the user — including mid-save.
54
+ */
55
+ const getInvoicingCatalogIncomeAccountIds = createSelector([
56
+ getInvoicingQboAccountOptions,
57
+ (state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
58
+ isInvoicingQboConnected,
59
+ ], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
60
+ ? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_INCOME_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
61
+ : NO_INVOICING_ACCOUNT_IDS);
47
62
  export const getInvoicingCatalogItemFormView = (state, id) => {
48
63
  const { editDraftById, error, fetchState, newItemDraft, savedItemId, savedKind, } = state.editInvoicingCatalogItemDetailViewState;
49
64
  const plan = id != null ? getInvoicingPlanById(state.invoicingPlanState, id) : undefined;
@@ -94,8 +109,9 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
94
109
  isFormLoading: isInvoicingAccountingPickerDataLoading(state) ||
95
110
  isInvoicingFormDraftPending(editDraftById, id),
96
111
  isQboPickerDataLoading: isInvoicingAccountingPickerDataLoading(state),
97
- qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
98
- qboClassHierarchy: qboView?.nestedClassHierarchy ?? [],
112
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
113
+ qboClassHierarchy: qboView?.nestedClassHierarchy ?? NO_INVOICING_CLASS_HIERARCHY,
114
+ qboIncomeAccountIds: getInvoicingCatalogIncomeAccountIds(state),
99
115
  applicabilityValues: INVOICING_CATALOG_APPLICABILITY_VALUES,
100
116
  applicableItemOptions,
101
117
  localData,
@@ -1,9 +1,10 @@
1
+ import { createSelector } from '@reduxjs/toolkit';
1
2
  import { reduceAllFetchState } from '../../../commonStateTypes/reduceFetchState';
2
3
  import { getInvoicingCouponById } from '../../../entity/invoicing/coupon/couponSelector';
3
- import { toInvoicingFormOption, toInvoicingQboAccountMappingSlot, } from '../../../entity/invoicing/invoicingCommonPayload';
4
+ import { toInvoicingFormOption, } from '../../../entity/invoicing/invoicingCommonPayload';
4
5
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
5
6
  import { getInvoicingFormDraftLocalData, isInvoicingFormDraftPending, } from '../invoicingFormDraft';
6
- import { getInvoicingQboView, invoicingAccountsForQboAccountTypes, invoicingQboAccountByQboId, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
7
+ import { INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, NO_INVOICING_ACCOUNT_HIERARCHY, NO_INVOICING_ACCOUNT_IDS, getInvoicingQboAccountOptions, getInvoicingQboView, invoicingQboAccountByQboId, invoicingQboAccountIdsForMappingSlot, isInvoicingAccountingPickerDataLoading, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
7
8
  import { getInvoicingSettings, getInvoicingSettingsFetchState, } from '../settingsView/settingsViewSelector';
8
9
  import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdForCreate, } from './editInvoicingCouponDetailViewState';
9
10
  /**
@@ -11,6 +12,21 @@ import { initialInvoicingCouponFormLocalData, invoicingCouponDiscountsAccountIdF
11
12
  * the id of the coupon last saved, and — when a `couponId` is passed (edit
12
13
  * mode) — the existing coupon entity to prefill the form.
13
14
  */
15
+ /**
16
+ * The QuickBooks ids the discounts account may be picked from.
17
+ *
18
+ * Memoized, and collapsing to one shared empty array while QBO is disconnected:
19
+ * the form turns this into its picker options and re-seeds its defaults when
20
+ * those change, so a fresh array on every store change would reset the form
21
+ * under the user until their first edit.
22
+ */
23
+ const getInvoicingCouponDiscountsAccountIds = createSelector([
24
+ getInvoicingQboAccountOptions,
25
+ (state) => getInvoicingConfigView(state).qboAccountMappingKindOptions,
26
+ isInvoicingQboConnected,
27
+ ], (accounts, qboAccountMappingKindOptions, isQboConnected) => isQboConnected
28
+ ? invoicingQboAccountIdsForMappingSlot(accounts, qboAccountMappingKindOptions, INVOICING_DISCOUNTS_ACCOUNT_MAPPING_SLOT, INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES)
29
+ : NO_INVOICING_ACCOUNT_IDS);
14
30
  const NOT_STARTED = {
15
31
  error: undefined,
16
32
  fetchState: 'Not-Started',
@@ -51,14 +67,6 @@ export const getEditInvoicingCouponDetailViewState = (state, couponId) => {
51
67
  version: 1.0,
52
68
  };
53
69
  };
54
- /**
55
- * The account types the discounts slot falls back to when the config has no
56
- * `discounts_account` kind. A QuickBooks discount line posts to an income
57
- * account; offering every account instead would let a save be rejected.
58
- */
59
- const DEFAULT_DISCOUNTS_ACCOUNT_TYPES = 'Income';
60
- /** The account-mapping slot a coupon's discounts account shares with settings. */
61
- const DISCOUNTS_ACCOUNT_MAPPING_SLOT = 'discounts_account';
62
70
  /**
63
71
  * Bundled view for the discount form screen: the edit view (save state + the
64
72
  * coupon to prefill in edit mode), the draft `localData` (seeded from the
@@ -74,7 +82,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
74
82
  const localData = couponId != null
75
83
  ? getInvoicingFormDraftLocalData(editDraftById, couponId, initialInvoicingCouponFormLocalData)
76
84
  : newCouponDraft;
77
- const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, qboAccountMappingKindOptions, } = getInvoicingConfigView(state);
85
+ const { billingPeriodUnitOptions, discountApplyOnOptions, discountDurationTypeOptions, discountTypeOptions, } = getInvoicingConfigView(state);
78
86
  const isQboConnected = isInvoicingQboConnected(state);
79
87
  const qboView = isQboConnected ? getInvoicingQboView(state) : undefined;
80
88
  const isQboPickerDataLoading = isInvoicingAccountingPickerDataLoading(state);
@@ -84,14 +92,6 @@ export const getInvoicingCouponFormView = (state, couponId) => {
84
92
  const isCreateSeedPending = couponId == null &&
85
93
  (settingsFetchState === 'Not-Started' ||
86
94
  settingsFetchState === 'In-Progress');
87
- // The account types the discounts slot accepts come from the same config the
88
- // QBO settings page reads, so the two pickers offer the same accounts and
89
- // widening the slot stays a config change. Matched on the slot rather than
90
- // the code as written: a config that names the kind `discounts_account_id`
91
- // is the same slot, and missing it here would quietly fall back to the
92
- // default and offer a different set than the settings page does.
93
- const discountsAccountTypes = qboAccountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) ===
94
- DISCOUNTS_ACCOUNT_MAPPING_SLOT)?.accountTypes ?? DEFAULT_DISCOUNTS_ACCOUNT_TYPES;
95
95
  return {
96
96
  ...base,
97
97
  applyOnOptions: discountApplyOnOptions.map(toInvoicingFormOption),
@@ -130,7 +130,7 @@ export const getInvoicingCouponFormView = (state, couponId) => {
130
130
  },
131
131
  mode: couponId != null ? 'edit' : 'create',
132
132
  periodUnitOptions: billingPeriodUnitOptions.map(toInvoicingFormOption),
133
- qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
134
- qboIncomeAccountIds: invoicingAccountsForQboAccountTypes(qboView?.accounts ?? [], discountsAccountTypes).map((account) => account.id),
133
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? NO_INVOICING_ACCOUNT_HIERARCHY,
134
+ qboIncomeAccountIds: getInvoicingCouponDiscountsAccountIds(state),
135
135
  };
136
136
  };