@zeniai/client-epic-state 5.2.41-beta1MM → 5.2.42-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.
@@ -3,7 +3,6 @@ import { toClassBasePayload } from '../../../entity/class/classPayload';
3
3
  import { blankInvoicingCatalogSubFamily, isTieredCatalogPricingModel, } from './invoicingCatalogItemFormHelper';
4
4
  const CURRENCY_CODE = 'USD';
5
5
  const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
6
- const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
7
6
  const toProductCode = (value) => value
8
7
  .trim()
9
8
  .toLowerCase()
@@ -91,13 +90,24 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
91
90
  /** `unit` is the item's unit of measure, not a currency; `CURRENCY_CODE` above prices Price points. */
92
91
  const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
93
92
  /**
94
- * Build the parent catalog-item request body from the form draft. Creates omit
95
- * both parent and price point IDs; edits retain returned price point IDs.
93
+ * Build the parent catalog-item request body from the form draft. Every field
94
+ * the form holds is sent, so a value cleared on the form is cleared on the
95
+ * item: the service reads an absent key as "leave it alone", which is why
96
+ * `description` travels as an empty string and `applicable_items` as an empty
97
+ * list rather than being dropped. Fields the form does not collect
98
+ * (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
99
+ * edit cannot overwrite them with a value nobody chose.
96
100
  */
97
101
  export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup) => {
98
- const description = trimOrUndefined(localData.description);
99
102
  const productCode = trimOrUndefined(localData.productCode) ?? toProductCode(localData.name);
100
103
  const itemPayload = {
104
+ // `applicable_items` only means anything while the item is restricted;
105
+ // sending the empty list otherwise is what clears a restriction that was
106
+ // just lifted.
107
+ applicable_items: localData.itemApplicability === 'restricted'
108
+ ? localData.applicableItems
109
+ : [],
110
+ description: trimOrUndefined(localData.description) ?? '',
101
111
  enabled_for_checkout: true,
102
112
  enabled_in_portal: true,
103
113
  is_giftable: false,
@@ -107,41 +117,40 @@ export const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate,
107
117
  name: localData.name.trim(),
108
118
  product_code: productCode,
109
119
  type: localData.catalogType,
120
+ // The form no longer collects a unit; an item that already has one keeps it.
121
+ unit: trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT,
110
122
  };
111
- if (description != null) {
112
- itemPayload.description = description;
113
- }
114
- // The form no longer collects a unit; an item that already has one keeps it.
115
- itemPayload.unit =
116
- trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT;
117
- if (localData.itemApplicability === 'restricted' &&
118
- localData.applicableItems.length > 0) {
119
- itemPayload.applicable_items = localData.applicableItems;
123
+ // The external id is assigned by the billing service and shown read-only, so
124
+ // it is echoed back only when the item already has one: sending the blank a
125
+ // create starts with would clear an id assigned in the meantime.
126
+ const externalId = trimOrUndefined(localData.externalId);
127
+ if (externalId != null) {
128
+ itemPayload.external_id = externalId;
120
129
  }
121
130
  const subFamilies = localData.subFamilies.length > 0
122
131
  ? localData.subFamilies
123
132
  : [blankInvoicingCatalogSubFamily()];
124
- itemPayload.sub_family_details = isCreate
133
+ itemPayload.sub_families = isCreate
125
134
  ? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup))
