@zeniai/client-epic-state 5.2.5 → 5.2.7

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 (32) hide show
  1. package/lib/epic.js +2 -4
  2. package/lib/esm/epic.js +2 -4
  3. package/lib/esm/index.js +2 -1
  4. package/lib/esm/view/accountList/accountListReducer.js +1 -0
  5. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +8 -0
  6. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +29 -0
  7. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewPayload.js +0 -23
  8. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewReducer.js +3 -40
  9. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +124 -6
  10. package/lib/index.d.ts +4 -2
  11. package/lib/index.js +6 -4
  12. package/lib/view/accountList/accountListReducer.js +1 -0
  13. package/lib/view/accountList/accountListState.d.ts +1 -1
  14. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +9 -0
  15. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +8 -0
  16. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.d.ts +5 -0
  17. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +33 -0
  18. package/lib/view/invoicing/invoicingQboView/epics/invoicingQboViewActionType.d.ts +4 -2
  19. package/lib/view/invoicing/invoicingQboView/invoicingQboViewPayload.d.ts +0 -26
  20. package/lib/view/invoicing/invoicingQboView/invoicingQboViewPayload.js +0 -24
  21. package/lib/view/invoicing/invoicingQboView/invoicingQboViewReducer.d.ts +2 -10
  22. package/lib/view/invoicing/invoicingQboView/invoicingQboViewReducer.js +4 -41
  23. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +43 -6
  24. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +128 -7
  25. package/lib/view/invoicing/invoicingQboView/invoicingQboViewState.d.ts +1 -5
  26. package/package.json +1 -1
  27. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.js +0 -25
  28. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.js +0 -15
  29. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.d.ts +0 -6
  30. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic.js +0 -29
  31. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.d.ts +0 -6
  32. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic.js +0 -19
