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