126
135
  : subFamilies.map((subFamily) => {
127
- const priceCode = trimOrUndefined(subFamily.priceCode) ?? toProductCode(subFamily.name);
128
- const detail = {
129
- description: trimOrNull(subFamily.description),
130
- invoice_notes: trimOrNull(subFamily.invoiceNotes),
131
- name: subFamily.name.trim(),
132
- price_code: priceCode,
133
- show_description_in_invoices: subFamily.showDescriptionInInvoices,
134
- };
135
- // Edits send a narrower body than creates, but the accounting mapping
136
- // is editable on this form, so leaving it out dropped the account and
137
- // class the user had just picked. `null` rather than omitted when it is
138
- // empty: an absent key on an update reads as "leave it alone", which
139
- // would make a cleared mapping impossible to save — the same reason
140
- // `description` and `invoice_notes` go through `trimOrNull`.
136
+ // Edits send the same body a create does. The narrower hand-built one
137
+ // this replaces had to be kept in step by hand and was not: it dropped
138
+ // `accounting_detail` once, and `billing_cycles`, `price`,
139
+ // `pricing_model`, `period`, the trial fields, `proration_type`,
140
+ // `free_quantity`, `is_taxable` and `tiers` were never added at all, so
141
+ // editing any of them looked saved and changed nothing.
142
+ const detail = buildSubFamilyPayload(localData, subFamily, true, lookup);
143
+ // `null` rather than omitted when empty: an absent key on an update
144
+ // reads as "leave it alone", which would make a cleared mapping
145
+ // impossible to save — the same reason `description` and
146
+ // `invoice_notes` are cleared rather than dropped above.
141
147
  detail.accounting_detail =
142
148
  buildAccountingDetail(subFamily, lookup) ?? null;
143
- const id = trimOrUndefined(subFamily.id);
144
- return id != null ? { ...detail, id } : detail;
149
+ // A price point added on this form has no id yet, and travels with a
150
+ // blank one. The update endpoint reads that as "create this one", so it
151
+ // lands in the same request as the edits around it rather than a second
152
+ // call that could fail on its own.
153
+ return { ...detail, id: subFamily.id.trim() };
145
154
  });
146
155
  return itemPayload;
147
156
  };
@@ -1,10 +1,30 @@
1
1
  import { AccountBase } from '../../../entity/account/accountState';
2
2
  import { ClassBase } from '../../../entity/class/classState';
3
3
  import { InvoicingPlanPayload } from '../../../entity/invoicing/plan/planPayload';
4
- import { InvoicingProductPayload } from '../../../entity/invoicing/product/productPayload';
4
+ import { InvoicingAccountingDetailPayload, InvoicingProductPayload } from '../../../entity/invoicing/product/productPayload';
5
5
  import { ZeniAPIResponse } from '../../../responsePayload';
6
6
  import { InvoicingCatalogItemFormLocalData } from './editInvoicingCatalogItemDetailViewState';
7
7
  export type SaveInvoicingCatalogItemResponse = ZeniAPIResponse<InvoicingPlanPayload>;
8
+ /**
9
+ * The price-point half of a save body. `InvoicingProductPayload` is the shape
10
+ * the service returns; a save differs in two ways: a field is cleared by
11
+ * sending an explicit `null` (an absent key reads as "leave it alone"), and a
12
+ * price point the form has only just added carries a blank `id`, which the
13
+ * update endpoint reads as "create this one".
14
+ */
15
+ export type InvoicingCatalogSubFamilySavePayload = Omit<Partial<InvoicingProductPayload>, 'accounting_detail' | 'description' | 'invoice_notes'> & {
16
+ accounting_detail?: InvoicingAccountingDetailPayload | null;
17
+ description?: string | null;
18
+ invoice_notes?: string | null;
19
+ };
20
+ /**
21
+ * The catalog-item save body: the item the service returns, minus the fields it
22
+ * assigns itself. Price points keep the `sub_families` name they are read back
23
+ * under, and differ only in allowing the cleared and blank-id forms above.
24
+ */
25
+ export type InvoicingCatalogItemSavePayload = Omit<Partial<InvoicingPlanPayload>, 'sub_families'> & {
26
+ sub_families?: InvoicingCatalogSubFamilySavePayload[];
27
+ };
8
28
  /**
9
29
  * Resolves the QuickBooks ids the pickers submit back to the entities they name.
10
30
  * The payload carries the whole account and class, so the ids alone are not
@@ -20,12 +40,17 @@ export interface InvoicingCatalogAccountingFields {
20
40
  accountingCode: string;
21
41
  sku: string;
22
42
  }
23
- export declare const buildAccountingDetail: (subFamily: InvoicingCatalogAccountingFields, lookup: InvoicingCatalogAccountingLookup) => Record<string, unknown> | undefined;
43
+ export declare const buildAccountingDetail: (subFamily: InvoicingCatalogAccountingFields, lookup: InvoicingCatalogAccountingLookup) => InvoicingAccountingDetailPayload | undefined;
24
44
  /**
25
- * Build the parent catalog-item request body from the form draft. Creates omit
26
- * both parent and price point IDs; edits retain returned price point IDs.
45
+ * Build the parent catalog-item request body from the form draft. Every field
46
+ * the form holds is sent, so a value cleared on the form is cleared on the
47
+ * item: the service reads an absent key as "leave it alone", which is why
48
+ * `description` travels as an empty string and `applicable_items` as an empty
49
+ * list rather than being dropped. Fields the form does not collect
50
+ * (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
51
+ * edit cannot overwrite them with a value nobody chose.
27
52
  */
