@zeniai/client-epic-state 5.2.88 → 5.2.89-beta0MM

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 (39) hide show
  1. package/lib/entity/invoicing/creditNote/creditNotePayload.d.ts +2 -0
  2. package/lib/entity/invoicing/creditNote/creditNotePayload.js +5 -0
  3. package/lib/entity/invoicing/creditNote/creditNoteState.d.ts +1 -0
  4. package/lib/entity/invoicing/invoicingCommonPayload.d.ts +9 -0
  5. package/lib/entity/invoicing/invoicingCommonPayload.js +8 -1
  6. package/lib/entity/tenant/tenantPayload.d.ts +1 -0
  7. package/lib/entity/tenant/tenantReducer.js +1 -0
  8. package/lib/entity/tenant/tenantState.d.ts +1 -0
  9. package/lib/esm/entity/invoicing/creditNote/creditNotePayload.js +5 -0
  10. package/lib/esm/entity/invoicing/invoicingCommonPayload.js +7 -0
  11. package/lib/esm/entity/tenant/tenantReducer.js +1 -0
  12. package/lib/esm/view/expenseAutomationView/payload/actualMatchingPayload.js +1 -0
  13. package/lib/esm/view/financeStatement/financeStatementReducer.js +6 -11
  14. package/lib/esm/view/invoicing/invoicingOverview/invoicingOverviewPayload.js +1 -0
  15. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +20 -12
  16. package/lib/esm/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +6 -1
  17. package/lib/esm/view/invoicing/settingsView/settingsViewReducer.js +19 -1
  18. package/lib/esm/view/invoicing/settingsView/settingsViewSelector.js +15 -1
  19. package/lib/esm/view/reportUIOptions/financeStatementCompareBalances.js +16 -0
  20. package/lib/esm/view/reportUIOptions/updateReportUIOptionCompareModeEpic.js +2 -5
  21. package/lib/esm/view/reportUIOptions/updateReportUIOptionIsCompareModeOnEpic.js +6 -10
  22. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.d.ts +1 -0
  23. package/lib/view/expenseAutomationView/payload/actualMatchingPayload.js +1 -0
  24. package/lib/view/expenseAutomationView/types/actualMatchingViewState.d.ts +2 -0
  25. package/lib/view/financeStatement/financeStatementReducer.js +6 -11
  26. package/lib/view/invoicing/invoicingOverview/invoicingOverviewPayload.d.ts +5 -0
  27. package/lib/view/invoicing/invoicingOverview/invoicingOverviewPayload.js +1 -0
  28. package/lib/view/invoicing/invoicingOverview/invoicingOverviewState.d.ts +2 -0
  29. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +6 -1
  30. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +20 -12
  31. package/lib/view/invoicing/settingsView/invoicingQboSettingsFormConfig.js +5 -0
  32. package/lib/view/invoicing/settingsView/settingsViewReducer.js +19 -1
  33. package/lib/view/invoicing/settingsView/settingsViewSelector.d.ts +3 -1
  34. package/lib/view/invoicing/settingsView/settingsViewSelector.js +15 -1
  35. package/lib/view/reportUIOptions/financeStatementCompareBalances.d.ts +2 -0
  36. package/lib/view/reportUIOptions/financeStatementCompareBalances.js +19 -0
  37. package/lib/view/reportUIOptions/updateReportUIOptionCompareModeEpic.js +2 -5
  38. package/lib/view/reportUIOptions/updateReportUIOptionIsCompareModeOnEpic.js +6 -10
  39. package/package.json +1 -1
@@ -39,6 +39,8 @@ export interface InvoicingCreditNotePayload {
39
39
  status?: InvoicingCreditNoteStatus;
40
40
  sub_total?: number;
41
41
  subscription_id?: string;
42
+ /** The subscription's display number (SUB-####), resolved by the service. */
43
+ subscription_number?: string | null;
42
44
  total?: number;
43
45
  type?: InvoicingCreditNoteType;
44
46
  }
