@peabody-soft/mbs-model-ts 1.0.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.
Files changed (212) hide show
  1. package/LICENCE +661 -0
  2. package/dist/Address.d.ts +33 -0
  3. package/dist/Address.js +97 -0
  4. package/dist/Attachment.d.ts +16 -0
  5. package/dist/Attachment.js +49 -0
  6. package/dist/AttachmentBaseUrl.d.ts +11 -0
  7. package/dist/AttachmentBaseUrl.js +41 -0
  8. package/dist/AuthTokens.d.ts +13 -0
  9. package/dist/AuthTokens.js +30 -0
  10. package/dist/Business.d.ts +44 -0
  11. package/dist/Business.js +87 -0
  12. package/dist/BusinessIdentifier.d.ts +8 -0
  13. package/dist/BusinessIdentifier.js +14 -0
  14. package/dist/BusinessLink.d.ts +21 -0
  15. package/dist/BusinessLink.js +61 -0
  16. package/dist/BusinessProfile.d.ts +11 -0
  17. package/dist/BusinessProfile.js +27 -0
  18. package/dist/CartEditor.d.ts +59 -0
  19. package/dist/CartEditor.js +324 -0
  20. package/dist/Catalog.d.ts +28 -0
  21. package/dist/Catalog.js +115 -0
  22. package/dist/Constants.d.ts +10 -0
  23. package/dist/Constants.js +43 -0
  24. package/dist/ContainerNames.d.ts +2 -0
  25. package/dist/ContainerNames.js +21 -0
  26. package/dist/Database.d.ts +15 -0
  27. package/dist/Database.js +1 -0
  28. package/dist/Entity.d.ts +27 -0
  29. package/dist/Entity.js +69 -0
  30. package/dist/Enums.d.ts +16 -0
  31. package/dist/Enums.js +18 -0
  32. package/dist/Exceptions.d.ts +4 -0
  33. package/dist/Exceptions.js +8 -0
  34. package/dist/GeneralSettings.d.ts +46 -0
  35. package/dist/GeneralSettings.js +99 -0
  36. package/dist/HttpStatusCodes.d.ts +2 -0
  37. package/dist/HttpStatusCodes.js +34 -0
  38. package/dist/Ids.d.ts +17 -0
  39. package/dist/Ids.js +78 -0
  40. package/dist/Item.d.ts +12 -0
  41. package/dist/Item.js +30 -0
  42. package/dist/LatLong.d.ts +7 -0
  43. package/dist/LatLong.js +17 -0
  44. package/dist/LiveData.d.ts +30 -0
  45. package/dist/LiveData.js +76 -0
  46. package/dist/Measures.d.ts +20 -0
  47. package/dist/Measures.js +34 -0
  48. package/dist/Message.d.ts +16 -0
  49. package/dist/Message.js +32 -0
  50. package/dist/MockDatabase.d.ts +19 -0
  51. package/dist/MockDatabase.js +73 -0
  52. package/dist/Model.d.ts +63 -0
  53. package/dist/Model.js +629 -0
  54. package/dist/Order.d.ts +31 -0
  55. package/dist/Order.js +70 -0
  56. package/dist/Package.d.ts +25 -0
  57. package/dist/Package.js +52 -0
  58. package/dist/PackageIdentifier.d.ts +8 -0
  59. package/dist/PackageIdentifier.js +14 -0
  60. package/dist/PaymentMethodDetail.d.ts +8 -0
  61. package/dist/PaymentMethodDetail.js +17 -0
  62. package/dist/Photo.d.ts +11 -0
  63. package/dist/Photo.js +29 -0
  64. package/dist/Price.d.ts +17 -0
  65. package/dist/Price.js +52 -0
  66. package/dist/PriceList.d.ts +3 -0
  67. package/dist/PriceList.js +5 -0
  68. package/dist/Product.d.ts +33 -0
  69. package/dist/Product.js +84 -0
  70. package/dist/ProductCategory.d.ts +10 -0
  71. package/dist/ProductCategory.js +18 -0
  72. package/dist/Profile.d.ts +20 -0
  73. package/dist/Profile.js +55 -0
  74. package/dist/ProtoMap.d.ts +71 -0
  75. package/dist/ProtoMap.js +1126 -0
  76. package/dist/Routes.d.ts +7 -0
  77. package/dist/Routes.js +12 -0
  78. package/dist/Service.d.ts +16 -0
  79. package/dist/Service.js +1 -0
  80. package/dist/ServiceExceptions.d.ts +40 -0
  81. package/dist/ServiceExceptions.js +84 -0
  82. package/dist/ServiceProxy.d.ts +25 -0
  83. package/dist/ServiceProxy.js +117 -0
  84. package/dist/ShoppingList.d.ts +64 -0
  85. package/dist/ShoppingList.js +188 -0
  86. package/dist/SyncCache.d.ts +28 -0
  87. package/dist/SyncCache.js +137 -0
  88. package/dist/SyncKeys.d.ts +5 -0
  89. package/dist/SyncKeys.js +41 -0
  90. package/dist/SyncState.d.ts +8 -0
  91. package/dist/SyncState.js +30 -0
  92. package/dist/Tax.d.ts +10 -0
  93. package/dist/Tax.js +30 -0
  94. package/dist/UserProfile.d.ts +16 -0
  95. package/dist/UserProfile.js +40 -0
  96. package/dist/WorkingDays.d.ts +33 -0
  97. package/dist/WorkingDays.js +121 -0
  98. package/dist/generated/Address.d.ts +77 -0
  99. package/dist/generated/Address.js +110 -0
  100. package/dist/generated/AddressList.d.ts +39 -0
  101. package/dist/generated/AddressList.js +60 -0
  102. package/dist/generated/AddressType.d.ts +15 -0
  103. package/dist/generated/AddressType.js +20 -0
  104. package/dist/generated/Attachment.d.ts +66 -0
  105. package/dist/generated/Attachment.js +89 -0
  106. package/dist/generated/AttachmentType.d.ts +154 -0
  107. package/dist/generated/AttachmentType.js +159 -0
  108. package/dist/generated/AuthTokens.d.ts +56 -0
  109. package/dist/generated/AuthTokens.js +74 -0
  110. package/dist/generated/AuthenticationMethod.d.ts +24 -0
  111. package/dist/generated/AuthenticationMethod.js +29 -0
  112. package/dist/generated/Business.d.ts +54 -0
  113. package/dist/generated/Business.js +75 -0
  114. package/dist/generated/BusinessIdentifier.d.ts +36 -0
  115. package/dist/generated/BusinessIdentifier.js +60 -0
  116. package/dist/generated/BusinessIdentifierType.d.ts +49 -0
  117. package/dist/generated/BusinessIdentifierType.js +54 -0
  118. package/dist/generated/BusinessLink.d.ts +76 -0
  119. package/dist/generated/BusinessLink.js +99 -0
  120. package/dist/generated/BusinessSyncInRequest.d.ts +42 -0
  121. package/dist/generated/BusinessSyncInRequest.js +60 -0
  122. package/dist/generated/BusinessSyncInResponse.d.ts +42 -0
  123. package/dist/generated/BusinessSyncInResponse.js +60 -0
  124. package/dist/generated/BusinessSyncOutRequest.d.ts +82 -0
  125. package/dist/generated/BusinessSyncOutRequest.js +107 -0
  126. package/dist/generated/BusinessSyncOutResponse.d.ts +106 -0
  127. package/dist/generated/BusinessSyncOutResponse.js +139 -0
  128. package/dist/generated/Configuration.d.ts +48 -0
  129. package/dist/generated/Configuration.js +63 -0
  130. package/dist/generated/Constants.d.ts +474 -0
  131. package/dist/generated/Constants.js +479 -0
  132. package/dist/generated/CreateUserRequest.d.ts +37 -0
  133. package/dist/generated/CreateUserRequest.js +59 -0
  134. package/dist/generated/DimensionUnit.d.ts +28 -0
  135. package/dist/generated/DimensionUnit.js +33 -0
  136. package/dist/generated/Dimensions.d.ts +41 -0
  137. package/dist/generated/Dimensions.js +76 -0
  138. package/dist/generated/Entity.d.ts +62 -0
  139. package/dist/generated/Entity.js +83 -0
  140. package/dist/generated/FulfillmentMode.d.ts +32 -0
  141. package/dist/generated/FulfillmentMode.js +37 -0
  142. package/dist/generated/GeneralSettings.d.ts +141 -0
  143. package/dist/generated/GeneralSettings.js +186 -0
  144. package/dist/generated/Item.d.ts +54 -0
  145. package/dist/generated/Item.js +82 -0
  146. package/dist/generated/LatLong.d.ts +36 -0
  147. package/dist/generated/LatLong.js +59 -0
  148. package/dist/generated/Message.d.ts +80 -0
  149. package/dist/generated/Message.js +114 -0
  150. package/dist/generated/MessageType.d.ts +34 -0
  151. package/dist/generated/MessageType.js +39 -0
  152. package/dist/generated/OrderStatus.d.ts +50 -0
  153. package/dist/generated/OrderStatus.js +55 -0
  154. package/dist/generated/Package.d.ts +63 -0
  155. package/dist/generated/Package.js +92 -0
  156. package/dist/generated/PackageIdentifier.d.ts +36 -0
  157. package/dist/generated/PackageIdentifier.js +60 -0
  158. package/dist/generated/PackageIdentifierType.d.ts +49 -0
  159. package/dist/generated/PackageIdentifierType.js +54 -0
  160. package/dist/generated/PartyPreferencesPayload.d.ts +41 -0
  161. package/dist/generated/PartyPreferencesPayload.js +60 -0
  162. package/dist/generated/PaymentMethod.d.ts +20 -0
  163. package/dist/generated/PaymentMethod.js +25 -0
  164. package/dist/generated/PaymentMethodDetail.d.ts +37 -0
  165. package/dist/generated/PaymentMethodDetail.js +59 -0
  166. package/dist/generated/Price.d.ts +52 -0
  167. package/dist/generated/Price.js +75 -0
  168. package/dist/generated/PriceList.d.ts +34 -0
  169. package/dist/generated/PriceList.js +51 -0
  170. package/dist/generated/Product.d.ts +77 -0
  171. package/dist/generated/Product.js +106 -0
  172. package/dist/generated/ProductCategory.d.ts +32 -0
  173. package/dist/generated/ProductCategory.js +51 -0
  174. package/dist/generated/ProductType.d.ts +42 -0
  175. package/dist/generated/ProductType.js +47 -0
  176. package/dist/generated/ProfileSyncOutRequest.d.ts +51 -0
  177. package/dist/generated/ProfileSyncOutRequest.js +75 -0
  178. package/dist/generated/ProfileSyncOutResponse.d.ts +56 -0
  179. package/dist/generated/ProfileSyncOutResponse.js +77 -0
  180. package/dist/generated/ReservedPackage.d.ts +34 -0
  181. package/dist/generated/ReservedPackage.js +39 -0
  182. package/dist/generated/Role.d.ts +71 -0
  183. package/dist/generated/Role.js +76 -0
  184. package/dist/generated/SalesOrderPayload.d.ts +85 -0
  185. package/dist/generated/SalesOrderPayload.js +123 -0
  186. package/dist/generated/ShoppingList.d.ts +99 -0
  187. package/dist/generated/ShoppingList.js +132 -0
  188. package/dist/generated/ShoppingListItem.d.ts +102 -0
  189. package/dist/generated/ShoppingListItem.js +139 -0
  190. package/dist/generated/TaskStatus.d.ts +20 -0
  191. package/dist/generated/TaskStatus.js +25 -0
  192. package/dist/generated/Tax.d.ts +37 -0
  193. package/dist/generated/Tax.js +60 -0
  194. package/dist/generated/TaxType.d.ts +25 -0
  195. package/dist/generated/TaxType.js +30 -0
  196. package/dist/generated/UrlAndToken.d.ts +54 -0
  197. package/dist/generated/UrlAndToken.js +67 -0
  198. package/dist/generated/User.d.ts +63 -0
  199. package/dist/generated/User.js +89 -0
  200. package/dist/generated/Weight.d.ts +33 -0
  201. package/dist/generated/Weight.js +60 -0
  202. package/dist/generated/WeightUnit.d.ts +28 -0
  203. package/dist/generated/WeightUnit.js +33 -0
  204. package/dist/generated/WorkingDays.d.ts +42 -0
  205. package/dist/generated/WorkingDays.js +68 -0
  206. package/dist/generated/WorkingHours.d.ts +33 -0
  207. package/dist/generated/WorkingHours.js +52 -0
  208. package/dist/generated/WorkingHoursRange.d.ts +42 -0
  209. package/dist/generated/WorkingHoursRange.js +59 -0
  210. package/dist/index.d.ts +48 -0
  211. package/dist/index.js +60 -0
  212. package/package.json +55 -0