28
- export declare const localDataToSaveInvoicingCatalogItemPayload: (localData: InvoicingCatalogItemFormLocalData, isCreate: boolean, lookup: InvoicingCatalogAccountingLookup) => Record<string, unknown>;
53
+ export declare const localDataToSaveInvoicingCatalogItemPayload: (localData: InvoicingCatalogItemFormLocalData, isCreate: boolean, lookup: InvoicingCatalogAccountingLookup) => InvoicingCatalogItemSavePayload;
29
54
  export type RunInvoicingCatalogPlanActionResponse = ZeniAPIResponse<InvoicingPlanPayload>;
30
55
  export type RunInvoicingCatalogProductActionResponse = ZeniAPIResponse<InvoicingProductPayload>;
31
56
  export type InvoicingCatalogItemDetailResponse = ZeniAPIResponse<InvoicingPlanPayload>;
@@ -6,7 +6,6 @@ const classPayload_1 = require("../../../entity/class/classPayload");
6
6
  const invoicingCatalogItemFormHelper_1 = require("./invoicingCatalogItemFormHelper");
7
7
  const CURRENCY_CODE = 'USD';
8
8
  const trimOrUndefined = (value) => value.trim() !== '' ? value.trim() : undefined;
9
- const trimOrNull = (value) => value.trim() !== '' ? value.trim() : null;
10
9
  const toProductCode = (value) => value
11
10
  .trim()
12
11
  .toLowerCase()
@@ -95,13 +94,24 @@ const buildSubFamilyPayload = (localData, subFamily, clearEmptyValues, lookup) =
95
94
  /** `unit` is the item's unit of measure, not a currency; `CURRENCY_CODE` above prices Price points. */
96
95
  const DEFAULT_CATALOG_ITEM_UNIT = 'USD';
97
96
  /**
98
- * Build the parent catalog-item request body from the form draft. Creates omit
99
- * both parent and price point IDs; edits retain returned price point IDs.
97
+ * Build the parent catalog-item request body from the form draft. Every field
98
+ * the form holds is sent, so a value cleared on the form is cleared on the
99
+ * item: the service reads an absent key as "leave it alone", which is why
100
+ * `description` travels as an empty string and `applicable_items` as an empty
101
+ * list rather than being dropped. Fields the form does not collect
102
+ * (`external_name`, `item_family_id`, `metadata`, …) stay off the body so an
103
+ * edit cannot overwrite them with a value nobody chose.
100
104
  */
