@zeniai/client-epic-state 5.2.12 → 5.2.13-betaAK0

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 (37) hide show
  1. package/lib/esm/index.js +5 -4
  2. package/lib/esm/view/expenseAutomationView/epics/accountRecon/confirmStatementMappingEpic.js +23 -7
  3. package/lib/esm/view/expenseAutomationView/epics/accountRecon/updateCombinedStatementInfoEpic.js +1 -1
  4. package/lib/esm/view/expenseAutomationView/payload/reconciliationPayload.js +1 -1
  5. package/lib/esm/view/expenseAutomationView/reducers/reconciliationViewReducer.js +5 -0
  6. package/lib/esm/view/expenseAutomationView/selectors/reconciliationViewSelector.js +33 -2
  7. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.js +24 -7
  8. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +1 -1
  9. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +6 -8
  10. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogSubFamilyEpics.js +8 -2
  11. package/lib/esm/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +15 -10
  12. package/lib/esm/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +0 -1
  13. package/lib/esm/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +15 -36
  14. package/lib/index.d.ts +7 -5
  15. package/lib/index.js +49 -46
  16. package/lib/view/expenseAutomationView/epics/accountRecon/confirmStatementMappingEpic.js +23 -7
  17. package/lib/view/expenseAutomationView/epics/accountRecon/updateCombinedStatementInfoEpic.js +1 -1
  18. package/lib/view/expenseAutomationView/payload/reconciliationPayload.d.ts +8 -3
  19. package/lib/view/expenseAutomationView/payload/reconciliationPayload.js +1 -1
  20. package/lib/view/expenseAutomationView/reducers/reconciliationViewReducer.js +5 -0
  21. package/lib/view/expenseAutomationView/selectors/reconciliationViewSelector.d.ts +3 -1
  22. package/lib/view/expenseAutomationView/selectors/reconciliationViewSelector.js +34 -2
  23. package/lib/view/expenseAutomationView/types/reconciliationViewState.d.ts +6 -0
  24. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.d.ts +11 -2
  25. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/catalogDetailEditPayload.js +24 -7
  26. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.d.ts +7 -0
  27. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewPayload.js +4 -3
  28. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewReducer.d.ts +19 -4
  29. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.d.ts +5 -4
  30. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/editInvoicingCatalogItemDetailViewSelector.js +5 -7
  31. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogSubFamilyEpics.d.ts +1 -1
  32. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/invoicingCatalogSubFamilyEpics.js +8 -2
  33. package/lib/view/invoicing/editInvoicingCatalogItemDetailView/saveInvoicingCatalogItemEpic.js +14 -9
  34. package/lib/view/invoicing/invoicingQboView/epics/fetchInvoicingAccountingPickersEpic.js +0 -1
  35. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.d.ts +12 -13
  36. package/lib/view/invoicing/invoicingQboView/invoicingQboViewSelector.js +16 -38
  37. package/package.json +1 -1
@@ -271,7 +271,7 @@ function transformConfirmStatementMappingConflictToState(response) {
271
271
  }
