@peabody-soft/mbs-model-ts 1.4.0 → 1.6.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/Address.js +8 -11
- package/dist/Attachment.js +7 -8
- package/dist/AttachmentBaseUrl.js +10 -10
- package/dist/AuthTokens.js +12 -7
- package/dist/Business.js +19 -19
- package/dist/BusinessIdentifier.js +2 -2
- package/dist/BusinessLink.js +16 -16
- package/dist/BusinessProfile.js +4 -4
- package/dist/CartEditor.d.ts +10 -5
- package/dist/CartEditor.js +258 -180
- package/dist/Catalog.js +31 -29
- package/dist/Constants.js +10 -10
- package/dist/ContainerNames.js +10 -11
- package/dist/Entity.js +11 -9
- package/dist/Enums.d.ts +1 -1
- package/dist/Enums.js +4 -3
- package/dist/Exceptions.js +2 -2
- package/dist/GeneralSettings.d.ts +4 -2
- package/dist/GeneralSettings.js +30 -18
- package/dist/HttpStatusCodes.js +10 -11
- package/dist/Ids.d.ts +2 -0
- package/dist/Ids.js +28 -24
- package/dist/Item.js +6 -7
- package/dist/LatLong.js +3 -3
- package/dist/LiveData.js +6 -6
- package/dist/Measures.js +2 -2
- package/dist/Message.js +6 -5
- package/dist/MockDatabase.js +2 -2
- package/dist/Model.d.ts +5 -0
- package/dist/Model.js +400 -254
- package/dist/Order.js +20 -17
- package/dist/Package.js +7 -8
- package/dist/PackageIdentifier.js +2 -2
- package/dist/PaymentMethodDetail.js +4 -4
- package/dist/Photo.js +6 -7
- package/dist/Price.js +9 -6
- package/dist/PriceList.js +2 -2
- package/dist/Product.d.ts +5 -3
- package/dist/Product.js +26 -19
- package/dist/ProductCategory.js +3 -4
- package/dist/Profile.d.ts +1 -0
- package/dist/Profile.js +94 -8
- package/dist/ProtoMap.d.ts +18 -8
- package/dist/ProtoMap.js +451 -331
- package/dist/Routes.js +3 -3
- package/dist/ServiceExceptions.js +11 -12
- package/dist/ServiceProxy.js +19 -23
- package/dist/ShoppingList.d.ts +30 -1
- package/dist/ShoppingList.js +339 -47
- package/dist/SyncCache.d.ts +12 -0
- package/dist/SyncCache.js +136 -56
- package/dist/SyncKeys.js +8 -8
- package/dist/SyncState.js +5 -5
- package/dist/Tax.d.ts +12 -4
- package/dist/Tax.js +37 -13
- package/dist/TaxHelper.d.ts +12 -0
- package/dist/TaxHelper.js +85 -0
- package/dist/TaxType.d.ts +7 -0
- package/dist/TaxType.js +31 -0
- package/dist/UserProfile.js +4 -4
- package/dist/WorkingDays.js +15 -15
- package/dist/generated/AttachmentType.d.ts +19 -1
- package/dist/generated/AttachmentType.js +18 -0
- package/dist/generated/AuthTokens.d.ts +3 -1
- package/dist/generated/BusinessLink.d.ts +4 -1
- package/dist/generated/BusinessSyncInRequest.d.ts +11 -3
- package/dist/generated/BusinessSyncInResponse.d.ts +5 -4
- package/dist/generated/BusinessSyncOutRequest.d.ts +37 -10
- package/dist/generated/BusinessSyncOutRequest.js +42 -29
- package/dist/generated/BusinessSyncOutResponse.d.ts +14 -1
- package/dist/generated/Configuration.d.ts +2 -1
- package/dist/generated/CreateUserRequest.d.ts +8 -2
- package/dist/generated/Entity.d.ts +7 -3
- package/dist/generated/GeneralSettings.d.ts +14 -4
- package/dist/generated/GeneralSettings.js +9 -1
- package/dist/generated/Message.d.ts +4 -2
- package/dist/generated/Product.d.ts +10 -1
- package/dist/generated/Product.js +9 -1
- package/dist/generated/ProfileSyncOutRequest.d.ts +17 -1
- package/dist/generated/ProfileSyncOutResponse.d.ts +8 -3
- package/dist/generated/SalesOrderPayload.d.ts +7 -2
- package/dist/generated/ShoppingList.d.ts +6 -3
- package/dist/generated/ShoppingListItem.d.ts +21 -6
- package/dist/generated/Tax.d.ts +4 -4
- package/dist/generated/Tax.js +7 -8
- package/dist/generated/WorkingHoursRange.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -10
- package/package.json +55 -55
- package/dist/generated/TaxType.d.ts +0 -25
- package/dist/generated/TaxType.js +0 -30
package/dist/ShoppingList.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
+
import { ReservedPackage as proto_ReservedPackage } from "./generated/ReservedPackage";
|
|
3
|
+
import { MAX_SCALED_AMOUNT, MERABILLS_ONLINE_PRICE_LIST_ID, amountFromScaledValue, amountToScaledValue, quantityFromScaledValue, quantityToScaledValue, } from "./Constants";
|
|
4
|
+
import { InvalidArgumentException } from "./Exceptions";
|
|
2
5
|
import Ids from "./Ids";
|
|
6
|
+
import { TaxAmount } from "./Tax";
|
|
7
|
+
import TaxHelper from "./TaxHelper";
|
|
3
8
|
// A list of packages being bought: a shopping cart, or what an order contains.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
9
|
+
//
|
|
10
|
+
// Every unit price on a list is WITHOUT taxes, with the taxes worked out on top (see
|
|
11
|
+
// ShoppingListItem). Each item also offers the price WITH taxes that a customer is shown.
|
|
12
|
+
// Everything that adds up (line totals, the list's total, a discount) already has its taxes in it.
|
|
13
|
+
// The taxes inside a total are available by type (computedTaxes).
|
|
14
|
+
//
|
|
15
|
+
// Wire totals are authoritative: totalAmount is the number to display. The computed ones reproduce
|
|
16
|
+
// Java's arithmetic to the unit for the items that are here, so they match the wire whenever the
|
|
17
|
+
// items were sent. A list can arrive without its items, and then only the wire total covers it.
|
|
6
18
|
class ShoppingList {
|
|
7
|
-
//
|
|
8
|
-
// Constants.AMOUNT_FACTOR.
|
|
19
|
+
// For ProtoMap, which sees totals still multiplied by Constants.AMOUNT_FACTOR.
|
|
9
20
|
static fromScaledValues(isFinal, autoAddDeliveryCharge, autoAddServiceCharge, inventoryLocationId, priceListId, scaledTotalAmountBeforeDiscount, scaledDiscount, existingItemCount, deletedItemCount, existingItems, deletedItems) {
|
|
10
21
|
return new ShoppingList(isFinal, autoAddDeliveryCharge, autoAddServiceCharge, inventoryLocationId, priceListId, amountFromScaledValue(scaledTotalAmountBeforeDiscount), amountFromScaledValue(scaledDiscount), existingItemCount, deletedItemCount, existingItems, deletedItems);
|
|
11
22
|
}
|
|
23
|
+
// What these items come to, taxes included and each item's own discount taken off: the sum of
|
|
24
|
+
// their finalPrice, which a list of them has as totalAmountBeforeDiscount. Added in scaled values
|
|
25
|
+
// and unscaled once, so adding unscaled numbers adds no rounding error.
|
|
26
|
+
static sumOfFinalPrices(items) {
|
|
27
|
+
return amountFromScaledValue(sumOf(items, (pricing) => pricing.finalPrice));
|
|
28
|
+
}
|
|
12
29
|
constructor(_isFinal, _autoAddDeliveryCharge, _autoAddServiceCharge, _inventoryLocationId, _priceListId, _totalAmountBeforeDiscount, _discount, _existingItemCount, _deletedItemCount, _existingItems, _deletedItems) {
|
|
13
30
|
this._isFinal = _isFinal;
|
|
14
31
|
this._autoAddDeliveryCharge = _autoAddDeliveryCharge;
|
|
@@ -22,8 +39,8 @@ class ShoppingList {
|
|
|
22
39
|
this._existingItems = _existingItems;
|
|
23
40
|
this._deletedItems = _deletedItems;
|
|
24
41
|
}
|
|
25
|
-
// True once the customer is done editing, after which the business may edit it
|
|
26
|
-
//
|
|
42
|
+
// True once the customer is done editing, after which the business may edit it. Only a final
|
|
43
|
+
// list can have deleted items. It is never unset.
|
|
27
44
|
get isFinal() {
|
|
28
45
|
return this._isFinal;
|
|
29
46
|
}
|
|
@@ -45,20 +62,25 @@ class ShoppingList {
|
|
|
45
62
|
get priceListId() {
|
|
46
63
|
return this._priceListId;
|
|
47
64
|
}
|
|
65
|
+
// The sum of the final prices of the existing items, which include their taxes
|
|
48
66
|
get totalAmountBeforeDiscount() {
|
|
49
67
|
return this._totalAmountBeforeDiscount;
|
|
50
68
|
}
|
|
51
|
-
// The discount on the whole list,
|
|
69
|
+
// The discount on the whole list, not on one line. It comes off a total that already has its
|
|
70
|
+
// taxes in it.
|
|
52
71
|
get discount() {
|
|
53
72
|
return this._discount;
|
|
54
73
|
}
|
|
55
|
-
// What the customer owes. Derived
|
|
56
|
-
//
|
|
74
|
+
// What the customer owes, taxes included. Derived from the authoritative totals above, so it is
|
|
75
|
+
// the number to display. Never less than zero, as in Java ShoppingList.getTotalAmount: a discount
|
|
76
|
+
// greater than the total leaves nothing to pay. Worked out in scaled values, so there is no
|
|
77
|
+
// floating point error.
|
|
57
78
|
get totalAmount() {
|
|
58
|
-
|
|
79
|
+
const total = amountToScaledValue(this._totalAmountBeforeDiscount) - amountToScaledValue(this._discount);
|
|
80
|
+
return total > 0n ? amountFromScaledValue(total) : 0;
|
|
59
81
|
}
|
|
60
|
-
// Read from the wire
|
|
61
|
-
//
|
|
82
|
+
// Read from the wire because the protocol sends the count even when it sends no items, so the
|
|
83
|
+
// array length below can differ
|
|
62
84
|
get existingItemCount() {
|
|
63
85
|
return this._existingItemCount;
|
|
64
86
|
}
|
|
@@ -72,33 +94,143 @@ class ShoppingList {
|
|
|
72
94
|
return this._existingItems;
|
|
73
95
|
}
|
|
74
96
|
// The items that were in this list when it was made final and have since been deleted or
|
|
75
|
-
// replaced.
|
|
97
|
+
// replaced. Empty until the list is final.
|
|
76
98
|
get deletedItems() {
|
|
77
99
|
return this._deletedItems;
|
|
78
100
|
}
|
|
79
|
-
//
|
|
80
|
-
//
|
|
101
|
+
// The existing items that are packages bought from the catalog, in the order of the list
|
|
102
|
+
// (shoppingListOrder). The server sends them by package id, so that differs from the order
|
|
103
|
+
// received. The charge lines are in chargeItems, and the items taken out since the list was made
|
|
104
|
+
// final are in removedItems.
|
|
105
|
+
get goodsItems() {
|
|
106
|
+
return this.derived.goods;
|
|
107
|
+
}
|
|
108
|
+
// The existing items that are charge lines (the delivery charge and the service charge), in the
|
|
109
|
+
// order of the list. The other two reserved ids, for a discount and a total, are summary lines a
|
|
110
|
+
// list does not carry as items. They are discount and totalAmount.
|
|
111
|
+
get chargeItems() {
|
|
112
|
+
return this.derived.charges;
|
|
113
|
+
}
|
|
114
|
+
// The delivery charge, an item priced like any other, or null when this list has none
|
|
115
|
+
get deliveryCharge() {
|
|
116
|
+
return (this._existingItems.find((item) => item.reservedPackage === proto_ReservedPackage.DELIVERY_CHARGE_PACKAGE) ?? null);
|
|
117
|
+
}
|
|
118
|
+
// The service charge, in the same way
|
|
119
|
+
get serviceCharge() {
|
|
120
|
+
return (this._existingItems.find((item) => item.reservedPackage === proto_ReservedPackage.SERVICE_CHARGE_PACKAGE) ?? null);
|
|
121
|
+
}
|
|
122
|
+
// The items the business took out of the list after the customer made it final, or replaced with
|
|
123
|
+
// a changed one, as the customer ordered them. It is deletedItems in the order of the list. A
|
|
124
|
+
// deleted charge line is among them, and is told apart by reservedPackage.
|
|
125
|
+
get removedItems() {
|
|
126
|
+
return this.derived.removed;
|
|
127
|
+
}
|
|
128
|
+
// What the business has done to one line of this list since the customer made it final. It is
|
|
129
|
+
// ShoppingListItem.change, except for a legacy order, written by a storefront that did not set
|
|
130
|
+
// isOriginal (CartEditor, up to 1.4.0). Every line of such an order reads as added by the
|
|
131
|
+
// business. A final list with no original, modified or deleted line is taken to be one, since no
|
|
132
|
+
// change the business can make leaves that (adding a line keeps the others original, taking one
|
|
133
|
+
// out leaves it in deletedItems, changing one leaves the old line there too).
|
|
134
|
+
// Show a list's marks with this, not ShoppingListItem.change.
|
|
135
|
+
changeOf(item) {
|
|
136
|
+
const change = item.change;
|
|
137
|
+
if (change !== ItemChange.ADDED)
|
|
138
|
+
return change;
|
|
139
|
+
return this.derived.isUntouchedLegacyOrder ? ItemChange.NONE : ItemChange.ADDED;
|
|
140
|
+
}
|
|
141
|
+
// Sum of finalPrice over existingItems, taxes included. Not authoritative (the wire total is),
|
|
142
|
+
// and only covers the whole list when existingItems.length equals existingItemCount.
|
|
81
143
|
get computedTotal() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
144
|
+
return ShoppingList.sumOfFinalPrices(this._existingItems);
|
|
145
|
+
}
|
|
146
|
+
// The same for the goods alone: what the packages come to, without the charges on top of them.
|
|
147
|
+
// This is what the free delivery threshold is measured against.
|
|
148
|
+
get computedGoodsTotal() {
|
|
149
|
+
return ShoppingList.sumOfFinalPrices(this.goodsItems);
|
|
86
150
|
}
|
|
87
|
-
// The sum of the taxes of the items that are here, with the same
|
|
151
|
+
// The taxes inside computedTotal: the sum of the taxes of the items that are here, with the same
|
|
152
|
+
// caveat as computedTotal
|
|
88
153
|
get computedTotalTax() {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
154
|
+
return amountFromScaledValue(sumOf(this._existingItems, (pricing) => pricing.totalTax));
|
|
155
|
+
}
|
|
156
|
+
// The taxes inside computedTotal, one entry per TYPE of tax, in the order of the types, added up
|
|
157
|
+
// over every item. A type none of the items has is not here. An amount can be zero, for a type
|
|
158
|
+
// that applies only to free items. Java ShoppingList.getTotalTaxesByTaxCode leaves such a type
|
|
159
|
+
// out (JavaVectorsTest pins that), and a bill shows no line for it.
|
|
160
|
+
// A discount does not change the taxes: it comes off a price that already has them in it.
|
|
161
|
+
// Worked out once, like derived but kept apart: this prices the items, and an item that cannot be
|
|
162
|
+
// priced must not stop the list's lines being listed.
|
|
163
|
+
get computedTaxes() {
|
|
164
|
+
const existing = TAXES.get(this);
|
|
165
|
+
if (existing !== undefined)
|
|
166
|
+
return existing;
|
|
167
|
+
const totals = new Map();
|
|
168
|
+
for (const item of this._existingItems) {
|
|
169
|
+
const taxAmounts = pricingOf(item).taxes;
|
|
170
|
+
item.taxes.forEach((tax, index) => {
|
|
171
|
+
totals.set(tax.type, (totals.get(tax.type) ?? 0n) + (taxAmounts[index] ?? 0n));
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const created = Object.freeze([...totals.entries()]
|
|
175
|
+
.sort(([left], [right]) => left - right)
|
|
176
|
+
.map(([type, amount]) => new TaxAmount(type, amountFromScaledValue(amount))));
|
|
177
|
+
TAXES.set(this, created);
|
|
178
|
+
return created;
|
|
179
|
+
}
|
|
180
|
+
// What the existing items come to at their regular prices, with their taxes and none of their own
|
|
181
|
+
// discounts taken off: the total had nothing been on offer. Compared with totalAmount it gives
|
|
182
|
+
// what the customer saved. Same caveat as computedTotal.
|
|
183
|
+
get computedTotalWithoutDiscounts() {
|
|
184
|
+
return amountFromScaledValue(sumOf(this._existingItems, (pricing) => pricing.finalPriceWithoutDiscounts));
|
|
185
|
+
}
|
|
186
|
+
// What the offers on the items, the discounts on them and the discount on the list took off,
|
|
187
|
+
// taxes included, shown on a bill as savings. A waived delivery charge counts too, being an item
|
|
188
|
+
// with a price discount like any other, as in the merchant app's total row. So a basket can save
|
|
189
|
+
// more once delivery is in the list. Zero when nothing was taken off, never negative. Same caveat
|
|
190
|
+
// as computedTotal: it needs the items.
|
|
191
|
+
get savings() {
|
|
192
|
+
const saved = sumOf(this._existingItems, (pricing) => pricing.finalPriceWithoutDiscounts) -
|
|
193
|
+
amountToScaledValue(this.totalAmount);
|
|
194
|
+
return saved > 0n ? amountFromScaledValue(saved) : 0;
|
|
195
|
+
}
|
|
196
|
+
// Worked out once, since the list never changes, so a UI reads the same array every time (a
|
|
197
|
+
// stable dependency) and the list is not sorted at every render
|
|
198
|
+
get derived() {
|
|
199
|
+
const existing = DERIVED.get(this);
|
|
200
|
+
if (existing !== undefined)
|
|
201
|
+
return existing;
|
|
202
|
+
const created = {
|
|
203
|
+
goods: Object.freeze(sortedByOrder(this._existingItems.filter((item) => !item.isReservedPackage))),
|
|
204
|
+
charges: Object.freeze(sortedByOrder(this._existingItems.filter((item) => item.reservedPackage === proto_ReservedPackage.DELIVERY_CHARGE_PACKAGE ||
|
|
205
|
+
item.reservedPackage === proto_ReservedPackage.SERVICE_CHARGE_PACKAGE))),
|
|
206
|
+
removed: Object.freeze(sortedByOrder(this._deletedItems)),
|
|
207
|
+
isUntouchedLegacyOrder: this._isFinal &&
|
|
208
|
+
this._existingItems.length > 0 &&
|
|
209
|
+
this._deletedItems.length === 0 &&
|
|
210
|
+
this._deletedItemCount === 0 &&
|
|
211
|
+
this._existingItems.every((item) => !item.isOriginal && !item.isModified),
|
|
212
|
+
};
|
|
213
|
+
DERIVED.set(this, created);
|
|
214
|
+
return created;
|
|
93
215
|
}
|
|
94
216
|
}
|
|
95
217
|
// An empty cart, for a customer who has not put anything in one yet
|
|
96
218
|
ShoppingList.EMPTY = new ShoppingList(false, false, false, 0, MERABILLS_ONLINE_PRICE_LIST_ID, 0, 0, 0, 0, Object.freeze([]), Object.freeze([]));
|
|
97
219
|
export default ShoppingList;
|
|
98
220
|
// One line of a shopping list: the package being bought, how much of it, and at what price.
|
|
221
|
+
//
|
|
222
|
+
// THE PRICE IS STORED WITHOUT TAXES: unitPrice and priceDiscount are before taxes, which are worked
|
|
223
|
+
// out on top, as in Java ShoppingListItem and ShoppingListItem.proto. A customer sees prices WITH
|
|
224
|
+
// taxes, so CartEditor takes them out on the way in and the *WithTaxes members put them back. Show
|
|
225
|
+
// those, never unitPrice, which is not what a customer is charged for a unit.
|
|
226
|
+
//
|
|
227
|
+
// The arithmetic is Java's to the unit and rounds in the same places: the tax base (unit price
|
|
228
|
+
// times quantity) to a whole amount, then each tax on its own, half up (see TaxHelper). An amount
|
|
229
|
+
// of Transaction.MAX_AMOUNT or more throws an InvalidArgumentException when something first reads
|
|
230
|
+
// a price off the item, as in Java (see validatePricing).
|
|
99
231
|
export class ShoppingListItem {
|
|
100
|
-
//
|
|
101
|
-
//
|
|
232
|
+
// For ProtoMap, which sees a quantity multiplied by Constants.QUANTITY_FACTOR and prices
|
|
233
|
+
// multiplied by Constants.AMOUNT_FACTOR.
|
|
102
234
|
static fromScaledValues(packageId, scaledQuantity, scaledUnitPrice, shoppingListOrder, isOriginal, isModified, isStockKeepingRequired, scaledPriceDiscount, scaledDiscount, taxes, expiryMinutes, description) {
|
|
103
235
|
return new ShoppingListItem(packageId, quantityFromScaledValue(scaledQuantity), amountFromScaledValue(scaledUnitPrice), shoppingListOrder, isOriginal, isModified, isStockKeepingRequired, amountFromScaledValue(scaledPriceDiscount), amountFromScaledValue(scaledDiscount), taxes, expiryMinutes, description);
|
|
104
236
|
}
|
|
@@ -116,8 +248,8 @@ export class ShoppingListItem {
|
|
|
116
248
|
this._expiryMinutes = _expiryMinutes;
|
|
117
249
|
this._description = _description;
|
|
118
250
|
}
|
|
119
|
-
// The package being bought. Four ids are reserved for charge and summary lines
|
|
120
|
-
// catalog
|
|
251
|
+
// The package being bought. Four ids are reserved for charge and summary lines and are not in the
|
|
252
|
+
// catalog. See isReservedPackage.
|
|
121
253
|
get packageId() {
|
|
122
254
|
return this._packageId;
|
|
123
255
|
}
|
|
@@ -126,10 +258,17 @@ export class ShoppingListItem {
|
|
|
126
258
|
get isReservedPackage() {
|
|
127
259
|
return Ids.isReservedPackageId(this._packageId);
|
|
128
260
|
}
|
|
261
|
+
// Which of the four that is, and null for a catalog package
|
|
262
|
+
get reservedPackage() {
|
|
263
|
+
return this.isReservedPackage
|
|
264
|
+
? Number(Ids.getLowPart(this._packageId))
|
|
265
|
+
: null;
|
|
266
|
+
}
|
|
129
267
|
get quantity() {
|
|
130
268
|
return this._quantity;
|
|
131
269
|
}
|
|
132
|
-
// What one unit costs
|
|
270
|
+
// What one unit costs WITHOUT taxes, the offer price when the package is on offer. This is what is
|
|
271
|
+
// stored. For what a customer is charged see unitPriceWithTaxes.
|
|
133
272
|
get unitPrice() {
|
|
134
273
|
return this._unitPrice;
|
|
135
274
|
}
|
|
@@ -137,19 +276,33 @@ export class ShoppingListItem {
|
|
|
137
276
|
get shoppingListOrder() {
|
|
138
277
|
return this._shoppingListOrder;
|
|
139
278
|
}
|
|
140
|
-
// True when this line was created before the list was made final
|
|
279
|
+
// True when this line was created before the list was made final. Everything a customer puts in
|
|
280
|
+
// a cart is, which separates it from what the business adds or changes afterwards.
|
|
141
281
|
get isOriginal() {
|
|
142
282
|
return this._isOriginal;
|
|
143
283
|
}
|
|
144
|
-
// True when this line has been deleted or replaced since the list was made final
|
|
284
|
+
// True when this line has been deleted or replaced since the list was made final. Original and
|
|
285
|
+
// modified: deleted (and in ShoppingList.deletedItems). Not original but modified: replaced an
|
|
286
|
+
// original one. Neither: added by the business.
|
|
145
287
|
get isModified() {
|
|
146
288
|
return this._isModified;
|
|
147
289
|
}
|
|
290
|
+
// What the business has done to this line since the customer made the list final, read off
|
|
291
|
+
// isOriginal and isModified as the merchant app does: a "+" beside a line it added, an "x" beside
|
|
292
|
+
// one it changed, and the old line listed apart as removed. This reads the line's own flags only.
|
|
293
|
+
// To show a list use ShoppingList.changeOf, which also knows that an order written before 1.5.0
|
|
294
|
+
// flagged its lines wrongly.
|
|
295
|
+
get change() {
|
|
296
|
+
if (this._isOriginal)
|
|
297
|
+
return this._isModified ? ItemChange.REMOVED : ItemChange.NONE;
|
|
298
|
+
return this._isModified ? ItemChange.MODIFIED : ItemChange.ADDED;
|
|
299
|
+
}
|
|
148
300
|
get isStockKeepingRequired() {
|
|
149
301
|
return this._isStockKeepingRequired;
|
|
150
302
|
}
|
|
151
|
-
// What the offer takes off the unit price
|
|
152
|
-
//
|
|
303
|
+
// What the offer takes off the unit price: the package's regular price less its offer price, both
|
|
304
|
+
// WITHOUT taxes. It is not a discount on this line (see discount). The regular price of one unit
|
|
305
|
+
// without taxes is unitPrice + priceDiscount.
|
|
153
306
|
get priceDiscount() {
|
|
154
307
|
return this._priceDiscount;
|
|
155
308
|
}
|
|
@@ -157,6 +310,7 @@ export class ShoppingListItem {
|
|
|
157
310
|
get discount() {
|
|
158
311
|
return this._discount;
|
|
159
312
|
}
|
|
313
|
+
// The taxes on this line, which are included in everything this line costs a customer
|
|
160
314
|
get taxes() {
|
|
161
315
|
return this._taxes;
|
|
162
316
|
}
|
|
@@ -167,22 +321,160 @@ export class ShoppingListItem {
|
|
|
167
321
|
get description() {
|
|
168
322
|
return this._description;
|
|
169
323
|
}
|
|
170
|
-
//
|
|
324
|
+
// The regular price of one unit WITHOUT taxes: unitPrice + priceDiscount
|
|
325
|
+
get regularUnitPrice() {
|
|
326
|
+
return amountFromScaledValue(pricingOf(this).regularUnitPrice);
|
|
327
|
+
}
|
|
328
|
+
// What one unit costs a customer: unitPrice with the taxes on it, the offer price when the
|
|
329
|
+
// package is on offer. It is the price the customer saw when adding it, and the one to show
|
|
330
|
+
// wherever the item is shown. Each tax is rounded, so not every price with taxes can be reached.
|
|
331
|
+
// When it cannot, this is the nearest that can, which differs from the catalog's by a thousandth
|
|
332
|
+
// or so (Java ShoppingListItem.setDetailsFromTaxInclusivePrices).
|
|
333
|
+
get unitPriceWithTaxes() {
|
|
334
|
+
return amountFromScaledValue(pricingOf(this).unitPriceWithTaxes);
|
|
335
|
+
}
|
|
336
|
+
// The same for the regular price of one unit, struck through when this line is on offer
|
|
337
|
+
// (priceDiscount greater than zero)
|
|
338
|
+
get regularUnitPriceWithTaxes() {
|
|
339
|
+
return amountFromScaledValue(pricingOf(this).regularUnitPriceWithTaxes);
|
|
340
|
+
}
|
|
341
|
+
// What this line costs before its taxes and discount, the tax base: the unit price times the
|
|
342
|
+
// quantity, rounded to a whole amount as Java does
|
|
171
343
|
get amount() {
|
|
172
|
-
return this.
|
|
344
|
+
return amountFromScaledValue(pricingOf(this).taxBase);
|
|
345
|
+
}
|
|
346
|
+
// Each tax on this line, in the order of taxes: that tax's rate on amount, rounded half up. All
|
|
347
|
+
// zero when amount is, since a tax only applies to something that costs. Included in finalPrice.
|
|
348
|
+
get taxAmounts() {
|
|
349
|
+
return pricingOf(this).unscaledTaxes;
|
|
173
350
|
}
|
|
174
|
-
//
|
|
175
|
-
// rates, not amounts, and left unrounded (unlike the server's authoritative total).
|
|
351
|
+
// The taxes on this line added up. Included in finalPrice.
|
|
176
352
|
get totalTax() {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
return result;
|
|
182
|
-
}
|
|
183
|
-
// What this line adds to the list: what it costs, plus the tax on it, less its discount. This is
|
|
184
|
-
// the definition ShoppingListItem.proto gives.
|
|
353
|
+
return amountFromScaledValue(pricingOf(this).totalTax);
|
|
354
|
+
}
|
|
355
|
+
// What this line adds to the list: its cost plus the tax on it, less its discount, and not less
|
|
356
|
+
// than zero. This is the definition in ShoppingListItem.proto, and it includes taxes.
|
|
185
357
|
get finalPrice() {
|
|
186
|
-
return this.
|
|
358
|
+
return amountFromScaledValue(pricingOf(this).finalPrice);
|
|
359
|
+
}
|
|
360
|
+
// What this line would add at the package's regular price, taxes included, with none of its own
|
|
361
|
+
// discount taken off: what a customer would have paid had nothing been on offer. Strike it
|
|
362
|
+
// through beside finalPrice when it is greater (Java calculateFinalPriceWithoutDiscounts).
|
|
363
|
+
get finalPriceWithoutDiscounts() {
|
|
364
|
+
return amountFromScaledValue(pricingOf(this).finalPriceWithoutDiscounts);
|
|
365
|
+
}
|
|
366
|
+
// True when this line costs less than at the regular price with none of its discount, so
|
|
367
|
+
// finalPriceWithoutDiscounts is worth striking through beside finalPrice
|
|
368
|
+
get isDiscounted() {
|
|
369
|
+
const pricing = pricingOf(this);
|
|
370
|
+
return pricing.finalPriceWithoutDiscounts > pricing.finalPrice;
|
|
371
|
+
}
|
|
372
|
+
// True when this line costs nothing though it would have cost something: a delivery charge that
|
|
373
|
+
// has been waived, a package given away. A bill reads FREE, with finalPriceWithoutDiscounts
|
|
374
|
+
// struck through.
|
|
375
|
+
get isFree() {
|
|
376
|
+
const pricing = pricingOf(this);
|
|
377
|
+
return pricing.finalPrice === 0n && pricing.finalPriceWithoutDiscounts > 0n;
|
|
187
378
|
}
|
|
188
379
|
}
|
|
380
|
+
// What the business has done to a line of an order since the customer made it final. The merchant
|
|
381
|
+
// app marks the second and third with a "+" and an "x" and explains them in a legend, and lists the
|
|
382
|
+
// fourth under "Removed by merchant".
|
|
383
|
+
export var ItemChange;
|
|
384
|
+
(function (ItemChange) {
|
|
385
|
+
// As the customer ordered it. Every line of a list that is not final yet is this.
|
|
386
|
+
ItemChange[ItemChange["NONE"] = 0] = "NONE";
|
|
387
|
+
// Added by the business after the order was placed
|
|
388
|
+
ItemChange[ItemChange["ADDED"] = 1] = "ADDED";
|
|
389
|
+
// A line the business changed (its price, its quantity): this is the new one, and the line as it
|
|
390
|
+
// was is in ShoppingList.deletedItems
|
|
391
|
+
ItemChange[ItemChange["MODIFIED"] = 2] = "MODIFIED";
|
|
392
|
+
// A line as the customer ordered it that the business has since deleted or replaced. Only in
|
|
393
|
+
// ShoppingList.deletedItems.
|
|
394
|
+
ItemChange[ItemChange["REMOVED"] = 3] = "REMOVED";
|
|
395
|
+
})(ItemChange || (ItemChange = {}));
|
|
396
|
+
// Kept beside the item so it stays a plain value: two equal items compare equal whether or not
|
|
397
|
+
// either has been priced.
|
|
398
|
+
const PRICINGS = new WeakMap();
|
|
399
|
+
const DERIVED = new WeakMap();
|
|
400
|
+
// The taxes of a list by type, which only a list whose items can be priced has
|
|
401
|
+
const TAXES = new WeakMap();
|
|
402
|
+
function pricingOf(item) {
|
|
403
|
+
const existing = PRICINGS.get(item);
|
|
404
|
+
if (existing !== undefined)
|
|
405
|
+
return existing;
|
|
406
|
+
const rates = TaxHelper.scaledRatesOf(item.taxes);
|
|
407
|
+
const unitPrice = amountToScaledValue(item.unitPrice);
|
|
408
|
+
const regularUnitPrice = unitPrice + amountToScaledValue(item.priceDiscount);
|
|
409
|
+
// Java recalculate: taxes only apply to a tax base greater than zero
|
|
410
|
+
const taxBase = scaledProduct(unitPrice, item.quantity);
|
|
411
|
+
const taxes = taxBase > 0n ? TaxHelper.calculateTaxes(taxBase, rates) : rates.map(() => 0n);
|
|
412
|
+
const totalTax = taxes.reduce((total, tax) => total + tax, 0n);
|
|
413
|
+
// The discount comes off a price that has its taxes in it, and a price is never less than zero
|
|
414
|
+
const discounted = taxBase + totalTax - amountToScaledValue(item.discount);
|
|
415
|
+
const created = {
|
|
416
|
+
regularUnitPrice: regularUnitPrice,
|
|
417
|
+
unitPriceWithTaxes: TaxHelper.addTaxes(unitPrice, rates),
|
|
418
|
+
regularUnitPriceWithTaxes: TaxHelper.addTaxes(regularUnitPrice, rates),
|
|
419
|
+
taxBase: taxBase,
|
|
420
|
+
taxes: taxes,
|
|
421
|
+
unscaledTaxes: Object.freeze(taxes.map(amountFromScaledValue)),
|
|
422
|
+
totalTax: totalTax,
|
|
423
|
+
finalPrice: discounted > 0n ? discounted : 0n,
|
|
424
|
+
finalPriceWithoutDiscounts: TaxHelper.addTaxes(scaledProduct(regularUnitPrice, item.quantity), rates),
|
|
425
|
+
};
|
|
426
|
+
// Before it is kept, so that an item that cannot be priced is refused every time it is asked
|
|
427
|
+
validateAmounts(created);
|
|
428
|
+
PRICINGS.set(item, created);
|
|
429
|
+
return created;
|
|
430
|
+
}
|
|
431
|
+
// Throws an InvalidArgumentException unless every amount the item works out is one the protocol can
|
|
432
|
+
// carry. CartEditor calls it before keeping a line. Anything else finds out when it first reads a
|
|
433
|
+
// price off the item.
|
|
434
|
+
export function validatePricing(item) {
|
|
435
|
+
pricingOf(item);
|
|
436
|
+
}
|
|
437
|
+
// Java Transaction.validateAmount refuses an amount of MAX_AMOUNT or more. This applies it to the
|
|
438
|
+
// amounts Java checks on an item: each tax and the final price (recalculate), and the results of
|
|
439
|
+
// calculateFinalPriceWithoutDiscounts, calculateUnitPriceWithTaxes and
|
|
440
|
+
// calculateUnitRegularPriceWithTaxes. They are checked together, once, where the item is priced, so
|
|
441
|
+
// an item answers as Java does however it was made (a cart line or the wire) and hands out no
|
|
442
|
+
// number the protocol cannot carry.
|
|
443
|
+
// The amounts are not independent. finalPriceWithoutDiscounts is the cost at the regular price,
|
|
444
|
+
// never less than the offer price, and each tax and the final price are parts of it. So none of
|
|
445
|
+
// them is over the limit unless that cost is (test/JavaVectors.ts shows this item by item).
|
|
446
|
+
function validateAmounts(pricing) {
|
|
447
|
+
const amounts = [
|
|
448
|
+
...pricing.taxes.map((tax) => ["tax", tax]),
|
|
449
|
+
["final price", pricing.finalPrice],
|
|
450
|
+
["final price without discounts", pricing.finalPriceWithoutDiscounts],
|
|
451
|
+
["unit price with taxes", pricing.unitPriceWithTaxes],
|
|
452
|
+
["regular unit price with taxes", pricing.regularUnitPriceWithTaxes],
|
|
453
|
+
];
|
|
454
|
+
for (const [name, amount] of amounts)
|
|
455
|
+
if (amount >= MAX_SCALED_AMOUNT)
|
|
456
|
+
throw new InvalidArgumentException(`The ${name} of an item, ${amountFromScaledValue(amount)}, is more than the ${amountFromScaledValue(MAX_SCALED_AMOUNT - 1n)} the protocol can carry`);
|
|
457
|
+
}
|
|
458
|
+
// A scaled amount times a quantity, rounded to a whole amount as Java does:
|
|
459
|
+
// Math.round(((double) amount) * NumberHelper.intToFloat(scaledQuantity, QUANTITY_FACTOR)). The
|
|
460
|
+
// quantity is a FLOAT there, so it is one here (fround), and a quantity like 0.7 is not quite 0.7
|
|
461
|
+
// in either. That matters only where the product lands on a half.
|
|
462
|
+
function scaledProduct(scaledAmount, quantity) {
|
|
463
|
+
const scaledQuantity = quantityToScaledValue(quantity);
|
|
464
|
+
const floatQuantity = Math.fround(Math.fround(scaledQuantity) / proto_Constants.QUANTITY_FACTOR);
|
|
465
|
+
const product = Math.round(Number(scaledAmount) * floatQuantity);
|
|
466
|
+
return Number.isFinite(product) ? BigInt(product) : 0n;
|
|
467
|
+
}
|
|
468
|
+
// A copy in the order of the list. Java sorts the existing items by shoppingListOrder before
|
|
469
|
+
// showing a list (ShoppingListAdapter), and an order arrives with its items in package id order.
|
|
470
|
+
// The merchant app shows the removed items in package id order, having no better one. This library
|
|
471
|
+
// uses the order they had, which is the customer's.
|
|
472
|
+
function sortedByOrder(items) {
|
|
473
|
+
return [...items].sort((left, right) => left.shoppingListOrder - right.shoppingListOrder);
|
|
474
|
+
}
|
|
475
|
+
function sumOf(items, select) {
|
|
476
|
+
let result = 0n;
|
|
477
|
+
for (const item of items)
|
|
478
|
+
result += select(pricingOf(item));
|
|
479
|
+
return result;
|
|
480
|
+
}
|
package/dist/SyncCache.d.ts
CHANGED
|
@@ -3,25 +3,37 @@ import { Message as proto_Message } from "./generated/Message";
|
|
|
3
3
|
import { ProfileSyncOutResponse as proto_ProfileSyncOutResponse } from "./generated/ProfileSyncOutResponse";
|
|
4
4
|
import AuthTokens from "./AuthTokens";
|
|
5
5
|
import Database from "./Database";
|
|
6
|
+
import SyncState from "./SyncState";
|
|
6
7
|
export default class SyncCache {
|
|
7
8
|
private readonly _database;
|
|
8
9
|
constructor(_database: Database);
|
|
9
10
|
readBusiness(containerName: string): Promise<proto_BusinessSyncOutResponse | null>;
|
|
11
|
+
readBusinessCursors(containerName: string): Promise<SyncState>;
|
|
10
12
|
writeBusiness(containerName: string, response: proto_BusinessSyncOutResponse): Promise<number>;
|
|
13
|
+
storeBusiness(containerName: string, response: proto_BusinessSyncOutResponse): Promise<number>;
|
|
14
|
+
readDeletedBusiness(containerName: string): Promise<proto_BusinessSyncOutResponse | null>;
|
|
11
15
|
readProfile(containerName: string): Promise<proto_ProfileSyncOutResponse | null>;
|
|
16
|
+
readDeletedProfile(containerName: string): Promise<proto_ProfileSyncOutResponse | null>;
|
|
17
|
+
readProfileCursors(containerName: string): Promise<SyncState>;
|
|
12
18
|
writeProfile(containerName: string, response: proto_ProfileSyncOutResponse): Promise<number>;
|
|
19
|
+
storeProfile(containerName: string, response: proto_ProfileSyncOutResponse): Promise<number>;
|
|
13
20
|
readAuthTokens(containerName: string): Promise<AuthTokens | null>;
|
|
14
21
|
writeAuthTokens(containerName: string, authTokens: AuthTokens): Promise<void>;
|
|
15
22
|
readOrders(containerName: string): Promise<ReadonlyArray<proto_Message>>;
|
|
23
|
+
readDeletedOrders(containerName: string): Promise<ReadonlyArray<proto_Message>>;
|
|
16
24
|
readMessageUpdateNumber(containerName: string): Promise<bigint>;
|
|
17
25
|
writeOrder(containerName: string, order: proto_Message): Promise<number>;
|
|
18
26
|
readPartyPreferences(containerName: string): Promise<proto_Message | null>;
|
|
19
27
|
writePartyPreferences(containerName: string, partyPreferences: proto_Message): Promise<number>;
|
|
28
|
+
deleteBusiness(containerName: string): Promise<void>;
|
|
20
29
|
deletePartyPreferences(containerName: string): Promise<number>;
|
|
21
30
|
writeMessages(containerName: string, messages: ReadonlyArray<proto_Message>): Promise<number>;
|
|
22
31
|
private readAll;
|
|
32
|
+
private writeOwnMessage;
|
|
23
33
|
private writeBlobs;
|
|
24
34
|
private write;
|
|
35
|
+
private writeRead;
|
|
36
|
+
private readCursors;
|
|
25
37
|
private getContainer;
|
|
26
38
|
private readonly _containers;
|
|
27
39
|
}
|