101
105
  const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup) => {
102
- const description = trimOrUndefined(localData.description);
103
106
  const productCode = trimOrUndefined(localData.productCode) ?? toProductCode(localData.name);
104
107
  const itemPayload = {
108
+ // `applicable_items` only means anything while the item is restricted;
109
+ // sending the empty list otherwise is what clears a restriction that was
110
+ // just lifted.
111
+ applicable_items: localData.itemApplicability === 'restricted'
112
+ ? localData.applicableItems
113
+ : [],
114
+ description: trimOrUndefined(localData.description) ?? '',
105
115
  enabled_for_checkout: true,
106
116
  enabled_in_portal: true,
107
117
  is_giftable: false,
@@ -111,41 +121,40 @@ const localDataToSaveInvoicingCatalogItemPayload = (localData, isCreate, lookup)
111
121
  name: localData.name.trim(),
112
122
  product_code: productCode,
113
123
  type: localData.catalogType,
124
+ // The form no longer collects a unit; an item that already has one keeps it.
125
+ unit: trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT,
114
126
  };
115
- if (description != null) {
116
- itemPayload.description = description;
117
- }
118
- // The form no longer collects a unit; an item that already has one keeps it.
119
- itemPayload.unit =
120
- trimOrUndefined(localData.unit) ?? DEFAULT_CATALOG_ITEM_UNIT;
121
- if (localData.itemApplicability === 'restricted' &&
122
- localData.applicableItems.length > 0) {
123
- itemPayload.applicable_items = localData.applicableItems;
127
+ // The external id is assigned by the billing service and shown read-only, so
128
+ // it is echoed back only when the item already has one: sending the blank a
129
+ // create starts with would clear an id assigned in the meantime.
130
+ const externalId = trimOrUndefined(localData.externalId);
131
+ if (externalId != null) {
132
+ itemPayload.external_id = externalId;
124
133
  }
125
134
  const subFamilies = localData.subFamilies.length > 0
126
135
  ? localData.subFamilies
127
136
  : [(0, invoicingCatalogItemFormHelper_1.blankInvoicingCatalogSubFamily)()];
128
- itemPayload.sub_family_details = isCreate
137
+ itemPayload.sub_families = isCreate
129
138
  ? subFamilies.map((subFamily) => buildSubFamilyPayload(localData, subFamily, false, lookup))
130
139
  : subFamilies.map((subFamily) => {
131
- const priceCode = trimOrUndefined(subFamily.priceCode) ?? toProductCode(subFamily.name);
132
- const detail = {
133
- description: trimOrNull(subFamily.description),
134
- invoice_notes: trimOrNull(subFamily.invoiceNotes),
135
- name: subFamily.name.trim(),
136
- price_code: priceCode,
137
- show_description_in_invoices: subFamily.showDescriptionInInvoices,
138
- };
139
- // Edits send a narrower body than creates, but the accounting mapping
140
- // is editable on this form, so leaving it out dropped the account and
141
- // class the user had just picked. `null` rather than omitted when it is
142
- // empty: an absent key on an update reads as "leave it alone", which
143
- // would make a cleared mapping impossible to save — the same reason
144
- // `description` and `invoice_notes` go through `trimOrNull`.
140
+ // Edits send the same body a create does. The narrower hand-built one
141
+ // this replaces had to be kept in step by hand and was not: it dropped
142
+ // `accounting_detail` once, and `billing_cycles`, `price`,
143
+ // `pricing_model`, `period`, the trial fields, `proration_type`,
144
+ // `free_quantity`, `is_taxable` and `tiers` were never added at all, so
145
+ // editing any of them looked saved and changed nothing.
146
+ const detail = buildSubFamilyPayload(localData, subFamily, true, lookup);
147
+ // `null` rather than omitted when empty: an absent key on an update
148
+ // reads as "leave it alone", which would make a cleared mapping
149
+ // impossible to save — the same reason `description` and
150
+ // `invoice_notes` are cleared rather than dropped above.
145
151
  detail.accounting_detail =
146
152
  (0, exports.buildAccountingDetail)(subFamily, lookup) ?? null;
147
- const id = trimOrUndefined(subFamily.id);
148
- return id != null ? { ...detail, id } : detail;
153
+ // A price point added on this form has no id yet, and travels with a
154
+ // blank one. The update endpoint reads that as "create this one", so it
155
+ // lands in the same request as the edits around it rather than a second
156
+ // call that could fail on its own.
157
+ return { ...detail, id: subFamily.id.trim() };
149
158
  });
150
159
  return itemPayload;
151
160
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeniai/client-epic-state",
3
- "version": "5.2.41-beta1MM",
3
+ "version": "5.2.42-betaAK0",
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",