@zeniai/client-epic-state 5.2.87 → 5.2.89

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 (28) hide show
  1. package/lib/entity/invoicing/invoicingCommonPayload.d.ts +9 -0
  2. package/lib/entity/invoicing/invoicingCommonPayload.js +8 -1
  3. package/lib/entity/tenant/tenantPayload.d.ts +1 -0
  4. package/lib/entity/tenant/tenantReducer.js +1 -0
  5. package/lib/entity/tenant/tenantState.d.ts +1 -0
  6. package/lib/esm/entity/invoicing/invoicingCommonPayload.js +7 -0
  7. package/lib/esm/entity/tenant/tenantReducer.js +1 -0
  8. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +20 -12
  9. package/lib/esm/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +6 -1
  10. package/lib/esm/view/invoicing/settingsView/settingsViewReducer.js +19 -1
  11. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +15 -1
  12. package/lib/esm/view/settingsView/epic/saveNotificationSettingsEpic.js +7 -0
  13. package/lib/esm/view/settingsView/settingsViewHelpers.js +1 -0
  14. package/lib/esm/view/settingsView/settingsViewReducer.js +16 -1
  15. package/lib/esm/view/settingsView/settingsViewState.js +1 -0
  16. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +6 -1
  17. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +20 -12
  18. package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +5 -0
  19. package/lib/view/invoicing/settingsView/settingsViewReducer.js +19 -1
  20. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +3 -1
  21. package/lib/view/invoicing/settingsView/settingsViewSelector.js +15 -1
  22. package/lib/view/settingsView/epic/saveNotificationSettingsEpic.js +7 -0
  23. package/lib/view/settingsView/settingsViewHelpers.js +1 -0
  24. package/lib/view/settingsView/settingsViewPayload.d.ts +2 -0
  25. package/lib/view/settingsView/settingsViewReducer.js +16 -1
  26. package/lib/view/settingsView/settingsViewState.d.ts +2 -2
  27. package/lib/view/settingsView/settingsViewState.js +1 -0
  28. package/package.json +1 -1
