@peabody-soft/mbs-model-ts 1.7.1 → 1.8.0

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.
package/dist/Catalog.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  import Item from "./Item";
2
2
  import Package from "./Package";
3
+ import Price from "./Price";
3
4
  import PriceList from "./PriceList";
4
5
  import Product from "./Product";
5
6
  import ProductCategory from "./ProductCategory";
6
7
  export default class Catalog {
7
- private readonly _priceList;
8
- constructor(categories: ReadonlyArray<ProductCategory>, products: ReadonlyArray<Product>, _priceList: PriceList | null);
8
+ constructor(categories: ReadonlyArray<ProductCategory>, products: ReadonlyArray<Product>, packages: ReadonlyArray<Package>, priceLists: ReadonlyArray<PriceList>, prices: ReadonlyArray<Price>);
9
9
  get categories(): ReadonlyArray<ProductCategory>;
10
10
  get products(): ReadonlyArray<Product>;
11
+ get currentSalePriceListId(): bigint;
12
+ private set currentSalePriceListId(value);
11
13
  get priceList(): PriceList | null;
12
14
  get productCount(): number;
13
15
  get packageCount(): number;
@@ -19,10 +21,20 @@ export default class Catalog {
19
21
  private static compareNames;
20
22
  private static compareIds;
21
23
  private static comparePrices;
24
+ private applySalePriceList;
22
25
  static readonly EMPTY: Catalog;
23
- private readonly _categories;
24
- private readonly _products;
25
- private readonly _categoryMap;
26
- private readonly _productMap;
27
- private readonly _packageMap;
26
+ private readonly _allCategories;
27
+ private readonly _allProducts;
28
+ private readonly _uncategorized;
29
+ private readonly _candidatePackages;
30
+ private readonly _priceListMap;
31
+ private _currentSalePriceListId;
32
+ private _categories;
33
+ private _products;
34
+ private _categoryMap;
35
+ private _productMap;
36
+ private _packageMap;
37
+ }
38
+ export interface UpdatableCatalog {
39
+ set currentSalePriceListId(value: bigint);
28
40
  }
package/dist/Catalog.js CHANGED
@@ -1,51 +1,70 @@
1
1
  import { Constants as proto_Constants } from "./generated/Constants";
2
- import { EntityHeader } from "./Entity";
2
+ import { DEFAULT_SALE_PRICE_LIST_ID } from "./Constants";
3
+ import { EntityHeader, UpdatableEntity } from "./Entity";
3
4
  import ProductCategory from "./ProductCategory";
4
5
  // The catalog of a business: its categories and the products in them. Not an entity and not on the
5
- // wire. The sync layer rebuilds it from the flat item entities of a sync response.
6
+ // wire. The sync layer builds it from the flat item entities of a sync response, as the merchant
7
+ // app's Catalog is built.
8
+ //
9
+ // The constructor links what it is given: a package to its product, a price to its price list and a
10
+ // price list to its parent. It does not depend on any price list being present. Packages are priced
11
+ // from currentSalePriceListId, so setting that id decides which packages and products are buyable.
6
12
  class Catalog {
7
- constructor(categories, products, _priceList) {
8
- this._priceList = _priceList;
13
+ constructor(categories, products, packages, priceLists, prices) {
14
+ // Every package that has a product, by that product's id, whether it can be bought or not
15
+ this._candidatePackages = new Map();
16
+ this._priceListMap = new Map();
17
+ this._currentSalePriceListId = DEFAULT_SALE_PRICE_LIST_ID;
18
+ // What a customer can reach, rebuilt by applySalePriceList
19
+ this._categories = Object.freeze([]);
20
+ this._products = Object.freeze([]);
9
21
  this._categoryMap = new Map();
10
22
  this._productMap = new Map();
11
23
  this._packageMap = new Map();
24
+ this._allCategories = categories;
25
+ this._allProducts = products;
12
26
  // A dummy category for products whose parentItemId is Constants.ENTITY_ID_NULL. Its id is also
13
27
  // ENTITY_ID_NULL, so getCategory(product.productCategoryId) resolves it like a real category.
14
- // It is left out below like any other category when it has no products.
15
- const uncategorized = new ProductCategory(new EntityHeader(BigInt(proto_Constants.ENTITY_ID_NULL), 0n, 0n, 0n, 0n), "", null, BigInt(proto_Constants.ENTITY_ID_NULL), false);
16
- // Only a buyable product is part of a catalog: enabled, not sold only offline, with at least one
17
- // buyable package. Any other is left out of products, getProduct and its categories, and its
18
- // packages are unreachable by id. Enforced here so no builder can expose a product that cannot
19
- // be bought.
20
- const buyableProducts = [];
21
- const productsByCategory = new Map();
22
- for (const product of products) {
23
- if (!product.isBuyable)
28
+ // It is left out like any other category when it has no buyable products.
29
+ this._uncategorized = new ProductCategory(new EntityHeader(BigInt(proto_Constants.ENTITY_ID_NULL), 0n, 0n, 0n, 0n), "", null, BigInt(proto_Constants.ENTITY_ID_NULL), false);
30
+ // A package with no product is dropped
31
+ const productsById = new Map();
32
+ for (const product of products)
33
+ productsById.set(product.id, product);
34
+ for (const pkg of packages) {
35
+ const product = productsById.get(pkg.productId);
36
+ if (product === undefined)
24
37
  continue;
25
- buyableProducts.push(product);
26
- this._productMap.set(product.id, product);
27
- const productsOfCategory = productsByCategory.get(product.productCategoryId);
28
- if (productsOfCategory === undefined)
29
- productsByCategory.set(product.productCategoryId, [product]);
38
+ UpdatableEntity.setParent(pkg, product);
39
+ const productPackages = this._candidatePackages.get(product.id);
40
+ if (productPackages === undefined)
41
+ this._candidatePackages.set(product.id, [pkg]);
30
42
  else
31
- productsOfCategory.push(product);
32
- for (const pkg of product.packages)
33
- this._packageMap.set(pkg.id, pkg);
43
+ productPackages.push(pkg);
34
44
  }
35
- this._products = Object.freeze(buyableProducts);
36
- // A category with no buyable products is left out instead of being kept empty. This includes
37
- // the dummy category.
38
- const nonEmptyCategories = [];
39
- for (const category of [...categories, uncategorized]) {
40
- const categoryProducts = productsByCategory.get(category.id);
41
- if (categoryProducts === undefined)
45
+ // The first of two price lists with one id is kept. A price with no price list is dropped, and a
46
+ // list holds one price per package, the first to arrive.
47
+ for (const priceList of priceLists)
48
+ if (!this._priceListMap.has(priceList.id))
49
+ this._priceListMap.set(priceList.id, priceList);
50
+ const pricesByList = new Map();
51
+ for (const price of prices) {
52
+ if (!this._priceListMap.has(price.priceListId))
42
53
  continue;
43
- category.products = Object.freeze(categoryProducts);
44
- nonEmptyCategories.push(category);
54
+ const listPrices = pricesByList.get(price.priceListId);
55
+ if (listPrices === undefined)
56
+ pricesByList.set(price.priceListId, new Map([[price.packageId, price]]));
57
+ else if (!listPrices.has(price.packageId))
58
+ listPrices.set(price.packageId, price);
45
59
  }
46
- this._categories = Object.freeze(nonEmptyCategories);
47
- for (const category of this._categories)
48
- this._categoryMap.set(category.id, category);
60
+ // A parent that was not read, or a list that names itself, leaves the list without a parent
61
+ for (const priceList of this._priceListMap.values()) {
62
+ const updatable = priceList;
63
+ updatable.prices = pricesByList.get(priceList.id) ?? new Map();
64
+ const parent = this._priceListMap.get(priceList.parentPriceListId) ?? null;
65
+ updatable.parentPriceList = parent === priceList ? null : parent;
66
+ }
67
+ this.applySalePriceList();
49
68
  }
50
69
  // In no particular order - a UI orders these for display
51
70
  get categories() {
@@ -55,11 +74,22 @@ class Catalog {
55
74
  get products() {
56
75
  return this._products;
57
76
  }
58
- // The one price list a storefront sells from (MeraBills Online). Null until it arrives. Every
59
- // price in this catalog belongs to it, and prices in other lists are left out. A shopping list
77
+ // The id of the price list packages are priced from, DEFAULT_SALE_PRICE_LIST_ID until the sync
78
+ // layer sets it. Setting it prices every package from that list, or from its parent where the list
79
+ // has no price, and keeps the packages, products and categories that can then be bought. An id
80
+ // that names no list prices nothing, so the catalog holds nothing.
81
+ get currentSalePriceListId() {
82
+ return this._currentSalePriceListId;
83
+ }
84
+ set currentSalePriceListId(value) {
85
+ this._currentSalePriceListId = value;
86
+ this.applySalePriceList();
87
+ }
88
+ // The price list packages are priced from (MeraBills Online for a storefront). Null when the
89
+ // catalog has no list with the current id, as before the catalog has arrived. A shopping list
60
90
  // built from this catalog carries its id, or MERABILLS_ONLINE_PRICE_LIST_ID while it is null.
61
91
  get priceList() {
62
- return this._priceList;
92
+ return this._priceListMap.get(this._currentSalePriceListId) ?? null;
63
93
  }
64
94
  get productCount() {
65
95
  return this._products.length;
@@ -75,7 +105,8 @@ class Catalog {
75
105
  return this._productMap.get(productId) ?? null;
76
106
  }
77
107
  // Null for the four reserved package ids a shopping list uses for its charge and summary lines
78
- // (see Ids.isReservedPackageId), and for a package whose product was deleted or is not buyable
108
+ // (see Ids.isReservedPackageId), and for a package that is not buyable or whose product is not
109
+ // buyable
79
110
  getPackage(packageId) {
80
111
  return this._packageMap.get(packageId) ?? null;
81
112
  }
@@ -111,7 +142,56 @@ class Catalog {
111
142
  return -1;
112
143
  return left.offerPrice - right.offerPrice;
113
144
  }
145
+ // Prices the packages from the current sale price list, then rebuilds what a customer can reach.
146
+ // Only a buyable product is part of a catalog: enabled, not sold only offline, with at least one
147
+ // buyable package. Any other is left out of products, getProduct and its categories, and its
148
+ // packages are unreachable by id. A category with no buyable products is left out too, the dummy
149
+ // one included.
150
+ applySalePriceList() {
151
+ const salePriceList = this.priceList;
152
+ const buyableProducts = [];
153
+ const productMap = new Map();
154
+ const packageMap = new Map();
155
+ const productsByCategory = new Map();
156
+ for (const product of this._allProducts) {
157
+ const candidates = this._candidatePackages.get(product.id) ?? [];
158
+ for (const pkg of candidates) {
159
+ const price = salePriceList === null ? null : salePriceList.getPrice(pkg.id);
160
+ pkg.price = price;
161
+ if (price !== null)
162
+ UpdatableEntity.setParent(price, pkg);
163
+ }
164
+ // The product keeps only the buyable ones, and is judged by them
165
+ product.packages = candidates;
166
+ if (!product.isBuyable)
167
+ continue;
168
+ buyableProducts.push(product);
169
+ productMap.set(product.id, product);
170
+ const productsOfCategory = productsByCategory.get(product.productCategoryId);
171
+ if (productsOfCategory === undefined)
172
+ productsByCategory.set(product.productCategoryId, [product]);
173
+ else
174
+ productsOfCategory.push(product);
175
+ for (const pkg of product.packages)
176
+ packageMap.set(pkg.id, pkg);
177
+ }
178
+ const nonEmptyCategories = [];
179
+ const categoryMap = new Map();
180
+ for (const category of [...this._allCategories, this._uncategorized]) {
181
+ const categoryProducts = productsByCategory.get(category.id);
182
+ category.products = Object.freeze(categoryProducts ?? []);
183
+ if (categoryProducts === undefined)
184
+ continue;
185
+ nonEmptyCategories.push(category);
186
+ categoryMap.set(category.id, category);
187
+ }
188
+ this._products = Object.freeze(buyableProducts);
189
+ this._categories = Object.freeze(nonEmptyCategories);
190
+ this._productMap = productMap;
191
+ this._packageMap = packageMap;
192
+ this._categoryMap = categoryMap;
193
+ }
114
194
  }
115
195
  // A business whose catalog has not arrived yet
116
- Catalog.EMPTY = new Catalog(Object.freeze([]), Object.freeze([]), null);
196
+ Catalog.EMPTY = new Catalog([], [], [], [], []);
117
197
  export default Catalog;
@@ -1,5 +1,6 @@
1
1
  export { Constants } from "./generated/Constants";
2
2
  export declare function buildPriceListId(priceList: number): bigint;
3
+ export declare const DEFAULT_SALE_PRICE_LIST_ID: bigint;
3
4
  export declare const MERABILLS_ONLINE_PRICE_LIST_ID: bigint;
4
5
  export declare const MAX_SCALED_AMOUNT: bigint;
5
6
  export declare function amountFromScaledValue(scaledAmount: bigint): number;
package/dist/Constants.js CHANGED
@@ -11,9 +11,11 @@ export { Constants } from "./generated/Constants";
11
11
  export function buildPriceListId(priceList) {
12
12
  return Ids.buildId(proto_Constants.PRICE_LIST_ENTITY_TYPE_ID, BigInt(priceList));
13
13
  }
14
- // The only price list this library reads. A business prices a package in several lists (over the
15
- // counter, on a marketplace), and only this one is the storefront's. Prices in other lists are
16
- // skipped, see ProtoMap.buildBusiness.
14
+ // The list a catalog sells from until another is set, as in the merchant app's Catalog
15
+ export const DEFAULT_SALE_PRICE_LIST_ID = buildPriceListId(proto_Constants.DEFAULT_SALE_PRICE_LIST);
16
+ // The list a storefront sells from. A business prices a package in several lists (over the counter,
17
+ // on a marketplace), and a response can carry any of its public ones. ProtoMap.buildBusiness sets
18
+ // the catalog to sell from this one, and a package with no price in it is priced from its parent.
17
19
  export const MERABILLS_ONLINE_PRICE_LIST_ID = buildPriceListId(proto_Constants.MERABILLS_ONLINE_PRICE_LIST);
18
20
  // The largest amount the protocol can carry (Java Transaction.MAX_AMOUNT). Kept here because it does
19
21
  // not fit a protobuf enum value. It is 2^44 - 1, well below 2^53, so an amount is safe as a number.
package/dist/Enums.d.ts CHANGED
@@ -5,6 +5,7 @@ export { BusinessIdentifierType } from "./generated/BusinessIdentifierType";
5
5
  export { DimensionUnit } from "./generated/DimensionUnit";
6
6
  export { FulfillmentMode } from "./generated/FulfillmentMode";
7
7
  export { MessageType } from "./generated/MessageType";
8
+ export { OnlineStoreTheme } from "./generated/OnlineStoreTheme";
8
9
  export { OrderStatus } from "./generated/OrderStatus";
9
10
  export { PackageIdentifierType } from "./generated/PackageIdentifierType";
10
11
  export { PaymentMethod } from "./generated/PaymentMethod";
package/dist/Enums.js CHANGED
@@ -8,6 +8,7 @@ export { BusinessIdentifierType } from "./generated/BusinessIdentifierType";
8
8
  export { DimensionUnit } from "./generated/DimensionUnit";
9
9
  export { FulfillmentMode } from "./generated/FulfillmentMode";
10
10
  export { MessageType } from "./generated/MessageType";
11
+ export { OnlineStoreTheme } from "./generated/OnlineStoreTheme";
11
12
  export { OrderStatus } from "./generated/OrderStatus";
12
13
  export { PackageIdentifierType } from "./generated/PackageIdentifierType";
13
14
  export { PaymentMethod } from "./generated/PaymentMethod";
@@ -1,4 +1,5 @@
1
1
  import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
2
+ import { OnlineStoreTheme as proto_OnlineStoreTheme } from "./generated/OnlineStoreTheme";
2
3
  import { AddressList } from "./Address";
3
4
  import BusinessIdentifier from "./BusinessIdentifier";
4
5
  import LatLong from "./LatLong";
@@ -22,10 +23,10 @@ export default class GeneralSettings {
22
23
  private readonly _primaryStoreLocation;
23
24
  private readonly _googleBusinessCategoryId;
24
25
  private readonly _googleBusinessCategoryName;
25
- private readonly _onlineStoreThemeId;
26
+ private readonly _onlineStoreTheme;
26
27
  private readonly _onlineShoppingEnabled;
27
- static fromScaledValues(paymentMethodDetails: ReadonlyArray<PaymentMethodDetail>, customerServicePhone: string | null, isWhatsAppSupported: boolean, isSmsSupported: boolean, businessAddresses: AddressList, businessIdentifiers: ReadonlyArray<BusinessIdentifier>, fulfillmentModes: ReadonlyArray<proto_FulfillmentMode>, scaledDeliveryCharge: bigint, deliveryChargeTaxes: ReadonlyArray<Tax>, scaledFreeDeliveryTotal: bigint, deliveryRadiusKilometres: number, storeTagline: string | null, workingDays: WorkingDays, primaryStoreLocation: LatLong | null, googleBusinessCategoryId: string | null, googleBusinessCategoryName: string | null, onlineStoreThemeId: string | null, onlineShoppingEnabled: boolean): GeneralSettings;
28
- constructor(_paymentMethodDetails: ReadonlyArray<PaymentMethodDetail>, _customerServicePhone: string | null, _isWhatsAppSupported: boolean, _isSmsSupported: boolean, _businessAddresses: AddressList, _businessIdentifiers: ReadonlyArray<BusinessIdentifier>, _fulfillmentModes: ReadonlyArray<proto_FulfillmentMode>, _deliveryCharge: number, _deliveryChargeTaxes: ReadonlyArray<Tax>, _freeDeliveryTotal: number, _deliveryRadiusKilometres: number, _storeTagline: string | null, _workingDays: WorkingDays, _primaryStoreLocation: LatLong | null, _googleBusinessCategoryId: string | null, _googleBusinessCategoryName: string | null, _onlineStoreThemeId: string | null, _onlineShoppingEnabled: boolean);
28
+ static fromScaledValues(paymentMethodDetails: ReadonlyArray<PaymentMethodDetail>, customerServicePhone: string | null, isWhatsAppSupported: boolean, isSmsSupported: boolean, businessAddresses: AddressList, businessIdentifiers: ReadonlyArray<BusinessIdentifier>, fulfillmentModes: ReadonlyArray<proto_FulfillmentMode>, scaledDeliveryCharge: bigint, deliveryChargeTaxes: ReadonlyArray<Tax>, scaledFreeDeliveryTotal: bigint, deliveryRadiusKilometres: number, storeTagline: string | null, workingDays: WorkingDays, primaryStoreLocation: LatLong | null, googleBusinessCategoryId: string | null, googleBusinessCategoryName: string | null, onlineStoreTheme: proto_OnlineStoreTheme | null, onlineShoppingEnabled: boolean): GeneralSettings;
29
+ constructor(_paymentMethodDetails: ReadonlyArray<PaymentMethodDetail>, _customerServicePhone: string | null, _isWhatsAppSupported: boolean, _isSmsSupported: boolean, _businessAddresses: AddressList, _businessIdentifiers: ReadonlyArray<BusinessIdentifier>, _fulfillmentModes: ReadonlyArray<proto_FulfillmentMode>, _deliveryCharge: number, _deliveryChargeTaxes: ReadonlyArray<Tax>, _freeDeliveryTotal: number, _deliveryRadiusKilometres: number, _storeTagline: string | null, _workingDays: WorkingDays, _primaryStoreLocation: LatLong | null, _googleBusinessCategoryId: string | null, _googleBusinessCategoryName: string | null, _onlineStoreTheme: proto_OnlineStoreTheme | null, _onlineShoppingEnabled: boolean);
29
30
  get paymentMethodDetails(): ReadonlyArray<PaymentMethodDetail>;
30
31
  get customerServicePhone(): string | null;
31
32
  get isWhatsAppSupported(): boolean;
@@ -42,7 +43,7 @@ export default class GeneralSettings {
42
43
  get primaryStoreLocation(): LatLong | null;
43
44
  get googleBusinessCategoryId(): string | null;
44
45
  get googleBusinessCategoryName(): string | null;
45
- get onlineStoreThemeId(): string | null;
46
+ get onlineStoreTheme(): proto_OnlineStoreTheme | null;
46
47
  get onlineShoppingEnabled(): boolean;
47
48
  static readonly EMPTY: GeneralSettings;
48
49
  }
@@ -7,10 +7,10 @@ import WorkingDays from "./WorkingDays";
7
7
  // merchant-only fields are absent.
8
8
  class GeneralSettings {
9
9
  // For ProtoMap, which sees amounts still multiplied by Constants.AMOUNT_FACTOR.
10
- static fromScaledValues(paymentMethodDetails, customerServicePhone, isWhatsAppSupported, isSmsSupported, businessAddresses, businessIdentifiers, fulfillmentModes, scaledDeliveryCharge, deliveryChargeTaxes, scaledFreeDeliveryTotal, deliveryRadiusKilometres, storeTagline, workingDays, primaryStoreLocation, googleBusinessCategoryId, googleBusinessCategoryName, onlineStoreThemeId, onlineShoppingEnabled) {
11
- return new GeneralSettings(paymentMethodDetails, customerServicePhone, isWhatsAppSupported, isSmsSupported, businessAddresses, businessIdentifiers, fulfillmentModes, amountFromScaledValue(scaledDeliveryCharge), deliveryChargeTaxes, amountFromScaledValue(scaledFreeDeliveryTotal), deliveryRadiusKilometres, storeTagline, workingDays, primaryStoreLocation, googleBusinessCategoryId, googleBusinessCategoryName, onlineStoreThemeId, onlineShoppingEnabled);
10
+ static fromScaledValues(paymentMethodDetails, customerServicePhone, isWhatsAppSupported, isSmsSupported, businessAddresses, businessIdentifiers, fulfillmentModes, scaledDeliveryCharge, deliveryChargeTaxes, scaledFreeDeliveryTotal, deliveryRadiusKilometres, storeTagline, workingDays, primaryStoreLocation, googleBusinessCategoryId, googleBusinessCategoryName, onlineStoreTheme, onlineShoppingEnabled) {
11
+ return new GeneralSettings(paymentMethodDetails, customerServicePhone, isWhatsAppSupported, isSmsSupported, businessAddresses, businessIdentifiers, fulfillmentModes, amountFromScaledValue(scaledDeliveryCharge), deliveryChargeTaxes, amountFromScaledValue(scaledFreeDeliveryTotal), deliveryRadiusKilometres, storeTagline, workingDays, primaryStoreLocation, googleBusinessCategoryId, googleBusinessCategoryName, onlineStoreTheme, onlineShoppingEnabled);
12
12
  }
13
- constructor(_paymentMethodDetails, _customerServicePhone, _isWhatsAppSupported, _isSmsSupported, _businessAddresses, _businessIdentifiers, _fulfillmentModes, _deliveryCharge, _deliveryChargeTaxes, _freeDeliveryTotal, _deliveryRadiusKilometres, _storeTagline, _workingDays, _primaryStoreLocation, _googleBusinessCategoryId, _googleBusinessCategoryName, _onlineStoreThemeId, _onlineShoppingEnabled) {
13
+ constructor(_paymentMethodDetails, _customerServicePhone, _isWhatsAppSupported, _isSmsSupported, _businessAddresses, _businessIdentifiers, _fulfillmentModes, _deliveryCharge, _deliveryChargeTaxes, _freeDeliveryTotal, _deliveryRadiusKilometres, _storeTagline, _workingDays, _primaryStoreLocation, _googleBusinessCategoryId, _googleBusinessCategoryName, _onlineStoreTheme, _onlineShoppingEnabled) {
14
14
  this._paymentMethodDetails = _paymentMethodDetails;
15
15
  this._customerServicePhone = _customerServicePhone;
16
16
  this._isWhatsAppSupported = _isWhatsAppSupported;
@@ -27,7 +27,7 @@ class GeneralSettings {
27
27
  this._primaryStoreLocation = _primaryStoreLocation;
28
28
  this._googleBusinessCategoryId = _googleBusinessCategoryId;
29
29
  this._googleBusinessCategoryName = _googleBusinessCategoryName;
30
- this._onlineStoreThemeId = _onlineStoreThemeId;
30
+ this._onlineStoreTheme = _onlineStoreTheme;
31
31
  this._onlineShoppingEnabled = _onlineShoppingEnabled;
32
32
  }
33
33
  get paymentMethodDetails() {
@@ -93,10 +93,11 @@ class GeneralSettings {
93
93
  get googleBusinessCategoryName() {
94
94
  return this._googleBusinessCategoryName;
95
95
  }
96
- // Which of the storefront's own themes this business picked. The storefront defines what an id
97
- // means. This library does not interpret it. Null when none was picked.
98
- get onlineStoreThemeId() {
99
- return this._onlineStoreThemeId;
96
+ // Which of the storefront's themes this business picked. Null when none was picked. A theme that a
97
+ // newer app added, which this version of the library has no entry for, is returned as it came: a
98
+ // number that is not a member of the enum. A caller that maps themes must have a fallback.
99
+ get onlineStoreTheme() {
100
+ return this._onlineStoreTheme;
100
101
  }
101
102
  // Whether the business has turned online shopping on. The server sends the business either way.
102
103
  // Model throws a NotFoundException, after reading everything it was sent, when the settings it
package/dist/Item.js CHANGED
@@ -17,7 +17,8 @@ export default class Item extends Entity {
17
17
  get description() {
18
18
  return this._description;
19
19
  }
20
- // Constants.ENTITY_ID_NULL when this item has no parent (e.g. an uncategorized product, or a price list).
20
+ // Constants.ENTITY_ID_NULL when this item has no parent (e.g. an uncategorized product, or a price
21
+ // list with no parent list).
21
22
  get parentItemId() {
22
23
  return this._parentItemId;
23
24
  }
package/dist/Package.d.ts CHANGED
@@ -11,8 +11,7 @@ export default class Package extends Item {
11
11
  private readonly _dimensions;
12
12
  private readonly _weight;
13
13
  private readonly _photos;
14
- private readonly _price;
15
- constructor(header: EntityHeader, name: string, description: string | null, productId: bigint, isDisabled: boolean, _isSoldLoose: boolean, _itemCode: bigint, _identifiers: ReadonlyArray<PackageIdentifier>, _dimensions: Dimensions | null, _weight: Weight | null, _photos: ReadonlyArray<Photo>, _price: Price | null);
14
+ constructor(header: EntityHeader, name: string, description: string | null, productId: bigint, isDisabled: boolean, _isSoldLoose: boolean, _itemCode: bigint, _identifiers: ReadonlyArray<PackageIdentifier>, _dimensions: Dimensions | null, _weight: Weight | null, _photos: ReadonlyArray<Photo>);
16
15
  get productId(): bigint;
17
16
  get isSoldLoose(): boolean;
18
17
  get itemCode(): bigint;
@@ -21,5 +20,10 @@ export default class Package extends Item {
21
20
  get weight(): Weight | null;
22
21
  get photos(): ReadonlyArray<Photo>;
23
22
  get price(): Price | null;
23
+ private set price(value);
24
24
  get isBuyable(): boolean;
25
+ private _price;
26
+ }
27
+ export interface UpdatablePackage {
28
+ set price(value: Price | null);
25
29
  }
package/dist/Package.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import Item from "./Item";
2
2
  // A package, or variant, of a product (a 500 gram bottle of mango pickle, say) and the thing a
3
- // customer buys. Built complete, with photos and price resolved, so everything here is readonly.
3
+ // customer buys. Built with its photos. Catalog sets the price, once the price list to sell from is
4
+ // known.
4
5
  export default class Package extends Item {
5
- constructor(header, name, description, productId, isDisabled, _isSoldLoose, _itemCode, _identifiers, _dimensions, _weight, _photos, _price) {
6
+ constructor(header, name, description, productId, isDisabled, _isSoldLoose, _itemCode, _identifiers, _dimensions, _weight, _photos) {
6
7
  super(header, name, description, productId, isDisabled);
7
8
  this._isSoldLoose = _isSoldLoose;
8
9
  this._itemCode = _itemCode;
@@ -10,7 +11,7 @@ export default class Package extends Item {
10
11
  this._dimensions = _dimensions;
11
12
  this._weight = _weight;
12
13
  this._photos = _photos;
13
- this._price = _price;
14
+ this._price = null;
14
15
  }
15
16
  // The product this package is a variant of, which is the parent id every item has
16
17
  get productId() {
@@ -37,11 +38,15 @@ export default class Package extends Item {
37
38
  get photos() {
38
39
  return this._photos;
39
40
  }
40
- // What this package costs, with the taxes of its product included (Product.taxes). Null when the
41
- // business has not priced it, so it cannot be bought.
41
+ // What this package costs, with the taxes of its product included (Product.taxes). It is the
42
+ // price in the catalog's sale price list, or in that list's parent. Null when neither has one, so
43
+ // the package cannot be bought.
42
44
  get price() {
43
45
  return this._price;
44
46
  }
47
+ set price(value) {
48
+ this._price = value;
49
+ }
45
50
  // True when this package is enabled and has an enabled price. Checked here because a
46
51
  // product-level filter does not guarantee that every package, or every price, of an otherwise
47
52
  // visible product is enabled.
@@ -1,3 +1,16 @@
1
1
  import Item from "./Item";
2
+ import Price from "./Price";
2
3
  export default class PriceList extends Item {
4
+ get parentPriceListId(): bigint;
5
+ get parentPriceList(): PriceList | null;
6
+ private set parentPriceList(value);
7
+ get prices(): ReadonlyMap<bigint, Price>;
8
+ private set prices(value);
9
+ getPrice(packageId: bigint): Price | null;
10
+ private _parentPriceList;
11
+ private _prices;
12
+ }
13
+ export interface UpdatablePriceList {
14
+ set parentPriceList(value: PriceList | null);
15
+ set prices(value: ReadonlyMap<bigint, Price>);
3
16
  }
package/dist/PriceList.js CHANGED
@@ -1,5 +1,37 @@
1
1
  import Item from "./Item";
2
- // A named list of prices. A storefront sells from one, so no package has two prices to choose
3
- // between. Catalog.priceList holds it, and CartEditor names its id on the carts it builds.
2
+ // A named list of prices, at most one per package. A list can have a parent, named by its
3
+ // parentItemId. A package with no price in the list is priced from the parent. Catalog sets the
4
+ // prices and the parent, once every price list and price has been read.
4
5
  export default class PriceList extends Item {
6
+ constructor() {
7
+ super(...arguments);
8
+ this._parentPriceList = null;
9
+ this._prices = new Map();
10
+ }
11
+ // The id of the list whose prices are used for the packages this list has no price for, or
12
+ // Constants.ENTITY_ID_NULL when this list has no parent
13
+ get parentPriceListId() {
14
+ return this.parentItemId;
15
+ }
16
+ // Null when this list has no parent, or when the parent was not read. Not Entity.parent, which is
17
+ // the business.
18
+ get parentPriceList() {
19
+ return this._parentPriceList;
20
+ }
21
+ set parentPriceList(value) {
22
+ this._parentPriceList = value;
23
+ }
24
+ // The prices of this list itself, by package id. getPrice also looks in the parent.
25
+ get prices() {
26
+ return this._prices;
27
+ }
28
+ set prices(value) {
29
+ this._prices = value;
30
+ }
31
+ // The price of a package: this list's own, else the parent's own, else null. Only the parent is
32
+ // consulted, not its parent. A price is returned even when disabled, so a package with a disabled
33
+ // price here is not priced from the parent.
34
+ getPrice(packageId) {
35
+ return this._prices.get(packageId) ?? this._parentPriceList?._prices.get(packageId) ?? null;
36
+ }
5
37
  }
package/dist/Product.d.ts CHANGED
@@ -16,7 +16,7 @@ export default class Product extends Item {
16
16
  private readonly _features;
17
17
  private readonly _isForOfflineSaleOnly;
18
18
  private readonly _photos;
19
- constructor(header: EntityHeader, name: string, description: string | null, productCategoryId: bigint, isDisabled: boolean, _productType: proto_ProductType, _brand: string | null, _isManufacturedByBusiness: boolean, _manufacturer: string | null, _manufacturerAddress: Address | null, _taxes: ReadonlyArray<Tax>, _features: ReadonlyArray<string>, _isForOfflineSaleOnly: boolean, _photos: ReadonlyArray<Photo>, packages: ReadonlyArray<Package>);
19
+ constructor(header: EntityHeader, name: string, description: string | null, productCategoryId: bigint, isDisabled: boolean, _productType: proto_ProductType, _brand: string | null, _isManufacturedByBusiness: boolean, _manufacturer: string | null, _manufacturerAddress: Address | null, _taxes: ReadonlyArray<Tax>, _features: ReadonlyArray<string>, _isForOfflineSaleOnly: boolean, _photos: ReadonlyArray<Photo>);
20
20
  get productCategoryId(): bigint;
21
21
  get productType(): proto_ProductType;
22
22
  get brand(): string | null;
@@ -28,8 +28,12 @@ export default class Product extends Item {
28
28
  get isForOfflineSaleOnly(): boolean;
29
29
  get photos(): ReadonlyArray<Photo>;
30
30
  get packages(): ReadonlyArray<Package>;
31
+ private set packages(value);
31
32
  get lowestPrice(): Price | null;
32
33
  get isOnOffer(): boolean;
33
34
  get isBuyable(): boolean;
34
- private readonly _packages;
35
+ private _packages;
36
+ }
37
+ export interface UpdatableProduct {
38
+ set packages(value: ReadonlyArray<Package>);
35
39
  }
package/dist/Product.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import Item from "./Item";
2
- // A product in the catalog: goods, a service, a raw material or a fixed asset. Built complete
3
- // (photos and packages resolved first), so everything here is readonly.
2
+ // A product in the catalog: goods, a service, a raw material or a fixed asset. Built with its
3
+ // photos. Catalog sets its packages, once their prices are known.
4
4
  export default class Product extends Item {
5
- constructor(header, name, description, productCategoryId, isDisabled, _productType, _brand, _isManufacturedByBusiness, _manufacturer, _manufacturerAddress, _taxes, _features, _isForOfflineSaleOnly, _photos, packages) {
5
+ constructor(header, name, description, productCategoryId, isDisabled, _productType, _brand, _isManufacturedByBusiness, _manufacturer, _manufacturerAddress, _taxes, _features, _isForOfflineSaleOnly, _photos) {
6
6
  super(header, name, description, productCategoryId, isDisabled);
7
7
  this._productType = _productType;
8
8
  this._brand = _brand;
@@ -13,8 +13,7 @@ export default class Product extends Item {
13
13
  this._features = _features;
14
14
  this._isForOfflineSaleOnly = _isForOfflineSaleOnly;
15
15
  this._photos = _photos;
16
- // A package that cannot be bought is dropped, whatever the caller passes in
17
- this._packages = Object.freeze(packages.filter((pkg) => pkg.isBuyable));
16
+ this._packages = Object.freeze([]);
18
17
  }
19
18
  // The category this product is in, or Constants.ENTITY_ID_NULL when it is in none
20
19
  get productCategoryId() {
@@ -61,6 +60,10 @@ export default class Product extends Item {
61
60
  get packages() {
62
61
  return this._packages;
63
62
  }
63
+ // Keeps the buyable packages among those given, and drops the rest
64
+ set packages(value) {
65
+ this._packages = Object.freeze(value.filter((pkg) => pkg.isBuyable));
66
+ }
64
67
  // The cheapest final price across variants (what a listing shows as "from"), or null if there
65
68
  // are no variants. Compares final price, since that is what a customer pays. Every variant is
66
69
  // taxed at this product's rates, so comparing prices with taxes gives the same order as
package/dist/ProtoMap.js CHANGED
@@ -13,6 +13,7 @@ import { Entity as proto_Entity } from "./generated/Entity";
13
13
  import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
14
14
  import { Message as proto_Message } from "./generated/Message";
15
15
  import { MessageType as proto_MessageType } from "./generated/MessageType";
16
+ import { OnlineStoreTheme as proto_OnlineStoreTheme } from "./generated/OnlineStoreTheme";
16
17
  import { OrderStatus as proto_OrderStatus } from "./generated/OrderStatus";
17
18
  import { Package as proto_Package } from "./generated/Package";
18
19
  import { PartyPreferencesPayload as proto_PartyPreferencesPayload } from "./generated/PartyPreferencesPayload";
@@ -692,57 +693,39 @@ export function buildBusiness(response) {
692
693
  updatableBusiness.settings = generalSettingsFromProto(generalSettings, countryCode);
693
694
  // The photos of the store itself
694
695
  updatableBusiness.photos = takePhotos(attachmentsByOwner, business.id, attachmentBaseUrl);
695
- // Prices, by the package each applies to. This storefront uses only the MeraBills Online list. A
696
- // business prices a package in several lists, and a price from another list is not what it sells at
697
- // here, so it is skipped. A package has one price in that list. If a second arrives, the first is
698
- // kept.
699
- const pricesByPackage = new Map();
696
+ // The catalog's items, each built alone. Catalog links them and drops what it cannot (a package
697
+ // with no product, a price with no list) and what cannot be bought. Photos go to packages and
698
+ // products here. A tombstone is skipped, and the business is the containment parent of the items
699
+ // that sit directly under it. Catalog parents the rest.
700
+ const prices = [];
700
701
  for (const priceProto of response.prices) {
701
702
  if (isDeletedItem(priceProto.item))
702
703
  continue;
703
- const price = priceFromProto(priceProto);
704
- if (price.priceListId !== MERABILLS_ONLINE_PRICE_LIST_ID)
704
+ prices.push(priceFromProto(priceProto));
705
+ }
706
+ const priceLists = [];
707
+ for (const priceListProto of response.priceLists) {
708
+ if (isDeletedItem(priceListProto.item))
705
709
  continue;
706
- if (!pricesByPackage.has(price.packageId))
707
- pricesByPackage.set(price.packageId, price);
710
+ const priceList = priceListFromProto(priceListProto);
711
+ UpdatableEntity.setParent(priceList, business);
712
+ priceLists.push(priceList);
708
713
  }
709
- // Packages, by the product each belongs to, built with their photos and price. A package that is not
710
- // buyable (disabled, unpriced, or priced with a disabled price) is left out. A product-level filter
711
- // alone would not guarantee that every package of a visible product is enabled.
712
- const packagesByProduct = new Map();
714
+ const packages = [];
713
715
  for (const packageProto of response.packages) {
714
716
  if (isDeletedItem(packageProto.item))
715
717
  continue;
716
- const packageId = getItemId(packageProto.item);
717
- const packagePhotos = takePhotos(attachmentsByOwner, packageId, attachmentBaseUrl);
718
- const pkg = packageFromProto(packageProto, packagePhotos, pricesByPackage.get(packageId) ?? null);
719
- if (!pkg.isBuyable)
720
- continue;
721
- const price = pkg.price;
722
- if (price !== null)
723
- UpdatableEntity.setParent(price, pkg);
724
- const siblings = packagesByProduct.get(pkg.productId);
725
- if (siblings === undefined)
726
- packagesByProduct.set(pkg.productId, [pkg]);
727
- else
728
- siblings.push(pkg);
718
+ const packagePhotos = takePhotos(attachmentsByOwner, getItemId(packageProto.item), attachmentBaseUrl);
719
+ packages.push(packageFromProto(packageProto, packagePhotos));
729
720
  }
730
- // Products, built with their photos and buyable packages, and parented to the business (their
731
- // category only groups them). A product that is not buyable is left out: disabled, sold only offline
732
- // (the server sends those, flagged), or left with no buyable package by the filtering above.
721
+ // A product's category only groups it, so its containment parent is the business
733
722
  const products = [];
734
723
  for (const productProto of response.products) {
735
724
  if (isDeletedItem(productProto.item))
736
725
  continue;
737
- const productId = getItemId(productProto.item);
738
- const packages = packagesByProduct.get(productId) ?? [];
739
- const productPhotos = takePhotos(attachmentsByOwner, productId, attachmentBaseUrl);
740
- const product = productFromProto(productProto, productPhotos, Object.freeze([...packages]), countryCode);
741
- if (!product.isBuyable)
742
- continue;
726
+ const productPhotos = takePhotos(attachmentsByOwner, getItemId(productProto.item), attachmentBaseUrl);
727
+ const product = productFromProto(productProto, productPhotos, countryCode);
743
728
  UpdatableEntity.setParent(product, business);
744
- for (const pkg of product.packages)
745
- UpdatableEntity.setParent(pkg, product);
746
729
  products.push(product);
747
730
  }
748
731
  const productCategories = [];
@@ -753,20 +736,11 @@ export function buildBusiness(response) {
753
736
  UpdatableEntity.setParent(productCategory, business);
754
737
  productCategories.push(productCategory);
755
738
  }
756
- // A storefront sells from the MeraBills Online list only, picked by id since the same sync can
757
- // carry others. The catalog must hold the list the prices above came from. Otherwise a cart would
758
- // name a list that does not contain them.
759
- let priceList = null;
760
- for (const priceListProto of response.priceLists) {
761
- if (isDeletedItem(priceListProto.item))
762
- continue;
763
- if (getItemId(priceListProto.item) !== MERABILLS_ONLINE_PRICE_LIST_ID)
764
- continue;
765
- priceList = priceListFromProto(priceListProto);
766
- UpdatableEntity.setParent(priceList, business);
767
- break;
768
- }
769
- updatableBusiness.catalog = new Catalog(productCategories, products, priceList);
739
+ // A storefront sells from the MeraBills Online list, picked by id because a response can carry
740
+ // other public lists. A package with no price in that list is priced from the list's parent.
741
+ const catalog = new Catalog(productCategories, products, packages, priceLists, prices);
742
+ catalog.currentSalePriceListId = MERABILLS_ONLINE_PRICE_LIST_ID;
743
+ updatableBusiness.catalog = catalog;
770
744
  // The orders of this customer, and their party preferences (the cart and the fulfillment mode to
771
745
  // default the next order to). A message of any other payload type is dropped. The business completes
772
746
  // an order by deleting its message, and the server sends that message whole, so the orders include
@@ -1119,6 +1093,11 @@ function getItemId(item) {
1119
1093
  function stringFromProto(value) {
1120
1094
  return value === undefined ? null : value;
1121
1095
  }
1096
+ // The enum's zero means no theme was picked. A number the enum has no entry for (a theme that a newer
1097
+ // app added) is kept as it came, so the caller decides what to show.
1098
+ function onlineStoreThemeFromProto(value) {
1099
+ return value === proto_OnlineStoreTheme.UNSPECIFIED_ONLINE_STORE_THEME ? null : value;
1100
+ }
1122
1101
  // The reverse of stringFromProto. An empty string goes out absent too: the two mean the same, and
1123
1102
  // sending one could bring the field back as the other.
1124
1103
  function stringToProto(value) {
@@ -1209,7 +1188,7 @@ function shoppingListFromProto(shoppingList, countryCode) {
1209
1188
  return ShoppingList.fromScaledValues(shoppingList.isFinal, shoppingList.autoAddDeliveryCharge, shoppingList.autoAddServiceCharge, shoppingList.inventoryLocationId, shoppingList.priceListId, shoppingList.totalAmountBeforeDiscount, shoppingList.discount, shoppingList.existingItemCount, shoppingList.deletedItemCount, Object.freeze(shoppingList.existingItems.map((item) => shoppingListItemFromProto(item, countryCode))), Object.freeze(shoppingList.deletedItems.map((item) => shoppingListItemFromProto(item, countryCode))));
1210
1189
  }
1211
1190
  function generalSettingsFromProto(settings, countryCode) {
1212
- return GeneralSettings.fromScaledValues(Object.freeze(settings.paymentMethodDetails.map(paymentMethodDetailFromProto)), stringFromProto(settings.customerServicePhone), settings.isWhatsAppSupported, settings.isSmsSupported, addressListFromProto(settings.businessAddresses), Object.freeze(settings.businessIdentifiers.map(businessIdentifierFromProto)), Object.freeze([...settings.fulfillmentModes]), settings.deliveryCharge, Object.freeze(settings.deliveryChargeTaxes.map((tax) => taxFromProto(tax, countryCode))), settings.freeDeliveryTotal, settings.deliveryRadiusKms, stringFromProto(settings.storeTagline), workingDaysFromProto(settings.workingDays), latLongFromProto(settings.primaryStoreLocation), stringFromProto(settings.googleBusinessCategoryId), stringFromProto(settings.googleBusinessCategoryName), stringFromProto(settings.onlineStoreThemeId), settings.onlineShoppingEnabled);
1191
+ return GeneralSettings.fromScaledValues(Object.freeze(settings.paymentMethodDetails.map(paymentMethodDetailFromProto)), stringFromProto(settings.customerServicePhone), settings.isWhatsAppSupported, settings.isSmsSupported, addressListFromProto(settings.businessAddresses), Object.freeze(settings.businessIdentifiers.map(businessIdentifierFromProto)), Object.freeze([...settings.fulfillmentModes]), settings.deliveryCharge, Object.freeze(settings.deliveryChargeTaxes.map((tax) => taxFromProto(tax, countryCode))), settings.freeDeliveryTotal, settings.deliveryRadiusKms, stringFromProto(settings.storeTagline), workingDaysFromProto(settings.workingDays), latLongFromProto(settings.primaryStoreLocation), stringFromProto(settings.googleBusinessCategoryId), stringFromProto(settings.googleBusinessCategoryName), onlineStoreThemeFromProto(settings.onlineStoreTheme), settings.onlineShoppingEnabled);
1213
1192
  }
1214
1193
  // #endregion
1215
1194
  // #region Entities
@@ -1249,18 +1228,18 @@ function priceFromProto(priceProto) {
1249
1228
  const entity = getEntity(item.entity);
1250
1229
  return Price.fromScaledValues(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, priceProto.packageId, priceProto.regularPrice, priceProto.offerPrice);
1251
1230
  }
1252
- function packageFromProto(packageProto, photos, price) {
1231
+ function packageFromProto(packageProto, photos) {
1253
1232
  const item = getItem(packageProto.item);
1254
1233
  const entity = getEntity(item.entity);
1255
- return new Package(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, packageProto.isSoldLoose, packageProto.itemCode, Object.freeze(packageProto.identifiers.map(packageIdentifierFromProto)), dimensionsFromProto(packageProto.dimensions), weightFromProto(packageProto.weight), photos, price);
1234
+ return new Package(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, packageProto.isSoldLoose, packageProto.itemCode, Object.freeze(packageProto.identifiers.map(packageIdentifierFromProto)), dimensionsFromProto(packageProto.dimensions), weightFromProto(packageProto.weight), photos);
1256
1235
  }
1257
- function productFromProto(productProto, photos, packages, countryCode) {
1236
+ function productFromProto(productProto, photos, countryCode) {
1258
1237
  const item = getItem(productProto.item);
1259
1238
  const entity = getEntity(item.entity);
1260
1239
  return new Product(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, productProto.productType, stringFromProto(productProto.brand), productProto.isManufacturedByBusiness ?? true, // Absent means true (Product.proto)
1261
1240
  stringFromProto(productProto.manufacturer), productProto.manufacturerAddress === undefined
1262
1241
  ? null
1263
- : addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map((tax) => taxFromProto(tax, countryCode))), Object.freeze([...productProto.features]), productProto.forOfflineSaleOnly, photos, packages);
1242
+ : addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map((tax) => taxFromProto(tax, countryCode))), Object.freeze([...productProto.features]), productProto.forOfflineSaleOnly, photos);
1264
1243
  }
1265
1244
  function orderFromProto(message, salesOrder, countryCode) {
1266
1245
  const entity = getEntity(message.entity);
@@ -58,9 +58,10 @@ class ShoppingList {
58
58
  get inventoryLocationId() {
59
59
  return this._inventoryLocationId;
60
60
  }
61
- // The id of the price list these prices came from. A storefront sells from
62
- // Constants.MERABILLS_ONLINE_PRICE_LIST_ID, the list in Catalog.priceList, and CartEditor falls
63
- // back to that id when the catalog has no price list.
61
+ // The id of the price list the cart is priced from. A storefront names
62
+ // Constants.MERABILLS_ONLINE_PRICE_LIST_ID, the list in Catalog.priceList, for every line, including
63
+ // one priced from that list's parent. CartEditor falls back to that id when the catalog has no
64
+ // price list.
64
65
  get priceListId() {
65
66
  return this._priceListId;
66
67
  }
@@ -21,7 +21,10 @@ import { AuthTokens } from "./AuthTokens";
21
21
  * Item.parentItemId, Attachment.owningEntityId and Price.packageId, processing the arrays below in
22
22
  * order (parent before child). An absent/empty field means that type has not changed; a tombstone
23
23
  * (updateNumber's update-type bits = Constants.UPDATE_TYPE_DELETED) means the entity, and what it
24
- * owned, was deleted. At most Constants.SYNC_OUT_LIMIT entities arrive together - that many means
24
+ * owned, was deleted; it is sent as it is held, like any entity - but for an item (a product, package, price,
25
+ * price list or product category), which is only its Entity - and a client reads nothing of it but its
26
+ * Entity - but for a Message, which holds the order that the business completed, if it was one (see
27
+ * Message). At most Constants.SYNC_OUT_LIMIT entities arrive together - that many means
25
28
  * call again with the raised cursors.
26
29
  * There is no response, but a 404 (NotFoundException), when the business is not one a storefront can
27
30
  * open: it does not exist or has been deleted, or it does not allow customers to join it. Both are
@@ -242,10 +242,6 @@ export declare enum Constants {
242
242
  * @generated from protobuf enum value: GOOGLE_BUSINESS_CATEGORY_NAME_MAX_LENGTH = 256;
243
243
  */
244
244
  GOOGLE_BUSINESS_CATEGORY_NAME_MAX_LENGTH = 256,
245
- /**
246
- * @generated from protobuf enum value: BUSINESS_ID_MAX_LENGTH = 64;
247
- */
248
- ONLINE_STORE_THEME_ID_MAX_LENGTH = 64,
249
245
  /**
250
246
  * PaymentMethodDetail.data
251
247
  *
@@ -247,10 +247,6 @@ export var Constants;
247
247
  * @generated from protobuf enum value: GOOGLE_BUSINESS_CATEGORY_NAME_MAX_LENGTH = 256;
248
248
  */
249
249
  Constants[Constants["GOOGLE_BUSINESS_CATEGORY_NAME_MAX_LENGTH"] = 256] = "GOOGLE_BUSINESS_CATEGORY_NAME_MAX_LENGTH";
250
- /**
251
- * @generated from protobuf enum value: BUSINESS_ID_MAX_LENGTH = 64;
252
- */
253
- Constants[Constants["ONLINE_STORE_THEME_ID_MAX_LENGTH"] = 64] = "ONLINE_STORE_THEME_ID_MAX_LENGTH";
254
250
  /**
255
251
  * PaymentMethodDetail.data
256
252
  *
@@ -4,6 +4,7 @@ import type { BinaryReadOptions } from "@protobuf-ts/runtime";
4
4
  import type { IBinaryReader } from "@protobuf-ts/runtime";
5
5
  import type { PartialMessage } from "@protobuf-ts/runtime";
6
6
  import { MessageType } from "@protobuf-ts/runtime";
7
+ import { OnlineStoreTheme } from "./OnlineStoreTheme";
7
8
  import { LatLong } from "./LatLong";
8
9
  import { WorkingDays } from "./WorkingDays";
9
10
  import { Tax } from "./Tax";
@@ -122,12 +123,12 @@ export interface GeneralSettings {
122
123
  */
123
124
  googleBusinessCategoryName?: string;
124
125
  /**
125
- * Id of the visual theme applied to the online store
126
- * (at most Constants.ONLINE_STORE_THEME_ID_MAX_LENGTH)
126
+ * Visual theme applied to the online store. UNSPECIFIED_ONLINE_STORE_THEME means no theme is set.
127
+ * A client must treat a number it has no entry for as no theme: a newer app may have added one.
127
128
  *
128
- * @generated from protobuf field: optional string onlineStoreThemeId = 17
129
+ * @generated from protobuf field: com.merabills.storefront.OnlineStoreTheme onlineStoreTheme = 17
129
130
  */
130
- onlineStoreThemeId?: string;
131
+ onlineStoreTheme: OnlineStoreTheme;
131
132
  /**
132
133
  * True if the business has turned online shopping on. The server does not refuse a business that has
133
134
  * not: it is for the client to, once it has read all it is sent (see BusinessSyncOutResponse). A
@@ -2,6 +2,7 @@ import { UnknownFieldHandler } from "@protobuf-ts/runtime";
2
2
  import { WireType } from "@protobuf-ts/runtime";
3
3
  import { reflectionMergePartial } from "@protobuf-ts/runtime";
4
4
  import { MessageType } from "@protobuf-ts/runtime";
5
+ import { OnlineStoreTheme } from "./OnlineStoreTheme";
5
6
  import { LatLong } from "./LatLong";
6
7
  import { WorkingDays } from "./WorkingDays";
7
8
  import { Tax } from "./Tax";
@@ -29,7 +30,7 @@ class GeneralSettings$Type extends MessageType {
29
30
  { no: 14, name: "primaryStoreLocation", kind: "message", T: () => LatLong },
30
31
  { no: 15, name: "googleBusinessCategoryId", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
31
32
  { no: 16, name: "googleBusinessCategoryName", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
32
- { no: 17, name: "onlineStoreThemeId", kind: "scalar", opt: true, T: 9 /*ScalarType.STRING*/ },
33
+ { no: 17, name: "onlineStoreTheme", kind: "enum", T: () => ["com.merabills.storefront.OnlineStoreTheme", OnlineStoreTheme] },
33
34
  { no: 18, name: "onlineShoppingEnabled", kind: "scalar", T: 8 /*ScalarType.BOOL*/ }
34
35
  ]);
35
36
  }
@@ -44,6 +45,7 @@ class GeneralSettings$Type extends MessageType {
44
45
  message.deliveryChargeTaxes = [];
45
46
  message.freeDeliveryTotal = 0n;
46
47
  message.deliveryRadiusKms = 0;
48
+ message.onlineStoreTheme = 0;
47
49
  message.onlineShoppingEnabled = false;
48
50
  if (value !== undefined)
49
51
  reflectionMergePartial(this, message, value);
@@ -106,8 +108,8 @@ class GeneralSettings$Type extends MessageType {
106
108
  case /* optional string googleBusinessCategoryName */ 16:
107
109
  message.googleBusinessCategoryName = reader.string();
108
110
  break;
109
- case /* optional string onlineStoreThemeId */ 17:
110
- message.onlineStoreThemeId = reader.string();
111
+ case /* com.merabills.storefront.OnlineStoreTheme onlineStoreTheme */ 17:
112
+ message.onlineStoreTheme = reader.int32();
111
113
  break;
112
114
  case /* bool onlineShoppingEnabled */ 18:
113
115
  message.onlineShoppingEnabled = reader.bool();
@@ -176,9 +178,9 @@ class GeneralSettings$Type extends MessageType {
176
178
  /* optional string googleBusinessCategoryName = 16; */
177
179
  if (message.googleBusinessCategoryName !== undefined)
178
180
  writer.tag(16, WireType.LengthDelimited).string(message.googleBusinessCategoryName);
179
- /* optional string onlineStoreThemeId = 17; */
180
- if (message.onlineStoreThemeId !== undefined)
181
- writer.tag(17, WireType.LengthDelimited).string(message.onlineStoreThemeId);
181
+ /* com.merabills.storefront.OnlineStoreTheme onlineStoreTheme = 17; */
182
+ if (message.onlineStoreTheme !== 0)
183
+ writer.tag(17, WireType.Varint).int32(message.onlineStoreTheme);
182
184
  /* bool onlineShoppingEnabled = 18; */
183
185
  if (message.onlineShoppingEnabled !== false)
184
186
  writer.tag(18, WireType.Varint).bool(message.onlineShoppingEnabled);
@@ -17,9 +17,10 @@ export interface Item {
17
17
  */
18
18
  entity?: Entity;
19
19
  /**
20
- * Id of the parent of this item - the category of a product, the product of a package, or the
21
- * price list of a price - or 0 when it has none. A client rebuilds the catalog hierarchy by
22
- * grouping items on this id, since nothing nests on the wire.
20
+ * Id of the parent of this item - the category of a product, the product of a package, the
21
+ * price list of a price, or the parent price list of a price list - or 0 when it has none. A
22
+ * client rebuilds the catalog hierarchy by grouping items on this id, since nothing nests on the
23
+ * wire.
23
24
  *
24
25
  * @generated from protobuf field: int64 parentItemId = 2
25
26
  */
@@ -13,6 +13,11 @@ import { Entity } from "./Entity";
13
13
  * A message sent to a business: an order (SalesOrderPayload) or a customer's shopping cart
14
14
  * (PartyPreferencesPayload), typed here rather than left as the serialized string Java stores.
15
15
  * MONEY_TRANSFER messages have no payload in this protocol.
16
+ * A message that has been deleted (see Entity.updateNumber) is converted as it is held, as every entity
17
+ * is, deleted or not, but for the items of the catalog. The merchant app completes an order, by fulfilling or cancelling it, by deleting
18
+ * its message, so, a deleted message that carries a sales order is an order that the business has
19
+ * completed, and is the whole of what a customer is shown of it afterwards. A deleted message is not
20
+ * a cart, whatever it carries.
16
21
  *
17
22
  * @generated from protobuf message com.merabills.storefront.Message
18
23
  */
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A visual theme of the online store. The palettes ship with the online store, so a business picks
3
+ * one of these and cannot mix colours. Values match the ids of the corresponding Java enum entries
4
+ * and must never be changed.
5
+ *
6
+ * @generated from protobuf enum com.merabills.storefront.OnlineStoreTheme
7
+ */
8
+ export declare enum OnlineStoreTheme {
9
+ /**
10
+ * Java has no zero value: it treats a theme of zero as not set
11
+ *
12
+ * @generated from protobuf enum value: UNSPECIFIED_ONLINE_STORE_THEME = 0;
13
+ */
14
+ UNSPECIFIED_ONLINE_STORE_THEME = 0,
15
+ /**
16
+ * Deep forest brand, terracotta accent, warm paper
17
+ *
18
+ * @generated from protobuf enum value: FOREST = 1;
19
+ */
20
+ FOREST = 1,
21
+ /**
22
+ * Indigo brand, rust accent, cooler paper
23
+ *
24
+ * @generated from protobuf enum value: INDIGO = 2;
25
+ */
26
+ INDIGO = 2,
27
+ /**
28
+ * Brick brand, amber accent, the warmest paper
29
+ *
30
+ * @generated from protobuf enum value: BRICK = 3;
31
+ */
32
+ BRICK = 3
33
+ }
@@ -0,0 +1,38 @@
1
+ // @ts-nocheck
2
+ // @generated by protobuf-ts 2.11.1 with parameter long_type_bigint
3
+ // @generated from protobuf file "OnlineStoreTheme.proto" (package "com.merabills.storefront", syntax proto3)
4
+ // tslint:disable
5
+ /**
6
+ * A visual theme of the online store. The palettes ship with the online store, so a business picks
7
+ * one of these and cannot mix colours. Values match the ids of the corresponding Java enum entries
8
+ * and must never be changed.
9
+ *
10
+ * @generated from protobuf enum com.merabills.storefront.OnlineStoreTheme
11
+ */
12
+ export var OnlineStoreTheme;
13
+ (function (OnlineStoreTheme) {
14
+ /**
15
+ * Java has no zero value: it treats a theme of zero as not set
16
+ *
17
+ * @generated from protobuf enum value: UNSPECIFIED_ONLINE_STORE_THEME = 0;
18
+ */
19
+ OnlineStoreTheme[OnlineStoreTheme["UNSPECIFIED_ONLINE_STORE_THEME"] = 0] = "UNSPECIFIED_ONLINE_STORE_THEME";
20
+ /**
21
+ * Deep forest brand, terracotta accent, warm paper
22
+ *
23
+ * @generated from protobuf enum value: FOREST = 1;
24
+ */
25
+ OnlineStoreTheme[OnlineStoreTheme["FOREST"] = 1] = "FOREST";
26
+ /**
27
+ * Indigo brand, rust accent, cooler paper
28
+ *
29
+ * @generated from protobuf enum value: INDIGO = 2;
30
+ */
31
+ OnlineStoreTheme[OnlineStoreTheme["INDIGO"] = 2] = "INDIGO";
32
+ /**
33
+ * Brick brand, amber accent, the warmest paper
34
+ *
35
+ * @generated from protobuf enum value: BRICK = 3;
36
+ */
37
+ OnlineStoreTheme[OnlineStoreTheme["BRICK"] = 3] = "BRICK";
38
+ })(OnlineStoreTheme || (OnlineStoreTheme = {}));
@@ -6,8 +6,9 @@ import type { PartialMessage } from "@protobuf-ts/runtime";
6
6
  import { MessageType } from "@protobuf-ts/runtime";
7
7
  import { Item } from "./Item";
8
8
  /**
9
- * A named list of prices; has no parent, so its own Item.parentItemId is 0. Contains nothing - its
10
- * prices are the Price entities whose Item.parentItemId is this price list's id.
9
+ * A named list of prices. Item.parentItemId is the id of its parent price list, or 0 when it has
10
+ * none. A package that has no price in a list is priced from the parent list. Contains
11
+ * nothing - its prices are the Price entities whose Item.parentItemId is this price list's id.
11
12
  * Excluded: isPurchasePriceList - a purchase price list is never sent, so the flag would always be
12
13
  * false.
13
14
  *
@@ -34,21 +34,22 @@ export interface ProfileSyncOutRequest {
34
34
  /**
35
35
  * The business to join as a customer, if the caller is not already linked to it. 0 means none, and
36
36
  * anything else has to be the id of a business: one that is not is refused, not ignored.
37
- * A call that carries a valid link token of this business is from a member of it already, and nothing is
38
- * joined: the response is what the cursors above return, as for a call that joins nothing, and it is not
39
- * refused for the business no longer allowing customers to join it. A link token that is not valid is
40
- * refused (401), and one of another business says nothing of this one.
41
- * Otherwise the response always carries the BusinessLink and the Business that a join returns, even
42
- * where the cursors above would not return them.
43
- * An invitation to this business for the caller (looked for by their phone number) takes precedence: it is
44
- * accepted in the role and party that the invitation names, which may be those of a manager or an owner as
45
- * well as of a customer, whether or not the business allows customers to join without an invitation, and
46
- * the invitation is deleted. Only when there is none does the business decide.
47
- * The call fails with 404 (NotFoundException) if there is no such business, or if the caller has no
48
- * invitation to it and it does not allow customers to join it, and the caller does not hold a valid link
49
- * token of it. It is the link token that is tested, and not membership: a member that does not hold one (on
50
- * a new device, say) of a business that has since stopped allowing customers to join is refused in the same
51
- * way, and then the whole call is a 404 - nothing of the profile is returned either, and not only no join.
37
+ * A member of the business is not joined again, and is not refused if the business no longer allows
38
+ * customers to join it. Nothing is changed for a member, so the response is what the cursors above
39
+ * return, as for a call that joins nothing.
40
+ * A call that does join makes a new BusinessLink. It is newer than the link cursor, so the response
41
+ * carries it, and the Business that it names whatever the business cursor is.
42
+ * An invitation to this business for the caller (looked for by their phone number) that names a party takes
43
+ * precedence: the newest one is accepted in the role and party that the invitation names, which may be those
44
+ * of a manager or an owner as well as of a customer, whether or not the business allows customers to join
45
+ * without an invitation, and the invitation is deleted. The call is refused if another member has that party.
46
+ * Only when there is no such invitation does the business decide.
47
+ * Without an invitation the caller joins as a customer: as the customer party of the business that has their
48
+ * phone number, if there is one, else as a new customer party. A caller that was a member before gets a new
49
+ * link and, unless a party has their phone number, a new party.
50
+ * The call fails with 404 (NotFoundException) if there is no such business, or if the caller is not a member,
51
+ * has no invitation to it, and it does not allow customers to join it. The whole call is then a 404:
52
+ * nothing of the profile is returned either.
52
53
  *
53
54
  * @generated from protobuf field: int64 joinBusinessId = 4
54
55
  */
@@ -11,9 +11,8 @@ import { AuthTokens } from "./AuthTokens";
11
11
  /**
12
12
  * The answer to a ProfileSyncOutRequest: who the caller is, the businesses they are linked to, and
13
13
  * those businesses. Nothing is nested - match BusinessLink.businessId against Business.entity.id.
14
- * Every field is absent/empty unless that type has changed, but for the caller's own user, which is
15
- * sent with every response; a tombstone (updateNumber's update-type bits =
16
- * Constants.UPDATE_TYPE_DELETED) means the entity, and what it owned, was deleted.
14
+ * Every field is absent/empty unless that type has changed. A tombstone (updateNumber's update-type
15
+ * bits = Constants.UPDATE_TYPE_DELETED) means the entity, and what it owned, was deleted.
17
16
  *
18
17
  * @generated from protobuf message com.merabills.storefront.ProfileSyncOutResponse
19
18
  */
@@ -26,9 +25,8 @@ export interface ProfileSyncOutResponse {
26
25
  */
27
26
  authTokens?: AuthTokens;
28
27
  /**
29
- * The caller. Sent with every response, whatever userUpdateNumber is: a client keeps the copy it holds
30
- * when it is as new as this one. The only user a response ever carries: nobody else's, such as the
31
- * owner of a business, is sent to a customer.
28
+ * The caller, if the user has changed since userUpdateNumber. The only user a response ever carries:
29
+ * nobody else's, such as the owner of a business, is sent to a customer.
32
30
  *
33
31
  * @generated from protobuf field: com.merabills.storefront.User user = 2
34
32
  */
@@ -27,19 +27,22 @@ export interface SalesOrderPayload {
27
27
  */
28
28
  fulfillmentMode: FulfillmentMode;
29
29
  /**
30
- * What was ordered
30
+ * What was ordered. Absent for an order that an older version of the merchant app completed: that
31
+ * version moved the list out of the order, into the record of the sale, which a storefront is never
32
+ * sent. A client shows such an order with its total and count alone.
31
33
  *
32
34
  * @generated from protobuf field: com.merabills.storefront.ShoppingList shoppingList = 3
33
35
  */
34
36
  shoppingList?: ShoppingList;
35
37
  /**
36
- * Total of the shopping list, multiplied by Constants.AMOUNT_FACTOR
38
+ * Total of the shopping list, multiplied by Constants.AMOUNT_FACTOR. Sent whether or not the list is
39
+ * (see above), so, it is all there is of what an order of that kind came to.
37
40
  *
38
41
  * @generated from protobuf field: int64 shoppingListTotal = 4
39
42
  */
40
43
  shoppingListTotal: bigint;
41
44
  /**
42
- * Number of items in the shopping list
45
+ * Number of items in the shopping list. Sent whether or not the list is, as the total is.
43
46
  *
44
47
  * @generated from protobuf field: int32 shoppingListCount = 5
45
48
  */
package/dist/index.d.ts CHANGED
@@ -14,13 +14,17 @@ export { default as Business } from "./Business";
14
14
  export type { UpdatableBusiness } from "./Business";
15
15
  export { default as CartEditor, CartLine } from "./CartEditor";
16
16
  export { default as Catalog } from "./Catalog";
17
+ export type { UpdatableCatalog } from "./Catalog";
17
18
  export { default as GeneralSettings } from "./GeneralSettings";
18
19
  export { default as Order } from "./Order";
19
20
  export { default as Package } from "./Package";
21
+ export type { UpdatablePackage } from "./Package";
20
22
  export { default as Photo } from "./Photo";
21
23
  export { default as Price } from "./Price";
22
24
  export { default as PriceList } from "./PriceList";
25
+ export type { UpdatablePriceList } from "./PriceList";
23
26
  export { default as Product } from "./Product";
27
+ export type { UpdatableProduct } from "./Product";
24
28
  export { default as ProductCategory } from "./ProductCategory";
25
29
  export type { UpdatableProductCategory } from "./ProductCategory";
26
30
  export { default as Profile } from "./Profile";
package/dist/index.js CHANGED
@@ -48,8 +48,8 @@ export { default as ShoppingList, ItemChange, ShoppingListItem } from "./Shoppin
48
48
  export { default as Tax, TaxAmount } from "./Tax";
49
49
  export { default as WorkingDays, WorkingHours, WorkingHoursRange } from "./WorkingDays";
50
50
  // The enums of the protocol, the limits it obeys, and how an id is read. The price list a
51
- // storefront sells from is exported because a UI that builds a cart must name it, and
52
- // Catalog.priceList is null until the catalog has arrived.
51
+ // storefront sells from is exported because a UI that builds a cart must name it. Catalog.priceList
52
+ // is null until the catalog has arrived, and when the response did not carry that list.
53
53
  export * from "./Enums";
54
54
  export { Constants, MERABILLS_ONLINE_PRICE_LIST_ID, buildPriceListId } from "./Constants";
55
55
  export { default as Ids } from "./Ids";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peabody-soft/mbs-model-ts",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "MeraBills storefront client-side model (TypeScript)",
5
5
  "author": "Peabody Soft",
6
6
  "license": "AGPL-3.0-or-later",
@@ -49,7 +49,7 @@
49
49
  "typescript": "^7.0.2"
50
50
  },
51
51
  "dependencies": {
52
- "@peabody-soft/mbs-protos": "^1.11.0",
52
+ "@peabody-soft/mbs-protos": "^1.12.1",
53
53
  "@protobuf-ts/runtime": "^2.11.1"
54
54
  }
55
55
  }