272
272
  function transformUpdateStatementInfoPayloadToState(payload) {
273
273
  if (isUpdateStatementInfoCombinedResponsePayload(payload) === true) {
274
- const statementUploads = payload.statement_uploads.map(transformStatementUploadPayloadToState);
274
+ const statementUploads = payload.statement_uploads.map((item) => transformStatementUploadPayloadToState(item.statement_upload));
275
275
  const anchorUpload = statementUploads.find((upload) => upload.statementMeta.isCombinedStatementAnchor === true) ?? statementUploads[0];
276
276
  return {
277
277
  statementUpload: anchorUpload,
@@ -469,6 +469,7 @@ const expenseAutomationReconciliationView = (0, toolkit_1.createSlice)({
469
469
  accountRecon.confirmStatementMappingConflict = null;
470
470
  if (accountRecon.localData != null) {
471
471
  accountRecon.localData.statementUpdateLocalDataByUploadId = undefined;
472
+ accountRecon.localData.combinedStatementAccountMappings = undefined;
472
473
  }
473
474
  if (statementAccountScope != null) {
474
475
  accountRecon.localData = {
@@ -547,6 +548,7 @@ const expenseAutomationReconciliationView = (0, toolkit_1.createSlice)({
547
548
  accountRecon.confirmStatementMappingConflict = null;
548
549
  if (accountRecon.localData != null) {
549
550
  accountRecon.localData.statementUpdateLocalDataByUploadId = undefined;
551
+ accountRecon.localData.combinedStatementAccountMappings = undefined;
550
552
  accountRecon.localData.statementAccountScope = undefined;
551
553
  }
552
554
  }
@@ -777,6 +779,9 @@ const expenseAutomationReconciliationView = (0, toolkit_1.createSlice)({
777
779
  };
778
780
  accountRecon.confirmStatementMappingConflict = null;
779
781
  accountRecon.confirmedCombinedStatementUploads = statementUploads;
782
+ if (accountRecon.localData != null) {
783
+ accountRecon.localData.combinedStatementAccountMappings = undefined;
784
+ }
780
785
  const combinedStatementDetails = accountRecon.parsedStatementData?.statementUpload
781
786
  .combinedStatementDetails;
782
787
  if (combinedStatementDetails != null) {
@@ -5,6 +5,8 @@ import { RootState } from '../../../rootStateTypes';
5
5
  import { AccountReconciliationByAccount, ExpenseAutomationReconciliationViewSelector } from '../selectorTypes/reconciliationViewSelectorTypes';
6
6
  export declare const getExpenseAutomationReconciliationView: (state: RootState) => ExpenseAutomationReconciliationViewSelector;
7
7
  export declare const getReparseStatementStatusByAccountId: (state: RootState, accountId: ID) => import("../../../commonStateTypes/common").FetchStateAndError;
8
- export declare function getLedgerAccountsForCombinedStatementMapping(accountReconciliationsByAccountID: Record<ID, AccountReconciliationByAccount>): AccountBase[];
8
+ export declare function getLedgerAccountsForCombinedStatementMapping(accountReconciliationsByAccountID: Record<ID, AccountReconciliationByAccount>, selectedAccountId: ID): AccountBase[];
9
9
  export declare function getUnavailableLedgerAccountIds(accountReconciliationsByAccountID: Record<ID, AccountReconciliationByAccount>): ID[];
10
+ /** Selectable accounts that already have a statement — mapping will override that work. */
11
+ export declare function getLedgerAccountIdsWithStatementOverrideWarning(accountReconciliationsByAccountID: Record<ID, AccountReconciliationByAccount>, selectedAccountId?: ID): ID[];
10
12
  export declare const isAccountReconReport: (reportId: ReportIDPlusForecastID) => boolean;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isAccountReconReport = exports.getReparseStatementStatusByAccountId = exports.getExpenseAutomationReconciliationView = void 0;
7
7
  exports.getLedgerAccountsForCombinedStatementMapping = getLedgerAccountsForCombinedStatementMapping;
8
8
  exports.getUnavailableLedgerAccountIds = getUnavailableLedgerAccountIds;
9
+ exports.getLedgerAccountIdsWithStatementOverrideWarning = getLedgerAccountIdsWithStatementOverrideWarning;
9
10
  const toolkit_1 = require("@reduxjs/toolkit");
10
11
  const big_js_1 = __importDefault(require("big.js"));
11
12
  const keys_1 = __importDefault(require("lodash/keys"));
@@ -484,8 +485,18 @@ exports.getExpenseAutomationReconciliationView = (0, toolkit_1.createSelector)((
484
485
  const getReparseStatementStatusByAccountId = (state, accountId) => (0, exports.getExpenseAutomationReconciliationView)(state)
485
486
  .accountReconciliationsByAccountID[accountId]?.reparseStatementStatus;
486
487
  exports.getReparseStatementStatusByAccountId = getReparseStatementStatusByAccountId;
487
- function getLedgerAccountsForCombinedStatementMapping(accountReconciliationsByAccountID) {
488
- return Object.values(accountReconciliationsByAccountID).map((accountReconciliation) => accountReconciliation.accountReconEntity.account);
488
+ function getLedgerAccountsForCombinedStatementMapping(accountReconciliationsByAccountID, selectedAccountId) {
489
+ const selectedAccountType = accountReconciliationsByAccountID[selectedAccountId]?.accountReconEntity
490
+ .account.accountType;
491
+ const shouldFilterByPaymentAccountType = selectedAccountType === 'bank' || selectedAccountType === 'credit_card';
492
+ return Object.values(accountReconciliationsByAccountID)
493
+ .map((accountReconciliation) => accountReconciliation.accountReconEntity.account)
494
+ .filter((account) => {
495
+ if (shouldFilterByPaymentAccountType !== true) {
496
+ return true;
497
+ }
498
+ return account.accountType === selectedAccountType;
499
+ });
489
500
  }
490
501
  const UNAVAILABLE_LEDGER_ACCOUNT_RECON_STATUSES = new Set(['in_progress', 'reconciled', 'reconciled_on_qbo']);
491
502
  function getUnavailableLedgerAccountIds(accountReconciliationsByAccountID) {
@@ -497,6 +508,27 @@ function getUnavailableLedgerAccountIds(accountReconciliationsByAccountID) {
497
508
  })
498
509
  .map((accountReconciliation) => accountReconciliation.accountReconEntity.account.accountId);
499
510
  }
511
+ /** Selectable accounts that already have a statement — mapping will override that work. */
512
+ function getLedgerAccountIdsWithStatementOverrideWarning(accountReconciliationsByAccountID, selectedAccountId) {
513
+ return Object.values(accountReconciliationsByAccountID)
514
+ .filter((accountReconciliation) => {
515
+ const entity = accountReconciliation.accountReconEntity;
516
+ const accountId = entity.account.accountId;
517
+ if (selectedAccountId != null && accountId === selectedAccountId) {
518
+ return false;
519
+ }
520
+ const hasUploadedStatement = entity.statementStatus?.code === 'uploaded' ||
521
+ entity.statementMetaData?.file != null ||
522
+ accountReconciliation.statementParseInProgress === true;
523
+ if (hasUploadedStatement !== true) {
524
+ return false;
525
+ }
526
+ const statusCode = entity.reconciliationStatus?.code;
527
+ return (statusCode == null ||
528
+ UNAVAILABLE_LEDGER_ACCOUNT_RECON_STATUSES.has(statusCode) !== true);
529
+ })
530
+ .map((accountReconciliation) => accountReconciliation.accountReconEntity.account.accountId);
531
+ }
500
532
  const isAccountReconReport = (reportId) => {
501
533
  return reportId.endsWith('reconciliation');
502
534
  };
@@ -358,8 +358,14 @@ export declare const toStatementAccountScope: (v: string) => "single_account" |
358
358
  export type StatementAccountScope = ReturnType<typeof toStatementAccountScope>;
359
359
  export declare const toStatementAccountScopeStrict: (v: string | null | undefined) => StatementAccountScope | undefined;
360
360
  export declare const STATEMENT_ACCOUNT_SCOPES: readonly StatementAccountScope[];
361
+ export interface CombinedStatementAccountMappingDraft {
362
+ accountId: ID;
363
+ ledgerIdentifier: string;
364
+ }
361
365
  export interface AccountReconciliationLocalData {
362
366
  accountSource?: ReconciliationAccountSourceType;
367
+ /** Draft ledger mappings saved when continuing combined mapping in background. */
368
+ combinedStatementAccountMappings?: CombinedStatementAccountMappingDraft[];
363
369
  statementAccountScope?: StatementAccountScope;
364
370
  /** Confirm & Save drafts keyed by statementUploadId (single and combined). */
365
371
  statementUpdateLocalDataByUploadId?: Record<string, StatementUpdateLocalData>;
@@ -1,6 +1,7 @@
1
1
  import { Amount } from '../../../commonStateTypes/amount';
2
2
  import type { InvoicingPlan } from '../../../entity/invoicing/plan/planState';
3
3
  import type { InvoicingProduct } from '../../../entity/invoicing/product/productState';
4
+ import { type InvoicingCatalogAccountingLookup } from './editInvoicingCatalogItemDetailViewPayload';
4
5
  import type { InvoicingCatalogItemFormSubFamily, InvoicingCatalogItemFormTier } from './editInvoicingCatalogItemDetailViewState';
5
6
  export type CatalogDetailSubFamilyEditValues = InvoicingCatalogItemFormSubFamily;
6
7
  export interface CatalogDetailEditValues {
@@ -30,18 +31,26 @@ export interface CatalogDetailSavePayload {
30
31
  }
31
32
  /** Build default inline-edit form values from a loaded catalog item. */
32
33
  export declare const planToCatalogDetailEditValues: (plan: InvoicingPlan) => CatalogDetailEditValues;
33
- export declare const catalogDetailEditValuesToSavePayload: (values: CatalogDetailEditValues, itemType?: string, originalSubFamilies?: readonly InvoicingProduct[]) => CatalogDetailSavePayload;
34
+ export declare const catalogDetailEditValuesToSavePayload: (values: CatalogDetailEditValues, lookup: InvoicingCatalogAccountingLookup, itemType?: string, originalSubFamilies?: readonly InvoicingProduct[]) => CatalogDetailSavePayload;
34
35
  export interface CreateSubFamilyFormValues {
36
+ /** The QuickBooks class id the price point posts to. */
37
+ accountingCategory: string;
38
+ /** The QuickBooks income account id the price point posts to. */
39
+ accountingCode: string;
35
40
  billingCycles: string;
36
41
  description: string;
37
42
  freeQuantity: string;
38
43
  invoiceNotes: string;
44
+ isTaxable: boolean;
39
45
  name: string;
40
46
  period: string;
41
47
  periodUnit: string;
42
48
  priceCode: string;
43
49
  pricingModel: string;
44
50
  prorationType: string;
51
+ showDescriptionInInvoices: boolean;
52
+ showDescriptionInQuotes: boolean;
53
+ sku: string;
45
54
  trialEndAction: string;
46
55
  trialPeriod: string;
47
56
  trialPeriodUnit: string;
@@ -50,5 +59,5 @@ export interface CreateSubFamilyFormValues {
50
59
  tiers?: InvoicingCatalogItemFormTier[];
51
60
  }
52
61
  /** Build POST /catalog/products body for Create sub-family. */
53
- export declare const createSubFamilyFormToPayload: (itemId: string, itemType: string, values: CreateSubFamilyFormValues) => Record<string, unknown>;
62
+ export declare const createSubFamilyFormToPayload: (itemId: string, itemType: string, values: CreateSubFamilyFormValues, lookup: InvoicingCatalogAccountingLookup) => Record<string, unknown>;
54
63
  export declare const productToCreateSubFamilyFormValues: (product?: InvoicingProduct) => CreateSubFamilyFormValues;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.productToCreateSubFamilyFormValues = exports.createSubFamilyFormToPayload = exports.catalogDetailEditValuesToSavePayload = exports.planToCatalogDetailEditValues = void 0;
4
+ const editInvoicingCatalogItemDetailViewPayload_1 = require("./editInvoicingCatalogItemDetailViewPayload");
4
5
  const invoicingCatalogItemFormConfig_1 = require("./invoicingCatalogItemFormConfig");
5
6
  const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
6
7
  const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
@@ -39,7 +40,7 @@ const tierToPayload = (tier) => ({
39
40
  pricing_type: tier.pricingType,
40
41
  starting_unit: Number(tier.startingUnit) || 1,
41
42
  });
42
- const subFamilyToProductUpdate = (subFamily, itemType) => {
43
+ const subFamilyToProductUpdate = (subFamily, itemType, lookup) => {
43
44
  const tiered = (0, invoicingCatalogItemFormConfig_1.isTieredCatalogPricingModel)(subFamily.pricingModel);
44
45
  const data = {
45
46
  billing_cycles: hasValue(subFamily.billingCycles)
@@ -66,9 +67,12 @@ const subFamilyToProductUpdate = (subFamily, itemType) => {
66
67
  if (itemType === 'addon') {
67
68
  data.proration_type = subFamily.prorationType;
68
69
  }
70
+ // `null` rather than omitted when empty: an absent key on an update reads as
71
+ // "leave it alone", so a cleared account or class could never be saved.
72
+ data.accounting_detail = (0, editInvoicingCatalogItemDetailViewPayload_1.buildAccountingDetail)(subFamily, lookup) ?? null;
69
73
  return { data, id: subFamily.id };
70
74
  };
71
- const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan', originalSubFamilies) => {
75
+ const catalogDetailEditValuesToSavePayload = (values, lookup, itemType = 'plan', originalSubFamilies) => {
72
76
  const planData = {
73
77
  applicable_items: values.itemApplicability === 'restricted' ? values.applicableItems : [],
74
78
  enabled_for_checkout: values.enabledForCheckout,
@@ -100,12 +104,12 @@ const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan', origina
100
104
  planData.unit = unit;
101
105
  }
102
106
  const productUpdates = values.subFamilies.flatMap((subFamily) => {
103
- const update = subFamilyToProductUpdate(subFamily, itemType);
107
+ const update = subFamilyToProductUpdate(subFamily, itemType, lookup);
104
108
  const original = originalSubFamilies?.find((product) => product.id === subFamily.id);
105
109
  if (original == null) {
106
110
  return [update];
107
111
  }
108
- const originalUpdate = subFamilyToProductUpdate(subFamilyToEditValues(original), itemType);
112
+ const originalUpdate = subFamilyToProductUpdate(subFamilyToEditValues(original), itemType, lookup);
109
113
  const changedData = Object.fromEntries(Object.entries(update.data).filter(([key, value]) => JSON.stringify(value) !== JSON.stringify(originalUpdate.data[key])));
110
114
  return Object.keys(changedData).length > 0
111
115
  ? [{ data: changedData, id: update.id }]
@@ -118,21 +122,28 @@ const catalogDetailEditValuesToSavePayload = (values, itemType = 'plan', origina
118
122
  };
119
123
  exports.catalogDetailEditValuesToSavePayload = catalogDetailEditValuesToSavePayload;
120
124
  /** Build POST /catalog/products body for Create sub-family. */
121
- const createSubFamilyFormToPayload = (itemId, itemType, values) => {
125
+ const createSubFamilyFormToPayload = (itemId, itemType, values, lookup) => {
122
126
  const name = values.name.trim();
123
127
  const priceCode = trimOrUndefined(values.priceCode) ?? toProductCode(name);
124
128
  const tiered = (0, invoicingCatalogItemFormConfig_1.isTieredCatalogPricingModel)(values.pricingModel);
125
129
  const payload = {
126
130
  currency_code: 'USD',
127
131
  free_quantity: Number(values.freeQuantity) || 0,
128
- is_taxable: true,
132
+ is_taxable: values.isTaxable,
129
133
  item_id: itemId,
130
134
  name,
131
135
  price: tiered ? 0 : (values.priceDollars?.amount ?? 0),
132
136
  price_code: priceCode,
133
137
  pricing_model: values.pricingModel || 'flat_fee',
134
- show_description_in_invoices: true,
138
+ show_description_in_invoices: values.showDescriptionInInvoices,
139
+ show_description_in_quotes: values.showDescriptionInQuotes,
135
140
  };
141
+ // The same accounting body the item payload sends, so a price point created
142
+ // from the dialog carries the account and class the form would have given it.
143
+ const accountingDetail = (0, editInvoicingCatalogItemDetailViewPayload_1.buildAccountingDetail)(values, lookup);
144
+ if (accountingDetail != null) {
145
+ payload.accounting_detail = accountingDetail;
146
+ }
136
147
  if (tiered) {
137
148
  payload.tiers = (values.tiers ?? []).map(tierToPayload);
138
149
  }
@@ -163,10 +174,13 @@ const createSubFamilyFormToPayload = (itemId, itemType, values) => {
163
174
  };
164
175
  exports.createSubFamilyFormToPayload = createSubFamilyFormToPayload;
165
176
  const productToCreateSubFamilyFormValues = (product) => ({
177
+ accountingCategory: product?.accountingDetail?.accountingCategory1 ?? '',
178
+ accountingCode: product?.accountingDetail?.accountingCode ?? '',
166
179
  billingCycles: product?.billingCycles != null ? String(product.billingCycles) : '',
167
180
  description: product?.description ?? '',
168
181
  freeQuantity: product?.freeQuantity != null ? String(product.freeQuantity) : '0',
169
182
  invoiceNotes: product?.invoiceNotes ?? '',
183
+ isTaxable: product?.isTaxable ?? true,
170
184
  name: product?.name != null ? `${product.name} (copy)` : '',
171
185
  period: product?.period != null ? String(product.period) : '1',
172
186
  periodUnit: product?.periodUnit ?? 'month',
@@ -174,6 +188,9 @@ const productToCreateSubFamilyFormValues = (product) => ({
174
188
  priceDollars: product?.price,
175
189
  pricingModel: product?.pricingModel ?? 'flat_fee',
176
190
  prorationType: product?.prorationType ?? 'site_default',
191
+ showDescriptionInInvoices: product?.showDescriptionInInvoices ?? true,
192
+ showDescriptionInQuotes: product?.showDescriptionInQuotes ?? false,
193
+ sku: product?.accountingDetail?.sku ?? '',
177
194
  trialEndAction: product?.trialEndAction ?? 'site_default',
178
195
  trialPeriod: product?.trialPeriod != null ? String(product.trialPeriod) : '',
179
196
  trialPeriodUnit: product?.trialPeriodUnit ?? 'day',
@@ -13,6 +13,13 @@ export interface InvoicingCatalogAccountingLookup {
13
13
  accountByQboId: (qboId: string) => AccountBase | undefined;
14
14
  classByQboId: (qboId: string) => ClassBase | undefined;
15
15
  }
16
+ /** The accounting fields any price-point form collects. */
17
+ export interface InvoicingCatalogAccountingFields {
18
+ accountingCategory: string;
19
+ accountingCode: string;
20
+ sku: string;
21
+ }
22
+ export declare const buildAccountingDetail: (subFamily: InvoicingCatalogAccountingFields, lookup: InvoicingCatalogAccountingLookup) => Record<string, unknown> | undefined;
16
23
  /**
17
24
  * Build the parent catalog-item request body from the form draft. Creates omit
18
25
  * both parent and sub-family IDs; edits retain returned sub-family IDs.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.localDataToSaveInvoicingCatalogItemPayload = void 0;
3
+ exports.localDataToSaveInvoicingCatalogItemPayload = exports.buildAccountingDetail = void 0;
4
4
  const accountPayload_1 = require("../../../entity/account/accountPayload");
5
5
  const classPayload_1 = require("../../../entity/class/classPayload");
6
6
  const invoicingCatalogItemFormConfig_1 = require("./invoicingCatalogItemFormConfig");
@@ -31,6 +31,7 @@ const buildAccountingDetail = (subFamily, lookup) => {
31
31
  }
32
32
  return Object.keys(detail).length > 0 ? detail : undefined;
33
33
  };
34
+ exports.buildAccountingDetail = buildAccountingDetail;
34
35
  const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) => {
35
36
  const isAddon = localData.catalogType === 'addon';
36
37
  const tiered = (0, invoicingCatalogItemFormConfig_1.isTieredCatalogPricingModel)(subFamily.pricingModel);
@@ -85,7 +86,7 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
85
86
  starting_unit: Number(tier.startingUnit) || 1,
86
87
  }));
87
88
  }
88
- const accountingDetail = buildAccountingDetail(subFamily, lookup);
89
+ const accountingDetail = (0, exports.buildAccountingDetail)(subFamily, lookup);
89
90
  if (accountingDetail != null) {
90
91
  payload.accounting_detail = accountingDetail;
91
92
  }
@@ -142,7 +143,7 @@ const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup)
142
143
  // would make a cleared mapping impossible to save — the same reason
143
144
  // `description` and `invoice_notes` go through `trimOrNull`.
144
145
  detail.accounting_detail =
145
- buildAccountingDetail(subFamily, lookup) ?? null;
146
+ (0, exports.buildAccountingDetail)(subFamily, lookup) ?? null;
146
147
  const id = trimOrUndefined(subFamily.id);
147
148
  return id != null ? { ...detail, id } : detail;
148
149
  });
@@ -1,7 +1,8 @@
1
1
  import { ID } from '../../../commonStateTypes/common';
2
+ import type { InvoicingProduct } from '../../../entity/invoicing/product/productState';
2
3
  import { ZeniAPIStatus } from '../../../responsePayload';
3
4
  import { InvoicingCatalogItemKind } from '../invoicingCatalogItemDetailView/invoicingCatalogItemDetailViewState';
4
- import type { CatalogDetailProductUpdate } from './catalogDetailEditPayload';
5
+ import type { CatalogDetailEditValues, CreateSubFamilyFormValues } from './catalogDetailEditPayload';
5
6
  import { EditInvoicingCatalogItemDetailViewState, InvoicingCatalogItemFormLocalData } from './editInvoicingCatalogItemDetailViewState';
6
7
  /** Push the full form draft into CES (edit tree when `id` is present). */
7
8
  export interface UpdateInvoicingCatalogItemFormDraftPayload {
@@ -18,14 +19,28 @@ export interface SubmitInvoicingCatalogItemFormPayload {
18
19
  * `submitInvoicingCatalogItemForm` instead.
19
20
  */
20
21
  export interface SaveInvoicingCatalogItemPayload {
21
- data: Record<string, unknown>;
22
+ /**
23
+ * The detail page's edit form values. The epic builds the parent body and the
24
+ * per-price-point updates from them, so it can resolve a picked account or
25
+ * class to the record the request carries — a page cannot.
26
+ */
27
+ editValues: CatalogDetailEditValues;
22
28
  kind: InvoicingCatalogItemKind;
23
29
  id?: ID;
24
- productUpdates?: CatalogDetailProductUpdate[];
30
+ /** The catalog item's type, for the price-point bodies. */
31
+ itemType?: string;
32
+ /** The saved price points, so only changed fields are sent. */
33
+ originalSubFamilies?: readonly InvoicingProduct[];
25
34
  }
26
35
  export interface CreateInvoicingCatalogSubFamilyPayload {
27
- data: Record<string, unknown>;
36
+ itemType: string;
28
37
  parentItemId: ID;
38
+ /**
39
+ * The dialog's form values. The request body is built in the epic, which can
40
+ * resolve a picked account id to the record the payload carries — the page
41
+ * holds ids and never needs to know the wire shape.
42
+ */
43
+ values: CreateSubFamilyFormValues;
29
44
  }
30
45
  export interface ArchiveInvoicingCatalogSubFamilyPayload {
31
46
  parentItemId: ID;
@@ -48,12 +48,13 @@ export interface InvoicingCatalogItemFormView extends SelectorView {
48
48
  pricingModelValues: readonly string[];
49
49
  prorationValues: readonly string[];
50
50
  /**
51
- * Account and class trees for the price-point pickers, already narrowed to the
52
- * income accounts a price may post to. Empty until QBO is connected — the form
53
- * must not offer a mapping that cannot be pushed.
51
+ * Account and class trees for the price-point pickers, straight from the
52
+ * shared accounting lists — the pickers drop the nodes they cannot submit.
53
+ * Empty until QBO is connected: the form must not offer a mapping that cannot
54
+ * be pushed.
54
55
  */
56
+ qboAccountHierarchy: NestedAccountHierarchyForReport[];
55
57
  qboClassHierarchy: NestedClassHierarchyForReport[];
56
- qboIncomeAccountHierarchy: NestedAccountHierarchyForReport[];
57
58
  tierTypeValues: readonly string[];
58
59
  trialEndActionOptions: InvoicingNamedOption[];
59
60
  trialUnitOptions: InvoicingNamedOption[];
@@ -55,17 +55,15 @@ const getInvoicingCatalogItemFormView = (state, id) => {
55
55
  const applicableItemOptions = id != null
56
56
  ? allApplicableItemOptions.filter((option) => option.id !== id)
57
57
  : allApplicableItemOptions;
58
- const isQboConnected = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state);
58
+ const qboView = (0, invoicingQboViewSelector_1.isInvoicingQboConnected)(state)
59
+ ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state)
60
+ : undefined;
59
61
  const { billingPeriodUnitOptions, catalogItemTypeOptions, catalogTierTypeOptionsByPricingModel, catalogTrialEndActionOptions, catalogTrialUnitOptions, } = (0, invoicingConfigViewSelector_1.getInvoicingConfigView)(state);
60
62
  return {
61
63
  fieldHelp: (0, invoicingConfigViewSelector_1.invoicingFieldHelpForGroup)(state, 'catalog'),
62
64
  isQboPickerDataLoading: (0, invoicingQboViewSelector_1.isInvoicingAccountingPickerDataLoading)(state),
63
- qboClassHierarchy: isQboConnected
64
- ? (0, invoicingQboViewSelector_1.getInvoicingQboView)(state).nestedClassHierarchy
65
- : [],
66
- qboIncomeAccountHierarchy: isQboConnected
67
- ? (0, invoicingQboViewSelector_1.invoicingAccountHierarchyForTypes)(state, invoicingQboViewSelector_1.INVOICING_INCOME_ACCOUNT_TYPES)
68
- : [],
65
+ qboAccountHierarchy: qboView?.nestedAccountHierarchy ?? [],
66
+ qboClassHierarchy: qboView?.nestedClassHierarchy ?? [],
69
67
  applicabilityValues: invoicingCatalogItemFormConfig_1.INVOICING_CATALOG_APPLICABILITY_VALUES,
70
68
  applicableItemOptions,
71
69
  localData,
@@ -5,5 +5,5 @@ import { RootState } from '../../../rootStateTypes';
5
5
  import { ZeniAPI } from '../../../zeniAPI';
6
6
  import { archiveInvoicingCatalogSubFamily, createInvoicingCatalogSubFamily, submitInvoicingCatalogItemFormFailure, submitInvoicingCatalogItemFormSuccess } from './editInvoicingCatalogItemDetailViewReducer';
7
7
  export type ActionType = ReturnType<typeof createInvoicingCatalogSubFamily> | ReturnType<typeof archiveInvoicingCatalogSubFamily> | ReturnType<typeof mergeInvoicingPlanSubFamily> | ReturnType<typeof submitInvoicingCatalogItemFormSuccess> | ReturnType<typeof submitInvoicingCatalogItemFormFailure>;
8
- export declare const createInvoicingCatalogSubFamilyEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
8
+ export declare const createInvoicingCatalogSubFamilyEpic: (actions$: ActionsObservable<ActionType>, state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
9
9
  export declare const archiveInvoicingCatalogSubFamilyEpic: (actions$: ActionsObservable<ActionType>, _state$: StateObservable<RootState>, zeniAPI: ZeniAPI) => Observable<ActionType>;
@@ -5,6 +5,8 @@ const rxjs_1 = require("rxjs");
5
5
  const operators_1 = require("rxjs/operators");
6
6
  const planReducer_1 = require("../../../entity/invoicing/plan/planReducer");
7
7
  const responsePayload_1 = require("../../../responsePayload");
8
+ const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
9
+ const catalogDetailEditPayload_1 = require("./catalogDetailEditPayload");
8
10
  const editInvoicingCatalogItemDetailViewReducer_1 = require("./editInvoicingCatalogItemDetailViewReducer");
9
11
  const subFamilySuccessActions = (parentItemId, subFamilyPayload) => [
10
12
  (0, planReducer_1.mergeInvoicingPlanSubFamily)(parentItemId, subFamilyPayload),
@@ -13,8 +15,12 @@ const subFamilySuccessActions = (parentItemId, subFamilyPayload) => [
13
15
  kind: 'plan',
14
16
  }),
15
17
  ];
16
- const createInvoicingCatalogSubFamilyEpic = (actions$, _state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(editInvoicingCatalogItemDetailViewReducer_1.createInvoicingCatalogSubFamily.match), (0, operators_1.mergeMap)((action) => {
17
- const { data, parentItemId } = action.payload;
18
+ const createInvoicingCatalogSubFamilyEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)(editInvoicingCatalogItemDetailViewReducer_1.createInvoicingCatalogSubFamily.match), (0, operators_1.mergeMap)((action) => {
19
+ const { itemType, parentItemId, values } = action.payload;
20
+ const state = state$.value;
21
+ // Built here so the dialog can stay in picked ids: resolving one to the
22
+ // account record the payload carries needs the shared account list.
23
+ const data = (0, catalogDetailEditPayload_1.createSubFamilyFormToPayload)(parentItemId, itemType, values, (0, invoicingQboViewSelector_1.getInvoicingCatalogAccountingLookup)(state));
18
24
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
19
25
  return zeniAPI
20
26
  .postAndGetJSON(`${baseUrl}/catalog/products`, data)
@@ -10,25 +10,30 @@ const snackbarReducer_1 = require("../../../entity/snackbar/snackbarReducer");
10
10
  const responsePayload_1 = require("../../../responsePayload");
11
11
  const invoicingCatalogListViewReducer_1 = require("../invoicingCatalogListView/invoicingCatalogListViewReducer");
12
12
  const invoicingQboViewSelector_1 = require("../invoicingQboView/invoicingQboViewSelector");
13
+ const catalogDetailEditPayload_1 = require("./catalogDetailEditPayload");
13
14
  const editInvoicingCatalogItemDetailViewPayload_1 = require("./editInvoicingCatalogItemDetailViewPayload");
14
15
  const editInvoicingCatalogItemDetailViewReducer_1 = require("./editInvoicingCatalogItemDetailViewReducer");
15
16
  const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actions$.pipe((0, operators_1.filter)((action) => editInvoicingCatalogItemDetailViewReducer_1.submitInvoicingCatalogItemForm.match(action) ||
16
17
  editInvoicingCatalogItemDetailViewReducer_1.saveInvoicingCatalogItem.match(action)), (0, operators_1.mergeMap)((action) => {
17
18
  const { id } = action.payload;
18
- // Draft-based create/edit form vs. detail-page inline edit (raw data).
19
+ const state = state$.value;
20
+ const accountingLookup = (0, invoicingQboViewSelector_1.getInvoicingCatalogAccountingLookup)(state);
21
+ // Both sources are form values, turned into request bodies here so the
22
+ // picked account and class become the records the service stores: the
23
+ // detail page's edit form, or the create/edit form's draft.
19
24
  let data;
25
+ let editProductUpdates;
20
26
  if (editInvoicingCatalogItemDetailViewReducer_1.saveInvoicingCatalogItem.match(action)) {
21
- data = action.payload.data;
27
+ const { editValues, itemType, originalSubFamilies } = action.payload;
28
+ const savePayload = (0, catalogDetailEditPayload_1.catalogDetailEditValuesToSavePayload)(editValues, accountingLookup, itemType, originalSubFamilies);
29
+ data = savePayload.planData;
30
+ editProductUpdates = savePayload.productUpdates;
22
31
  }
23
32
  else {
24
- const viewState = state$.value.editInvoicingCatalogItemDetailViewState;
33
+ const viewState = state.editInvoicingCatalogItemDetailViewState;
25
34
  const localData = (id != null ? viewState.editDraftById[id] : viewState.newItemDraft) ??
26
35
  viewState.newItemDraft;
27
- const state = state$.value;
28
- data = (0, editInvoicingCatalogItemDetailViewPayload_1.localDataToSaveInvoicingCatalogItemPayload)(localData, id == null, {
29
- accountByQboId: (qboId) => (0, invoicingQboViewSelector_1.invoicingQboAccountByQboId)(state, qboId),
30
- classByQboId: (qboId) => (0, invoicingQboViewSelector_1.invoicingQboClassByQboId)(state, qboId),
31
- });
36
+ data = (0, editInvoicingCatalogItemDetailViewPayload_1.localDataToSaveInvoicingCatalogItemPayload)(localData, id == null, accountingLookup);
32
37
  }
33
38
  const baseUrl = `${zeniAPI.apiEndPoints.invoicingMicroServiceBaseUrl}/1.0`;
34
39
  const path = id != null
@@ -91,7 +96,7 @@ const saveInvoicingCatalogItemEpic = (actions$, state$, zeniAPI) => actions$.pip
91
96
  return resultActions;
92
97
  };
93
98
  const productUpdates = editInvoicingCatalogItemDetailViewReducer_1.saveInvoicingCatalogItem.match(action)
94
- ? (action.payload.productUpdates ?? [])
99
+ ? (editProductUpdates ?? [])
95
100
  : [];
96
101
  if (productUpdates.length === 0) {
97
102
  return completeSave([]);
@@ -21,7 +21,6 @@ const fetchInvoicingAccountingPickersEpic = (actions$, state$) => actions$.pipe(
21
21
  accountList.fetchState !== 'In-Progress') {
22
22
  // Same arguments the vendor and categorization screens pass, so this
23
23
  // shares their cached list rather than replacing it with a narrower one.
24
- // Invoicing narrows to income accounts when it builds its options.
25
24
  actions.push((0, accountListReducer_1.fetchAccountList)(invoicingQboViewSelector_1.INVOICING_ACCOUNT_LIST_KEY, true));
26
25
  }
27
26
  const classList = state.classListState;
@@ -1,7 +1,7 @@
1
1
  import { NestedAccountHierarchyForReport } from '../../../commonStateTypes/accountView/nestedAccountID';
2
2
  import { NestedClassHierarchyForReport } from '../../../commonStateTypes/classesView/nestedClassID';
3
3
  import { FetchStateAndError } from '../../../commonStateTypes/common';
4
- import { AccountBase, AccountType } from '../../../entity/account/accountState';
4
+ import { AccountBase } from '../../../entity/account/accountState';
5
5
  import { ClassBase } from '../../../entity/class/classState';
6
6
  import { RootState } from '../../../rootStateTypes';
7
7
  import { InvoicingQboConnection, InvoicingQboSyncHealth } from './invoicingQboViewPayload';
@@ -16,11 +16,6 @@ export declare const isInvoicingQboConnected: (state: RootState) => boolean;
16
16
  export declare const isInvoicingAccountingPickerDataLoading: (state: RootState) => boolean;
17
17
  /** The QBO connection fetch on its own, without building the bundled view. */
18
18
  export declare const getInvoicingQboConnectionFetchState: (state: RootState) => FetchStateAndError;
19
- /**
20
- * Account types that can back invoicing revenue. Named here so screens ask for
21
- * "income accounts" rather than repeating the accounting service's vocabulary.
22
- */
23
- export declare const INVOICING_INCOME_ACCOUNT_TYPES: readonly AccountType[];
24
19
  /**
25
20
  * The account behind a picker value. The pickers submit the QuickBooks id, so
26
21
  * that is what callers hold; the payload builders need the whole entity to send
@@ -29,6 +24,17 @@ export declare const INVOICING_INCOME_ACCOUNT_TYPES: readonly AccountType[];
29
24
  export declare const invoicingQboAccountByQboId: (state: RootState, qboId: string | undefined) => AccountBase | undefined;
30
25
  /** The class behind a picker value, for the same reason as the account above. */
31
26
  export declare const invoicingQboClassByQboId: (state: RootState, qboId: string | undefined) => ClassBase | undefined;
27
+ /**
28
+ * The resolver every catalog and settings payload builder needs: a picked
29
+ * QuickBooks id turned back into the account or class record the request body
30
+ * carries. Memoized so callers get one stable object rather than a fresh pair of
31
+ * closures per store change, which in `mapStateToProps` would re-render on every
32
+ * unrelated action.
33
+ */
34
+ export declare const getInvoicingCatalogAccountingLookup: (state: RootState) => {
35
+ accountByQboId: (qboId: string) => AccountBase | undefined;
36
+ classByQboId: (qboId: string) => ClassBase | undefined;
37
+ };
32
38
  /** An account or class as the invoicing pickers render and submit it. */
33
39
  export interface InvoicingAccountingOption {
34
40
  id: string;
@@ -60,13 +66,6 @@ export interface InvoicingQboView {
60
66
  * required would post a mapping QuickBooks rejects.
61
67
  */
62
68
  export declare const invoicingAccountsForQboAccountTypes: (accounts: InvoicingAccountingOption[], accountTypes: string) => InvoicingAccountingOption[];
63
- /**
64
- * The account tree narrowed to the given account types. The tree itself carries
65
- * no account type, so the flat account list supplies that and the matching
66
- * `qboId`s are kept — which is why callers that only need "income accounts, as a
67
- * tree" do not have to hold a second flat list to filter with.
68
- */
69
- export declare const invoicingAccountHierarchyForTypes: (state: RootState, accountTypes: readonly AccountType[]) => NestedAccountHierarchyForReport[];
70
69
  /**
71
70
  * Single bundled view for the QBO integration settings screen — the
72
71
  * connection, the account and class options, sync health, and the fetch/action