@@ -54,6 +54,11 @@ const mapInvoicingCreditNotePayloadToState = (payload) => {
54
54
  status: payload.status,
55
55
  subTotal: payload.sub_total != null ? toAmt(payload.sub_total) : undefined,
56
56
  subscriptionID: payload.subscription_id,
57
+ // Blank when the credit note has no subscription; the API sends "".
58
+ subscriptionNumber: payload.subscription_number != null &&
59
+ payload.subscription_number.trim() !== ''
60
+ ? payload.subscription_number.trim()
61
+ : undefined,
57
62
  total: payload.total != null ? toAmt(payload.total) : undefined,
58
63
  type: payload.type,
59
64
  };
@@ -39,6 +39,7 @@ export interface InvoicingCreditNote {
39
39
  referenceInvoiceNumber?: string;
40
40
  status?: InvoicingCreditNoteStatus;
41
41
  subscriptionID?: ID;
42
+ subscriptionNumber?: string;
42
43
  subTotal?: Amount;
43
44
  total?: Amount;
44
45
  type?: InvoicingCreditNoteType;
@@ -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;
@@ -49,6 +49,11 @@ export const mapInvoicingCreditNotePayloadToState = (payload) => {
49
49
  status: payload.status,
50
50
  subTotal: payload.sub_total != null ? toAmt(payload.sub_total) : undefined,
51
51
  subscriptionID: payload.subscription_id,
52
+ // Blank when the credit note has no subscription; the API sends "".
53
+ subscriptionNumber: payload.subscription_number != null &&
54
+ payload.subscription_number.trim() !== ''
55
+ ? payload.subscription_number.trim()
56
+ : undefined,
52
57
  total: payload.total != null ? toAmt(payload.total) : undefined,
53
58
  type: payload.type,
54
59
  };
@@ -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
@@ -47,6 +47,7 @@ export function toActualMatchingTransactionSearchResult(payload, fallbackCurrenc
47
47
  currencyCode,
48
48
  currencySymbol: resolveMatchSearchCurrencySymbol(payload, currencyCode, fallbackCurrency),
49
49
  transactionMemo: payload.transaction_memo ?? undefined,
50
+ lineDescription: payload.line_description ?? undefined,
50
51
  transactionDescription: payload.transaction_description ?? undefined,
51
52
  transactionTypeName: payload.transaction_type_name ?? undefined,
52
53
  syncToken: payload.sync_token == null ? '' : String(payload.sync_token),
@@ -1,4 +1,5 @@
1
1
  import { createSlice } from '@reduxjs/toolkit';
2
+ import { stripCompareAdditionalBalanceTypes, } from '../../commonStateTypes/coaBalance/coaBalanceType';
2
3
  import { extractThisPeriod, getMatchingThisPeriod, } from '../../commonStateTypes/viewAndReport/thisPeriodHelpers';
3
4
  import { clearAll } from '../../rootActions';
4
5
  import { dateNow } from '../../zeniDayJS';
@@ -88,17 +89,11 @@ const financeStatement = createSlice({
88
89
  updateFinanceStatementAdditionalBalancesSelection(draft, action) {
89
90
  const { additionalBalances: additionalBalancesPayload, maxNumOfPeriodsToHighlight, } = action.payload;
90
91
  const additionalBalances = additionalBalancesPayload ?? [];
91
- let tempAdditionalBalances = [...additionalBalances];
92
- if (additionalBalances.includes('this_period_vs_last_period') ||
93
- additionalBalances.includes('this_period_vs_last_period_percent')) {
94
- const id = tempAdditionalBalances.findIndex((balance) => balance === 'this_period_vs_last_period' ||
95
- balance === 'this_period_vs_last_period_percent');
96
- if (id > -1) {
97
- const balance = tempAdditionalBalances.splice(id, 1);
98
- tempAdditionalBalances = [...balance, ...tempAdditionalBalances];
99
- }
100
- }
101
- draft.selectedAdditionalBalances = tempAdditionalBalances;
92
+ const otherBalances = stripCompareAdditionalBalanceTypes(additionalBalances);
93
+ draft.selectedAdditionalBalances = [
94
+ ...additionalBalances.filter((balance) => !otherBalances.includes(balance)),
95
+ ...otherBalances,
96
+ ];
102
97
  draft.maxNumOfPeriodsToHighlight = maxNumOfPeriodsToHighlight;
103
98
  draft.isAdditionalBalancesShown =
104
99
  additionalBalances.length != 0 ? true : false;
@@ -41,6 +41,7 @@ export const mapInvoicingPlanRevenuePointPayloadToState = (payload) => ({
41
41
  export const mapInvoicingDashboardSummaryPayloadToState = (payload) => ({
42
42
  customerMovement: {
43
43
  churned: payload.customer_movement.churned,
44
+ customerChurnRate: payload.customer_movement.customer_churn_rate ?? undefined,
44
45
  netChange: payload.customer_movement.net_change,
45
46
  new: payload.customer_movement.new,
46
47
  paused: payload.customer_movement.paused,
@@ -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,
@@ -0,0 +1,16 @@
1
+ import { stripCompareAdditionalBalanceTypes, } from '../../commonStateTypes/coaBalance/coaBalanceType';
2
+ // Reports can show the $ and % change columns together, so every requested compare type is kept;
3
+ // compareMode picks one only when none was requested.
4
+ export function getFinanceStatementAdditionalBalances(requestedBalances, isCompareModeOn, compareBalanceFromMode) {
5
+ const otherBalances = stripCompareAdditionalBalanceTypes(requestedBalances);
6
+ if (!isCompareModeOn) {
7
+ return otherBalances;
8
+ }
9
+ const requestedCompareBalances = requestedBalances.filter((balance) => !otherBalances.includes(balance));
10
+ return [
11
+ ...otherBalances,
12
+ ...(requestedCompareBalances.length > 0
13
+ ? requestedCompareBalances
14
+ : [compareBalanceFromMode]),
15
+ ];
16
+ }
@@ -7,6 +7,7 @@ import { getFinanceStatement } from '../financeStatement/financeStatementSelecto
7
7
  import { updateNetBurnOrIncomeAdditionalBalancesSelection } from '../netBurnOrIncome/netBurnOrIncomeReducer';
8
8
  import { updateOpExAdditionalBalancesSelection } from '../opEx/opExReducer';
9
9
  import { updateRevenueAdditionalBalancesSelection } from '../revenue/revenueReducer';
10
+ import { getFinanceStatementAdditionalBalances } from './financeStatementCompareBalances';
10
11
  import { updateReportUIOptionCompareMode } from './reportUIOptionsReducer';
11
12
  export const updateReportUIOptionIsCompareModeEpic = (actions$, state) => actions$.pipe(filter(updateReportUIOptionCompareMode.match), switchMap((action) => {
12
13
  const { additionalBalances } = action.payload;
@@ -15,11 +16,7 @@ export const updateReportUIOptionIsCompareModeEpic = (actions$, state) => action
15
16
  ? 'this_period_vs_last_period_percent'
16
17
  : 'this_period_vs_last_period';
17
18
  const financeStatementState = getFinanceStatement(state.value);
18
- const filteredFinanceAdditionalBalances = additionalBalances.filter((balance) => balance !== 'this_period_vs_last_period' &&
19
- balance !== 'this_period_vs_last_period_percent');
20
- const financeAdditionalBalances = isCompareModeOn
21
- ? [...filteredFinanceAdditionalBalances, additionalBalance]
22
- : filteredFinanceAdditionalBalances;
19
+ const financeAdditionalBalances = getFinanceStatementAdditionalBalances(additionalBalances, isCompareModeOn, additionalBalance);
23
20
  return of(updateNetBurnOrIncomeAdditionalBalancesSelection({
24
21
  isCompareModeOn,
25
22
  additionalBalances: isCompareModeOn ? [additionalBalance] : [],
@@ -7,22 +7,18 @@ import { getFinanceStatement } from '../financeStatement/financeStatementSelecto
7
7
  import { updateNetBurnOrIncomeAdditionalBalancesSelection } from '../netBurnOrIncome/netBurnOrIncomeReducer';
8
8
  import { updateOpExAdditionalBalancesSelection } from '../opEx/opExReducer';
9
9
  import { updateRevenueAdditionalBalancesSelection } from '../revenue/revenueReducer';
10
+ import { getFinanceStatementAdditionalBalances } from './financeStatementCompareBalances';
10
11
  import { toggleReportUIOptionForecastMode, updateReportUIOptionIsCompareModeOn, } from './reportUIOptionsReducer';
11
12
  export const updateReportUIOptionIsCompareModeOnEpic = (actions$, state) => actions$.pipe(filter(updateReportUIOptionIsCompareModeOn.match), switchMap((action) => {
12
13
  const { additionalBalances } = action.payload;
14
+ const compareBalanceFromMode = state.value.reportUIOptionsState.compareMode === 'compare_mode_percent'
15
+ ? 'this_period_vs_last_period_percent'
16
+ : 'this_period_vs_last_period';
13
17
  const additionalBalance = action.payload.isCompareModeOn
14
- ? state.value.reportUIOptionsState.compareMode ===
15
- 'compare_mode_percent'
16
- ? ['this_period_vs_last_period_percent']
17
- : ['this_period_vs_last_period']
18
+ ? [compareBalanceFromMode]
18
19
  : [];
19
20
  const financeStatementState = getFinanceStatement(state.value);
20
- const filteredFinanceAdditionalBalances = additionalBalances.filter((balance) => balance !== 'this_period_vs_last_period' &&
21
- balance !== 'this_period_vs_last_period_percent');
22
- const financeAdditionalBalances = [
23
- ...filteredFinanceAdditionalBalances,
24
- ...additionalBalance,
25
- ];
21
+ const financeAdditionalBalances = getFinanceStatementAdditionalBalances(additionalBalances, action.payload.isCompareModeOn, compareBalanceFromMode);
26
22
  const actions = [
27
23
  updateNetBurnOrIncomeAdditionalBalancesSelection({
28
24
  isCompareModeOn: action.payload.isCompareModeOn,
@@ -34,6 +34,7 @@ export interface MatchTransactionSearchResultPayload {
34
34
  amount?: number | null;
35
35
  currency_code?: string | null;
36
36
  currency_symbol?: string | null;
37
+ line_description?: string | null;
37
38
  sync_token?: string | null;
38
39
  total_amount?: number | null;
39
40
  transaction_description?: string | null;
@@ -53,6 +53,7 @@ function toActualMatchingTransactionSearchResult(payload, fallbackCurrency) {
53
53
  currencyCode,
54
54
  currencySymbol: resolveMatchSearchCurrencySymbol(payload, currencyCode, fallbackCurrency),
55
55
  transactionMemo: payload.transaction_memo ?? undefined,
56
+ lineDescription: payload.line_description ?? undefined,
56
57
  transactionDescription: payload.transaction_description ?? undefined,
57
58
  transactionTypeName: payload.transaction_type_name ?? undefined,
58
59
  syncToken: payload.sync_token == null ? '' : String(payload.sync_token),
@@ -46,6 +46,8 @@ export interface ActualMatchingTransactionSearchResult {
46
46
  accountingClassId?: ID;
47
47
  accountingClassName?: string;
48
48
  accountName?: string;
49
+ /** QBO line description. Distinct from the header transaction description. */
50
+ lineDescription?: string;
49
51
  transactionDescription?: string;
50
52
  transactionMemo?: string;
51
53
  transactionTypeName?: string;
@@ -3,6 +3,7 @@ var _a;
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.updateFinanceStatementFormat = exports.updateFinanceStatementDisplayOrder = exports.updateDownloadState = exports.updateFinanceStatementAdditionalBalancesSelection = exports.updateSelectedReportId = exports.updateCOABalancesRange = exports.updateFinanceStatementThisPeriod = exports.updateFinanceStatementTimeframe = exports.fetchFinanceStatement = exports.initialFinanceStatementState = void 0;
5
5
  const toolkit_1 = require("@reduxjs/toolkit");
6
+ const coaBalanceType_1 = require("../../commonStateTypes/coaBalance/coaBalanceType");
6
7
  const thisPeriodHelpers_1 = require("../../commonStateTypes/viewAndReport/thisPeriodHelpers");
7
8
  const rootActions_1 = require("../../rootActions");
8
9
  const zeniDayJS_1 = require("../../zeniDayJS");
@@ -92,17 +93,11 @@ const financeStatement = (0, toolkit_1.createSlice)({
92
93
  updateFinanceStatementAdditionalBalancesSelection(draft, action) {
93
94
  const { additionalBalances: additionalBalancesPayload, maxNumOfPeriodsToHighlight, } = action.payload;
94
95
  const additionalBalances = additionalBalancesPayload ?? [];
95
- let tempAdditionalBalances = [...additionalBalances];
96
- if (additionalBalances.includes('this_period_vs_last_period') ||
97
- additionalBalances.includes('this_period_vs_last_period_percent')) {
98
- const id = tempAdditionalBalances.findIndex((balance) => balance === 'this_period_vs_last_period' ||
99
- balance === 'this_period_vs_last_period_percent');
100
- if (id > -1) {
101
- const balance = tempAdditionalBalances.splice(id, 1);
102
- tempAdditionalBalances = [...balance, ...tempAdditionalBalances];
103
- }
104
- }
105
- draft.selectedAdditionalBalances = tempAdditionalBalances;
96
+ const otherBalances = (0, coaBalanceType_1.stripCompareAdditionalBalanceTypes)(additionalBalances);
97
+ draft.selectedAdditionalBalances = [
98
+ ...additionalBalances.filter((balance) => !otherBalances.includes(balance)),
99
+ ...otherBalances,
100
+ ];
106
101
  draft.maxNumOfPeriodsToHighlight = maxNumOfPeriodsToHighlight;
107
102
  draft.isAdditionalBalancesShown =
108
103
  additionalBalances.length != 0 ? true : false;
@@ -46,6 +46,11 @@ export interface InvoicingDashboardSummaryPayload {
46
46
  resumed: number;
47
47
  total_active: number;
48
48
  total_active_value: number;
49
+ /**
50
+ * Churned opening-cohort customers ÷ customers at month start, as a 0-100
51
+ * percent. Customer-scoped, unlike the subscription-scoped KPI churn rate.
52
+ */
53
+ customer_churn_rate?: number | null;
49
54
  };
50
55
  failed_payments: {
51
56
  amount: number;
@@ -49,6 +49,7 @@ exports.mapInvoicingPlanRevenuePointPayloadToState = mapInvoicingPlanRevenuePoin
49
49
  const mapInvoicingDashboardSummaryPayloadToState = (payload) => ({
50
50
  customerMovement: {
51
51
  churned: payload.customer_movement.churned,
52
+ customerChurnRate: payload.customer_movement.customer_churn_rate ?? undefined,
52
53
  netChange: payload.customer_movement.net_change,
53
54
  new: payload.customer_movement.new,
54
55
  paused: payload.customer_movement.paused,
@@ -50,6 +50,8 @@ export interface InvoicingDashboardSummary {
50
50
  resumed: number;
51
51
  totalActive: number;
52
52
  totalActiveValue: Amount;
53
+ /** 0-100 percent; absent from a backend that predates it. */
54
+ customerChurnRate?: number;
53
55
  };
54
56
  failedPayments: {
55
57
  amount: Amount;
@@ -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,
@@ -0,0 +1,2 @@
1
+ import { COABalanceType } from '../../commonStateTypes/coaBalance/coaBalanceType';
2
+ export declare function getFinanceStatementAdditionalBalances(requestedBalances: COABalanceType[], isCompareModeOn: boolean, compareBalanceFromMode: COABalanceType): COABalanceType[];
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getFinanceStatementAdditionalBalances = getFinanceStatementAdditionalBalances;
4
+ const coaBalanceType_1 = require("../../commonStateTypes/coaBalance/coaBalanceType");
5
+ // Reports can show the $ and % change columns together, so every requested compare type is kept;
6
+ // compareMode picks one only when none was requested.
7
+ function getFinanceStatementAdditionalBalances(requestedBalances, isCompareModeOn, compareBalanceFromMode) {
8
+ const otherBalances = (0, coaBalanceType_1.stripCompareAdditionalBalanceTypes)(requestedBalances);
9
+ if (!isCompareModeOn) {
10
+ return otherBalances;
11
+ }
12
+ const requestedCompareBalances = requestedBalances.filter((balance) => !otherBalances.includes(balance));
13
+ return [
14
+ ...otherBalances,
15
+ ...(requestedCompareBalances.length > 0
16
+ ? requestedCompareBalances
17
+ : [compareBalanceFromMode]),
18
+ ];
19
+ }
@@ -10,6 +10,7 @@ const financeStatementSelector_1 = require("../financeStatement/financeStatement
10
10
  const netBurnOrIncomeReducer_1 = require("../netBurnOrIncome/netBurnOrIncomeReducer");
11
11
  const opExReducer_1 = require("../opEx/opExReducer");
12
12
  const revenueReducer_1 = require("../revenue/revenueReducer");
13
+ const financeStatementCompareBalances_1 = require("./financeStatementCompareBalances");
13
14
  const reportUIOptionsReducer_1 = require("./reportUIOptionsReducer");
14
15
  const updateReportUIOptionIsCompareModeEpic = (actions$, state) => actions$.pipe((0, operators_1.filter)(reportUIOptionsReducer_1.updateReportUIOptionCompareMode.match), (0, operators_1.switchMap)((action) => {
15
16
  const { additionalBalances } = action.payload;
@@ -18,11 +19,7 @@ const updateReportUIOptionIsCompareModeEpic = (actions$, state) => actions$.pipe
18
19
  ? 'this_period_vs_last_period_percent'
19
20
  : 'this_period_vs_last_period';
20
21
  const financeStatementState = (0, financeStatementSelector_1.getFinanceStatement)(state.value);
21
- const filteredFinanceAdditionalBalances = additionalBalances.filter((balance) => balance !== 'this_period_vs_last_period' &&
22
- balance !== 'this_period_vs_last_period_percent');
23
- const financeAdditionalBalances = isCompareModeOn
24
- ? [...filteredFinanceAdditionalBalances, additionalBalance]
25
- : filteredFinanceAdditionalBalances;
22
+ const financeAdditionalBalances = (0, financeStatementCompareBalances_1.getFinanceStatementAdditionalBalances)(additionalBalances, isCompareModeOn, additionalBalance);
26
23
  return (0, rxjs_1.of)((0, netBurnOrIncomeReducer_1.updateNetBurnOrIncomeAdditionalBalancesSelection)({
27
24
  isCompareModeOn,
28
25
  additionalBalances: isCompareModeOn ? [additionalBalance] : [],
@@ -10,22 +10,18 @@ const financeStatementSelector_1 = require("../financeStatement/financeStatement
10
10
  const netBurnOrIncomeReducer_1 = require("../netBurnOrIncome/netBurnOrIncomeReducer");
11
11
  const opExReducer_1 = require("../opEx/opExReducer");
12
12
  const revenueReducer_1 = require("../revenue/revenueReducer");
13
+ const financeStatementCompareBalances_1 = require("./financeStatementCompareBalances");
13
14
  const reportUIOptionsReducer_1 = require("./reportUIOptionsReducer");
14
15
  const updateReportUIOptionIsCompareModeOnEpic = (actions$, state) => actions$.pipe((0, operators_1.filter)(reportUIOptionsReducer_1.updateReportUIOptionIsCompareModeOn.match), (0, operators_1.switchMap)((action) => {
15
16
  const { additionalBalances } = action.payload;
17
+ const compareBalanceFromMode = state.value.reportUIOptionsState.compareMode === 'compare_mode_percent'
18
+ ? 'this_period_vs_last_period_percent'
19
+ : 'this_period_vs_last_period';
16
20
  const additionalBalance = action.payload.isCompareModeOn
17
- ? state.value.reportUIOptionsState.compareMode ===
18
- 'compare_mode_percent'
19
- ? ['this_period_vs_last_period_percent']
20
- : ['this_period_vs_last_period']
21
+ ? [compareBalanceFromMode]
21
22
  : [];
22
23
  const financeStatementState = (0, financeStatementSelector_1.getFinanceStatement)(state.value);
23
- const filteredFinanceAdditionalBalances = additionalBalances.filter((balance) => balance !== 'this_period_vs_last_period' &&
24
- balance !== 'this_period_vs_last_period_percent');
25
- const financeAdditionalBalances = [
26
- ...filteredFinanceAdditionalBalances,
27
- ...additionalBalance,
28
- ];
24
+ const financeAdditionalBalances = (0, financeStatementCompareBalances_1.getFinanceStatementAdditionalBalances)(additionalBalances, action.payload.isCompareModeOn, compareBalanceFromMode);
29
25
  const actions = [
30
26
  (0, netBurnOrIncomeReducer_1.updateNetBurnOrIncomeAdditionalBalancesSelection)({
31
27
  isCompareModeOn: action.payload.isCompareModeOn,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.88",
3
+ "version": "5.2.89-beta0MM",
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",