package/lib/epic.js CHANGED
@@ -297,8 +297,7 @@ const fetchInvoicingOverviewKPIsEpic_1 = require("./view/invoicing/invoicingOver
297
297
  const fetchInvoicingOverviewMRREpic_1 = require("./view/invoicing/invoicingOverview/epics/fetchInvoicingOverviewMRREpic");
298
298
  const fetchInvoicingOverviewPlanRevenueEpic_1 = require("./view/invoicing/invoicingOverview/epics/fetchInvoicingOverviewPlanRevenueEpic");
299
299
  const fetchInvoicingOverviewRevenueEpic_1 = require("./view/invoicing/invoicingOverview/epics/fetchInvoicingOverviewRevenueEpic");
300
- const fetchInvoicingQboAccountsEpic_1 = require("./view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic");
301
- const fetchInvoicingQboClassesEpic_1 = require("./view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic");
300
+ const fetchInvoicingAccountingPickersEpic_1 = require("./view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic");
302
301
  const fetchInvoicingQboConnectionEpic_1 = require("./view/invoicing/invoicingQboView/epics/fetchInvoicingQboConnectionEpic");
303
302
  const fetchInvoicingQboSyncHealthEpic_1 = require("./view/invoicing/invoicingQboView/epics/fetchInvoicingQboSyncHealthEpic");
304
303
  const retryInvoicingQboSyncTaskEpic_1 = require("./view/invoicing/invoicingQboView/epics/retryInvoicingQboSyncTaskEpic");
@@ -983,6 +982,7 @@ const ALL_EPICS = [
983
982
  fetchInvoiceKPIsEpic_1.fetchInvoiceKPIsEpic,
984
983
  fetchInvoiceListEpic_1.fetchInvoiceListEpic,
985
984
  fetchInvoiceListPageEpic_1.fetchInvoiceListPageEpic,
985
+ fetchInvoicingAccountingPickersEpic_1.fetchInvoicingAccountingPickersEpic,
986
986
  auditViewEpics_1.fetchInvoicingAuditLogEpic,
987
987
  fetchInvoicingCatalogCountsEpic_1.fetchInvoicingCatalogCountsEpic,
988
988
  invoicingCatalogItemDetailViewEpics_1.fetchInvoicingCatalogItemDetailEpic,
@@ -1021,8 +1021,6 @@ const ALL_EPICS = [
1021
1021
  fetchInvoicingOverviewPlanRevenueEpic_1.fetchInvoicingOverviewPlanRevenueEpic,
1022
1022
  fetchInvoicingOverviewRevenueEpic_1.fetchInvoicingOverviewRevenueEpic,
1023
1023
  customerPaymentMethodEpic_1.fetchInvoicingPlaidLinkTokenEpic,
1024
- fetchInvoicingQboAccountsEpic_1.fetchInvoicingQboAccountsEpic,
1025
- fetchInvoicingQboClassesEpic_1.fetchInvoicingQboClassesEpic,
1026
1024
  fetchInvoicingQboConnectionEpic_1.fetchInvoicingQboConnectionEpic,
1027
1025
  fetchInvoicingQboSyncHealthEpic_1.fetchInvoicingQboSyncHealthEpic,
1028
1026
  settingsViewEpics_1.fetchInvoicingSettingsEpic,
package/lib/esm/epic.js CHANGED
@@ -294,8 +294,7 @@ import { fetchInvoicingOverviewKPIsEpic } from './view/invoicing/invoicingOvervi
294
294
  import { fetchInvoicingOverviewMRREpic } from './view/invoicing/invoicingOverview/epics/fetchInvoicingOverviewMRREpic';
295
295
  import { fetchInvoicingOverviewPlanRevenueEpic } from './view/invoicing/invoicingOverview/epics/fetchInvoicingOverviewPlanRevenueEpic';
296
296
  import { fetchInvoicingOverviewRevenueEpic } from './view/invoicing/invoicingOverview/epics/fetchInvoicingOverviewRevenueEpic';
297
- import { fetchInvoicingQboAccountsEpic } from './view/invoicing/invoicingQboView/epics/fetchInvoicingQboAccountsEpic';
298
- import { fetchInvoicingQboClassesEpic } from './view/invoicing/invoicingQboView/epics/fetchInvoicingQboClassesEpic';
297
+ import { fetchInvoicingAccountingPickersEpic } from './view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic';
299
298
  import { fetchInvoicingQboConnectionEpic } from './view/invoicing/invoicingQboView/epics/fetchInvoicingQboConnectionEpic';
300
299
  import { fetchInvoicingQboSyncHealthEpic } from './view/invoicing/invoicingQboView/epics/fetchInvoicingQboSyncHealthEpic';
301
300
  import { retryInvoicingQboSyncTaskEpic } from './view/invoicing/invoicingQboView/epics/retryInvoicingQboSyncTaskEpic';
@@ -980,6 +979,7 @@ const ALL_EPICS = [
980
979
  fetchInvoiceKPIsEpic,
981
980
  fetchInvoiceListEpic,
982
981
  fetchInvoiceListPageEpic,
982
+ fetchInvoicingAccountingPickersEpic,
983
983
  fetchInvoicingAuditLogEpic,
984
984
  fetchInvoicingCatalogCountsEpic,
985
985
  fetchInvoicingCatalogItemDetailEpic,
@@ -1018,8 +1018,6 @@ const ALL_EPICS = [
1018
1018
  fetchInvoicingOverviewPlanRevenueEpic,
1019
1019
  fetchInvoicingOverviewRevenueEpic,
1020
1020
  fetchInvoicingPlaidLinkTokenEpic,
1021
- fetchInvoicingQboAccountsEpic,
1022
- fetchInvoicingQboClassesEpic,
1023
1021
  fetchInvoicingQboConnectionEpic,
1024
1022
  fetchInvoicingQboSyncHealthEpic,
1025
1023
  fetchInvoicingSettingsEpic,
package/lib/esm/index.js CHANGED
@@ -853,7 +853,8 @@ export { getInvoicingBrandingFormView, getInvoicingSettings, getInvoicingSetting
853
853
  export { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, initialInvoicingBrandingFormLocalData, } from './view/invoicing/settingsView/invoicingBrandingFormConfig';
854
854
  export { isStripeConnectedFromSettings } from './view/invoicing/settingsView/invoicingSettingsHelpers';
855
855
  // ── Invoicing: QBO integration ────────────────────────────────────────────────
856
- export { fetchInvoicingQboAccounts, fetchInvoicingQboClasses, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, startInvoicingQboBackfill, } from './view/invoicing/invoicingQboView/invoicingQboViewReducer';
856
+ export { fetchInvoicingAccountingPickers, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, startInvoicingQboBackfill, } from './view/invoicing/invoicingQboView/invoicingQboViewReducer';
857
+ export { INVOICING_INCOME_ACCOUNT_TYPES, invoicingAccountsForQboAccountTypes, } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
857
858
  export { getInvoicingQboView } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
858
859
  export { INVOICING_QBO_CUSTOMER_MATCHING_MODES, getInvoicingQboAccountMappingId, toInvoicingNamedOptionsByKey, toInvoicingQboCustomerMatchingMode, withInvoicingQboAccountMappingId, } from './entity/invoicing/invoicingCommonPayload';
859
860
  // ── Invoicing: config ─────────────────────────────────────────────────────────
@@ -28,6 +28,7 @@ export const initialState = {
28
28
  reimbursementAccountList: initialAccountList,
29
29
  schedulesAccountList: initialAccountList,
30
30
  billPayAccountList: initialAccountList,
31
+ invoicingAccountList: initialAccountList,
31
32
  },
32
33
  };
33
34
  const accountList = createSlice({
@@ -1,6 +1,7 @@
1
1
  import { createSelector } from '@reduxjs/toolkit';
2
2
  import { getInvoicingPlanById, getInvoicingPlansByIds, } from '../../../entity/invoicing/plan/planSelector';
3
3
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
4
+ import { INVOICING_INCOME_ACCOUNT_TYPES, getInvoicingQboView, invoicingAccountHierarchyForTypes, isInvoicingQboConnected, } from '../invoicingQboView/invoicingQboViewSelector';
4
5
  import { INVOICING_CATALOG_APPLICABILITY_VALUES, INVOICING_CATALOG_PRICING_MODEL_VALUES, INVOICING_CATALOG_PRORATION_VALUES, INVOICING_CATALOG_TIER_TYPE_VALUES, entityToInvoicingCatalogItemFormLocalData, } from './invoicingCatalogItemFormConfig';
5
6
  export const getEditInvoicingCatalogItemDetailViewState = (state) => ({
6
7
  fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
@@ -48,9 +49,16 @@ export const getInvoicingCatalogItemFormView = (state, id) => {
48
49
  const applicableItemOptions = id != null
49
50
  ? allApplicableItemOptions.filter((option) => option.id !== id)
50
51
  : allApplicableItemOptions;
52
+ const isQboConnected = isInvoicingQboConnected(state);
51
53
  const { billingPeriodUnitOptions, catalogItemTypeOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, } = getInvoicingConfigView(state);
52
54
  return {
53
55
  fieldHelp: invoicingFieldHelpForGroup(state, 'catalog'),
56
+ qboClassHierarchy: isQboConnected
57
+ ? getInvoicingQboView(state).nestedClassHierarchy
58
+ : [],
59
+ qboIncomeAccountHierarchy: isQboConnected
60
+ ? invoicingAccountHierarchyForTypes(state, INVOICING_INCOME_ACCOUNT_TYPES)
61
+ : [],
54
62
  applicabilityValues: INVOICING_CATALOG_APPLICABILITY_VALUES,
55
63
  applicableItemOptions,
56
64
  localData,
@@ -0,0 +1,29 @@
1
+ import { EMPTY, from } from 'rxjs';
2
+ import { filter, mergeMap, withLatestFrom } from 'rxjs/operators';
3
+ import { fetchAccountList } from '../../../accountList/accountListReducer';
4
+ import { fetchClassList } from '../../../classList/classListReducer';
5
+ import { fetchInvoicingAccountingPickers } from '../invoicingQboViewReducer';
6
+ import { INVOICING_ACCOUNT_LIST_KEY } from '../invoicingQboViewSelector';
7
+ // Tops up the shared account and class lists, skipping either when the store
8
+ // already holds it or has it in flight.
9
+ export const fetchInvoicingAccountingPickersEpic = (actions$, state$) => actions$.pipe(filter(fetchInvoicingAccountingPickers.match), withLatestFrom(state$), mergeMap(([, state]) => {
10
+ const actions = [];
11
+ const accountList = state.accountListState.byReportId[INVOICING_ACCOUNT_LIST_KEY];
12
+ if (accountList.hasValidState() === false &&
13
+ accountList.fetchState !== 'In-Progress') {
14
+ // AP/AR accounts cannot back an invoicing mapping, so they are excluded
15
+ // here — which is why this uses invoicing's own bucket rather than the
16
+ // general `accountList` other screens fill without those exclusions.
17
+ actions.push(fetchAccountList(INVOICING_ACCOUNT_LIST_KEY, true, true, true));
18
+ }
19
+ const classList = state.classListState;
20
+ if (classList.hasValidState() === false &&
21
+ classList.fetchState !== 'In-Progress') {
22
+ // Without deleted classes, matching almost every other caller of this
23
+ // single global slice. Asking for deleted rows here would have handed
24
+ // them to the vendor and transaction screens too — and the picker
25
+ // filters deleted classes out anyway, so there is nothing to gain.
26
+ actions.push(fetchClassList());
27
+ }
28
+ return actions.length > 0 ? from(actions) : EMPTY;
29
+ }));
@@ -6,29 +6,6 @@ export function mapInvoicingQboConnection(payload) {
6
6
  realmId: payload.realm_id ?? undefined,
7
7
  };
8
8
  }
9
- export function mapInvoicingQboAccountOptions(rows) {
10
- return rows.flatMap((row) => {
11
- // Only qbo_id is a valid QBO AccountRef/ClassRef value — platform-internal
12
- // ids (account_id / id) are display metadata and must never be submitted.
13
- const accountId = row.qbo_id ?? row.id ?? row.account_id ?? '';
14
- if (accountId === '') {
15
- return [];
16
- }
17
- const accountName = row.name ??
18
- row.account_name ??
19
- row.accounting_class_name ??
20
- row.qbo_accounting_class_name ??
21
- row.fully_qualified_name ??
22
- accountId;
23
- return [
24
- {
25
- id: accountId,
26
- name: accountName,
27
- accountType: row.account_type ?? undefined,
28
- },
29
- ];
30
- });
31
- }
32
9
  export function mapInvoicingQboSyncHealth(payload) {
33
10
  return {
34
11
  connection: payload.connection != null
@@ -1,11 +1,7 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
2
  import { clearAll } from '../../../rootActions';
3
3
  export const initialState = {
4
- accountsByAccountTypes: {},
5
- accountsFetchStateByAccountTypes: {},
6
4
  backfillFetchState: { fetchState: 'Not-Started', error: undefined },
7
- classes: [],
8
- classesFetchState: { fetchState: 'Not-Started', error: undefined },
9
5
  connection: undefined,
10
6
  connectionFetchState: { fetchState: 'Not-Started', error: undefined },
11
7
  retryFetchStateByTaskId: {},
@@ -29,41 +25,8 @@ const invoicingQboView = createSlice({
29
25
  updateInvoicingQboConnectionFailure(draft, action) {
30
26
  draft.connectionFetchState = { fetchState: 'Error', error: action.payload };
31
27
  },
32
- fetchInvoicingQboAccounts: {
33
- reducer(draft, action) {
34
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
35
- fetchState: 'In-Progress',
36
- error: undefined,
37
- };
38
- },
39
- prepare(accountTypes) {
40
- return { payload: { accountTypes } };
41
- },
42
- },
43
- updateInvoicingQboAccounts(draft, action) {
44
- draft.accountsByAccountTypes[action.payload.accountTypes] =
45
- action.payload.accounts;
46
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
47
- fetchState: 'Completed',
48
- error: undefined,
49
- };
50
- },
51
- updateInvoicingQboAccountsFailure(draft, action) {
52
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
53
- fetchState: 'Error',
54
- error: action.payload.status,
55
- };
56
- },
57
- fetchInvoicingQboClasses(draft) {
58
- draft.classesFetchState = { fetchState: 'In-Progress', error: undefined };
59
- },
60
- updateInvoicingQboClasses(draft, action) {
61
- draft.classes = action.payload;
62
- draft.classesFetchState = { fetchState: 'Completed', error: undefined };
63
- },
64
- updateInvoicingQboClassesFailure(draft, action) {
65
- draft.classesFetchState = { fetchState: 'Error', error: action.payload };
66
- },
28
+ // No state of its own: the epic decides which shared lists to load.
29
+ fetchInvoicingAccountingPickers() { },
67
30
  fetchInvoicingQboSyncHealth(draft) {
68
31
  draft.syncHealthFetchState = {
69
32
  fetchState: 'In-Progress',
@@ -127,5 +90,5 @@ const invoicingQboView = createSlice({
127
90
  });
128
91
  },
129
92
  });
130
- export const { fetchInvoicingQboConnection, updateInvoicingQboConnection, updateInvoicingQboConnectionFailure, fetchInvoicingQboAccounts, updateInvoicingQboAccounts, updateInvoicingQboAccountsFailure, fetchInvoicingQboClasses, updateInvoicingQboClasses, updateInvoicingQboClassesFailure, fetchInvoicingQboSyncHealth, updateInvoicingQboSyncHealth, updateInvoicingQboSyncHealthFailure, retryInvoicingQboSyncTask, retryInvoicingQboSyncTaskSuccess, retryInvoicingQboSyncTaskFailure, startInvoicingQboBackfill, startInvoicingQboBackfillSuccess, startInvoicingQboBackfillFailure, } = invoicingQboView.actions;
93
+ export const { fetchInvoicingQboConnection, updateInvoicingQboConnection, updateInvoicingQboConnectionFailure, fetchInvoicingAccountingPickers, fetchInvoicingQboSyncHealth, updateInvoicingQboSyncHealth, updateInvoicingQboSyncHealthFailure, retryInvoicingQboSyncTask, retryInvoicingQboSyncTaskSuccess, retryInvoicingQboSyncTaskFailure, startInvoicingQboBackfill, startInvoicingQboBackfillSuccess, startInvoicingQboBackfillFailure, } = invoicingQboView.actions;
131
94
  export default invoicingQboView.reducer;
@@ -1,16 +1,134 @@
1
+ import { getAccountList, getNestedAccountListHierarchy, } from '../../accountList/accountListSelector';
2
+ import { getClassList, getNestedClassListHierarchy, } from '../../classList/classListSelector';
3
+ // Its own bucket: invoicing needs the AP/AR exclusions that the vendor and
4
+ // transaction screens sharing `accountList` do not pass.
5
+ export const INVOICING_ACCOUNT_LIST_KEY = 'invoicingAccountList';
6
+ /** Whether QBO is connected, without building the option lists to find out. */
7
+ export const isInvoicingQboConnected = (state) => state.invoicingQboViewState.connection?.connected === true;
8
+ /**
9
+ * Account types that can back invoicing revenue. Named here so screens ask for
10
+ * "income accounts" rather than repeating the accounting service's vocabulary.
11
+ */
12
+ export const INVOICING_INCOME_ACCOUNT_TYPES = [
13
+ 'income',
14
+ 'other_income',
15
+ ];
16
+ /**
17
+ * Accounts and classes as the invoicing pickers need them. Both come from the
18
+ * shared accounting-service lists, so whatever another screen already loaded
19
+ * is reused. `qboId` is the submitted value because that is what the invoicing
20
+ * API stores; an account without one cannot be posted, so it is dropped rather
21
+ * than offered.
22
+ */
23
+ const invoicingAccountingOptions = (state, accountTypes) => {
24
+ const { accounts } = getAccountList(state.accountState, state.accountListState, INVOICING_ACCOUNT_LIST_KEY);
25
+ return accounts.flatMap((account) => {
26
+ if (account.qboId == null || account.isDeleted === true) {
27
+ return [];
28
+ }
29
+ if (accountTypes != null &&
30
+ (account.accountType == null ||
31
+ !accountTypes.includes(account.accountType))) {
32
+ return [];
33
+ }
34
+ return [
35
+ {
36
+ id: String(account.qboId),
37
+ name: account.accountName,
38
+ accountType: account.accountType,
39
+ },
40
+ ];
41
+ });
42
+ };
43
+ /**
44
+ * Normalises an account-type name for comparison. The config's mapping kinds
45
+ * name QuickBooks types the way QuickBooks displays them ("Other Current
46
+ * Asset", "Expense"), while the accounting service uses snake_case and
47
+ * sometimes pluralises ("other_current_assets", "expenses"). Comparing the
48
+ * normalised, de-pluralised forms keeps a picker from silently emptying because
49
+ * of a trailing "s".
50
+ */
51
+ const normalizeAccountTypeName = (value) => {
52
+ const normalized = value
53
+ .trim()
54
+ .toLowerCase()
55
+ .replace(/[^a-z0-9]+/g, '_');
56
+ // Singularise so "Other Current Liability" meets `other_current_liabilities`
57
+ // and "Expense" meets `expenses`. `-ies` needs its own case: stripping the
58
+ // trailing "s" alone would leave "liabilitie", matching nothing.
59
+ return normalized.replace(/ies$/, 'y').replace(/([^s])s$/, '$1');
60
+ };
61
+ /**
62
+ * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
63
+ * comma-separated list of QuickBooks type names. An unrecognised list yields no
64
+ * accounts rather than every account: offering a bank account where income is
65
+ * required would post a mapping QuickBooks rejects.
66
+ */
67
+ export const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
68
+ const wanted = new Set(accountTypes
69
+ .split(',')
70
+ .map(normalizeAccountTypeName)
71
+ .filter((accountType) => accountType !== ''));
72
+ if (wanted.size === 0) {
73
+ return accounts;
74
+ }
75
+ return accounts.filter((account) => account.accountType != null &&
76
+ wanted.has(normalizeAccountTypeName(account.accountType)));
77
+ };
78
+ /** Classes as the invoicing pickers need them, from the shared class list. */
79
+ const invoicingAccountingClassOptions = (state) => {
80
+ const { classes } = getClassList(state.classState, state.classListState);
81
+ return classes.flatMap((accountingClass) => accountingClass.qboId == null || accountingClass.isDeleted === true
82
+ ? []
83
+ : [
84
+ {
85
+ id: String(accountingClass.qboId),
86
+ name: accountingClass.className,
87
+ },
88
+ ]);
89
+ };
90
+ /**
91
+ * The account tree narrowed to the given account types. The tree itself carries
92
+ * no account type, so the flat account list supplies that and the matching
93
+ * `qboId`s are kept — which is why callers that only need "income accounts, as a
94
+ * tree" do not have to hold a second flat list to filter with.
95
+ */
96
+ export const invoicingAccountHierarchyForTypes = (state, accountTypes) => {
97
+ const allowedQboIds = new Set(invoicingAccountingOptions(state, accountTypes).map((account) => account.id));
98
+ const keepMatching = (nodes) => nodes.flatMap((node) => {
99
+ const children = keepMatching(node.children);
100
+ const isMatch = node.qboId != null && allowedQboIds.has(String(node.qboId));
101
+ // A parent that is not itself a match is still kept when it holds one, so
102
+ // the tree's shape survives the filter.
103
+ if (!isMatch && children.length === 0) {
104
+ return [];
105
+ }
106
+ return [{ ...node, children }];
107
+ });
108
+ return keepMatching(getNestedAccountListHierarchy(state.accountListState, INVOICING_ACCOUNT_LIST_KEY));
109
+ };
1
110
  /**
2
111
  * Single bundled view for the QBO integration settings screen — the
3
- * connection, per-account-types account options, classes, sync health, and the
4
- * fetch/action states for each of them.
112
+ * connection, the account and class options, sync health, and the fetch/action
113
+ * states for each of them.
5
114
  */
6
115
  export const getInvoicingQboView = (state) => {
7
116
  const qboState = state.invoicingQboViewState;
117
+ const accountList = state.accountListState.byReportId[INVOICING_ACCOUNT_LIST_KEY];
8
118
  return {
9
- accountsByAccountTypes: qboState.accountsByAccountTypes,
10
- accountsFetchStateByAccountTypes: qboState.accountsFetchStateByAccountTypes,
119
+ accounts: invoicingAccountingOptions(state),
120
+ accountsFetchState: {
121
+ fetchState: accountList.fetchState,
122
+ error: accountList.error,
123
+ },
124
+ nestedAccountHierarchy: getNestedAccountListHierarchy(state.accountListState, INVOICING_ACCOUNT_LIST_KEY),
125
+ nestedClassHierarchy: getNestedClassListHierarchy(state.classListState),
11
126
  backfillState: qboState.backfillFetchState,
12
- classes: qboState.classes,
13
- classesFetchState: qboState.classesFetchState,
127
+ classes: invoicingAccountingClassOptions(state),
128
+ classesFetchState: {
129
+ fetchState: state.classListState.fetchState,
130
+ error: state.classListState.error,
131
+ },
14
132
  connection: qboState.connection,
15
133
  connectionFetchState: qboState.connectionFetchState,
16
134
  retryFetchStateByTaskId: qboState.retryFetchStateByTaskId,
package/lib/index.d.ts CHANGED
@@ -1240,10 +1240,12 @@ export type { InvoicingBrandingFormView, InvoicingSettingsHubView, InvoicingSett
1240
1240
  export type { InvoicingBrandingFormLocalData } from './view/invoicing/settingsView/invoicingBrandingFormConfig';
1241
1241
  export { INVOICING_BUSINESS_ADDRESS_TYPE, entityToInvoicingBrandingFormLocalData, initialInvoicingBrandingFormLocalData, } from './view/invoicing/settingsView/invoicingBrandingFormConfig';
1242
1242
  export { isStripeConnectedFromSettings } from './view/invoicing/settingsView/invoicingSettingsHelpers';
1243
- export { fetchInvoicingQboAccounts, fetchInvoicingQboClasses, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, startInvoicingQboBackfill, } from './view/invoicing/invoicingQboView/invoicingQboViewReducer';
1243
+ export { fetchInvoicingAccountingPickers, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, startInvoicingQboBackfill, } from './view/invoicing/invoicingQboView/invoicingQboViewReducer';
1244
+ export { INVOICING_INCOME_ACCOUNT_TYPES, invoicingAccountsForQboAccountTypes, } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
1245
+ export type { InvoicingAccountingOption } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
1244
1246
  export { getInvoicingQboView } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
1245
1247
  export type { InvoicingQboView } from './view/invoicing/invoicingQboView/invoicingQboViewSelector';
1246
- export type { InvoicingQboAccountOption, InvoicingQboConnection, InvoicingQboFailedTask, InvoicingQboSyncHealth, } from './view/invoicing/invoicingQboView/invoicingQboViewPayload';
1248
+ export type { InvoicingQboConnection, InvoicingQboFailedTask, InvoicingQboSyncHealth, } from './view/invoicing/invoicingQboView/invoicingQboViewPayload';
1247
1249
  export { INVOICING_QBO_CUSTOMER_MATCHING_MODES, getInvoicingQboAccountMappingId, toInvoicingNamedOptionsByKey, toInvoicingQboCustomerMatchingMode, withInvoicingQboAccountMappingId, } from './entity/invoicing/invoicingCommonPayload';
1248
1250
  export type { InvoicingNamedOption, InvoicingFieldHelp, InvoicingFieldHelpByCode, InvoicingFieldHelpGroup, InvoicingNamedOptionsByKey, InvoicingQboAccountMappingKindOption, InvoicingQboAccountMappingsPayload, InvoicingQboCustomerMatchingMode, InvoicingQboSettingsPayload, } from './entity/invoicing/invoicingCommonPayload';
1249
1251
  export { acceptInvoicingTerms, enableInvoicing, fetchInvoicingConfig, resetInvoicingConfigActionStates, updateInvoicingConfig, } from './view/invoicing/invoicingConfigView/invoicingConfigViewReducer';
package/lib/index.js CHANGED
@@ -83,7 +83,7 @@ exports.getInvoicingDiscountActionState = exports.runInvoicingDiscountAction = e
83
83
  exports.updateInvoicingSubscriptions = exports.removeInvoicingSubscription = exports.resetPromotionalCreditAction = exports.submitAddPromotionalCredits = exports.getInvoicingSetupIntentFetchState = exports.getInvoicingSetupIntent = exports.getInvoicingSentPaymentLinkMagicUrl = exports.getInvoicingSentPaymentLinkEmail = exports.getInvoicingPlaidLinkTokenFetchState = exports.getInvoicingPlaidLinkToken = exports.getInvoicingPaymentMethodSaveState = exports.getInvoicingPaymentLinkSendState = exports.sendInvoicingPaymentLink = exports.saveInvoicingPaymentMethod = exports.resetInvoicingCustomerPaymentMethod = exports.fetchInvoicingPlaidLinkToken = exports.createInvoicingSetupIntent = exports.getInvoicingCustomerDetail = exports.fetchInvoicingCustomerDetailPage = exports.fetchInvoicingCustomerDetail = exports.getInvoicingCustomerListView = exports.INVOICING_CUSTOMER_COLUMNS = exports.ALL_INVOICING_CUSTOMER_TAB_IDS = exports.ALL_INVOICING_CUSTOMER_SORT_KEYS = exports.updateInvoicingCustomerFilters = exports.setInvoicingCustomerActiveTab = exports.fetchInvoicingCustomerListPage = exports.fetchInvoicingCustomerList = exports.fetchInvoicingCustomerCounts = exports.updateInvoicingCustomers = exports.removeInvoicingCustomer = exports.INVOICING_LIST_PAGE_SIZE = exports.getInvoicingDataImportActionState = exports.uploadInvoicingMigrationFiles = exports.startInvoicingMigration = exports.rollbackInvoicingMigration = exports.retryInvoicingMigration = exports.connectInvoicingChargebee = exports.getInvoicingDataImportView = exports.setInvoicingActiveMigrationSessionId = exports.fetchInvoicingMigrationSessions = exports.fetchInvoicingMigrationSession = exports.fetchInvoicingMigrationDiagnostics = exports.fetchInvoicingDataImportStatus = exports.getInvoicingPaymentActionState = exports.runInvoicingPaymentAction = exports.getInvoicingDunningActionState = exports.runInvoicingDunningAction = exports.getInvoicingSubscriptionActionState = exports.runInvoicingSubscriptionAction = void 0;
84
84
  exports.getInvoicingCouponCounts = exports.INVOICING_COUPON_COLUMNS = exports.ALL_INVOICING_COUPON_TAB_IDS = exports.ALL_INVOICING_COUPON_SORT_KEYS = exports.updateInvoicingCouponFilters = exports.setInvoicingCouponActiveTab = exports.fetchInvoicingCouponListPage = exports.fetchInvoicingCouponList = exports.fetchInvoicingCouponCounts = exports.updateInvoicingCoupons = exports.removeInvoicingCoupon = exports.getInvoicingCreditNoteDetail = exports.getInvoicingCreditNoteById = exports.fetchInvoicingCreditNoteDetail = exports.getInvoicingCreditNoteListView = exports.INVOICING_CREDIT_NOTE_COLUMNS = exports.ALL_INVOICING_CREDIT_NOTE_TAB_IDS = exports.ALL_INVOICING_CREDIT_NOTE_SORT_KEYS = exports.updateInvoicingCreditNoteFilters = exports.setInvoicingCreditNoteActiveTab = exports.fetchInvoicingCreditNoteListPage = exports.fetchInvoicingCreditNoteList = exports.fetchInvoicingCreditNoteCounts = exports.updateInvoicingCreditNotes = exports.removeInvoicingCreditNote = exports.getInvoicingTransactionDetail = exports.getInvoicingTransactionById = exports.fetchInvoicingTransactionDetail = exports.getInvoicingTransactionListView = exports.INVOICING_TRANSACTION_COLUMNS = exports.ALL_INVOICING_TRANSACTION_TAB_IDS = exports.ALL_INVOICING_TRANSACTION_SORT_KEYS = exports.updateInvoicingTransactionFilters = exports.setInvoicingTransactionActiveTab = exports.fetchInvoicingTransactionListPage = exports.fetchInvoicingTransactionList = exports.updateInvoicingTransactions = exports.removeInvoicingTransaction = exports.getInvoicingSubscriptionDetail = exports.getInvoicingSubscriptionById = exports.fetchInvoicingSubscriptionDetail = exports.getInvoicingSubscriptionListView = exports.INVOICING_SUBSCRIPTION_COLUMNS = exports.ALL_INVOICING_SUBSCRIPTION_TAB_IDS = exports.ALL_INVOICING_SUBSCRIPTION_SORT_KEYS = exports.updateInvoicingSubscriptionFilters = exports.setInvoicingSubscriptionActiveTab = exports.fetchInvoicingSubscriptionListPage = exports.fetchInvoicingSubscriptionList = exports.fetchInvoicingSubscriptionCounts = void 0;
85
85
  exports.resetSaveInvoicingSettings = exports.resetInvoicingBrandingFormDraft = exports.fetchInvoicingSettings = exports.disconnectInvoicingStripe = exports.connectInvoicingStripe = exports.getInvoicingProductById = exports.getInvoicingPlanById = exports.getInvoicingCatalogItemDetail = exports.fetchInvoicingCatalogItemDetail = exports.getInvoicingCatalogListView = exports.INVOICING_PRODUCT_COLUMNS = exports.ALL_INVOICING_PRODUCT_SORT_KEYS = exports.updateInvoicingCatalogProductFilters = exports.updateInvoicingCatalogPlanFilters = exports.fetchInvoicingCatalogProductList = exports.fetchInvoicingCatalogPlanList = exports.fetchInvoicingCatalogListPage = exports.fetchInvoicingCatalogCounts = exports.updateInvoicingProducts = exports.removeInvoicingProduct = exports.updateInvoicingPlans = exports.removeInvoicingPlan = exports.mergeInvoicingPlanSubFamily = exports.getInvoicingDunningEmailPreview = exports.getInvoicingDunningCaseDetail = exports.getInvoicingDunningCaseById = exports.invalidateInvoicingDunningEmailPreview = exports.fetchInvoicingDunningEmailPreviewPage = exports.fetchInvoicingDunningCaseDetail = exports.clearInvoicingDunningCaseDetailView = exports.getInvoicingDunningCaseListView = exports.INVOICING_DUNNING_CASE_COLUMNS = exports.ALL_INVOICING_DUNNING_CASE_TAB_IDS = exports.ALL_INVOICING_DUNNING_CASE_SORT_KEYS = exports.updateInvoicingDunningCaseFilters = exports.setInvoicingDunningCaseActiveTab = exports.fetchInvoicingDunningCaseListPage = exports.fetchInvoicingDunningCaseList = exports.fetchInvoicingDunningCaseCounts = exports.updateInvoicingDunningCases = exports.removeInvoicingDunningCase = exports.getInvoicingCouponDetail = exports.fetchInvoicingCouponDetail = exports.getInvoicingCouponById = exports.getInvoicingCouponListView = exports.getInvoicingCouponListNextCursor = exports.getInvoicingCouponListItems = exports.getInvoicingCouponListHasMore = exports.getInvoicingCouponListFilters = exports.getInvoicingCouponListFetchState = void 0;
86
- exports.getInvoicingOnboardingGateView = exports.getInvoicingConfigView = exports.updateInvoicingConfig = exports.resetInvoicingConfigActionStates = exports.fetchInvoicingConfig = exports.enableInvoicing = exports.acceptInvoicingTerms = exports.withInvoicingQboAccountMappingId = exports.toInvoicingQboCustomerMatchingMode = exports.toInvoicingNamedOptionsByKey = exports.getInvoicingQboAccountMappingId = exports.INVOICING_QBO_CUSTOMER_MATCHING_MODES = exports.getInvoicingQboView = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTask = exports.fetchInvoicingQboSyncHealth = exports.fetchInvoicingQboConnection = exports.fetchInvoicingQboClasses = exports.fetchInvoicingQboAccounts = exports.isStripeConnectedFromSettings = exports.initialInvoicingBrandingFormLocalData = exports.entityToInvoicingBrandingFormLocalData = exports.INVOICING_BUSINESS_ADDRESS_TYPE = exports.getInvoicingSettingsView = exports.getInvoicingSettingsHubView = exports.getInvoicingSettingsFetchState = exports.getInvoicingSettings = exports.getInvoicingBrandingFormView = exports.updateInvoicingSettings = exports.updateInvoicingBrandingFormDraft = exports.submitInvoicingBrandingForm = exports.saveInvoicingSettings = void 0;
86
+ exports.getInvoicingOnboardingGateView = exports.getInvoicingConfigView = exports.updateInvoicingConfig = exports.resetInvoicingConfigActionStates = exports.fetchInvoicingConfig = exports.enableInvoicing = exports.acceptInvoicingTerms = exports.withInvoicingQboAccountMappingId = exports.toInvoicingQboCustomerMatchingMode = exports.toInvoicingNamedOptionsByKey = exports.getInvoicingQboAccountMappingId = exports.INVOICING_QBO_CUSTOMER_MATCHING_MODES = exports.getInvoicingQboView = exports.invoicingAccountsForQboAccountTypes = exports.INVOICING_INCOME_ACCOUNT_TYPES = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTask = exports.fetchInvoicingQboSyncHealth = exports.fetchInvoicingQboConnection = exports.fetchInvoicingAccountingPickers = exports.isStripeConnectedFromSettings = exports.initialInvoicingBrandingFormLocalData = exports.entityToInvoicingBrandingFormLocalData = exports.INVOICING_BUSINESS_ADDRESS_TYPE = exports.getInvoicingSettingsView = exports.getInvoicingSettingsHubView = exports.getInvoicingSettingsFetchState = exports.getInvoicingSettings = exports.getInvoicingBrandingFormView = exports.updateInvoicingSettings = exports.updateInvoicingBrandingFormDraft = exports.submitInvoicingBrandingForm = exports.saveInvoicingSettings = void 0;
87
87
  const allowedValue_1 = require("./commonStateTypes/allowedValue");
88
88
  Object.defineProperty(exports, "isAllowedValueWithCode", { enumerable: true, get: function () { return allowedValue_1.isAllowedValueWithCode; } });
89
89
  Object.defineProperty(exports, "isAllowedValueWithID", { enumerable: true, get: function () { return allowedValue_1.isAllowedValueWithID; } });
@@ -3105,14 +3105,16 @@ var invoicingSettingsHelpers_1 = require("./view/invoicing/settingsView/invoicin
3105
3105
  Object.defineProperty(exports, "isStripeConnectedFromSettings", { enumerable: true, get: function () { return invoicingSettingsHelpers_1.isStripeConnectedFromSettings; } });
3106
3106
  // ── Invoicing: QBO integration ────────────────────────────────────────────────
3107
3107
  var invoicingQboViewReducer_1 = require("./view/invoicing/invoicingQboView/invoicingQboViewReducer");
3108
- Object.defineProperty(exports, "fetchInvoicingQboAccounts", { enumerable: true, get: function () { return invoicingQboViewReducer_1.fetchInvoicingQboAccounts; } });
3109
- Object.defineProperty(exports, "fetchInvoicingQboClasses", { enumerable: true, get: function () { return invoicingQboViewReducer_1.fetchInvoicingQboClasses; } });
3108
+ Object.defineProperty(exports, "fetchInvoicingAccountingPickers", { enumerable: true, get: function () { return invoicingQboViewReducer_1.fetchInvoicingAccountingPickers; } });
3110
3109
  Object.defineProperty(exports, "fetchInvoicingQboConnection", { enumerable: true, get: function () { return invoicingQboViewReducer_1.fetchInvoicingQboConnection; } });
3111
3110
  Object.defineProperty(exports, "fetchInvoicingQboSyncHealth", { enumerable: true, get: function () { return invoicingQboViewReducer_1.fetchInvoicingQboSyncHealth; } });
3112
3111
  Object.defineProperty(exports, "retryInvoicingQboSyncTask", { enumerable: true, get: function () { return invoicingQboViewReducer_1.retryInvoicingQboSyncTask; } });
3113
3112
  Object.defineProperty(exports, "startInvoicingQboBackfill", { enumerable: true, get: function () { return invoicingQboViewReducer_1.startInvoicingQboBackfill; } });
3114
3113
  var invoicingQboViewSelector_1 = require("./view/invoicing/invoicingQboView/invoicingQboViewSelector");
3115
- Object.defineProperty(exports, "getInvoicingQboView", { enumerable: true, get: function () { return invoicingQboViewSelector_1.getInvoicingQboView; } });
3114
+ Object.defineProperty(exports, "INVOICING_INCOME_ACCOUNT_TYPES", { enumerable: true, get: function () { return invoicingQboViewSelector_1.INVOICING_INCOME_ACCOUNT_TYPES; } });
3115
+ Object.defineProperty(exports, "invoicingAccountsForQboAccountTypes", { enumerable: true, get: function () { return invoicingQboViewSelector_1.invoicingAccountsForQboAccountTypes; } });
3116
+ var invoicingQboViewSelector_2 = require("./view/invoicing/invoicingQboView/invoicingQboViewSelector");
3117
+ Object.defineProperty(exports, "getInvoicingQboView", { enumerable: true, get: function () { return invoicingQboViewSelector_2.getInvoicingQboView; } });
3116
3118
  var invoicingCommonPayload_2 = require("./entity/invoicing/invoicingCommonPayload");
3117
3119
  Object.defineProperty(exports, "INVOICING_QBO_CUSTOMER_MATCHING_MODES", { enumerable: true, get: function () { return invoicingCommonPayload_2.INVOICING_QBO_CUSTOMER_MATCHING_MODES; } });
3118
3120
  Object.defineProperty(exports, "getInvoicingQboAccountMappingId", { enumerable: true, get: function () { return invoicingCommonPayload_2.getInvoicingQboAccountMappingId; } });
@@ -35,6 +35,7 @@ exports.initialState = {
35
35
  reimbursementAccountList: exports.initialAccountList,
36
36
  schedulesAccountList: exports.initialAccountList,
37
37
  billPayAccountList: exports.initialAccountList,
38
+ invoicingAccountList: exports.initialAccountList,
38
39
  },
39
40
  };
40
41
  const accountList = (0, toolkit_1.createSlice)({
@@ -1,7 +1,7 @@
1
1
  import { NestedAccountHierarchyForReport } from '../../commonStateTypes/accountView/nestedAccountID';
2
2
  import { FetchedState, ID } from '../../commonStateTypes/common';
3
3
  import { AccountLabel } from '../../entity/account/accountState';
4
- export type AccountListKeyType = 'accountList' | 'reimbursementAccountList' | 'schedulesAccountList' | 'billPayAccountList';
4
+ export type AccountListKeyType = 'accountList' | 'reimbursementAccountList' | 'schedulesAccountList' | 'billPayAccountList' | 'invoicingAccountList';
5
5
  export interface AccountList extends FetchedState {
6
6
  accountIds: ID[];
7
7
  accountIdsByLabel: Record<AccountLabel, ID[]>;
@@ -1,3 +1,5 @@
1
+ import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
+ import { NestedClassHierarchyForReport } from '../../../commonStateTypes/classesView/nestedClassID';
1
3
  import { FetchStateAndError, ID } from '../../../commonStateTypes/common';
2
4
  import { SelectorView } from '../../../commonStateTypes/viewAndReport/viewAndReport';
3
5
  import { InvoicingFieldHelpByCode, InvoicingNamedOption, InvoicingNamedOptionsByKey } from '../../../entity/invoicing/invoicingCommonPayload';
@@ -40,6 +42,13 @@ export interface InvoicingCatalogItemFormView extends SelectorView {
40
42
  periodUnitOptions: InvoicingNamedOption[];
41
43
  pricingModelValues: readonly string[];
42
44
  prorationValues: readonly string[];
45
+ /**
46
+ * Account and class trees for the price-point pickers, already narrowed to the
47
+ * income accounts a price may post to. Empty until QBO is connected — the form
48
+ * must not offer a mapping that cannot be pushed.
49
+ */
50
+ qboClassHierarchy: NestedClassHierarchyForReport[];
51
+ qboIncomeAccountHierarchy: NestedAccountHierarchyForReport[];
43
52
  tierTypeValues: readonly string[];
44
53
  trialEndActionOptions: InvoicingNamedOption[];
45
54
  trialUnitOptions: InvoicingNamedOption[];
@@ -4,6 +4,7 @@ exports.getInvoicingCatalogItemFormView = exports.getInvoicingApplicableCatalogI
4
4
  const toolkit_1 = require("@reduxjs/toolkit");
5
5
  const planSelector_1 = require("../../../entity/invoicing/plan/planSelector");
6
6
  const invoicingConfigViewSelector_1 = require("../invoicingConfigView/invoicingConfigViewSelector");
7
+ const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
7
8
  const invoicingCatalogItemFormConfig_1 = require("./invoicingCatalogItemFormConfig");
8
9
  const getEditInvoicingCatalogItemDetailViewState = (state) => ({
9
10
  fetchState: state.editInvoicingCatalogItemDetailViewState.fetchState,
@@ -54,9 +55,16 @@ const getInvoicingCatalogItemFormView = (state, id) => {
54
55
  const applicableItemOptions = id != null
55
56
  ? allApplicableItemOptions.filter((option) => option.id !== id)
56
57
  : allApplicableItemOptions;
58
+ const isQboConnected = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state);
57
59
  const { billingPeriodUnitOptions, catalogItemTypeOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
58
60
  return {
59
61
  fieldHelp: (0, invoicingConfigViewSelector_1.invoicingFieldHelpForGroup)(state, 'catalog'),
62
+ qboClassHierarchy: isQboConnected
63
+ ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state).nestedClassHierarchy
64
+ : [],
65
+ qboIncomeAccountHierarchy: isQboConnected
66
+ ? (0, invoicingQboViewSelector_1.invoicingAccountHierarchyForTypes)(state, invoicingQboViewSelector_1.INVOICING_INCOME_ACCOUNT_TYPES)
67
+ : [],
60
68
  applicabilityValues: invoicingCatalogItemFormConfig_1.INVOICING_CATALOG_APPLICABILITY_VALUES,
61
69
  applicableItemOptions,
62
70
  localData,
@@ -0,0 +1,5 @@
1
+ import { ActionsObservable, StateObservable } from 'redux-observable';
2
+ import { Observable } from 'rxjs';
3
+ import { RootState } from '../../../../rootStateTypes';
4
+ import { InvoicingQboViewActionType } from './invoicingQboViewActionType';
5
+ export declare const fetchInvoicingAccountingPickersEpic: (actions$: ActionsObservable<InvoicingQboViewActionType>, state$: StateObservable<RootState>) => Observable<InvoicingQboViewActionType>;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchInvoicingAccountingPickersEpic = void 0;
4
+ const rxjs_1 = require("rxjs");
5
+ const operators_1 = require("rxjs/operators");
6
+ const accountListReducer_1 = require("../../../accountList/accountListReducer");
7
+ const classListReducer_1 = require("../../../classList/classListReducer");
8
+ const invoicingQboViewReducer_1 = require("../invoicingQboViewReducer");
9
+ const invoicingQboViewSelector_1 = require("../invoicingQboViewSelector");
10
+ // Tops up the shared account and class lists, skipping either when the store
11
+ // already holds it or has it in flight.
12
+ const fetchInvoicingAccountingPickersEpic = (actions$, state$) => actions$.pipe((0, operators_1.filter)(invoicingQboViewReducer_1.fetchInvoicingAccountingPickers.match), (0, operators_1.withLatestFrom)(state$), (0, operators_1.mergeMap)(([, state]) => {
13
+ const actions = [];
14
+ const accountList = state.accountListState.byReportId[invoicingQboViewSelector_1.INVOICING_ACCOUNT_LIST_KEY];
15
+ if (accountList.hasValidState() === false &&
16
+ accountList.fetchState !== 'In-Progress') {
17
+ // AP/AR accounts cannot back an invoicing mapping, so they are excluded
18
+ // here — which is why this uses invoicing's own bucket rather than the
19
+ // general `accountList` other screens fill without those exclusions.
20
+ actions.push((0, accountListReducer_1.fetchAccountList)(invoicingQboViewSelector_1.INVOICING_ACCOUNT_LIST_KEY, true, true, true));
21
+ }
22
+ const classList = state.classListState;
23
+ if (classList.hasValidState() === false &&
24
+ classList.fetchState !== 'In-Progress') {
25
+ // Without deleted classes, matching almost every other caller of this
26
+ // single global slice. Asking for deleted rows here would have handed
27
+ // them to the vendor and transaction screens too — and the picker
28
+ // filters deleted classes out anyway, so there is nothing to gain.
29
+ actions.push((0, classListReducer_1.fetchClassList)());
30
+ }
31
+ return actions.length > 0 ? (0, rxjs_1.from)(actions) : rxjs_1.EMPTY;
32
+ }));
33
+ exports.fetchInvoicingAccountingPickersEpic = fetchInvoicingAccountingPickersEpic;
@@ -1,3 +1,5 @@
1
1
  import { openSnackbar } from '../../../../entity/snackbar/snackbarReducer';
2
- import { fetchInvoicingQboAccounts, fetchInvoicingQboClasses, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, retryInvoicingQboSyncTaskFailure, retryInvoicingQboSyncTaskSuccess, startInvoicingQboBackfill, startInvoicingQboBackfillFailure, startInvoicingQboBackfillSuccess, updateInvoicingQboAccounts, updateInvoicingQboAccountsFailure, updateInvoicingQboClasses, updateInvoicingQboClassesFailure, updateInvoicingQboConnection, updateInvoicingQboConnectionFailure, updateInvoicingQboSyncHealth, updateInvoicingQboSyncHealthFailure } from '../invoicingQboViewReducer';
3
- export type InvoicingQboViewActionType = ReturnType<typeof openSnackbar> | ReturnType<typeof fetchInvoicingQboConnection> | ReturnType<typeof updateInvoicingQboConnection> | ReturnType<typeof updateInvoicingQboConnectionFailure> | ReturnType<typeof fetchInvoicingQboAccounts> | ReturnType<typeof updateInvoicingQboAccounts> | ReturnType<typeof updateInvoicingQboAccountsFailure> | ReturnType<typeof fetchInvoicingQboClasses> | ReturnType<typeof updateInvoicingQboClasses> | ReturnType<typeof updateInvoicingQboClassesFailure> | ReturnType<typeof fetchInvoicingQboSyncHealth> | ReturnType<typeof updateInvoicingQboSyncHealth> | ReturnType<typeof updateInvoicingQboSyncHealthFailure> | ReturnType<typeof retryInvoicingQboSyncTask> | ReturnType<typeof retryInvoicingQboSyncTaskSuccess> | ReturnType<typeof retryInvoicingQboSyncTaskFailure> | ReturnType<typeof startInvoicingQboBackfill> | ReturnType<typeof startInvoicingQboBackfillSuccess> | ReturnType<typeof startInvoicingQboBackfillFailure>;
2
+ import { fetchAccountList } from '../../../accountList/accountListReducer';
3
+ import { fetchClassList } from '../../../classList/classListReducer';
4
+ import { fetchInvoicingAccountingPickers, fetchInvoicingQboConnection, fetchInvoicingQboSyncHealth, retryInvoicingQboSyncTask, retryInvoicingQboSyncTaskFailure, retryInvoicingQboSyncTaskSuccess, startInvoicingQboBackfill, startInvoicingQboBackfillFailure, startInvoicingQboBackfillSuccess, updateInvoicingQboConnection, updateInvoicingQboConnectionFailure, updateInvoicingQboSyncHealth, updateInvoicingQboSyncHealthFailure } from '../invoicingQboViewReducer';
5
+ export type InvoicingQboViewActionType = ReturnType<typeof openSnackbar> | ReturnType<typeof fetchInvoicingQboConnection> | ReturnType<typeof updateInvoicingQboConnection> | ReturnType<typeof updateInvoicingQboConnectionFailure> | ReturnType<typeof fetchInvoicingAccountingPickers> | ReturnType<typeof fetchAccountList> | ReturnType<typeof fetchClassList> | ReturnType<typeof fetchInvoicingQboSyncHealth> | ReturnType<typeof updateInvoicingQboSyncHealth> | ReturnType<typeof updateInvoicingQboSyncHealthFailure> | ReturnType<typeof retryInvoicingQboSyncTask> | ReturnType<typeof retryInvoicingQboSyncTaskSuccess> | ReturnType<typeof retryInvoicingQboSyncTaskFailure> | ReturnType<typeof startInvoicingQboBackfill> | ReturnType<typeof startInvoicingQboBackfillSuccess> | ReturnType<typeof startInvoicingQboBackfillFailure>;
@@ -12,31 +12,6 @@ export interface InvoicingQboConnection {
12
12
  realmId?: string;
13
13
  }
14
14
  export declare function mapInvoicingQboConnection(payload: InvoicingQboConnectionPayload): InvoicingQboConnection;
15
- /**
16
- * A single row from the QBO accounts / classes endpoints. The rows carry the
17
- * QBO id plus one or more name fields depending on the entity — inspect
18
- * loosely, render the name, submit the id.
19
- */
20
- export interface InvoicingQboAccountRowPayload {
21
- account_id?: string | null;
22
- account_name?: string | null;
23
- account_type?: string | null;
24
- accounting_class_name?: string | null;
25
- fully_qualified_name?: string | null;
26
- id?: string | null;
27
- name?: string | null;
28
- qbo_accounting_class_name?: string | null;
29
- qbo_id?: string | null;
30
- }
31
- export interface InvoicingQboAccountOption {
32
- id: string;
33
- name: string;
34
- accountType?: string;
35
- }
36
- export declare function mapInvoicingQboAccountOptions(rows: InvoicingQboAccountRowPayload[]): InvoicingQboAccountOption[];
37
- export interface InvoicingQboAccountsPayload {
38
- accounts: InvoicingQboAccountRowPayload[];
39
- }
40
15
  export interface InvoicingQboFailedTaskPayload {
41
16
  attempts: number;
42
17
  entity_id: string;
@@ -73,7 +48,6 @@ export interface InvoicingQboBackfillPayload {
73
48
  start_date?: string | null;
74
49
  }
75
50
  export type InvoicingQboConnectionResponse = ZeniAPIResponse<InvoicingQboConnectionPayload>;
76
- export type InvoicingQboAccountsResponse = ZeniAPIResponse<InvoicingQboAccountsPayload>;
77
51
  export type InvoicingQboSyncHealthResponse = ZeniAPIResponse<InvoicingQboSyncHealthPayload>;
78
52
  export type InvoicingQboRetryTaskResponse = ZeniAPIResponse<InvoicingQboRetryTaskPayload>;
79
53
  export type InvoicingQboBackfillResponse = ZeniAPIResponse<InvoicingQboBackfillPayload>;
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.mapInvoicingQboConnection = mapInvoicingQboConnection;
4
- exports.mapInvoicingQboAccountOptions = mapInvoicingQboAccountOptions;
5
4
  exports.mapInvoicingQboSyncHealth = mapInvoicingQboSyncHealth;
6
5
  function mapInvoicingQboConnection(payload) {
7
6
  return {
@@ -11,29 +10,6 @@ function mapInvoicingQboConnection(payload) {
11
10
  realmId: payload.realm_id ?? undefined,
12
11
  };
13
12
  }
14
- function mapInvoicingQboAccountOptions(rows) {
15
- return rows.flatMap((row) => {
16
- // Only qbo_id is a valid QBO AccountRef/ClassRef value — platform-internal
17
- // ids (account_id / id) are display metadata and must never be submitted.
18
- const accountId = row.qbo_id ?? row.id ?? row.account_id ?? '';
19
- if (accountId === '') {
20
- return [];
21
- }
22
- const accountName = row.name ??
23
- row.account_name ??
24
- row.accounting_class_name ??
25
- row.qbo_accounting_class_name ??
26
- row.fully_qualified_name ??
27
- accountId;
28
- return [
29
- {
30
- id: accountId,
31
- name: accountName,
32
- accountType: row.account_type ?? undefined,
33
- },
34
- ];
35
- });
36
- }
37
13
  function mapInvoicingQboSyncHealth(payload) {
38
14
  return {
39
15
  connection: payload.connection != null
@@ -1,16 +1,8 @@
1
1
  import { ZeniAPIStatus } from '../../../responsePayload';
2
- import { InvoicingQboAccountOption, InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
2
+ import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
3
3
  import { InvoicingQboViewState } from './invoicingQboViewState';
4
4
  export declare const initialState: InvoicingQboViewState;
5
- export declare const fetchInvoicingQboConnection: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"invoicingQboView/fetchInvoicingQboConnection">, updateInvoicingQboConnection: import("@reduxjs/toolkit").ActionCreatorWithPayload<InvoicingQboConnection, "invoicingQboView/updateInvoicingQboConnection">, updateInvoicingQboConnectionFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "invoicingQboView/updateInvoicingQboConnectionFailure">, fetchInvoicingQboAccounts: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[accountTypes: string], {
6
- accountTypes: string;
7
- }, "invoicingQboView/fetchInvoicingQboAccounts", never, never>, updateInvoicingQboAccounts: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
8
- accounts: InvoicingQboAccountOption[];
9
- accountTypes: string;
10
- }, "invoicingQboView/updateInvoicingQboAccounts">, updateInvoicingQboAccountsFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
11
- accountTypes: string;
12
- status: ZeniAPIStatus;
13
- }, "invoicingQboView/updateInvoicingQboAccountsFailure">, fetchInvoicingQboClasses: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"invoicingQboView/fetchInvoicingQboClasses">, updateInvoicingQboClasses: import("@reduxjs/toolkit").ActionCreatorWithPayload<InvoicingQboAccountOption[], "invoicingQboView/updateInvoicingQboClasses">, updateInvoicingQboClassesFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "invoicingQboView/updateInvoicingQboClassesFailure">, fetchInvoicingQboSyncHealth: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"invoicingQboView/fetchInvoicingQboSyncHealth">, updateInvoicingQboSyncHealth: import("@reduxjs/toolkit").ActionCreatorWithPayload<InvoicingQboSyncHealth, "invoicingQboView/updateInvoicingQboSyncHealth">, updateInvoicingQboSyncHealthFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "invoicingQboView/updateInvoicingQboSyncHealthFailure">, retryInvoicingQboSyncTask: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[taskId: string], {
5
+ export declare const fetchInvoicingQboConnection: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"invoicingQboView/fetchInvoicingQboConnection">, updateInvoicingQboConnection: import("@reduxjs/toolkit").ActionCreatorWithPayload<InvoicingQboConnection, "invoicingQboView/updateInvoicingQboConnection">, updateInvoicingQboConnectionFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "invoicingQboView/updateInvoicingQboConnectionFailure">, fetchInvoicingAccountingPickers: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"invoicingQboView/fetchInvoicingAccountingPickers">, fetchInvoicingQboSyncHealth: import("@reduxjs/toolkit").ActionCreatorWithoutPayload<"invoicingQboView/fetchInvoicingQboSyncHealth">, updateInvoicingQboSyncHealth: import("@reduxjs/toolkit").ActionCreatorWithPayload<InvoicingQboSyncHealth, "invoicingQboView/updateInvoicingQboSyncHealth">, updateInvoicingQboSyncHealthFailure: import("@reduxjs/toolkit").ActionCreatorWithPayload<ZeniAPIStatus<Record<string, unknown>>, "invoicingQboView/updateInvoicingQboSyncHealthFailure">, retryInvoicingQboSyncTask: import("@reduxjs/toolkit").ActionCreatorWithPreparedPayload<[taskId: string], {
14
6
  taskId: string;
15
7
  }, "invoicingQboView/retryInvoicingQboSyncTask", never, never>, retryInvoicingQboSyncTaskSuccess: import("@reduxjs/toolkit").ActionCreatorWithPayload<{
16
8
  status: string;
@@ -1,15 +1,11 @@
1
1
  "use strict";
2
2
  var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.startInvoicingQboBackfillFailure = exports.startInvoicingQboBackfillSuccess = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTaskFailure = exports.retryInvoicingQboSyncTaskSuccess = exports.retryInvoicingQboSyncTask = exports.updateInvoicingQboSyncHealthFailure = exports.updateInvoicingQboSyncHealth = exports.fetchInvoicingQboSyncHealth = exports.updateInvoicingQboClassesFailure = exports.updateInvoicingQboClasses = exports.fetchInvoicingQboClasses = exports.updateInvoicingQboAccountsFailure = exports.updateInvoicingQboAccounts = exports.fetchInvoicingQboAccounts = exports.updateInvoicingQboConnectionFailure = exports.updateInvoicingQboConnection = exports.fetchInvoicingQboConnection = exports.initialState = void 0;
4
+ exports.startInvoicingQboBackfillFailure = exports.startInvoicingQboBackfillSuccess = exports.startInvoicingQboBackfill = exports.retryInvoicingQboSyncTaskFailure = exports.retryInvoicingQboSyncTaskSuccess = exports.retryInvoicingQboSyncTask = exports.updateInvoicingQboSyncHealthFailure = exports.updateInvoicingQboSyncHealth = exports.fetchInvoicingQboSyncHealth = exports.fetchInvoicingAccountingPickers = exports.updateInvoicingQboConnectionFailure = exports.updateInvoicingQboConnection = exports.fetchInvoicingQboConnection = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
6
  const rootActions_1 = require("../../../rootActions");
7
7
  exports.initialState = {
8
- accountsByAccountTypes: {},
9
- accountsFetchStateByAccountTypes: {},
10
8
  backfillFetchState: { fetchState: 'Not-Started', error: undefined },
11
- classes: [],
12
- classesFetchState: { fetchState: 'Not-Started', error: undefined },
13
9
  connection: undefined,
14
10
  connectionFetchState: { fetchState: 'Not-Started', error: undefined },
15
11
  retryFetchStateByTaskId: {},
@@ -33,41 +29,8 @@ const invoicingQboView = (0, toolkit_1.createSlice)({
33
29
  updateInvoicingQboConnectionFailure(draft, action) {
34
30
  draft.connectionFetchState = { fetchState: 'Error', error: action.payload };
35
31
  },
36
- fetchInvoicingQboAccounts: {
37
- reducer(draft, action) {
38
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
39
- fetchState: 'In-Progress',
40
- error: undefined,
41
- };
42
- },
43
- prepare(accountTypes) {
44
- return { payload: { accountTypes } };
45
- },
46
- },
47
- updateInvoicingQboAccounts(draft, action) {
48
- draft.accountsByAccountTypes[action.payload.accountTypes] =
49
- action.payload.accounts;
50
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
51
- fetchState: 'Completed',
52
- error: undefined,
53
- };
54
- },
55
- updateInvoicingQboAccountsFailure(draft, action) {
56
- draft.accountsFetchStateByAccountTypes[action.payload.accountTypes] = {
57
- fetchState: 'Error',
58
- error: action.payload.status,
59
- };
60
- },
61
- fetchInvoicingQboClasses(draft) {
62
- draft.classesFetchState = { fetchState: 'In-Progress', error: undefined };
63
- },
64
- updateInvoicingQboClasses(draft, action) {
65
- draft.classes = action.payload;
66
- draft.classesFetchState = { fetchState: 'Completed', error: undefined };
67
- },
68
- updateInvoicingQboClassesFailure(draft, action) {
69
- draft.classesFetchState = { fetchState: 'Error', error: action.payload };
70
- },
32
+ // No state of its own: the epic decides which shared lists to load.
33
+ fetchInvoicingAccountingPickers() { },
71
34
  fetchInvoicingQboSyncHealth(draft) {
72
35
  draft.syncHealthFetchState = {
73
36
  fetchState: 'In-Progress',
@@ -131,5 +94,5 @@ const invoicingQboView = (0, toolkit_1.createSlice)({
131
94
  });
132
95
  },
133
96
  });
134
- _a = invoicingQboView.actions, exports.fetchInvoicingQboConnection = _a.fetchInvoicingQboConnection, exports.updateInvoicingQboConnection = _a.updateInvoicingQboConnection, exports.updateInvoicingQboConnectionFailure = _a.updateInvoicingQboConnectionFailure, exports.fetchInvoicingQboAccounts = _a.fetchInvoicingQboAccounts, exports.updateInvoicingQboAccounts = _a.updateInvoicingQboAccounts, exports.updateInvoicingQboAccountsFailure = _a.updateInvoicingQboAccountsFailure, exports.fetchInvoicingQboClasses = _a.fetchInvoicingQboClasses, exports.updateInvoicingQboClasses = _a.updateInvoicingQboClasses, exports.updateInvoicingQboClassesFailure = _a.updateInvoicingQboClassesFailure, exports.fetchInvoicingQboSyncHealth = _a.fetchInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealth = _a.updateInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealthFailure = _a.updateInvoicingQboSyncHealthFailure, exports.retryInvoicingQboSyncTask = _a.retryInvoicingQboSyncTask, exports.retryInvoicingQboSyncTaskSuccess = _a.retryInvoicingQboSyncTaskSuccess, exports.retryInvoicingQboSyncTaskFailure = _a.retryInvoicingQboSyncTaskFailure, exports.startInvoicingQboBackfill = _a.startInvoicingQboBackfill, exports.startInvoicingQboBackfillSuccess = _a.startInvoicingQboBackfillSuccess, exports.startInvoicingQboBackfillFailure = _a.startInvoicingQboBackfillFailure;
97
+ _a = invoicingQboView.actions, exports.fetchInvoicingQboConnection = _a.fetchInvoicingQboConnection, exports.updateInvoicingQboConnection = _a.updateInvoicingQboConnection, exports.updateInvoicingQboConnectionFailure = _a.updateInvoicingQboConnectionFailure, exports.fetchInvoicingAccountingPickers = _a.fetchInvoicingAccountingPickers, exports.fetchInvoicingQboSyncHealth = _a.fetchInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealth = _a.updateInvoicingQboSyncHealth, exports.updateInvoicingQboSyncHealthFailure = _a.updateInvoicingQboSyncHealthFailure, exports.retryInvoicingQboSyncTask = _a.retryInvoicingQboSyncTask, exports.retryInvoicingQboSyncTaskSuccess = _a.retryInvoicingQboSyncTaskSuccess, exports.retryInvoicingQboSyncTaskFailure = _a.retryInvoicingQboSyncTaskFailure, exports.startInvoicingQboBackfill = _a.startInvoicingQboBackfill, exports.startInvoicingQboBackfillSuccess = _a.startInvoicingQboBackfillSuccess, exports.startInvoicingQboBackfillFailure = _a.startInvoicingQboBackfillFailure;
135
98
  exports.default = invoicingQboView.reducer;
@@ -1,21 +1,58 @@
1
+ import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
+ import { NestedClassHierarchyForReport } from '../../../commonStateTypes/classesView/nestedClassID';
1
3
  import { FetchStateAndError } from '../../../commonStateTypes/common';
4
+ import { AccountType } from '../../../entity/account/accountState';
2
5
  import { RootState } from '../../../rootStateTypes';
3
- import { InvoicingQboAccountOption, InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
6
+ import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
7
+ export declare const INVOICING_ACCOUNT_LIST_KEY: "invoicingAccountList";
8
+ /** Whether QBO is connected, without building the option lists to find out. */
9
+ export declare const isInvoicingQboConnected: (state: RootState) => boolean;
10
+ /**
11
+ * Account types that can back invoicing revenue. Named here so screens ask for
12
+ * "income accounts" rather than repeating the accounting service's vocabulary.
13
+ */
14
+ export declare const INVOICING_INCOME_ACCOUNT_TYPES: readonly AccountType[];
15
+ /** An account or class as the invoicing pickers render and submit it. */
16
+ export interface InvoicingAccountingOption {
17
+ id: string;
18
+ name: string;
19
+ accountType?: string;
20
+ }
4
21
  export interface InvoicingQboView {
5
- accountsByAccountTypes: Record<string, InvoicingQboAccountOption[]>;
6
- accountsFetchStateByAccountTypes: Record<string, FetchStateAndError>;
22
+ accounts: InvoicingAccountingOption[];
23
+ accountsFetchState: FetchStateAndError;
7
24
  backfillState: FetchStateAndError;
8
- classes: InvoicingQboAccountOption[];
25
+ classes: InvoicingAccountingOption[];
9
26
  classesFetchState: FetchStateAndError;
10
27
  connectionFetchState: FetchStateAndError;
28
+ /**
29
+ * The account and class trees the grouped pickers render. The flat option
30
+ * lists above stay for callers that only need id and name.
31
+ */
32
+ nestedAccountHierarchy: NestedAccountHierarchyForReport[];
33
+ nestedClassHierarchy: NestedClassHierarchyForReport[];
11
34
  retryFetchStateByTaskId: Record<string, FetchStateAndError>;
12
35
  syncHealthFetchState: FetchStateAndError;
13
36
  connection?: InvoicingQboConnection;
14
37
  syncHealth?: InvoicingQboSyncHealth;
15
38
  }
39
+ /**
40
+ * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
41
+ * comma-separated list of QuickBooks type names. An unrecognised list yields no
42
+ * accounts rather than every account: offering a bank account where income is
43
+ * required would post a mapping QuickBooks rejects.
44
+ */
45
+ export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string) => InvoicingAccountingOption[];
46
+ /**
47
+ * The account tree narrowed to the given account types. The tree itself carries
48
+ * no account type, so the flat account list supplies that and the matching
49
+ * `qboId`s are kept — which is why callers that only need "income accounts, as a
50
+ * tree" do not have to hold a second flat list to filter with.
51
+ */
52
+ export declare const invoicingAccountHierarchyForTypes: (state: RootState, accountTypes: readonly AccountType[]) => NestedAccountHierarchyForReport[];
16
53
  /**
17
54
  * Single bundled view for the QBO integration settings screen — the
18
- * connection, per-account-types account options, classes, sync health, and the
19
- * fetch/action states for each of them.
55
+ * connection, the account and class options, sync health, and the fetch/action
56
+ * states for each of them.
20
57
  */
21
58
  export declare const getInvoicingQboView: (state: RootState) => InvoicingQboView;
@@ -1,19 +1,140 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getInvoicingQboView = void 0;
3
+ exports.getInvoicingQboView = exports.invoicingAccountHierarchyForTypes = exports.invoicingAccountsForQboAccountTypes = exports.INVOICING_INCOME_ACCOUNT_TYPES = exports.isInvoicingQboConnected = exports.INVOICING_ACCOUNT_LIST_KEY = void 0;
4
+ const accountListSelector_1 = require("../../accountList/accountListSelector");
5
+ const classListSelector_1 = require("../../classList/classListSelector");
6
+ // Its own bucket: invoicing needs the AP/AR exclusions that the vendor and
7
+ // transaction screens sharing `accountList` do not pass.
8
+ exports.INVOICING_ACCOUNT_LIST_KEY = 'invoicingAccountList';
9
+ /** Whether QBO is connected, without building the option lists to find out. */
10
+ const isInvoicingQboConnected = (state) => state.invoicingQboViewState.connection?.connected === true;
11
+ exports.isInvoicingQboConnected = isInvoicingQboConnected;
12
+ /**
13
+ * Account types that can back invoicing revenue. Named here so screens ask for
14
+ * "income accounts" rather than repeating the accounting service's vocabulary.
15
+ */
16
+ exports.INVOICING_INCOME_ACCOUNT_TYPES = [
17
+ 'income',
18
+ 'other_income',
19
+ ];
20
+ /**
21
+ * Accounts and classes as the invoicing pickers need them. Both come from the
22
+ * shared accounting-service lists, so whatever another screen already loaded
23
+ * is reused. `qboId` is the submitted value because that is what the invoicing
24
+ * API stores; an account without one cannot be posted, so it is dropped rather
25
+ * than offered.
26
+ */
27
+ const invoicingAccountingOptions = (state, accountTypes) => {
28
+ const { accounts } = (0, accountListSelector_1.getAccountList)(state.accountState, state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY);
29
+ return accounts.flatMap((account) => {
30
+ if (account.qboId == null || account.isDeleted === true) {
31
+ return [];
32
+ }
33
+ if (accountTypes != null &&
34
+ (account.accountType == null ||
35
+ !accountTypes.includes(account.accountType))) {
36
+ return [];
37
+ }
38
+ return [
39
+ {
40
+ id: String(account.qboId),
41
+ name: account.accountName,
42
+ accountType: account.accountType,
43
+ },
44
+ ];
45
+ });
46
+ };
47
+ /**
48
+ * Normalises an account-type name for comparison. The config's mapping kinds
49
+ * name QuickBooks types the way QuickBooks displays them ("Other Current
50
+ * Asset", "Expense"), while the accounting service uses snake_case and
51
+ * sometimes pluralises ("other_current_assets", "expenses"). Comparing the
52
+ * normalised, de-pluralised forms keeps a picker from silently emptying because
53
+ * of a trailing "s".
54
+ */
55
+ const normalizeAccountTypeName = (value) => {
56
+ const normalized = value
57
+ .trim()
58
+ .toLowerCase()
59
+ .replace(/[^a-z0-9]+/g, '_');
60
+ // Singularise so "Other Current Liability" meets `other_current_liabilities`
61
+ // and "Expense" meets `expenses`. `-ies` needs its own case: stripping the
62
+ // trailing "s" alone would leave "liabilitie", matching nothing.
63
+ return normalized.replace(/ies$/, 'y').replace(/([^s])s$/, '$1');
64
+ };
65
+ /**
66
+ * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
67
+ * comma-separated list of QuickBooks type names. An unrecognised list yields no
68
+ * accounts rather than every account: offering a bank account where income is
69
+ * required would post a mapping QuickBooks rejects.
70
+ */
71
+ const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
72
+ const wanted = new Set(accountTypes
73
+ .split(',')
74
+ .map(normalizeAccountTypeName)
75
+ .filter((accountType) => accountType !== ''));
76
+ if (wanted.size === 0) {
77
+ return accounts;
78
+ }
79
+ return accounts.filter((account) => account.accountType != null &&
80
+ wanted.has(normalizeAccountTypeName(account.accountType)));
81
+ };
82
+ exports.invoicingAccountsForQboAccountTypes = invoicingAccountsForQboAccountTypes;
83
+ /** Classes as the invoicing pickers need them, from the shared class list. */
84
+ const invoicingAccountingClassOptions = (state) => {
85
+ const { classes } = (0, classListSelector_1.getClassList)(state.classState, state.classListState);
86
+ return classes.flatMap((accountingClass) => accountingClass.qboId == null || accountingClass.isDeleted === true
87
+ ? []
88
+ : [
89
+ {
90
+ id: String(accountingClass.qboId),
91
+ name: accountingClass.className,
92
+ },
93
+ ]);
94
+ };
95
+ /**
96
+ * The account tree narrowed to the given account types. The tree itself carries
97
+ * no account type, so the flat account list supplies that and the matching
98
+ * `qboId`s are kept — which is why callers that only need "income accounts, as a
99
+ * tree" do not have to hold a second flat list to filter with.
100
+ */
101
+ const invoicingAccountHierarchyForTypes = (state, accountTypes) => {
102
+ const allowedQboIds = new Set(invoicingAccountingOptions(state, accountTypes).map((account) => account.id));
103
+ const keepMatching = (nodes) => nodes.flatMap((node) => {
104
+ const children = keepMatching(node.children);
105
+ const isMatch = node.qboId != null && allowedQboIds.has(String(node.qboId));
106
+ // A parent that is not itself a match is still kept when it holds one, so
107
+ // the tree's shape survives the filter.
108
+ if (!isMatch && children.length === 0) {
109
+ return [];
110
+ }
111
+ return [{ ...node, children }];
112
+ });
113
+ return keepMatching((0, accountListSelector_1.getNestedAccountListHierarchy)(state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY));
114
+ };
115
+ exports.invoicingAccountHierarchyForTypes = invoicingAccountHierarchyForTypes;
4
116
  /**
5
117
  * Single bundled view for the QBO integration settings screen — the
6
- * connection, per-account-types account options, classes, sync health, and the
7
- * fetch/action states for each of them.
118
+ * connection, the account and class options, sync health, and the fetch/action
119
+ * states for each of them.
8
120
  */
9
121
  const getInvoicingQboView = (state) => {
10
122
  const qboState = state.invoicingQboViewState;
123
+ const accountList = state.accountListState.byReportId[exports.INVOICING_ACCOUNT_LIST_KEY];
11
124
  return {
12
- accountsByAccountTypes: qboState.accountsByAccountTypes,
13
- accountsFetchStateByAccountTypes: qboState.accountsFetchStateByAccountTypes,
125
+ accounts: invoicingAccountingOptions(state),
126
+ accountsFetchState: {
127
+ fetchState: accountList.fetchState,
128
+ error: accountList.error,
129
+ },
130
+ nestedAccountHierarchy: (0, accountListSelector_1.getNestedAccountListHierarchy)(state.accountListState, exports.INVOICING_ACCOUNT_LIST_KEY),
131
+ nestedClassHierarchy: (0, classListSelector_1.getNestedClassListHierarchy)(state.classListState),
14
132
  backfillState: qboState.backfillFetchState,
15
- classes: qboState.classes,
16
- classesFetchState: qboState.classesFetchState,
133
+ classes: invoicingAccountingClassOptions(state),
134
+ classesFetchState: {
135
+ fetchState: state.classListState.fetchState,
136
+ error: state.classListState.error,
137
+ },
17
138
  connection: qboState.connection,
18
139
  connectionFetchState: qboState.connectionFetchState,
19
140
  retryFetchStateByTaskId: qboState.retryFetchStateByTaskId,
@@ -1,11 +1,7 @@
1
1
  import { FetchStateAndError } from '../../../commonStateTypes/common';
2
- import { InvoicingQboAccountOption, InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
2
+ import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
3
3
  export interface InvoicingQboViewState {
4
- accountsByAccountTypes: Record<string, InvoicingQboAccountOption[]>;
5
- accountsFetchStateByAccountTypes: Record<string, FetchStateAndError>;
6
4
  backfillFetchState: FetchStateAndError;
7
- classes: InvoicingQboAccountOption[];
8
- classesFetchState: FetchStateAndError;
9
5
  connectionFetchState: FetchStateAndError;
10
6
  retryFetchStateByTaskId: Record<string, FetchStateAndError>;
11
7
  syncHealthFetchState: FetchStateAndError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.5",
3
+ "version": "5.2.7",
4
4
  "description": "Shared module between Web & Mobile containing required abstractions for state management, async network communication. ",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/esm/index.js",
@@ -1,25 +0,0 @@
1
- import { of } from 'rxjs';
2
- import { catchError, filter, map, mergeMap } from 'rxjs/operators';
3
- import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
- import { mapInvoicingQboAccountOptions, } from '../invoicingQboViewPayload';
5
- import { fetchInvoicingQboAccounts, updateInvoicingQboAccounts, updateInvoicingQboAccountsFailure, } from '../invoicingQboViewReducer';
6
- export const fetchInvoicingQboAccountsEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingQboAccounts.match), mergeMap((action) => {
7
- const { accountTypes } = action.payload;
8
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/accounts?account_types=${encodeURIComponent(accountTypes)}`;
9
- return zeniAPI.getJSON(apiUrl).pipe(map((response) => {
10
- if (isSuccessResponse(response)) {
11
- return updateInvoicingQboAccounts({
12
- accountTypes,
13
- accounts: mapInvoicingQboAccountOptions(response.data?.accounts ?? []),
14
- });
15
- }
16
- return updateInvoicingQboAccountsFailure({
17
- accountTypes,
18
- status: response.status,
19
- });
20
- }), catchError((error) => of(updateInvoicingQboAccountsFailure({
21
- accountTypes,
22
- status: createZeniAPIStatus('Unexpected Error', 'Fetch QBO accounts REST API call errored out ' +
23
- JSON.stringify(error)),
24
- }))));
25
- }));
@@ -1,15 +0,0 @@
1
- import { of } from 'rxjs';
2
- import { catchError, filter, map, switchMap } from 'rxjs/operators';
3
- import { createZeniAPIStatus, isSuccessResponse, } from '../../../../responsePayload';
4
- import { mapInvoicingQboAccountOptions, } from '../invoicingQboViewPayload';
5
- import { fetchInvoicingQboClasses, updateInvoicingQboClasses, updateInvoicingQboClassesFailure, } from '../invoicingQboViewReducer';
6
- export const fetchInvoicingQboClassesEpic = (actions$, _state$, zeniAPI) => actions$.pipe(filter(fetchInvoicingQboClasses.match), switchMap(() => {
7
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/classes`;
8
- return zeniAPI.getJSON(apiUrl).pipe(map((response) => {
9
- if (isSuccessResponse(response)) {
10
- return updateInvoicingQboClasses(mapInvoicingQboAccountOptions(response.data?.accounts ?? []));
11
- }
12
- return updateInvoicingQboClassesFailure(response.status);
13
- }), catchError((error) => of(updateInvoicingQboClassesFailure(createZeniAPIStatus('Unexpected Error', 'Fetch QBO classes REST API call errored out ' +
14
- JSON.stringify(error))))));
15
- }));
@@ -1,6 +0,0 @@
1
- import { ActionsObservable, StateObservable } from 'redux-observable';
2
- import { Observable } from 'rxjs';
3
- import { RootState } from '../../../../rootStateTypes';
4
- import { ZeniAPI } from '../../../../zeniAPI';
5
- import { InvoicingQboViewActionType } from './invoicingQboViewActionType';
6
- export declare const fetchInvoicingQboAccountsEpic: (actions$: ActionsObservable<InvoicingQboViewActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<InvoicingQboViewActionType>;
@@ -1,29 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchInvoicingQboAccountsEpic = void 0;
4
- const rxjs_1 = require("rxjs");
5
- const operators_1 = require("rxjs/operators");
6
- const responsePayload_1 = require("../../../../responsePayload");
7
- const invoicingQboViewPayload_1 = require("../invoicingQboViewPayload");
8
- const invoicingQboViewReducer_1 = require("../invoicingQboViewReducer");
9
- const fetchInvoicingQboAccountsEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(invoicingQboViewReducer_1.fetchInvoicingQboAccounts.match), (0, operators_1.mergeMap)((action) => {
10
- const { accountTypes } = action.payload;
11
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/accounts?account_types=${encodeURIComponent(accountTypes)}`;
12
- return zeniAPI.getJSON(apiUrl).pipe((0, operators_1.map)((response) => {
13
- if ((0, responsePayload_1.isSuccessResponse)(response)) {
14
- return (0, invoicingQboViewReducer_1.updateInvoicingQboAccounts)({
15
- accountTypes,
16
- accounts: (0, invoicingQboViewPayload_1.mapInvoicingQboAccountOptions)(response.data?.accounts ?? []),
17
- });
18
- }
19
- return (0, invoicingQboViewReducer_1.updateInvoicingQboAccountsFailure)({
20
- accountTypes,
21
- status: response.status,
22
- });
23
- }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, invoicingQboViewReducer_1.updateInvoicingQboAccountsFailure)({
24
- accountTypes,
25
- status: (0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch QBO accounts REST API call errored out ' +
26
- JSON.stringify(error)),
27
- }))));
28
- }));
29
- exports.fetchInvoicingQboAccountsEpic = fetchInvoicingQboAccountsEpic;
@@ -1,6 +0,0 @@
1
- import { ActionsObservable, StateObservable } from 'redux-observable';
2
- import { Observable } from 'rxjs';
3
- import { RootState } from '../../../../rootStateTypes';
4
- import { ZeniAPI } from '../../../../zeniAPI';
5
- import { InvoicingQboViewActionType } from './invoicingQboViewActionType';
6
- export declare const fetchInvoicingQboClassesEpic: (actions$: ActionsObservable<InvoicingQboViewActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<InvoicingQboViewActionType>;
@@ -1,19 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchInvoicingQboClassesEpic = void 0;
4
- const rxjs_1 = require("rxjs");
5
- const operators_1 = require("rxjs/operators");
6
- const responsePayload_1 = require("../../../../responsePayload");
7
- const invoicingQboViewPayload_1 = require("../invoicingQboViewPayload");
8
- const invoicingQboViewReducer_1 = require("../invoicingQboViewReducer");
9
- const fetchInvoicingQboClassesEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(invoicingQboViewReducer_1.fetchInvoicingQboClasses.match), (0, operators_1.switchMap)(() => {
10
- const apiUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0/qbo/classes`;
11
- return zeniAPI.getJSON(apiUrl).pipe((0, operators_1.map)((response) => {
12
- if ((0, responsePayload_1.isSuccessResponse)(response)) {
13
- return (0, invoicingQboViewReducer_1.updateInvoicingQboClasses)((0, invoicingQboViewPayload_1.mapInvoicingQboAccountOptions)(response.data?.accounts ?? []));
14
- }
15
- return (0, invoicingQboViewReducer_1.updateInvoicingQboClassesFailure)(response.status);
16
- }), (0, operators_1.catchError)((error) => (0, rxjs_1.of)((0, invoicingQboViewReducer_1.updateInvoicingQboClassesFailure)((0, responsePayload_1.createZeniAPIStatus)('Unexpected Error', 'Fetch QBO classes REST API call errored out ' +
17
- JSON.stringify(error))))));
18
- }));
19
- exports.fetchInvoicingQboClassesEpic = fetchInvoicingQboClassesEpic;