@@ -120,6 +120,12 @@ export declare const firstSelectableInvoicingId: (candidates: readonly (string |
120
120
  export declare const toInvoicingQboAccountMappingSlot: (code: string) => string;
121
121
  /** Read one account-mapping dropdown value by its config `code`. */
122
122
  export declare function getInvoicingQboAccountMappingId(accountMappings: InvoicingQboAccountMappingsPayload | undefined, code: string): string | undefined;
123
+ /**
124
+ * The Zeni-owned account the service posts sales tax to. It arrives with the
125
+ * settings for display only: it is not a mapping the user picks, and the
126
+ * service ignores it on save.
127
+ */
128
+ export declare const INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT = "sales_tax_posting_account";
123
129
  /**
124
130
  * Build the `account_mappings` request body from the ids the pickers submitted.
125
131
  * Each kind is sent as the whole account, which is what the service stores; an
@@ -140,11 +146,14 @@ export interface InvoicingQboAccountMappingKindOptionWire {
140
146
  account_types: string;
141
147
  code: string;
142
148
  name: string;
149
+ /** Comma-separated QuickBooks account sub types this kind must not offer. */
150
+ excluded_account_sub_types?: string | null;
143
151
  }
144
152
  export interface InvoicingQboAccountMappingKindOption {
145
153
  accountTypes: string;
146
154
  code: string;
147
155
  name: string;
156
+ excludedAccountSubTypes?: string;
148
157
  }
149
158
  export declare function toInvoicingQboAccountMappingKindOption(option: InvoicingQboAccountMappingKindOptionWire): InvoicingQboAccountMappingKindOption;
150
159
  export interface InvoicingSettingsPayload {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ALL_INVOICING_FIELD_HELP_GROUPS = exports.toInvoicingQboAccountMappingsPayload = exports.toInvoicingQboAccountMappingSlot = exports.firstSelectableInvoicingId = exports.INVOICING_QBO_CUSTOMER_MATCHING_MODES = exports.INVOICING_DISCOUNT_LIFECYCLE_ACTIONS = exports.INVOICING_CATALOG_PRODUCT_LIFECYCLE_ACTIONS = exports.INVOICING_CATALOG_ITEM_LIFECYCLE_ACTIONS = exports.INVOICING_SUBSCRIPTION_LIFECYCLE_ACTIONS = exports.INVOICING_DUNNING_ACTIONS = void 0;
3
+ exports.ALL_INVOICING_FIELD_HELP_GROUPS = exports.toInvoicingQboAccountMappingsPayload = exports.INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT = exports.toInvoicingQboAccountMappingSlot = exports.firstSelectableInvoicingId = exports.INVOICING_QBO_CUSTOMER_MATCHING_MODES = exports.INVOICING_DISCOUNT_LIFECYCLE_ACTIONS = exports.INVOICING_CATALOG_PRODUCT_LIFECYCLE_ACTIONS = exports.INVOICING_CATALOG_ITEM_LIFECYCLE_ACTIONS = exports.INVOICING_SUBSCRIPTION_LIFECYCLE_ACTIONS = exports.INVOICING_DUNNING_ACTIONS = void 0;
4
4
  exports.toInvoicingDunningAction = toInvoicingDunningAction;
5
5
  exports.toInvoicingSubscriptionLifecycleAction = toInvoicingSubscriptionLifecycleAction;
6
6
  exports.toInvoicingSubscriptionLifecycleActionOption = toInvoicingSubscriptionLifecycleActionOption;
@@ -178,6 +178,12 @@ function getInvoicingQboAccountMappingId(accountMappings, code) {
178
178
  }
179
179
  return (0, exports.firstSelectableInvoicingId)([slot?.qbo_id, slot?.account_id]);
180
180
  }
181
+ /**
182
+ * The Zeni-owned account the service posts sales tax to. It arrives with the
183
+ * settings for display only: it is not a mapping the user picks, and the
184
+ * service ignores it on save.
185
+ */
186
+ exports.INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT = 'sales_tax_posting_account';
181
187
  /**
182
188
  * Build the `account_mappings` request body from the ids the pickers submitted.
183
189
  * Each kind is sent as the whole account, which is what the service stores; an
@@ -201,6 +207,7 @@ function toInvoicingQboAccountMappingKindOption(option) {
201
207
  return {
202
208
  accountTypes: option.account_types,
203
209
  code: option.code,
210
+ excludedAccountSubTypes: option.excluded_account_sub_types ?? undefined,
204
211
  name: option.name,
205
212
  };
206
213
  }
@@ -98,6 +98,7 @@ interface TenantPayload {
98
98
  master_tos_info?: MasterTOSInfoPayload;
99
99
  receipts_email?: string | null;
100
100
  reimbursement_info?: UserReimbursementInfoDataPayload;
101
+ unit_connected_account_application_id?: ID | null;
101
102
  zeni_book_close_date?: string;
102
103
  zeni_book_close_month_and_year?: string;
103
104
  }
@@ -1025,6 +1025,7 @@ function mapTenantPayloadToTenant(payload, userTenantPayload) {
1025
1025
  hubSpotId: payload.hubspot_id ?? undefined,
1026
1026
  isOnboardingCompleted: true,
1027
1027
  qboRealmId: payload.qbo_realm_id,
1028
+ unitConnectedAccountApplicationId: payload.unit_connected_account_application_id ?? undefined,
1028
1029
  isCustomerCreated: payload.is_customer_created,
1029
1030
  isEmailGroupCreated: payload.is_email_group_created,
1030
1031
  userReimbursementInfo: payload.reimbursement_info != null
@@ -113,6 +113,7 @@ export interface Tenant {
113
113
  masterTOSInfo?: MasterTOSInfo;
114
114
  receiptsEmail?: string;
115
115
  rewardsLastViewedTime?: ZeniDate;
116
+ unitConnectedAccountApplicationId?: ID;
116
117
  userReimbursementInfo?: UserReimbursementInfo;
117
118
  zeniBookCloseDate?: ZeniDate;
118
119
  zeniBookCloseMonthAndYear?: MonthYearPeriod;
@@ -152,6 +152,12 @@ export function getInvoicingQboAccountMappingId(accountMappings, code) {
152
152
  }
153
153
  return firstSelectableInvoicingId([slot?.qbo_id, slot?.account_id]);
154
154
  }
155
+ /**
156
+ * The Zeni-owned account the service posts sales tax to. It arrives with the
157
+ * settings for display only: it is not a mapping the user picks, and the
158
+ * service ignores it on save.
159
+ */
160
+ export const INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT = 'sales_tax_posting_account';
155
161
  /**
156
162
  * Build the `account_mappings` request body from the ids the pickers submitted.
157
163
  * Each kind is sent as the whole account, which is what the service stores; an
@@ -174,6 +180,7 @@ export function toInvoicingQboAccountMappingKindOption(option) {
174
180
  return {
175
181
  accountTypes: option.account_types,
176
182
  code: option.code,
183
+ excludedAccountSubTypes: option.excluded_account_sub_types ?? undefined,
177
184
  name: option.name,
178
185
  };
179
186
  }
@@ -1014,6 +1014,7 @@ function mapTenantPayloadToTenant(payload, userTenantPayload) {
1014
1014
  hubSpotId: payload.hubspot_id ?? undefined,
1015
1015
  isOnboardingCompleted: true,
1016
1016
  qboRealmId: payload.qbo_realm_id,
1017
+ unitConnectedAccountApplicationId: payload.unit_connected_account_application_id ?? undefined,
1017
1018
  isCustomerCreated: payload.is_customer_created,
1018
1019
  isEmailGroupCreated: payload.is_email_group_created,
1019
1020
  userReimbursementInfo: payload.reimbursement_info != null
@@ -106,6 +106,7 @@ export const getInvoicingQboAccountOptions = createSelector([
106
106
  {
107
107
  id: String(account.qboId),
108
108
  name: account.accountName,
109
+ accountSubType: account.accountSubType,
109
110
  accountType: account.accountType,
110
111
  },
111
112
  ];
@@ -129,22 +130,29 @@ const normalizeAccountTypeName = (value) => {
129
130
  // trailing "s" alone would leave "liabilitie", matching nothing.
130
131
  return normalized.replace(/ies$/, 'y').replace(/([^s])s$/, '$1');
131
132
  };
133
+ const toAccountTypeNameSet = (names) => new Set((names ?? '')
134
+ .split(',')
135
+ .map(normalizeAccountTypeName)
136
+ .filter((name) => name !== ''));
132
137
  /**
133
138
  * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
134
139
  * comma-separated list of QuickBooks type names. An unrecognised list yields no
135
140
  * accounts rather than every account: offering a bank account where income is
136
141
  * required would post a mapping QuickBooks rejects.
142
+ *
143
+ * `excludedAccountSubTypes` drops accounts QuickBooks will not post to for that
144
+ * kind even though their type matches, e.g. a tax agency's system payable for
145
+ * the sales tax liability.
137
146
  */
138
- export const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
139
- const wanted = new Set(accountTypes
140
- .split(',')
141
- .map(normalizeAccountTypeName)
142
- .filter((accountType) => accountType !== ''));
143
- if (wanted.size === 0) {
144
- return accounts;
145
- }
146
- return accounts.filter((account) => account.accountType != null &&
147
- wanted.has(normalizeAccountTypeName(account.accountType)));
147
+ export const invoicingAccountsForQboAccountTypes = (accounts, accountTypes, excludedAccountSubTypes) => {
148
+ const wanted = toAccountTypeNameSet(accountTypes);
149
+ const excluded = toAccountTypeNameSet(excludedAccountSubTypes);
150
+ return accounts.filter((account) => (wanted.size === 0 ||
151
+ (account.accountType != null &&
152
+ wanted.has(normalizeAccountTypeName(account.accountType)))) &&
153
+ (excluded.size === 0 ||
154
+ account.accountSubType == null ||
155
+ !excluded.has(normalizeAccountTypeName(account.accountSubType))));
148
156
  };
149
157
  /**
150
158
  * The account-mapping slot a catalog price point's income account shares with
@@ -172,8 +180,8 @@ export const INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = 'Income';
172
180
  * not carry falls back to `defaultAccountTypes` rather than to every account.
173
181
  */
174
182
  export const invoicingQboAccountIdsForMappingSlot = (accounts, accountMappingKindOptions, mappingSlot, defaultAccountTypes) => {
175
- const accountTypes = accountMappingKindOptions.find((kindOption) => toInvoicingQboAccountMappingSlot(kindOption.code) === mappingSlot)?.accountTypes ?? defaultAccountTypes;
176
- return invoicingAccountsForQboAccountTypes(accounts, accountTypes).map((account) => account.id);
183
+ const kindOption = accountMappingKindOptions.find((option) => toInvoicingQboAccountMappingSlot(option.code) === mappingSlot);
184
+ return invoicingAccountsForQboAccountTypes(accounts, kindOption?.accountTypes ?? defaultAccountTypes, kindOption?.excludedAccountSubTypes).map((account) => account.id);
177
185
  };
178
186
  /**
179
187
  * Stable empties for a tenant with no QBO connection.
@@ -1,4 +1,4 @@
1
- import { getInvoicingQboAccountMappingId, toInvoicingQboAccountMappingSlot, toInvoicingQboAccountMappingsPayload, } from '../../../entity/invoicing/invoicingCommonPayload';
1
+ import { INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT, getInvoicingQboAccountMappingId, toInvoicingQboAccountMappingSlot, toInvoicingQboAccountMappingsPayload, } from '../../../entity/invoicing/invoicingCommonPayload';
2
2
  export const initialInvoicingQboSettingsLocalData = {
3
3
  accountIdByMappingCode: {},
4
4
  autoCreateItems: false,
@@ -15,6 +15,11 @@ export const entityToInvoicingQboSettingsLocalData = (qbo) => {
15
15
  // Keyed by the slot the payload used, which is the config code the page will
16
16
  // ask for — the reader tolerates the older `_id` form on the way in.
17
17
  Object.keys(accountMappings).forEach((key) => {
18
+ // Display-only, so it is never edited or written back on save.
19
+ if (toInvoicingQboAccountMappingSlot(key) ===
20
+ INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT) {
21
+ return;
22
+ }
18
23
  const accountId = getInvoicingQboAccountMappingId(accountMappings, key);
19
24
  if (accountId != null) {
20
25
  accountIdByMappingCode[toInvoicingQboAccountMappingSlot(key)] = accountId;
@@ -1,4 +1,5 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
+ import { INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT, } from '../../../entity/invoicing/invoicingCommonPayload';
2
3
  import { clearAll } from '../../../rootActions';
3
4
  import { entityToInvoicingQboSettingsLocalData, } from './invoicingQboSettingsFormConfig';
4
5
  export const initialState = {
@@ -40,7 +41,24 @@ const settingsView = createSlice({
40
41
  saveInvoicingSettings(draft, action) {
41
42
  draft.saveFetchState = { fetchState: 'In-Progress', error: undefined };
42
43
  if (draft.settings != null) {
43
- draft.settings = { ...draft.settings, ...action.payload.settings };
44
+ // The display-only sales tax posting account is never in a save body,
45
+ // so carry it over rather than blank it until the response lands.
46
+ const postingAccount = draft.settings.qbo?.account_mappings?.[INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT];
47
+ const settings = { ...draft.settings, ...action.payload.settings };
48
+ const accountMappings = settings.qbo?.account_mappings;
49
+ draft.settings =
50
+ postingAccount != null && accountMappings != null
51
+ ? {
52
+ ...settings,
53
+ qbo: {
54
+ ...settings.qbo,
55
+ account_mappings: {
56
+ ...accountMappings,
57
+ [INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT]: postingAccount,
58
+ },
59
+ },
60
+ }
61
+ : settings;
44
62
  }
45
63
  },
46
64
  saveInvoicingSettingsSuccess(draft, action) {
@@ -1,6 +1,6 @@
1
- import { getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
2
1
  import { getAddressByAddressId } from '../../../entity/address/addressSelector';
3
2
  import { getFileByFileId } from '../../../entity/file/fileSelector';
3
+ import { INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT, getInvoicingQboAccountMappingId, } from '../../../entity/invoicing/invoicingCommonPayload';
4
4
  import { getFormattedAddress } from '../../addressView/addressViewSelector';
5
5
  import { getInvoicingConfigView, invoicingFieldHelpForGroup, } from '../invoicingConfigView/invoicingConfigViewSelector';
6
6
  import { getInvoicingQboView, isInvoicingAccountingPickerDataLoading, } from '../invoicingQboView/invoicingQboViewSelector';
@@ -102,6 +102,19 @@ export const getInvoicingSettingsHubView = (state) => {
102
102
  status,
103
103
  };
104
104
  };
105
+ /**
106
+ * The name of the account the service posts sales tax to. Read from the whole
107
+ * account the settings carry, or looked up by id in the account list for a
108
+ * payload that sends only the id.
109
+ */
110
+ const invoicingSalesTaxPostingAccountName = (accountMappings, accounts) => {
111
+ const slot = accountMappings?.[INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT];
112
+ const name = typeof slot === 'string'
113
+ ? accounts.find((account) => account.id === slot)?.name
114
+ : slot?.account_name;
115
+ const trimmed = name?.trim() ?? '';
116
+ return trimmed !== '' ? trimmed : undefined;
117
+ };
105
118
  /**
106
119
  * Bundled view for the QBO configuration page: the draft it edits, the accounts
107
120
  * and option lists its dropdowns render, the connection and sync health it
@@ -135,6 +148,7 @@ export const getInvoicingQboSettingsFormView = (state) => {
135
148
  localData: qboDraft ?? initialInvoicingQboSettingsLocalData,
136
149
  nestedAccountHierarchy: qboView.nestedAccountHierarchy,
137
150
  retryFetchStateByTaskId: qboView.retryFetchStateByTaskId,
151
+ salesTaxPostingAccountName: invoicingSalesTaxPostingAccountName(settings?.qbo?.account_mappings, qboView.accounts),
138
152
  saveState: saveFetchState.fetchState === 'Not-Started' && error != null
139
153
  ? { fetchState, error }
140
154
  : saveFetchState,
@@ -50,6 +50,8 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
50
50
  pending_bill_approval_notification: null,
51
51
  pending_reimbursement_approval_notification: null,
52
52
  is_pending_reimbursement_approval_notification_active: null,
53
+ is_pending_task_notification_active: null,
54
+ pending_task_notification: null,
53
55
  is_month_end_insights_active: monthEndInsightsNotificationSettings.enabled != null
54
56
  ? monthEndInsights.notify
55
57
  : null,
@@ -77,5 +79,10 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
77
79
  0;
78
80
  payload.pending_reimbursement_approval_notification = toRemindersPayload(reminders.pendingReimbursementApprovalNotification);
79
81
  }
82
+ if (reminderNotificationSettings.pendingTaskNotification != null) {
83
+ payload.is_pending_task_notification_active =
84
+ reminders.pendingTaskNotification.preferredMode.length > 0;
85
+ payload.pending_task_notification = toRemindersPayload(reminders.pendingTaskNotification);
86
+ }
80
87
  return payload;
81
88
  }
@@ -140,6 +140,7 @@ export const isRemindersSectionVisibleOnSettingsPage = (reminderNotificationSett
140
140
  reminderNotificationSettings.pendingBillApprovalNotification != null ||
141
141
  reminderNotificationSettings.pendingReimbursementApprovalNotification !=
142
142
  null ||
143
+ reminderNotificationSettings.pendingTaskNotification != null ||
143
144
  reminderNotificationSettings.uncategorizedTransactionNotification != null);
144
145
  };
145
146
  export const isMonthEndNotificationsSectionVisibleOnSettingsPage = (monthEndInsightsNotificationSettings, signedInUseEmail) => {
@@ -4,6 +4,7 @@ import { toNotificationMode, toRemindersFrequencyType, } from './settingsViewSta
4
4
  const initialNotificationSettingsData = {
5
5
  reminderNotificationSettings: {
6
6
  missingReceiptNotification: null,
7
+ pendingTaskNotification: null,
7
8
  uncategorizedTransactionNotification: null,
8
9
  pendingBillApprovalNotification: null,
9
10
  pendingReimbursementApprovalNotification: null,
@@ -44,6 +45,7 @@ const initialNotificationSettingsLocalData = {
44
45
  missingReceiptNotification: initialRemindersLocalData,
45
46
  pendingBillApprovalNotification: initialRemindersLocalData,
46
47
  pendingReimbursementApprovalNotification: initialRemindersLocalData,
48
+ pendingTaskNotification: initialRemindersLocalData,
47
49
  uncategorizedTransactionNotification: initialRemindersLocalData,
48
50
  },
49
51
  };
@@ -373,7 +375,7 @@ function toRemindersLocalData(reminderNotificationSettingDetails) {
373
375
  }
374
376
  const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
375
377
  const { monthEndInsightsNotificationSettings, reminderNotificationSettings } = data;
376
- const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
378
+ const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, pendingTaskNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
377
379
  return {
378
380
  monthEndInsights: {
379
381
  notify: monthEndInsightsNotificationSettings.enabled ??
@@ -384,10 +386,22 @@ const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
384
386
  missingReceiptNotification: toRemindersLocalData(missingReceiptNotification),
385
387
  pendingBillApprovalNotification: toRemindersLocalData(pendingBillApprovalNotification),
386
388
  pendingReimbursementApprovalNotification: toRemindersLocalData(pendingReimbursementApprovalNotification),
389
+ pendingTaskNotification: toRemindersLocalData(pendingTaskNotification),
387
390
  uncategorizedTransactionNotification: toRemindersLocalData(uncategorizedTransactionNotification),
388
391
  },
389
392
  };
390
393
  };
394
+ function toPendingTaskNotificationActive(data) {
395
+ if (data.is_pending_task_notification_active != null) {
396
+ return data.is_pending_task_notification_active;
397
+ }
398
+ // Rows saved before this flag existed store null. It shares the
399
+ // transactions-reminder scope with missing receipts and uncategorized
400
+ // transactions, so a set sibling means the user has access.
401
+ const hasTransactionReminderAccess = data.is_missing_receipt_notification_active != null ||
402
+ data.is_uncategorized_transaction_notification_active != null;
403
+ return hasTransactionReminderAccess ? false : null;
404
+ }
391
405
  function toReminderNotificationSettingsDetails(isNotificationActive, remindersPayload) {
392
406
  if (isNotificationActive != null) {
393
407
  return {
@@ -408,6 +422,7 @@ const toNotificationSettings = (data) => {
408
422
  missingReceiptNotification: toReminderNotificationSettingsDetails(data.is_missing_receipt_notification_active, data.missing_receipt_notification),
409
423
  pendingBillApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_bill_approval_notification_active, data.pending_bill_approval_notification),
410
424
  pendingReimbursementApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_reimbursement_approval_notification_active, data.pending_reimbursement_approval_notification),
425
+ pendingTaskNotification: toReminderNotificationSettingsDetails(toPendingTaskNotificationActive(data), data.pending_task_notification),
411
426
  uncategorizedTransactionNotification: toReminderNotificationSettingsDetails(data.is_uncategorized_transaction_notification_active, data.uncategorized_transaction_notification),
412
427
  },
413
428
  monthEndInsightsNotificationSettings: {
@@ -1,6 +1,7 @@
1
1
  import { stringToUnion } from '../../commonStateTypes/stringToUnion';
2
2
  export const REMINDER_NOTIFICATION_TYPES = [
3
3
  'missingReceiptNotification',
4
+ 'pendingTaskNotification',
4
5
  'uncategorizedTransactionNotification',
5
6
  'pendingBillApprovalNotification',
6
7
  'pendingReimbursementApprovalNotification',
@@ -41,6 +41,7 @@ export declare const getInvoicingCatalogAccountingLookup: (state: RootState) =>
41
41
  export interface InvoicingAccountingOption {
42
42
  id: string;
43
43
  name: string;
44
+ accountSubType?: string;
44
45
  accountType?: string;
45
46
  }
46
47
  export interface InvoicingQboView {
@@ -77,8 +78,12 @@ export declare const getInvoicingQboAccountOptions: (state: RootState) => Invoic
77
78
  * comma-separated list of QuickBooks type names. An unrecognised list yields no
78
79
  * accounts rather than every account: offering a bank account where income is
79
80
  * required would post a mapping QuickBooks rejects.
81
+ *
82
+ * `excludedAccountSubTypes` drops accounts QuickBooks will not post to for that
83
+ * kind even though their type matches, e.g. a tax agency's system payable for
84
+ * the sales tax liability.
80
85
  */
81
- export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string) => InvoicingAccountingOption[];
86
+ export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string, excludedAccountSubTypes?: string) => InvoicingAccountingOption[];
82
87
  /**
83
88
  * The account-mapping slot a catalog price point's income account shares with
84
89
  * the settings page, and the one a discount's account shares with it.
@@ -115,6 +115,7 @@ exports.getInvoicingQboAccountOptions = (0, toolkit_1.createSelector)([
115
115
  {
116
116
  id: String(account.qboId),
117
117
  name: account.accountName,
118
+ accountSubType: account.accountSubType,
118
119
  accountType: account.accountType,
119
120
  },
120
121
  ];
@@ -138,22 +139,29 @@ const normalizeAccountTypeName = (value) => {
138
139
  // trailing "s" alone would leave "liabilitie", matching nothing.
139
140
  return normalized.replace(/ies$/, 'y').replace(/([^s])s$/, '$1');
140
141
  };
142
+ const toAccountTypeNameSet = (names) => new Set((names ?? '')
143
+ .split(',')
144
+ .map(normalizeAccountTypeName)
145
+ .filter((name) => name !== ''));
141
146
  /**
142
147
  * The accounts valid for one QBO mapping kind, whose `accountTypes` is a
143
148
  * comma-separated list of QuickBooks type names. An unrecognised list yields no
144
149
  * accounts rather than every account: offering a bank account where income is
145
150
  * required would post a mapping QuickBooks rejects.
151
+ *
152
+ * `excludedAccountSubTypes` drops accounts QuickBooks will not post to for that
153
+ * kind even though their type matches, e.g. a tax agency's system payable for
154
+ * the sales tax liability.
146
155
  */