@@ -0,0 +1,324 @@
1
+ import { Constants as proto_Constants } from "./generated/Constants";
2
+ import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
3
+ import { ReservedPackage as proto_ReservedPackage } from "./generated/ReservedPackage";
4
+ import { MERABILLS_ONLINE_PRICE_LIST_ID } from "./Constants";
5
+ import { InvalidArgumentException } from "./Exceptions";
6
+ import Ids from "./Ids";
7
+ import ShoppingList, { ShoppingListItem } from "./ShoppingList";
8
+ // Builds the cart a customer is filling, in the terms a UI has to hand: packages rather than
9
+ // package ids, quantities rather than quantities multiplied by Constants.QUANTITY_FACTOR, and
10
+ // prices taken off the catalog rather than worked out and passed in. Get one from
11
+ // Business.createCartEditor, edit it, then hand build() to Model.updateCartLocalOnly.
12
+ //
13
+ // Mutable, and deliberately the only mutable thing in this object model: it is a scratch copy of
14
+ // the cart, not the cart. Nothing here writes anything - editing an editor changes no Business, no
15
+ // store and nothing on the server until a built list is pushed. It is also a snapshot: it reads the
16
+ // catalog, settings and cart once, at construction, so a sync arriving mid-edit does not move
17
+ // prices under the customer. Make a fresh one when the Business republishes.
18
+ class CartEditor {
19
+ // Business.createCartEditor is how a UI gets one of these; this takes the three things it needs
20
+ // rather than the Business itself, so a caller can build a cart against a catalog and settings
21
+ // that are not the ones currently published.
22
+ //
23
+ // Every line is priced off the catalog now, not off what the cart was holding: a cart lives
24
+ // until it is ordered, the merchant may have repriced in between, and what an order freezes is
25
+ // the price at the moment it is placed. Lines the catalog can no longer price - a package
26
+ // withdrawn, disabled, or left without a price - are dropped, and left in unavailableItems for a
27
+ // UI to tell the customer about. The charge lines of the list it started from (see
28
+ // ShoppingListItem.isReservedPackage) are dropped too, being this class's to work out rather
29
+ // than a customer's to carry: build() puts back whatever the settings now say.
30
+ constructor(_catalog, _settings, cart) {
31
+ this._catalog = _catalog;
32
+ this._settings = _settings;
33
+ // Insertion ordered, which is what keeps a cart in the order the customer filled it and what
34
+ // gives each line its shoppingListOrder
35
+ this._quantities = new Map();
36
+ this._inventoryLocationId = cart?.inventoryLocationId ?? 0;
37
+ const unavailableItems = [];
38
+ for (const item of cart?.existingItems ?? []) {
39
+ if (item.isReservedPackage)
40
+ continue;
41
+ const buyable = this.getBuyablePackage(item.packageId);
42
+ if (buyable === null)
43
+ unavailableItems.push(item);
44
+ else
45
+ this._quantities.set(buyable.id, item.quantity);
46
+ }
47
+ this._unavailableItems = Object.freeze(unavailableItems);
48
+ }
49
+ // What the customer has picked, in the order it was picked in, each line priced as it would be
50
+ // bought right now. Rebuilt on every read, so a line held across an edit is stale.
51
+ get lines() {
52
+ const lines = [];
53
+ for (const [packageId, quantity] of this._quantities) {
54
+ const buyable = this.getBuyablePackage(packageId);
55
+ if (buyable === null)
56
+ continue; // Cannot happen: only a buyable package is ever put in
57
+ lines.push(new CartLine(buyable, this._catalog.getProduct(buyable.productId), quantity));
58
+ }
59
+ return Object.freeze(lines);
60
+ }
61
+ // The lines of the cart this editor started from that its catalog can no longer price - see the
62
+ // note on the constructor. Empty for a cart built from nothing, and never added to by an edit.
63
+ get unavailableItems() {
64
+ return this._unavailableItems;
65
+ }
66
+ get isEmpty() {
67
+ return this._quantities.size === 0;
68
+ }
69
+ // How many lines the cart has, which is what a cart badge counts - not how many units are in it
70
+ get lineCount() {
71
+ return this._quantities.size;
72
+ }
73
+ // Every unit of every line added up. Fractional where a package is sold loose.
74
+ get totalQuantity() {
75
+ let result = 0;
76
+ for (const quantity of this._quantities.values())
77
+ result += quantity;
78
+ return result;
79
+ }
80
+ // Zero for a package that is not in the cart
81
+ getQuantity(item) {
82
+ return this._quantities.get(item.id) ?? 0;
83
+ }
84
+ // Sets how much of one package the cart holds; zero takes it out. A package a customer cannot
85
+ // buy - one disabled, or priced in no list this storefront sells from - is refused, as is a
86
+ // fractional quantity of a package that is not sold loose, and a quantity past what the wire can
87
+ // carry. Returns this, so edits chain.
88
+ setQuantity(item, quantity) {
89
+ if (!Number.isFinite(quantity) || quantity < 0)
90
+ throw new InvalidArgumentException(`${quantity} is not a quantity`);
91
+ if (quantity === 0) {
92
+ this._quantities.delete(item.id);
93
+ return this;
94
+ }
95
+ if (this.getBuyablePackage(item.id) === null)
96
+ throw new InvalidArgumentException(`Package ${Ids.toStorageHexString(item.id)} cannot be bought from this catalog`);
97
+ if (!item.isSoldLoose && !Number.isInteger(quantity))
98
+ throw new InvalidArgumentException(`${item.name} is not sold loose, so it cannot be bought ${quantity} at a time`);
99
+ // A quantity travels as an int32 multiplied by Constants.QUANTITY_FACTOR, so this is the most
100
+ // of one package a cart can hold - and it is refused here rather than silently wrapping there.
101
+ if (quantity > CartEditor.MAX_QUANTITY)
102
+ throw new InvalidArgumentException(`${quantity} is more than the ${CartEditor.MAX_QUANTITY} the protocol can carry`);
103
+ if (!this._quantities.has(item.id) &&
104
+ this._quantities.size >= proto_Constants.SHOPPING_LIST_MAX_ITEMS)
105
+ throw new InvalidArgumentException(`A cart holds at most ${proto_Constants.SHOPPING_LIST_MAX_ITEMS} lines`);
106
+ this._quantities.set(item.id, quantity);
107
+ return this;
108
+ }
109
+ // Adds to what the cart already holds of this package, which is what a quantity stepper does.
110
+ // A negative amount takes away, and never below nothing.
111
+ add(item, quantity = 1) {
112
+ return this.setQuantity(item, Math.max(0, this.getQuantity(item) + quantity));
113
+ }
114
+ remove(item) {
115
+ return this.setQuantity(item, 0);
116
+ }
117
+ clear() {
118
+ this._quantities.clear();
119
+ return this;
120
+ }
121
+ // What the packages cost before tax, offers already taken off
122
+ get itemsTotal() {
123
+ let result = 0;
124
+ for (const line of this.lines)
125
+ result += line.amount;
126
+ return result;
127
+ }
128
+ // What the offers took off, which is what a bill shows as savings. Zero when nothing is on offer.
129
+ get savings() {
130
+ let result = 0;
131
+ for (const line of this.lines)
132
+ result += line.savings;
133
+ return result;
134
+ }
135
+ get itemsTax() {
136
+ let result = 0;
137
+ for (const line of this.lines)
138
+ result += line.totalTax;
139
+ return result;
140
+ }
141
+ // The packages with their tax, and what the free delivery threshold is measured against - the
142
+ // same total Java ShoppingCartViewModel.calculateDeliveryCharge compares, less its delivery line
143
+ get itemsFinalTotal() {
144
+ let result = 0;
145
+ for (const line of this.lines)
146
+ result += line.finalPrice;
147
+ return result;
148
+ }
149
+ // True when this business delivers at all. False means no delivery charge exists to add, whatever
150
+ // build() is asked for, and a UI has no fulfillment choice to offer either.
151
+ get isDeliveryOffered() {
152
+ return this._settings.fulfillmentModes.includes(proto_FulfillmentMode.DELIVERY);
153
+ }
154
+ // What delivering this cart would cost before tax: the business's flat charge, or zero where it
155
+ // has been waived by the cart reaching freeDeliveryTotal, or where the business delivers free
156
+ // anyway. Always zero when the business does not deliver.
157
+ get deliveryCharge() {
158
+ return this.getDeliveryCharge(this.itemsFinalTotal);
159
+ }
160
+ // What delivery costs before any waiver - the price a bill strikes through when it reads FREE.
161
+ // Zero where the business delivers free anyway, in which case there is nothing to strike through.
162
+ get regularDeliveryCharge() {
163
+ if (!this.isDeliveryOffered)
164
+ return 0;
165
+ // Java clamps a negative charge to zero rather than treating it as a refund, and so does this
166
+ return Math.max(0, this._settings.deliveryCharge);
167
+ }
168
+ // True when delivering this cart costs nothing, whether because the business never charges or
169
+ // because this cart has reached the threshold
170
+ get isDeliveryFree() {
171
+ return this.isDeliveryOffered && this.deliveryCharge === 0;
172
+ }
173
+ // How much more the customer would have to add for delivery to stop being charged, which is the
174
+ // "add X more for free delivery" note. Zero once it is already free, and null when the business
175
+ // has set no threshold, so there is no amount that would ever make it free.
176
+ get amountToFreeDelivery() {
177
+ if (!this.isDeliveryOffered)
178
+ return null;
179
+ if (this.regularDeliveryCharge === 0)
180
+ return 0; // Already free, and free at any total
181
+ const freeDeliveryTotal = this._settings.freeDeliveryTotal;
182
+ if (freeDeliveryTotal <= 0)
183
+ return null; // No threshold: adding more changes nothing
184
+ return Math.max(0, freeDeliveryTotal - this.itemsFinalTotal);
185
+ }
186
+ // Tax on the delivery charge, at the rates GeneralSettings.deliveryChargeTaxes carries - which
187
+ // are the business's own and have nothing to do with a product's. Zero when delivery is free,
188
+ // there being nothing to tax.
189
+ get deliveryTax() {
190
+ return CartEditor.getTax(this.deliveryCharge, this._settings.deliveryChargeTaxes);
191
+ }
192
+ // What the cart comes to. Delivery is left out unless asked for, the way a cart page leaves it
193
+ // out and a checkout page puts it in - the same choice build() takes.
194
+ total(withDeliveryCharge = false) {
195
+ const total = this.itemsFinalTotal;
196
+ if (!withDeliveryCharge || this.isEmpty)
197
+ return total;
198
+ // Worked out from the total already in hand, rather than through the two getters above, each
199
+ // of which would add the lines up again
200
+ const charge = this.getDeliveryCharge(total);
201
+ return total + charge + CartEditor.getTax(charge, this._settings.deliveryChargeTaxes);
202
+ }
203
+ // The cart as a list to write: what Model.updateCartLocalOnly and Model.updateCart take, and
204
+ // what a later Model.createOrder turns into an order.
205
+ //
206
+ // withDeliveryCharge decides whether the delivery charge rides along as a line of its own,
207
+ // carrying ReservedPackage.DELIVERY_CHARGE_PACKAGE and flagged by
208
+ // ShoppingList.autoAddDeliveryCharge. A cart page has no fulfillment chosen yet and so leaves it
209
+ // out, which is the default; a checkout page that has settled on delivery asks for it. The line
210
+ // is added even when the charge has been waived - at zero, with the regular charge as its
211
+ // priceDiscount - so a bill can show FREE with the original struck through, which is what Java
212
+ // ShoppingCartViewModel.setDeliveryCharge writes too.
213
+ //
214
+ // Nothing is added to an empty cart: a list holding a delivery charge and no goods is not an
215
+ // order, and Java removes exactly that line when it is all that is left.
216
+ // The list comes back not final. Making it final is placing an order, which is
217
+ // Model.createOrder's to do.
218
+ build(withDeliveryCharge = false) {
219
+ const items = [];
220
+ for (const line of this.lines)
221
+ items.push(line.toShoppingListItem(items.length));
222
+ const isDeliveryCharged = withDeliveryCharge && items.length > 0 && this.isDeliveryOffered;
223
+ if (isDeliveryCharged)
224
+ items.push(this.buildDeliveryChargeItem(items.length));
225
+ // The total is written off the lines being sent, the way ShoppingList.proto defines it: the
226
+ // sum of their final prices, tax included. There is no list-wide discount - an offer comes off
227
+ // a line, and a storefront gives no other.
228
+ let totalAmountBeforeDiscount = 0;
229
+ for (const item of items)
230
+ totalAmountBeforeDiscount += item.finalPrice;
231
+ return new ShoppingList(false, isDeliveryCharged, false, this._inventoryLocationId, this._catalog.priceList?.id ?? MERABILLS_ONLINE_PRICE_LIST_ID, totalAmountBeforeDiscount, 0, items.length, 0, Object.freeze(items), Object.freeze([]));
232
+ }
233
+ // The delivery charge as a line: one of it, at whatever it costs after any waiver, with the
234
+ // waived amount as its price discount - the same shape as a package bought on offer, which is
235
+ // what lets a bill render it with no special case beyond its label.
236
+ buildDeliveryChargeItem(shoppingListOrder) {
237
+ const charge = this.deliveryCharge;
238
+ return new ShoppingListItem(Ids.buildId(proto_Constants.PACKAGE_ENTITY_TYPE_ID, BigInt(proto_ReservedPackage.DELIVERY_CHARGE_PACKAGE)), 1, charge, shoppingListOrder, false, false, false, this.regularDeliveryCharge - charge, 0, this._settings.deliveryChargeTaxes, 0, null);
239
+ }
240
+ // The charge against a cart that comes to this much, which is the whole of the policy. Takes
241
+ // the total rather than reading it, every caller here having worked it out already.
242
+ getDeliveryCharge(itemsFinalTotal) {
243
+ const charge = this.regularDeliveryCharge;
244
+ if (charge === 0)
245
+ return 0; // Free for everyone, or not delivered at all
246
+ const freeDeliveryTotal = this._settings.freeDeliveryTotal;
247
+ if (freeDeliveryTotal <= 0)
248
+ return charge; // Never free, whatever the cart totals
249
+ return itemsFinalTotal >= freeDeliveryTotal ? 0 : charge;
250
+ }
251
+ static getTax(amount, taxes) {
252
+ let result = 0;
253
+ for (const tax of taxes)
254
+ result += (amount * tax.rate) / 100;
255
+ return result;
256
+ }
257
+ // The package this id names, and null unless a customer could actually buy it right now
258
+ getBuyablePackage(packageId) {
259
+ const item = this._catalog.getPackage(packageId);
260
+ return item !== null && item.isBuyable ? item : null;
261
+ }
262
+ }
263
+ // What an int32 quantity multiplied by Constants.QUANTITY_FACTOR reaches
264
+ CartEditor.MAX_QUANTITY = Math.floor(0x7fffffff / proto_Constants.QUANTITY_FACTOR);
265
+ export default CartEditor;
266
+ // One line of a cart being edited: the package, the product it is a variant of, and how much of it
267
+ // - with the prices the catalog gives it now. A read-only view over what CartEditor holds, rebuilt
268
+ // on every read of CartEditor.lines rather than kept.
269
+ export class CartLine {
270
+ constructor(_package, _product, _quantity) {
271
+ this._package = _package;
272
+ this._product = _product;
273
+ this._quantity = _quantity;
274
+ }
275
+ get package() {
276
+ return this._package;
277
+ }
278
+ // Null only for a package whose product is not in the catalog, which a buyable package's never is
279
+ get product() {
280
+ return this._product;
281
+ }
282
+ get quantity() {
283
+ return this._quantity;
284
+ }
285
+ // What one unit costs: the offer price where the package is on offer, else the regular price.
286
+ // A buyable package always has a price, so this is never guessed.
287
+ get unitPrice() {
288
+ return this._package.price?.finalPrice ?? 0;
289
+ }
290
+ get regularUnitPrice() {
291
+ return this._package.price?.regularPrice ?? 0;
292
+ }
293
+ get isOnOffer() {
294
+ return this._package.price?.isOnOffer ?? false;
295
+ }
296
+ // The taxes of the product this package belongs to, which are what the line is taxed at
297
+ get taxes() {
298
+ return this._product?.taxes ?? EMPTY_TAXES;
299
+ }
300
+ // What this line costs before tax
301
+ get amount() {
302
+ return this.unitPrice * this._quantity;
303
+ }
304
+ // What the offer takes off this line
305
+ get savings() {
306
+ return (this.regularUnitPrice - this.unitPrice) * this._quantity;
307
+ }
308
+ get totalTax() {
309
+ let result = 0;
310
+ for (const tax of this.taxes)
311
+ result += (this.amount * tax.rate) / 100;
312
+ return result;
313
+ }
314
+ // What this line adds to the cart: what it costs plus the tax on it
315
+ get finalPrice() {
316
+ return this.amount + this.totalTax;
317
+ }
318
+ // This line as the protocol carries it. Only CartEditor.build calls this, which is what assigns
319
+ // the order.
320
+ toShoppingListItem(shoppingListOrder) {
321
+ return new ShoppingListItem(this._package.id, this._quantity, this.unitPrice, shoppingListOrder, false, false, false, this.regularUnitPrice - this.unitPrice, 0, this.taxes, 0, null);
322
+ }
323
+ }
324
+ const EMPTY_TAXES = Object.freeze([]);
@@ -0,0 +1,28 @@
1
+ import Item from "./Item";
2
+ import Package from "./Package";
3
+ import PriceList from "./PriceList";
4
+ import Product from "./Product";
5
+ import ProductCategory from "./ProductCategory";
6
+ export default class Catalog {
7
+ private readonly _priceList;
8
+ constructor(categories: ReadonlyArray<ProductCategory>, products: ReadonlyArray<Product>, _priceList: PriceList | null);
9
+ get categories(): ReadonlyArray<ProductCategory>;
10
+ get products(): ReadonlyArray<Product>;
11
+ get priceList(): PriceList | null;
12
+ get productCount(): number;
13
+ get packageCount(): number;
14
+ getCategory(productCategoryId: bigint): ProductCategory | null;
15
+ getProduct(productId: bigint): Product | null;
16
+ getPackage(packageId: bigint): Package | null;
17
+ static comparePackages(left: Package, right: Package): number;
18
+ static compareByName(left: Item, right: Item): number;
19
+ private static compareNames;
20
+ private static compareIds;
21
+ private static comparePrices;
22
+ static readonly EMPTY: Catalog;
23
+ private readonly _categories;
24
+ private readonly _products;
25
+ private readonly _categoryMap;
26
+ private readonly _productMap;
27
+ private readonly _packageMap;
28
+ }
@@ -0,0 +1,115 @@
1
+ import { Constants as proto_Constants } from "./generated/Constants";
2
+ import { EntityHeader } from "./Entity";
3
+ import ProductCategory from "./ProductCategory";
4
+ // The catalog of a business: its categories, and every product in it. Not an entity and not on
5
+ // the wire - the sync layer rebuilds this hierarchy from the flat item entities of a sync response.
6
+ class Catalog {
7
+ constructor(categories, products, _priceList) {
8
+ this._priceList = _priceList;
9
+ this._categoryMap = new Map();
10
+ this._productMap = new Map();
11
+ this._packageMap = new Map();
12
+ // A dummy category standing in for a product whose parentItemId is Constants.ENTITY_ID_NULL.
13
+ // Its own id is that same ENTITY_ID_NULL, so getCategory(product.productCategoryId) resolves
14
+ // it exactly like a real category - including being left out below, the same as any other
15
+ // category, when nothing buyable ends up in it.
16
+ const uncategorized = new ProductCategory(new EntityHeader(BigInt(proto_Constants.ENTITY_ID_NULL), 0n, 0n, 0n, 0n), "", null, BigInt(proto_Constants.ENTITY_ID_NULL), false);
17
+ this._products = Object.freeze([...products]);
18
+ // Only a buyable product - one a customer can actually order - counts towards a category. A
19
+ // product that cannot be bought right now keeps its place in Catalog.products and stays
20
+ // reachable by getProduct, but does not keep a category alive on its own.
21
+ const productsByCategory = new Map();
22
+ for (const product of this._products) {
23
+ this._productMap.set(product.id, product);
24
+ if (product.isBuyable) {
25
+ const productsOfCategory = productsByCategory.get(product.productCategoryId);
26
+ if (productsOfCategory === undefined)
27
+ productsByCategory.set(product.productCategoryId, [product]);
28
+ else
29
+ productsOfCategory.push(product);
30
+ }
31
+ for (const pkg of product.packages)
32
+ this._packageMap.set(pkg.id, pkg);
33
+ }
34
+ // A category with nothing a customer can order is left out rather than shown empty - there is
35
+ // no point showing it to a customer. Nothing here is special-cased for the dummy: it is left
36
+ // out exactly the same way when nothing buyable is uncategorized.
37
+ const nonEmptyCategories = [];
38
+ for (const category of [...categories, uncategorized]) {
39
+ const categoryProducts = productsByCategory.get(category.id);
40
+ if (categoryProducts === undefined)
41
+ continue;
42
+ category.products = Object.freeze(categoryProducts);
43
+ nonEmptyCategories.push(category);
44
+ }
45
+ this._categories = Object.freeze(nonEmptyCategories);
46
+ for (const category of this._categories)
47
+ this._categoryMap.set(category.id, category);
48
+ }
49
+ // In no particular order - a UI orders these for display
50
+ get categories() {
51
+ return this._categories;
52
+ }
53
+ // Every product of the business, whether or not it is in a category, in no particular order
54
+ get products() {
55
+ return this._products;
56
+ }
57
+ // The one price list a storefront sells from - MeraBills Online - and null until it arrives.
58
+ // Every price in this catalog belongs to it; a price in any other list is not sold here and is
59
+ // left out. A shopping list names this list, or falls back to its id when it has none set.
60
+ get priceList() {
61
+ return this._priceList;
62
+ }
63
+ get productCount() {
64
+ return this._products.length;
65
+ }
66
+ get packageCount() {
67
+ return this._packageMap.size;
68
+ }
69
+ getCategory(productCategoryId) {
70
+ return this._categoryMap.get(productCategoryId) ?? null;
71
+ }
72
+ getProduct(productId) {
73
+ return this._productMap.get(productId) ?? null;
74
+ }
75
+ // Returns null for the four reserved package ids a shopping list uses for its charge and
76
+ // summary lines (see Ids.isReservedPackageId), and for a package whose product was deleted
77
+ getPackage(packageId) {
78
+ return this._packageMap.get(packageId) ?? null;
79
+ }
80
+ // Display comparator for packages, matching the merchant app's PackageComparator (minus clauses
81
+ // a storefront can never exercise: disabled packages and the purchase price list, neither sent)
82
+ static comparePackages(left, right) {
83
+ const byName = Catalog.compareNames(left.name, right.name);
84
+ if (byName !== 0)
85
+ return byName;
86
+ const byPrice = Catalog.comparePrices(left.price, right.price);
87
+ if (byPrice !== 0)
88
+ return byPrice;
89
+ return Catalog.compareIds(left, right);
90
+ }
91
+ // Display comparator for items generally: case insensitive, numeric, id-tiebroken
92
+ static compareByName(left, right) {
93
+ const byName = Catalog.compareNames(left.name, right.name);
94
+ if (byName !== 0)
95
+ return byName;
96
+ return Catalog.compareIds(left, right);
97
+ }
98
+ static compareNames(left, right) {
99
+ return left.localeCompare(right, undefined, { numeric: true, sensitivity: "accent" });
100
+ }
101
+ static compareIds(left, right) {
102
+ return left.id < right.id ? -1 : left.id > right.id ? 1 : 0;
103
+ }
104
+ static comparePrices(left, right) {
105
+ // A package with no price sorts after one with a price: it cannot be bought
106
+ if (left === null)
107
+ return right === null ? 0 : 1;
108
+ if (right === null)
109
+ return -1;
110
+ return left.offerPrice - right.offerPrice;
111
+ }
112
+ }
113
+ // A business whose catalog has not arrived yet
114
+ Catalog.EMPTY = new Catalog(Object.freeze([]), Object.freeze([]), null);
115
+ export default Catalog;
@@ -0,0 +1,10 @@
1
+ export { Constants } from "./generated/Constants";
2
+ export declare function buildPriceListId(priceList: number): bigint;
3
+ export declare const MERABILLS_ONLINE_PRICE_LIST_ID: bigint;
4
+ export declare const MAX_SCALED_AMOUNT: bigint;
5
+ export declare function amountFromScaledValue(scaledAmount: bigint): number;
6
+ export declare function amountToScaledValue(amount: number): bigint;
7
+ export declare function quantityFromScaledValue(scaledQuantity: number): number;
8
+ export declare function quantityToScaledValue(quantity: number): number;
9
+ export declare function taxRateFromScaledValue(scaledTaxRate: number): number;
10
+ export declare function taxRateToScaledValue(taxRate: number): number;
@@ -0,0 +1,43 @@
1
+ import { Constants as proto_Constants } from "./generated/Constants";
2
+ import Ids from "./Ids";
3
+ // The protocol's limits and scaling factors, and the wire-to-UI value conversions.
4
+ // Limits are re-exported rather than redeclared, so a Java constant has only one place to be wrong.
5
+ export { Constants } from "./generated/Constants";
6
+ // The id of a built-in price list, from the low part Constants carries for it:
7
+ // DEFAULT_SALE_PRICE_LIST, DEFAULT_PURCHASE_PRICE_LIST, MERABILLS_ONLINE_PRICE_LIST,
8
+ // AMAZON_SELLER_PRICE_LIST, FACEBOOK_MARKETPLACE_PRICE_LIST or WHATSAPP_BUSINESS_PRICE_LIST. These
9
+ // are the six ids of Java PriceListIds, built the way it builds them: the low part is a constant
10
+ // of the protocol, the id is not, 64 bits not fitting a protobuf enum value.
11
+ export function buildPriceListId(priceList) {
12
+ return Ids.buildId(proto_Constants.PRICE_LIST_ENTITY_TYPE_ID, BigInt(priceList));
13
+ }
14
+ // The only price list this library reads. A business prices the same package in several lists -
15
+ // what it sells at over the counter, what a marketplace it also sells on charges - and only this
16
+ // one is its storefront's. A price in any other list is passed over; see ProtoMap.buildBusiness.
17
+ export const MERABILLS_ONLINE_PRICE_LIST_ID = buildPriceListId(proto_Constants.MERABILLS_ONLINE_PRICE_LIST);
18
+ // The largest amount the protocol can carry (Java Transaction.MAX_AMOUNT); kept here since it doesn't fit
19
+ // a protobuf enum value. Unscaled it's well below 2^53, so an amount is always safe as a number.
20
+ export const MAX_SCALED_AMOUNT = 17592186044415n;
21
+ // An amount, price or discount travels multiplied by Constants.AMOUNT_FACTOR, bounded by
22
+ // MAX_SCALED_AMOUNT, so it's safe as a number even though it arrives as an int64.
23
+ export function amountFromScaledValue(scaledAmount) {
24
+ return Number(scaledAmount) / proto_Constants.AMOUNT_FACTOR;
25
+ }
26
+ export function amountToScaledValue(amount) {
27
+ return BigInt(Math.round(amount * proto_Constants.AMOUNT_FACTOR));
28
+ }
29
+ // A quantity travels multiplied by Constants.QUANTITY_FACTOR, so 1.5 kg arrives as 1500
30
+ export function quantityFromScaledValue(scaledQuantity) {
31
+ return scaledQuantity / proto_Constants.QUANTITY_FACTOR;
32
+ }
33
+ export function quantityToScaledValue(quantity) {
34
+ return Math.round(quantity * proto_Constants.QUANTITY_FACTOR);
35
+ }
36
+ // A tax rate travels multiplied by Constants.TAX_RATE_FACTOR, so 18% arrives as 1800. What comes
37
+ // back is a percentage and not a fraction: 18, not 0.18.
38
+ export function taxRateFromScaledValue(scaledTaxRate) {
39
+ return scaledTaxRate / (proto_Constants.TAX_RATE_FACTOR / 100);
40
+ }
41
+ export function taxRateToScaledValue(taxRate) {
42
+ return Math.round(taxRate * (proto_Constants.TAX_RATE_FACTOR / 100));
43
+ }
@@ -0,0 +1,2 @@
1
+ export declare function forBusiness(nameSuffix: string): string;
2
+ export declare function forProfile(nameSuffix: string): string;
@@ -0,0 +1,21 @@
1
+ // What each container of the local store is called: a business container prefixes Business.businessId,
2
+ // a profile container prefixes the subject id. The prefix keeps the two id spaces from ever colliding.
3
+ // One business, holding that business and everything in its business database
4
+ export function forBusiness(nameSuffix) {
5
+ return `${VERSION_PREFIX}${BUSINESS_PREFIX}${nameSuffix}`;
6
+ }
7
+ // One signed in subject, holding their profile database and the tokens that sign their requests.
8
+ // Several can coexist so signing back in after a sign-out is incremental rather than a fresh download.
9
+ export function forProfile(nameSuffix) {
10
+ return `${VERSION_PREFIX}${PROFILE_PREFIX}${nameSuffix}`;
11
+ }
12
+ // Every name carries the layout version of what is inside it, so that a change to how this library
13
+ // stores a container does not have to be migrated: the new version simply reads and writes names
14
+ // nothing has written before, and what the old one left behind is inert until it is deleted.
15
+ // Bump this whenever a stored blob stops meaning what it used to - a caller cannot be relied on to
16
+ // clear a store it did not know had changed, and a half-understood container is worse than no cache.
17
+ const VERSION_PREFIX = "V1.";
18
+ // Neither id space is this library's to constrain, so a subject id that happened to equal a business
19
+ // id would otherwise put a profile and a catalog in one container
20
+ const BUSINESS_PREFIX = "b.";
21
+ const PROFILE_PREFIX = "p.";
@@ -0,0 +1,15 @@
1
+ export default interface Database {
2
+ getContainer(name: string): Promise<Container>;
3
+ getContainerNames(): Promise<ReadonlyArray<string>>;
4
+ deleteContainer(name: string): Promise<void>;
5
+ }
6
+ export interface Container {
7
+ read(id: bigint): Promise<StoredBlob | null>;
8
+ readAll(): Promise<ReadonlyArray<StoredBlob>>;
9
+ write(blobs: ReadonlyArray<StoredBlob>, deletes: ReadonlyArray<bigint>): Promise<number>;
10
+ }
11
+ export interface StoredBlob {
12
+ readonly id: bigint;
13
+ readonly eTag: bigint | null;
14
+ readonly bytes: Uint8Array;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ export default abstract class Entity {
2
+ protected constructor(header: EntityHeader);
3
+ get id(): bigint;
4
+ get createdAt(): Date;
5
+ get updatedAt(): Date;
6
+ get updateNumber(): bigint;
7
+ get updateInfo(): bigint;
8
+ get entityType(): number;
9
+ get updateType(): number;
10
+ get isDeleted(): boolean;
11
+ get parent(): Entity | null;
12
+ protected set parent(value: Entity | null);
13
+ private static readonly UPDATE_TYPE_MASK;
14
+ private readonly _header;
15
+ private _parent;
16
+ }
17
+ export declare class EntityHeader {
18
+ readonly id: bigint;
19
+ readonly createdAt: bigint;
20
+ readonly updatedAt: bigint;
21
+ readonly updateNumber: bigint;
22
+ readonly updateInfo: bigint;
23
+ constructor(id: bigint, createdAt: bigint, updatedAt: bigint, updateNumber: bigint, updateInfo: bigint);
24
+ }
25
+ export declare class UpdatableEntity extends Entity {
26
+ static setParent(child: Entity, parent: Entity | null): void;
27
+ }
package/dist/Entity.js ADDED
@@ -0,0 +1,69 @@
1
+ import { Constants as proto_Constants } from "./generated/Constants";
2
+ import Ids from "./Ids";
3
+ // Something the protocol syncs one at a time: an id, create/update times, an update number, and
4
+ // the database it was last updated in. Rebuilt from the wire rather than edited in place, so the
5
+ // only mutable field is parent, set once containment is rebuilt after a sync (nothing nests on the wire).
6
+ class Entity {
7
+ constructor(header) {
8
+ this._parent = null;
9
+ this._header = header;
10
+ }
11
+ get id() {
12
+ return this._header.id;
13
+ }
14
+ get createdAt() {
15
+ return new Date(Number(this._header.createdAt));
16
+ }
17
+ get updatedAt() {
18
+ return new Date(Number(this._header.updatedAt));
19
+ }
20
+ // Monotonically increasing, and only comparable within the same entity type and database. Basis of a sync cursor.
21
+ get updateNumber() {
22
+ return this._header.updateNumber;
23
+ }
24
+ // The database this entity was last updated in (Constants.MASTER_DB_ID = the server); sync out uses it
25
+ // to avoid echoing a store's own changes back. Business entities only - always ENTITY_ID_NULL on profile entities.
26
+ get updateInfo() {
27
+ return this._header.updateInfo;
28
+ }
29
+ // One of the Constants.*_ENTITY_TYPE_ID values, read from the id's most significant byte (not on the wire).
30
+ get entityType() {
31
+ return Ids.getEntityTypeId(this.id);
32
+ }
33
+ // One of Constants.UPDATE_TYPE_CREATED, _MODIFIED or _DELETED, held in the update number's last two bits.
34
+ get updateType() {
35
+ return Number(this._header.updateNumber & Entity.UPDATE_TYPE_MASK);
36
+ }
37
+ // A deleted entity is a tombstone: only the header fields are meaningful. A UI never reaches one.
38
+ get isDeleted() {
39
+ return this.updateType === proto_Constants.UPDATE_TYPE_DELETED;
40
+ }
41
+ // What contains this entity, once the sync layer has rebuilt containment (e.g. the Business of a
42
+ // Configuration). Null while being built, and on Business itself, which is the root.
43
+ get parent() {
44
+ return this._parent;
45
+ }
46
+ set parent(value) {
47
+ this._parent = value;
48
+ }
49
+ }
50
+ Entity.UPDATE_TYPE_MASK = BigInt(proto_Constants.UPDATE_TYPE_MASK);
51
+ export default Entity;
52
+ // The fields of the Entity message every entity carries, passed as one value since they're all
53
+ // bigint and would otherwise transpose silently at a call site (the protocol has moved one before).
54
+ export class EntityHeader {
55
+ constructor(id, createdAt, updatedAt, updateNumber, updateInfo) {
56
+ this.id = id;
57
+ this.createdAt = createdAt;
58
+ this.updatedAt = updatedAt;
59
+ this.updateNumber = updateNumber;
60
+ this.updateInfo = updateInfo;
61
+ }
62
+ }
63
+ // Lets the sync layer set an entity's parent via casting, kept off the surface a UI sees.
64
+ // Static because the setter it reaches is protected.
65
+ export class UpdatableEntity extends Entity {
66
+ static setParent(child, parent) {
67
+ child.parent = parent;
68
+ }
69
+ }