147
- const invoicingAccountsForQboAccountTypes = (accounts, accountTypes) => {
148
- const wanted = new Set(accountTypes
149
- .split(',')
150
- .map(normalizeAccountTypeName)
151
- .filter((accountType) => accountType !== ''));
152
- if (wanted.size === 0) {
153
- return accounts;
154
- }
155
- return accounts.filter((account) => account.accountType != null &&
156
- wanted.has(normalizeAccountTypeName(account.accountType)));
156
+ const invoicingAccountsForQboAccountTypes = (accounts, accountTypes, excludedAccountSubTypes) => {
157
+ const wanted = toAccountTypeNameSet(accountTypes);
158
+ const excluded = toAccountTypeNameSet(excludedAccountSubTypes);
159
+ return accounts.filter((account) => (wanted.size === 0 ||
160
+ (account.accountType != null &&
161
+ wanted.has(normalizeAccountTypeName(account.accountType)))) &&
162
+ (excluded.size === 0 ||
163
+ account.accountSubType == null ||
164
+ !excluded.has(normalizeAccountTypeName(account.accountSubType))));
157
165
  };
158
166
  exports.invoicingAccountsForQboAccountTypes = invoicingAccountsForQboAccountTypes;
159
167
  /**
@@ -182,8 +190,8 @@ exports.INVOICING_DEFAULT_INCOME_ACCOUNT_TYPES = 'Income';
182
190
  * not carry falls back to `defaultAccountTypes` rather than to every account.
183
191
  */
184
192
  const invoicingQboAccountIdsForMappingSlot = (accounts, accountMappingKindOptions, mappingSlot, defaultAccountTypes) => {
185
- const accountTypes = accountMappingKindOptions.find((kindOption) => (0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(kindOption.code) === mappingSlot)?.accountTypes ?? defaultAccountTypes;
186
- return (0, exports.invoicingAccountsForQboAccountTypes)(accounts, accountTypes).map((account) => account.id);
193
+ const kindOption = accountMappingKindOptions.find((option) => (0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(option.code) === mappingSlot);
194
+ return (0, exports.invoicingAccountsForQboAccountTypes)(accounts, kindOption?.accountTypes ?? defaultAccountTypes, kindOption?.excludedAccountSubTypes).map((account) => account.id);
187
195
  };
188
196
  exports.invoicingQboAccountIdsForMappingSlot = invoicingQboAccountIdsForMappingSlot;
189
197
  /**
@@ -18,6 +18,11 @@ const entityToInvoicingQboSettingsLocalData = (qbo) => {
18
18
  // Keyed by the slot the payload used, which is the config code the page will
19
19
  // ask for — the reader tolerates the older `_id` form on the way in.
20
20
  Object.keys(accountMappings).forEach((key) => {
21
+ // Display-only, so it is never edited or written back on save.
22
+ if ((0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(key) ===
23
+ invoicingCommonPayload_1.INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT) {
24
+ return;
25
+ }
21
26
  const accountId = (0, invoicingCommonPayload_1.getInvoicingQboAccountMappingId)(accountMappings, key);
22
27
  if (accountId != null) {
23
28
  accountIdByMappingCode[(0, invoicingCommonPayload_1.toInvoicingQboAccountMappingSlot)(key)] = accountId;
@@ -3,6 +3,7 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.invoicingStripeConnectionFailure = exports.updateInvoicingStripeConnection = exports.disconnectInvoicingStripe = exports.connectInvoicingStripe = exports.resetInvoicingQboSettingsDraft = exports.submitInvoicingQboSettings = exports.updateInvoicingQboSettingsDraft = exports.resetInvoicingBrandingFormDraft = exports.submitInvoicingBrandingForm = exports.updateInvoicingBrandingFormDraft = exports.resetSaveInvoicingSettings = exports.saveInvoicingSettingsFailure = exports.saveInvoicingSettingsSuccess = exports.saveInvoicingSettings = exports.updateInvoicingSettingsFailure = exports.updateInvoicingSettings = exports.fetchInvoicingSettings = exports.initialState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
+ const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
6
7
  const rootActions_1 = require("../../../rootActions");
7
8
  const invoicingQboSettingsFormConfig_1 = require("./invoicingQboSettingsFormConfig");
8
9
  exports.initialState = {
@@ -44,7 +45,24 @@ const settingsView = (0, toolkit_1.createSlice)({
44
45
  saveInvoicingSettings(draft, action) {
45
46
  draft.saveFetchState = { fetchState: 'In-Progress', error: undefined };
46
47
  if (draft.settings != null) {
47
- draft.settings = { ...draft.settings, ...action.payload.settings };
48
+ // The display-only sales tax posting account is never in a save body,
49
+ // so carry it over rather than blank it until the response lands.
50
+ const postingAccount = draft.settings.qbo?.account_mappings?.[invoicingCommonPayload_1.INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT];
51
+ const settings = { ...draft.settings, ...action.payload.settings };
52
+ const accountMappings = settings.qbo?.account_mappings;
53
+ draft.settings =
54
+ postingAccount != null && accountMappings != null
55
+ ? {
56
+ ...settings,
57
+ qbo: {
58
+ ...settings.qbo,
59
+ account_mappings: {
60
+ ...accountMappings,
61
+ [invoicingCommonPayload_1.INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT]: postingAccount,
62
+ },
63
+ },
64
+ }
65
+ : settings;
48
66
  }
49
67
  },
50
68
  saveInvoicingSettingsSuccess(draft, action) {
@@ -1,7 +1,7 @@
1
1
  import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
2
  import { FetchState, FetchStateAndError } from '../../../commonStateTypes/common';
3
- import { InvoicingDunningActionOption, InvoicingFieldHelpByCode, InvoicingNamedOption, InvoicingQboAccountMappingKindOption, InvoicingSettingsPayload } from '../../../entity/invoicing/invoicingCommonPayload';
4
3
  import { File } from '../../../entity/file/fileState';
4
+ import { InvoicingDunningActionOption, InvoicingFieldHelpByCode, InvoicingNamedOption, InvoicingQboAccountMappingKindOption, InvoicingSettingsPayload } from '../../../entity/invoicing/invoicingCommonPayload';
5
5
  import { ZeniAPIStatus } from '../../../responsePayload';
6
6
  import { RootState } from '../../../rootStateTypes';
7
7
  import { AddressUpdatableInfo } from '../../addressView/addressViewState';
@@ -120,6 +120,8 @@ export interface InvoicingQboSettingsFormView {
120
120
  settingsFetchState: FetchState;
121
121
  syncStatusOptions: InvoicingNamedOption[];
122
122
  connection?: InvoicingQboConnection;
123
+ /** The Zeni-owned account sales tax posts to, shown read-only when present. */
124
+ salesTaxPostingAccountName?: string;
123
125
  syncHealth?: InvoicingQboSyncHealth;
124
126
  }
125
127
  /**
@@ -2,9 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getInvoicingBrandingFormView = exports.getInvoicingQboSettingsFormView = exports.getInvoicingSettingsHubView = exports.getInvoicingSettingsView = exports.getInvoicingSettingsFetchState = exports.getInvoicingSettings = void 0;
4
4
  exports.deriveInvoicingEnablePrerequisites = deriveInvoicingEnablePrerequisites;
5
- const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
6
5
  const addressSelector_1 = require("../../../entity/address/addressSelector");
7
6
  const fileSelector_1 = require("../../../entity/file/fileSelector");
7
+ const invoicingCommonPayload_1 = require("../../../entity/invoicing/invoicingCommonPayload");
8
8
  const addressViewSelector_1 = require("../../addressView/addressViewSelector");
9
9
  const invoicingConfigViewSelector_1 = require("../invoicingConfigView/invoicingConfigViewSelector");
10
10
  const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
@@ -110,6 +110,19 @@ const getInvoicingSettingsHubView = (state) => {
110
110
  };
111
111
  };
112
112
  exports.getInvoicingSettingsHubView = getInvoicingSettingsHubView;
113
+ /**
114
+ * The name of the account the service posts sales tax to. Read from the whole
115
+ * account the settings carry, or looked up by id in the account list for a
116
+ * payload that sends only the id.
117
+ */
118
+ const invoicingSalesTaxPostingAccountName = (accountMappings, accounts) => {
119
+ const slot = accountMappings?.[invoicingCommonPayload_1.INVOICING_QBO_SALES_TAX_POSTING_ACCOUNT_SLOT];
120
+ const name = typeof slot === 'string'
121
+ ? accounts.find((account) => account.id === slot)?.name
122
+ : slot?.account_name;
123
+ const trimmed = name?.trim() ?? '';
124
+ return trimmed !== '' ? trimmed : undefined;
125
+ };
113
126
  /**
114
127
  * Bundled view for the QBO configuration page: the draft it edits, the accounts
115
128
  * and option lists its dropdowns render, the connection and sync health it
@@ -143,6 +156,7 @@ const getInvoicingQboSettingsFormView = (state) => {
143
156
  localData: qboDraft ?? invoicingQboSettingsFormConfig_1.initialInvoicingQboSettingsLocalData,
144
157
  nestedAccountHierarchy: qboView.nestedAccountHierarchy,
145
158
  retryFetchStateByTaskId: qboView.retryFetchStateByTaskId,
159
+ salesTaxPostingAccountName: invoicingSalesTaxPostingAccountName(settings?.qbo?.account_mappings, qboView.accounts),
146
160
  saveState: saveFetchState.fetchState === 'Not-Started' && error != null
147
161
  ? { fetchState, error }
148
162
  : saveFetchState,
@@ -54,6 +54,8 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
54
54
  pending_bill_approval_notification: null,
55
55
  pending_reimbursement_approval_notification: null,
56
56
  is_pending_reimbursement_approval_notification_active: null,
57
+ is_pending_task_notification_active: null,
58
+ pending_task_notification: null,
57
59
  is_month_end_insights_active: monthEndInsightsNotificationSettings.enabled != null
58
60
  ? monthEndInsights.notify
59
61
  : null,
@@ -81,5 +83,10 @@ function toSaveNotificationSettingsPayload(localData, notificationSettingsData)
81
83
  0;
82
84
  payload.pending_reimbursement_approval_notification = toRemindersPayload(reminders.pendingReimbursementApprovalNotification);
83
85
  }
86
+ if (reminderNotificationSettings.pendingTaskNotification != null) {
87
+ payload.is_pending_task_notification_active =
88
+ reminders.pendingTaskNotification.preferredMode.length > 0;
89
+ payload.pending_task_notification = toRemindersPayload(reminders.pendingTaskNotification);
90
+ }
84
91
  return payload;
85
92
  }
@@ -153,6 +153,7 @@ const isRemindersSectionVisibleOnSettingsPage = (reminderNotificationSettings) =
153
153
  reminderNotificationSettings.pendingBillApprovalNotification != null ||
154
154
  reminderNotificationSettings.pendingReimbursementApprovalNotification !=
155
155
  null ||
156
+ reminderNotificationSettings.pendingTaskNotification != null ||
156
157
  reminderNotificationSettings.uncategorizedTransactionNotification != null);
157
158
  };
158
159
  exports.isRemindersSectionVisibleOnSettingsPage = isRemindersSectionVisibleOnSettingsPage;
@@ -32,10 +32,12 @@ export interface NotificationSettingsPayload {
32
32
  is_month_end_insights_active: boolean | null;
33
33
  is_pending_bill_approval_notification_active: boolean | null;
34
34
  is_pending_reimbursement_approval_notification_active: boolean | null;
35
+ is_pending_task_notification_active: boolean | null;
35
36
  is_uncategorized_transaction_notification_active: boolean | null;
36
37
  missing_receipt_notification: RemindersPayload | null;
37
38
  pending_bill_approval_notification: RemindersPayload | null;
38
39
  pending_reimbursement_approval_notification: RemindersPayload | null;
40
+ pending_task_notification: RemindersPayload | null;
39
41
  uncategorized_transaction_notification: RemindersPayload | null;
40
42
  month_end_email_recipient_email_ids?: string[];
41
43
  task_checklist_preferences?: NotificationPreferencesPayload;
@@ -8,6 +8,7 @@ const settingsViewState_1 = require("./settingsViewState");
8
8
  const initialNotificationSettingsData = {
9
9
  reminderNotificationSettings: {
10
10
  missingReceiptNotification: null,
11
+ pendingTaskNotification: null,
11
12
  uncategorizedTransactionNotification: null,
12
13
  pendingBillApprovalNotification: null,
13
14
  pendingReimbursementApprovalNotification: null,
@@ -48,6 +49,7 @@ const initialNotificationSettingsLocalData = {
48
49
  missingReceiptNotification: initialRemindersLocalData,
49
50
  pendingBillApprovalNotification: initialRemindersLocalData,
50
51
  pendingReimbursementApprovalNotification: initialRemindersLocalData,
52
+ pendingTaskNotification: initialRemindersLocalData,
51
53
  uncategorizedTransactionNotification: initialRemindersLocalData,
52
54
  },
53
55
  };
@@ -377,7 +379,7 @@ function toRemindersLocalData(reminderNotificationSettingDetails) {
377
379
  }
378
380
  const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
379
381
  const { monthEndInsightsNotificationSettings, reminderNotificationSettings } = data;
380
- const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
382
+ const { missingReceiptNotification, pendingBillApprovalNotification, pendingReimbursementApprovalNotification, pendingTaskNotification, uncategorizedTransactionNotification, } = reminderNotificationSettings;
381
383
  return {
382
384
  monthEndInsights: {
383
385
  notify: monthEndInsightsNotificationSettings.enabled ??
@@ -388,10 +390,22 @@ const toNotificationSettingsLocalData = (data, loggedInUserEmail) => {
388
390
  missingReceiptNotification: toRemindersLocalData(missingReceiptNotification),
389
391
  pendingBillApprovalNotification: toRemindersLocalData(pendingBillApprovalNotification),
390
392
  pendingReimbursementApprovalNotification: toRemindersLocalData(pendingReimbursementApprovalNotification),
393
+ pendingTaskNotification: toRemindersLocalData(pendingTaskNotification),
391
394
  uncategorizedTransactionNotification: toRemindersLocalData(uncategorizedTransactionNotification),
392
395
  },
393
396
  };
394
397
  };
398
+ function toPendingTaskNotificationActive(data) {
399
+ if (data.is_pending_task_notification_active != null) {
400
+ return data.is_pending_task_notification_active;
401
+ }
402
+ // Rows saved before this flag existed store null. It shares the
403
+ // transactions-reminder scope with missing receipts and uncategorized
404
+ // transactions, so a set sibling means the user has access.
405
+ const hasTransactionReminderAccess = data.is_missing_receipt_notification_active != null ||
406
+ data.is_uncategorized_transaction_notification_active != null;
407
+ return hasTransactionReminderAccess ? false : null;
408
+ }
395
409
  function toReminderNotificationSettingsDetails(isNotificationActive, remindersPayload) {
396
410
  if (isNotificationActive != null) {
397
411
  return {
@@ -412,6 +426,7 @@ const toNotificationSettings = (data) => {
412
426
  missingReceiptNotification: toReminderNotificationSettingsDetails(data.is_missing_receipt_notification_active, data.missing_receipt_notification),
413
427
  pendingBillApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_bill_approval_notification_active, data.pending_bill_approval_notification),
414
428
  pendingReimbursementApprovalNotification: toReminderNotificationSettingsDetails(data.is_pending_reimbursement_approval_notification_active, data.pending_reimbursement_approval_notification),
429
+ pendingTaskNotification: toReminderNotificationSettingsDetails(toPendingTaskNotificationActive(data), data.pending_task_notification),
415
430
  uncategorizedTransactionNotification: toReminderNotificationSettingsDetails(data.is_uncategorized_transaction_notification_active, data.uncategorized_transaction_notification),
416
431
  },
417
432
  monthEndInsightsNotificationSettings: {
@@ -9,8 +9,8 @@ interface BillingAddress extends FetchStateAndError {
9
9
  export interface IntegrationsSettings {
10
10
  refreshQBO: FetchStateAndError;
11
11
  }
12
- export declare const REMINDER_NOTIFICATION_TYPES: readonly ["missingReceiptNotification", "uncategorizedTransactionNotification", "pendingBillApprovalNotification", "pendingReimbursementApprovalNotification"];
13
- declare const toReminderTypeKey: (v: string) => "missingReceiptNotification" | "uncategorizedTransactionNotification" | "pendingBillApprovalNotification" | "pendingReimbursementApprovalNotification";
12
+ export declare const REMINDER_NOTIFICATION_TYPES: readonly ["missingReceiptNotification", "pendingTaskNotification", "uncategorizedTransactionNotification", "pendingBillApprovalNotification", "pendingReimbursementApprovalNotification"];
13
+ declare const toReminderTypeKey: (v: string) => "missingReceiptNotification" | "pendingTaskNotification" | "uncategorizedTransactionNotification" | "pendingBillApprovalNotification" | "pendingReimbursementApprovalNotification";
14
14
  export type ReminderTypeKey = ReturnType<typeof toReminderTypeKey>;
15
15
  export declare const toRemindersFrequencyType: (v: string) => "weekly" | "monthly" | "daily" | "bi-weekly";
16
16
  export type FrequencyTypeKey = ReturnType<typeof toRemindersFrequencyType>;
@@ -4,6 +4,7 @@ exports.toNotificationMode = exports.NOTIFICATION_MODES = exports.toRemindersFre
4
4
  const stringToUnion_1 = require("../../commonStateTypes/stringToUnion");
5
5
  exports.REMINDER_NOTIFICATION_TYPES = [
6
6
  'missingReceiptNotification',
7
+ 'pendingTaskNotification',
7
8
  'uncategorizedTransactionNotification',
8
9
  'pendingBillApprovalNotification',
9
10
  'pendingReimbursementApprovalNotification',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.87",
3
+ "version": "5.2.89",
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",