@peabody-soft/mbs-model-ts 1.4.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 +265 -139
- 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/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 +4 -3
- package/package.json +55 -55
- package/dist/generated/TaxType.d.ts +0 -25
- package/dist/generated/TaxType.js +0 -30
package/dist/CartEditor.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import GeneralSettings from "./GeneralSettings";
|
|
|
3
3
|
import Package from "./Package";
|
|
4
4
|
import Product from "./Product";
|
|
5
5
|
import ShoppingList, { ShoppingListItem } from "./ShoppingList";
|
|
6
|
-
import Tax from "./Tax";
|
|
6
|
+
import Tax, { TaxAmount } from "./Tax";
|
|
7
7
|
export default class CartEditor {
|
|
8
8
|
private readonly _catalog;
|
|
9
9
|
private readonly _settings;
|
|
@@ -21,7 +21,7 @@ export default class CartEditor {
|
|
|
21
21
|
get itemsTotal(): number;
|
|
22
22
|
get savings(): number;
|
|
23
23
|
get itemsTax(): number;
|
|
24
|
-
|
|
24
|
+
taxes(withDeliveryCharge?: boolean): ReadonlyArray<TaxAmount>;
|
|
25
25
|
get isDeliveryOffered(): boolean;
|
|
26
26
|
get deliveryCharge(): number;
|
|
27
27
|
get regularDeliveryCharge(): number;
|
|
@@ -31,10 +31,13 @@ export default class CartEditor {
|
|
|
31
31
|
total(withDeliveryCharge?: boolean): number;
|
|
32
32
|
build(withDeliveryCharge?: boolean): ShoppingList;
|
|
33
33
|
private buildDeliveryChargeItem;
|
|
34
|
+
private get regularCharge();
|
|
34
35
|
private getDeliveryCharge;
|
|
35
|
-
private static getTax;
|
|
36
36
|
private getBuyablePackage;
|
|
37
|
-
private
|
|
37
|
+
private tryKeep;
|
|
38
|
+
private static toWireQuantity;
|
|
39
|
+
private checkQuantity;
|
|
40
|
+
private static readonly MAX_WIRE_QUANTITY;
|
|
38
41
|
private readonly _inventoryLocationId;
|
|
39
42
|
private readonly _unavailableItems;
|
|
40
43
|
private readonly _quantities;
|
|
@@ -47,13 +50,15 @@ export declare class CartLine {
|
|
|
47
50
|
get package(): Package;
|
|
48
51
|
get product(): Product | null;
|
|
49
52
|
get quantity(): number;
|
|
53
|
+
get item(): ShoppingListItem;
|
|
54
|
+
get taxes(): ReadonlyArray<Tax>;
|
|
50
55
|
get unitPrice(): number;
|
|
51
56
|
get regularUnitPrice(): number;
|
|
52
57
|
get isOnOffer(): boolean;
|
|
53
|
-
get taxes(): ReadonlyArray<Tax>;
|
|
54
58
|
get amount(): number;
|
|
55
59
|
get savings(): number;
|
|
56
60
|
get totalTax(): number;
|
|
57
61
|
get finalPrice(): number;
|
|
58
62
|
toShoppingListItem(shoppingListOrder: number): ShoppingListItem;
|
|
63
|
+
private _item;
|
|
59
64
|
}
|
package/dist/CartEditor.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
2
|
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
3
3
|
import { ReservedPackage as proto_ReservedPackage } from "./generated/ReservedPackage";
|
|
4
|
-
import { MERABILLS_ONLINE_PRICE_LIST_ID } from "./Constants";
|
|
4
|
+
import { MERABILLS_ONLINE_PRICE_LIST_ID, amountFromScaledValue, amountToScaledValue, } from "./Constants";
|
|
5
5
|
import { InvalidArgumentException } from "./Exceptions";
|
|
6
6
|
import Ids from "./Ids";
|
|
7
|
-
import ShoppingList, { ShoppingListItem } from "./ShoppingList";
|
|
7
|
+
import ShoppingList, { ShoppingListItem, validatePricing } from "./ShoppingList";
|
|
8
|
+
import TaxHelper from "./TaxHelper";
|
|
8
9
|
// Builds the cart a customer is filling, in the terms a UI has to hand: packages rather than
|
|
9
10
|
// package ids, quantities rather than quantities multiplied by Constants.QUANTITY_FACTOR, and
|
|
10
11
|
// prices taken off the catalog rather than worked out and passed in. Get one from
|
|
@@ -15,6 +16,15 @@ import ShoppingList, { ShoppingListItem } from "./ShoppingList";
|
|
|
15
16
|
// store and nothing on the server until a built list is pushed. It is also a snapshot: it reads the
|
|
16
17
|
// catalog, settings and cart once, at construction, so a sync arriving mid-edit does not move
|
|
17
18
|
// prices under the customer. Make a fresh one when the Business republishes.
|
|
19
|
+
//
|
|
20
|
+
// EVERY PRICE HERE INCLUDES TAXES. The price of a package includes the taxes of its product, and
|
|
21
|
+
// the delivery charge the taxes the settings name for it, so what a customer sees in the catalog
|
|
22
|
+
// is what they pay, and every total below is a total with its taxes already in it. A shopping list
|
|
23
|
+
// item is the other way round - it stores a price WITHOUT taxes and works the taxes out on top -
|
|
24
|
+
// so building one takes the taxes out of the price, exactly as Java ShoppingListItem
|
|
25
|
+
// .setDetailsFromTaxInclusivePrices does, and all the arithmetic is Java's to the unit (see
|
|
26
|
+
// TaxHelper). The taxes are there to be shown as what the total includes (taxes), never to be
|
|
27
|
+
// added to it.
|
|
18
28
|
class CartEditor {
|
|
19
29
|
// Business.createCartEditor is how a UI gets one of these; this takes the three things it needs
|
|
20
30
|
// rather than the Business itself, so a caller can build a cart against a catalog and settings
|
|
@@ -24,9 +34,14 @@ class CartEditor {
|
|
|
24
34
|
// until it is ordered, the merchant may have repriced in between, and what an order freezes is
|
|
25
35
|
// the price at the moment it is placed. Lines the catalog can no longer price - a package
|
|
26
36
|
// withdrawn, disabled, or left without a price - are dropped, and left in unavailableItems for a
|
|
27
|
-
// UI to tell the customer about.
|
|
28
|
-
//
|
|
29
|
-
//
|
|
37
|
+
// UI to tell the customer about. So is a line that cannot be bought as it is, which setQuantity
|
|
38
|
+
// would have refused, a cart that was read not being known to be one this class could have made:
|
|
39
|
+
// a quantity that is nothing or is not one, a fraction of what is not sold loose, more than the
|
|
40
|
+
// wire carries, or that comes to more than the protocol can at today's price, and any line past
|
|
41
|
+
// the most a cart holds (the first lines are kept, being in the order picked). The charge lines
|
|
42
|
+
// of the list it started from (see ShoppingListItem.isReservedPackage) are dropped too, being
|
|
43
|
+
// this class's to work out rather than a customer's to carry: build() puts back whatever the
|
|
44
|
+
// settings now say.
|
|
30
45
|
constructor(_catalog, _settings, cart) {
|
|
31
46
|
this._catalog = _catalog;
|
|
32
47
|
this._settings = _settings;
|
|
@@ -34,15 +49,14 @@ class CartEditor {
|
|
|
34
49
|
// gives each line its shoppingListOrder
|
|
35
50
|
this._quantities = new Map();
|
|
36
51
|
this._inventoryLocationId = cart?.inventoryLocationId ?? 0;
|
|
52
|
+
// In the order of the list, which is the order the customer picked them in: the goods items are in it, and the items
|
|
53
|
+
// of the list are not, the server sending those by package id, so a cart that is read back would reorder itself
|
|
37
54
|
const unavailableItems = [];
|
|
38
|
-
for (const item of cart?.
|
|
55
|
+
for (const item of cart?.goodsItems ?? []) {
|
|
39
56
|
if (item.isReservedPackage)
|
|
40
57
|
continue;
|
|
41
|
-
|
|
42
|
-
if (buyable === null)
|
|
58
|
+
if (!this.tryKeep(item))
|
|
43
59
|
unavailableItems.push(item);
|
|
44
|
-
else
|
|
45
|
-
this._quantities.set(buyable.id, item.quantity);
|
|
46
60
|
}
|
|
47
61
|
this._unavailableItems = Object.freeze(unavailableItems);
|
|
48
62
|
}
|
|
@@ -84,26 +98,17 @@ class CartEditor {
|
|
|
84
98
|
// Sets how much of one package the cart holds; zero takes it out. A package a customer cannot
|
|
85
99
|
// buy - one disabled, or priced in no list this storefront sells from - is refused, as is a
|
|
86
100
|
// fractional quantity of a package that is not sold loose, and a quantity past what the wire can
|
|
87
|
-
// carry.
|
|
101
|
+
// carry. Whether a package is sold loose is the catalog's word, and not that of the Package
|
|
102
|
+
// passed in, which may be one held from before the catalog changed. Returns this, so edits chain.
|
|
88
103
|
setQuantity(item, quantity) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
const wireQuantity = CartEditor.toWireQuantity(quantity);
|
|
105
|
+
// Less than half a thousandth is nothing on the wire - a line of zero, which would cost nothing
|
|
106
|
+
// - so it takes the line out, as zero does
|
|
107
|
+
if (wireQuantity === 0) {
|
|
92
108
|
this._quantities.delete(item.id);
|
|
93
109
|
return this;
|
|
94
110
|
}
|
|
95
|
-
|
|
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);
|
|
111
|
+
this._quantities.set(item.id, this.checkQuantity(item.id, quantity, wireQuantity));
|
|
107
112
|
return this;
|
|
108
113
|
}
|
|
109
114
|
// Adds to what the cart already holds of this package, which is what a quantity stepper does.
|
|
@@ -118,52 +123,44 @@ class CartEditor {
|
|
|
118
123
|
this._quantities.clear();
|
|
119
124
|
return this;
|
|
120
125
|
}
|
|
121
|
-
// What the packages
|
|
126
|
+
// What the packages come to, taxes included and offers already taken off: the sum of the final
|
|
127
|
+
// prices of the lines, and what a customer sees to be the price of what is in the cart. Delivery
|
|
128
|
+
// is not in it.
|
|
122
129
|
get itemsTotal() {
|
|
123
|
-
|
|
124
|
-
for (const line of this.lines)
|
|
125
|
-
result += line.amount;
|
|
126
|
-
return result;
|
|
130
|
+
return ShoppingList.sumOfFinalPrices(this.lines.map((line) => line.item));
|
|
127
131
|
}
|
|
128
|
-
// What the offers took off, which is what a bill shows as savings
|
|
132
|
+
// What the offers took off the packages, which is what a bill shows as savings: what they would
|
|
133
|
+
// have come to at their regular prices, less what they come to. Zero when nothing is on offer.
|
|
134
|
+
// Taxes are included on both sides.
|
|
129
135
|
get savings() {
|
|
130
|
-
|
|
131
|
-
for (const line of this.lines)
|
|
132
|
-
result += line.savings;
|
|
133
|
-
return result;
|
|
136
|
+
return this.build().savings;
|
|
134
137
|
}
|
|
138
|
+
// The taxes inside itemsTotal, all types together. They are part of the total and are not
|
|
139
|
+
// added to it: see taxes.
|
|
135
140
|
get itemsTax() {
|
|
136
|
-
|
|
137
|
-
for (const line of this.lines)
|
|
138
|
-
result += line.totalTax;
|
|
139
|
-
return result;
|
|
141
|
+
return this.build().computedTotalTax;
|
|
140
142
|
}
|
|
141
|
-
// The
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
result += line.finalPrice;
|
|
147
|
-
return result;
|
|
143
|
+
// The taxes the cart's total includes, one entry per TYPE of tax - a bill shows a line for each
|
|
144
|
+
// that is not zero. withDeliveryCharge is as for total: the taxes in the delivery charge are
|
|
145
|
+
// counted only when it is.
|
|
146
|
+
taxes(withDeliveryCharge = false) {
|
|
147
|
+
return this.build(withDeliveryCharge).computedTaxes;
|
|
148
148
|
}
|
|
149
149
|
// True when this business delivers at all. False means no delivery charge exists to add, whatever
|
|
150
150
|
// build() is asked for, and a UI has no fulfillment choice to offer either.
|
|
151
151
|
get isDeliveryOffered() {
|
|
152
152
|
return this._settings.fulfillmentModes.includes(proto_FulfillmentMode.DELIVERY);
|
|
153
153
|
}
|
|
154
|
-
// What delivering this cart would cost
|
|
155
|
-
// has been waived by the cart reaching freeDeliveryTotal, or where the business delivers
|
|
156
|
-
// anyway. Always zero when the business does not deliver.
|
|
154
|
+
// What delivering this cart would cost, taxes included: the business's flat charge, or zero
|
|
155
|
+
// where it has been waived by the cart reaching freeDeliveryTotal, or where the business delivers
|
|
156
|
+
// free anyway. Always zero when the business does not deliver.
|
|
157
157
|
get deliveryCharge() {
|
|
158
|
-
return this.getDeliveryCharge(this.
|
|
158
|
+
return this.buildDeliveryChargeItem(this.getDeliveryCharge(this.itemsTotal), 0)?.finalPrice ?? 0;
|
|
159
159
|
}
|
|
160
160
|
// What delivery costs before any waiver - the price a bill strikes through when it reads FREE.
|
|
161
161
|
// Zero where the business delivers free anyway, in which case there is nothing to strike through.
|
|
162
162
|
get regularDeliveryCharge() {
|
|
163
|
-
|
|
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);
|
|
163
|
+
return this.buildDeliveryChargeItem(this.regularCharge, 0)?.finalPrice ?? 0;
|
|
167
164
|
}
|
|
168
165
|
// True when delivering this cart costs nothing, whether because the business never charges or
|
|
169
166
|
// because this cart has reached the threshold
|
|
@@ -176,40 +173,45 @@ class CartEditor {
|
|
|
176
173
|
get amountToFreeDelivery() {
|
|
177
174
|
if (!this.isDeliveryOffered)
|
|
178
175
|
return null;
|
|
179
|
-
if (this.
|
|
176
|
+
if (this.regularCharge === 0)
|
|
180
177
|
return 0; // Already free, and free at any total
|
|
181
178
|
const freeDeliveryTotal = this._settings.freeDeliveryTotal;
|
|
182
179
|
if (freeDeliveryTotal <= 0)
|
|
183
180
|
return null; // No threshold: adding more changes nothing
|
|
184
|
-
|
|
181
|
+
const remaining = amountToScaledValue(freeDeliveryTotal) - amountToScaledValue(this.itemsTotal);
|
|
182
|
+
return remaining > 0n ? amountFromScaledValue(remaining) : 0;
|
|
185
183
|
}
|
|
186
|
-
// Tax on the delivery charge
|
|
187
|
-
// are the business's own and have nothing to do with a
|
|
188
|
-
//
|
|
184
|
+
// Tax on the delivery charge: how much of deliveryCharge is the taxes GeneralSettings
|
|
185
|
+
// .deliveryChargeTaxes carries - which are the business's own and have nothing to do with a
|
|
186
|
+
// product's. Included in the charge, not added to it. Zero when delivery is free, there being
|
|
187
|
+
// nothing to tax.
|
|
189
188
|
get deliveryTax() {
|
|
190
|
-
return
|
|
189
|
+
return this.buildDeliveryChargeItem(this.getDeliveryCharge(this.itemsTotal), 0)?.totalTax ?? 0;
|
|
191
190
|
}
|
|
192
|
-
// What the cart comes to. Delivery is left out unless asked for, the way a cart
|
|
193
|
-
// out and a checkout page puts it in - the same choice build() takes.
|
|
191
|
+
// What the cart comes to, taxes included. Delivery is left out unless asked for, the way a cart
|
|
192
|
+
// page leaves it out and a checkout page puts it in - the same choice build() takes. It is the
|
|
193
|
+
// total of the list build() answers, so it is the total an order placed from it carries.
|
|
194
194
|
total(withDeliveryCharge = false) {
|
|
195
|
-
|
|
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);
|
|
195
|
+
return this.build(withDeliveryCharge).totalAmount;
|
|
202
196
|
}
|
|
203
197
|
// 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.
|
|
198
|
+
// what a later Model.createOrder turns into an order. It is also the list to SHOW - every price,
|
|
199
|
+
// total and tax a cart or a checkout page displays can be read off it, which is how a placed
|
|
200
|
+
// order is shown too.
|
|
201
|
+
//
|
|
202
|
+
// Each line is an item that stores the price without taxes, as Java does, and is flagged
|
|
203
|
+
// original: a customer's lines are created before the list is final (Java ShoppingList.addItem
|
|
204
|
+
// sets isOriginal to !isFinal(), and the order becomes final only after its items are in it), and
|
|
205
|
+
// an item that is not original is what the merchant app marks as added by the business.
|
|
205
206
|
//
|
|
206
207
|
// withDeliveryCharge decides whether the delivery charge rides along as a line of its own,
|
|
207
208
|
// carrying ReservedPackage.DELIVERY_CHARGE_PACKAGE and flagged by
|
|
208
209
|
// ShoppingList.autoAddDeliveryCharge. A cart page has no fulfillment chosen yet and so leaves it
|
|
209
210
|
// 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
|
|
211
|
-
// priceDiscount - so a bill can show FREE with the
|
|
212
|
-
// ShoppingCartViewModel.setDeliveryCharge writes
|
|
211
|
+
// is added even when the charge has been waived - at zero, with the regular charge (less its
|
|
212
|
+
// taxes, like every price in an item) as its priceDiscount - so a bill can show FREE with the
|
|
213
|
+
// original struck through, which is what Java ShoppingCartViewModel.setDeliveryCharge writes
|
|
214
|
+
// too.
|
|
213
215
|
//
|
|
214
216
|
// Nothing is added to an empty cart: a list holding a delivery charge and no goods is not an
|
|
215
217
|
// order, and Java removes exactly that line when it is all that is left.
|
|
@@ -219,58 +221,128 @@ class CartEditor {
|
|
|
219
221
|
const items = [];
|
|
220
222
|
for (const line of this.lines)
|
|
221
223
|
items.push(line.toShoppingListItem(items.length));
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
224
|
+
const delivery = withDeliveryCharge && items.length > 0
|
|
225
|
+
? this.buildDeliveryChargeItem(this.getDeliveryCharge(ShoppingList.sumOfFinalPrices(items)), items.length)
|
|
226
|
+
: null;
|
|
227
|
+
if (delivery !== null)
|
|
228
|
+
items.push(delivery);
|
|
225
229
|
// The total is written off the lines being sent, the way ShoppingList.proto defines it: the
|
|
226
230
|
// sum of their final prices, tax included. There is no list-wide discount - an offer comes off
|
|
227
231
|
// a line, and a storefront gives no other.
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
// The charge
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
232
|
+
return new ShoppingList(false, delivery !== null, false, this._inventoryLocationId, this._catalog.priceList?.id ?? MERABILLS_ONLINE_PRICE_LIST_ID, ShoppingList.sumOfFinalPrices(items), 0, items.length, 0, Object.freeze(items), Object.freeze([]));
|
|
233
|
+
}
|
|
234
|
+
// The delivery charge as a line: one of it, at what it costs now (charge - the regular charge, or
|
|
235
|
+
// zero once waived), with the waived amount as its price discount - the same shape as a package
|
|
236
|
+
// bought on offer, which is what lets a bill render it with no special case beyond its label.
|
|
237
|
+
// Null when the business does not deliver. The charge in the settings includes the settings'
|
|
238
|
+
// delivery charge taxes, so they come out of it like those of any other price.
|
|
239
|
+
buildDeliveryChargeItem(charge, shoppingListOrder) {
|
|
240
|
+
if (!this.isDeliveryOffered)
|
|
241
|
+
return null;
|
|
242
|
+
return buildItem(Ids.buildId(proto_Constants.PACKAGE_ENTITY_TYPE_ID, BigInt(proto_ReservedPackage.DELIVERY_CHARGE_PACKAGE)), 1, charge, this.regularCharge, this._settings.deliveryChargeTaxes, shoppingListOrder);
|
|
243
|
+
}
|
|
244
|
+
// The charge before any waiver. Java clamps a negative charge to zero rather than treating it as
|
|
245
|
+
// a refund, and so does this. Zero when the business does not deliver.
|
|
246
|
+
get regularCharge() {
|
|
247
|
+
if (!this.isDeliveryOffered)
|
|
248
|
+
return 0;
|
|
249
|
+
return Math.max(0, this._settings.deliveryCharge);
|
|
250
|
+
}
|
|
251
|
+
// The charge against a cart whose goods come to this much, which is the whole of the policy: the
|
|
252
|
+
// regular charge, or nothing once the goods reach the threshold. Java ShoppingCartViewModel
|
|
253
|
+
// .calculateDeliveryCharge compares the same total, taxes included and the delivery line left out.
|
|
254
|
+
getDeliveryCharge(itemsTotal) {
|
|
255
|
+
const charge = this.regularCharge;
|
|
244
256
|
if (charge === 0)
|
|
245
257
|
return 0; // Free for everyone, or not delivered at all
|
|
246
258
|
const freeDeliveryTotal = this._settings.freeDeliveryTotal;
|
|
247
259
|
if (freeDeliveryTotal <= 0)
|
|
248
260
|
return charge; // Never free, whatever the cart totals
|
|
249
|
-
return
|
|
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;
|
|
261
|
+
return itemsTotal >= freeDeliveryTotal ? 0 : charge;
|
|
256
262
|
}
|
|
257
263
|
// The package this id names, and null unless a customer could actually buy it right now
|
|
258
264
|
getBuyablePackage(packageId) {
|
|
259
265
|
const item = this._catalog.getPackage(packageId);
|
|
260
266
|
return item !== null && item.isBuyable ? item : null;
|
|
261
267
|
}
|
|
268
|
+
// Keeps a line of a cart that was read, as setQuantity would, and says whether it could: a line
|
|
269
|
+
// that cannot be kept is not a caller's mistake to be refused but one for unavailableItems
|
|
270
|
+
tryKeep(item) {
|
|
271
|
+
try {
|
|
272
|
+
const wireQuantity = CartEditor.toWireQuantity(item.quantity);
|
|
273
|
+
if (wireQuantity === 0)
|
|
274
|
+
return false; // A line of nothing is no line
|
|
275
|
+
this._quantities.set(item.packageId, this.checkQuantity(item.packageId, item.quantity, wireQuantity));
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
if (error instanceof InvalidArgumentException)
|
|
280
|
+
return false;
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
// A quantity travels as an int32 multiplied by Constants.QUANTITY_FACTOR (Java rounds it to that:
|
|
285
|
+
// NumberHelper.floatToInt), so the least there is is a thousandth. This is a quantity as that,
|
|
286
|
+
// and what is kept is the quantity the wire will hold, so getQuantity never reports one the
|
|
287
|
+
// order will not have.
|
|
288
|
+
static toWireQuantity(quantity) {
|
|
289
|
+
if (!Number.isFinite(quantity) || quantity < 0)
|
|
290
|
+
throw new InvalidArgumentException(`${quantity} is not a quantity`);
|
|
291
|
+
return Math.round(quantity * proto_Constants.QUANTITY_FACTOR);
|
|
292
|
+
}
|
|
293
|
+
// The quantity to keep for this much of a package, which is what the wire will hold of it - or an
|
|
294
|
+
// InvalidArgumentException saying why the cart cannot have it: the package is not one the catalog
|
|
295
|
+
// sells, or it is not sold loose and the quantity is a fraction, or the quantity, or what it comes
|
|
296
|
+
// to, is more than the protocol carries, or the cart is full. All of it is read off the catalog's
|
|
297
|
+
// package, which is what the order will be priced from, and not off the caller's.
|
|
298
|
+
checkQuantity(packageId, quantity, wireQuantity) {
|
|
299
|
+
const buyable = this.getBuyablePackage(packageId);
|
|
300
|
+
if (buyable === null)
|
|
301
|
+
throw new InvalidArgumentException(`Package ${Ids.toStorageHexString(packageId)} cannot be bought from this catalog`);
|
|
302
|
+
if (!buyable.isSoldLoose && !Number.isInteger(quantity))
|
|
303
|
+
throw new InvalidArgumentException(`${buyable.name} is not sold loose, so it cannot be bought ${quantity} at a time`);
|
|
304
|
+
// This is the most of one package a cart can hold, and it is refused here rather than silently
|
|
305
|
+
// wrapping on the wire
|
|
306
|
+
if (wireQuantity > CartEditor.MAX_WIRE_QUANTITY)
|
|
307
|
+
throw new InvalidArgumentException(`${quantity} is more than the ${CartEditor.MAX_WIRE_QUANTITY / proto_Constants.QUANTITY_FACTOR} the protocol can carry`);
|
|
308
|
+
// A list holds at most Constants.SHOPPING_LIST_MAX_ITEMS items, and the delivery charge is one
|
|
309
|
+
// of them (Java ShoppingList.addItem counts it like any other, and a server refuses a list a
|
|
310
|
+
// line over). So where the business delivers, one place is kept for it: a cart that could be
|
|
311
|
+
// filled to the limit could not be ordered for delivery.
|
|
312
|
+
const maxLines = proto_Constants.SHOPPING_LIST_MAX_ITEMS - (this.isDeliveryOffered ? 1 : 0);
|
|
313
|
+
if (!this._quantities.has(packageId) && this._quantities.size >= maxLines)
|
|
314
|
+
throw new InvalidArgumentException(`A cart holds at most ${maxLines} lines`);
|
|
315
|
+
// Java refuses an amount past Transaction.MAX_AMOUNT (validateAmount) in the item that comes to
|
|
316
|
+
// it, and so does pricing an item here, which is done before the line is kept rather than when
|
|
317
|
+
// a list cannot be written
|
|
318
|
+
const stored = wireQuantity / proto_Constants.QUANTITY_FACTOR;
|
|
319
|
+
try {
|
|
320
|
+
validatePricing(new CartLine(buyable, this._catalog.getProduct(buyable.productId), stored).item);
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (error instanceof InvalidArgumentException)
|
|
324
|
+
throw new InvalidArgumentException(`${quantity} of ${buyable.name} comes to more than the protocol can carry (${error.message})`);
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
return stored;
|
|
328
|
+
}
|
|
262
329
|
}
|
|
263
|
-
//
|
|
264
|
-
|
|
330
|
+
// The most an int32 holds, which is the most that a quantity multiplied by
|
|
331
|
+
// Constants.QUANTITY_FACTOR can be
|
|
332
|
+
CartEditor.MAX_WIRE_QUANTITY = 0x7fffffff;
|
|
265
333
|
export default CartEditor;
|
|
266
334
|
// 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
|
|
268
|
-
// on every read of CartEditor.lines rather than kept.
|
|
335
|
+
// - with the prices the catalog gives it now, taxes included. A read-only view over what
|
|
336
|
+
// CartEditor holds, rebuilt on every read of CartEditor.lines rather than kept.
|
|
337
|
+
//
|
|
338
|
+
// Everything here is read off item, the line as a shopping list would store it, so the cart page
|
|
339
|
+
// shows exactly what the order will: no price is worked out twice.
|
|
269
340
|
export class CartLine {
|
|
270
341
|
constructor(_package, _product, _quantity) {
|
|
271
342
|
this._package = _package;
|
|
272
343
|
this._product = _product;
|
|
273
344
|
this._quantity = _quantity;
|
|
345
|
+
this._item = null;
|
|
274
346
|
}
|
|
275
347
|
get package() {
|
|
276
348
|
return this._package;
|
|
@@ -282,43 +354,78 @@ export class CartLine {
|
|
|
282
354
|
get quantity() {
|
|
283
355
|
return this._quantity;
|
|
284
356
|
}
|
|
285
|
-
//
|
|
286
|
-
//
|
|
357
|
+
// This line as a shopping list holds it: priced without taxes, with the taxes beside it. Show
|
|
358
|
+
// this to show the line the way an order shows it. Its place in the list is not meaningful here.
|
|
359
|
+
get item() {
|
|
360
|
+
this._item ?? (this._item = this.toShoppingListItem(0));
|
|
361
|
+
return this._item;
|
|
362
|
+
}
|
|
363
|
+
// The taxes of the product this package belongs to, which its price includes. A rate that is not
|
|
364
|
+
// greater than zero is no tax, as in Java, and is not here.
|
|
365
|
+
get taxes() {
|
|
366
|
+
return includedTaxes(this._product?.taxes ?? []);
|
|
367
|
+
}
|
|
368
|
+
// What one unit costs, taxes included: the offer price where the package is on offer, else the
|
|
369
|
+
// regular price. This is the catalog's price, to within the rounding of taxes that a price with
|
|
370
|
+
// taxes in it is subject to (a thousandth or two, which no one is shown). A buyable package
|
|
371
|
+
// always has a price, so this is never guessed.
|
|
287
372
|
get unitPrice() {
|
|
288
|
-
return this.
|
|
373
|
+
return this.item.unitPriceWithTaxes;
|
|
289
374
|
}
|
|
375
|
+
// The regular price of a unit, taxes included - what is struck through when it is on offer
|
|
290
376
|
get regularUnitPrice() {
|
|
291
|
-
return this.
|
|
377
|
+
return this.item.regularUnitPriceWithTaxes;
|
|
292
378
|
}
|
|
293
379
|
get isOnOffer() {
|
|
294
380
|
return this._package.price?.isOnOffer ?? false;
|
|
295
381
|
}
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
return this._product?.taxes ?? EMPTY_TAXES;
|
|
299
|
-
}
|
|
300
|
-
// What this line costs before tax
|
|
382
|
+
// What this line costs before its taxes: its tax base. A bill that shows prices with taxes shows
|
|
383
|
+
// finalPrice, not this.
|
|
301
384
|
get amount() {
|
|
302
|
-
return this.
|
|
385
|
+
return this.item.amount;
|
|
303
386
|
}
|
|
304
|
-
// What the offer takes off this line
|
|
387
|
+
// What the offer takes off this line, taxes included
|
|
305
388
|
get savings() {
|
|
306
|
-
|
|
389
|
+
const saved = amountToScaledValue(this.item.finalPriceWithoutDiscounts) -
|
|
390
|
+
amountToScaledValue(this.item.finalPrice);
|
|
391
|
+
return saved > 0n ? amountFromScaledValue(saved) : 0;
|
|
307
392
|
}
|
|
393
|
+
// The taxes inside finalPrice
|
|
308
394
|
get totalTax() {
|
|
309
|
-
|
|
310
|
-
for (const tax of this.taxes)
|
|
311
|
-
result += (this.amount * tax.rate) / 100;
|
|
312
|
-
return result;
|
|
395
|
+
return this.item.totalTax;
|
|
313
396
|
}
|
|
314
|
-
// What this line adds to the cart
|
|
397
|
+
// What this line adds to the cart, and what a customer pays for it: the price of a unit
|
|
398
|
+
// multiplied by the quantity, with its taxes already in it
|
|
315
399
|
get finalPrice() {
|
|
316
|
-
return this.
|
|
400
|
+
return this.item.finalPrice;
|
|
317
401
|
}
|
|
318
402
|
// This line as the protocol carries it. Only CartEditor.build calls this, which is what assigns
|
|
319
|
-
// the order.
|
|
403
|
+
// the order. Taxes come out of the price on the way (see buildItem).
|
|
320
404
|
toShoppingListItem(shoppingListOrder) {
|
|
321
|
-
|
|
405
|
+
const price = this._package.price;
|
|
406
|
+
return buildItem(this._package.id, this._quantity, price?.finalPrice ?? 0, price?.regularPrice ?? 0, this._product?.taxes ?? [], shoppingListOrder);
|
|
322
407
|
}
|
|
323
408
|
}
|
|
324
|
-
|
|
409
|
+
// The taxes a price includes, as an item is to carry them: only those with a rate, which is what
|
|
410
|
+
// Java does with the taxes of a product (TaxHelper.getTaxCodesAndRates drops the rest).
|
|
411
|
+
function includedTaxes(taxes) {
|
|
412
|
+
return Object.freeze(taxes.filter((tax) => Number.isFinite(tax.rate) && tax.rate > 0));
|
|
413
|
+
}
|
|
414
|
+
// The one place a price with taxes in it becomes a shopping list item, and so the one place
|
|
415
|
+
// CartEditor takes a tax out of a price. The item stores the prices without them, as Java
|
|
416
|
+
// ShoppingListItem.setDetailsFromTaxInclusivePrices does: the offer price and the regular price
|
|
417
|
+
// each have their taxes removed, and what the offer takes off is the difference of the two - which
|
|
418
|
+
// is not the difference of the prices with taxes. Everything but the price is as Java's
|
|
419
|
+
// ProductSelectionCard leaves a new item: unit quantity or the customer's, no discount of its own,
|
|
420
|
+
// no description (that is for lines that have no package), and no stock keeping or expiry, a
|
|
421
|
+
// storefront being told neither. It is flagged original, for the reason build gives.
|
|
422
|
+
//
|
|
423
|
+
// A price is a whole number of thousandths and each tax is rounded, so some prices cannot be
|
|
424
|
+
// reached: the item then has the price nearest to it (TaxHelper.removeTaxes).
|
|
425
|
+
function buildItem(packageId, quantity, offerPriceWithTaxes, regularPriceWithTaxes, taxes, shoppingListOrder) {
|
|
426
|
+
const included = includedTaxes(taxes);
|
|
427
|
+
const rates = TaxHelper.scaledRatesOf(included);
|
|
428
|
+
const offerPrice = TaxHelper.removeTaxes(amountToScaledValue(offerPriceWithTaxes), rates);
|
|
429
|
+
const regularPrice = TaxHelper.removeTaxes(amountToScaledValue(regularPriceWithTaxes), rates);
|
|
430
|
+
return new ShoppingListItem(packageId, quantity, amountFromScaledValue(offerPrice), shoppingListOrder, true, false, false, amountFromScaledValue(regularPrice > offerPrice ? regularPrice - offerPrice : 0n), 0, included, 0, null);
|
|
431
|
+
}
|
package/dist/Catalog.js
CHANGED
|
@@ -12,28 +12,33 @@ class Catalog {
|
|
|
12
12
|
// A dummy category standing in for a product whose parentItemId is Constants.ENTITY_ID_NULL.
|
|
13
13
|
// Its own id is that same ENTITY_ID_NULL, so getCategory(product.productCategoryId) resolves
|
|
14
14
|
// it exactly like a real category - including being left out below, the same as any other
|
|
15
|
-
// category, when
|
|
15
|
+
// category, when no product ends up in it.
|
|
16
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
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
17
|
+
// Only a buyable product - itself enabled, not one that the business sells only offline, with at
|
|
18
|
+
// least one buyable package, so one a customer can actually order - is part of a catalog. Any other is left out of products, getProduct and
|
|
19
|
+
// its categories, and so are its packages: they are not reachable by id either. This is
|
|
20
|
+
// enforced here, and not left to whoever builds the catalog to have filtered, so that no way
|
|
21
|
+
// of making one can expose a product that cannot be bought.
|
|
22
|
+
const buyableProducts = [];
|
|
21
23
|
const productsByCategory = new Map();
|
|
22
|
-
for (const product of
|
|
24
|
+
for (const product of products) {
|
|
25
|
+
if (!product.isBuyable)
|
|
26
|
+
continue;
|
|
27
|
+
buyableProducts.push(product);
|
|
23
28
|
this._productMap.set(product.id, product);
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
productsOfCategory.push(product);
|
|
30
|
-
}
|
|
29
|
+
const productsOfCategory = productsByCategory.get(product.productCategoryId);
|
|
30
|
+
if (productsOfCategory === undefined)
|
|
31
|
+
productsByCategory.set(product.productCategoryId, [product]);
|
|
32
|
+
else
|
|
33
|
+
productsOfCategory.push(product);
|
|
31
34
|
for (const pkg of product.packages)
|
|
32
35
|
this._packageMap.set(pkg.id, pkg);
|
|
33
36
|
}
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
// out
|
|
37
|
+
this._products = Object.freeze(buyableProducts);
|
|
38
|
+
// A category with nothing a customer can order, which is one that no product reached above, is
|
|
39
|
+
// left out rather than shown empty - there is no point showing it to a customer. Nothing here
|
|
40
|
+
// is special-cased for the dummy: it is left out exactly the same way when nothing buyable is
|
|
41
|
+
// uncategorized.
|
|
37
42
|
const nonEmptyCategories = [];
|
|
38
43
|
for (const category of [...categories, uncategorized]) {
|
|
39
44
|
const categoryProducts = productsByCategory.get(category.id);
|
|
@@ -50,7 +55,8 @@ class Catalog {
|
|
|
50
55
|
get categories() {
|
|
51
56
|
return this._categories;
|
|
52
57
|
}
|
|
53
|
-
// Every product of the business
|
|
58
|
+
// Every buyable product of the business - itself enabled, with at least one buyable package -
|
|
59
|
+
// whether or not it is in a category, in no particular order. A product that is not is not here.
|
|
54
60
|
get products() {
|
|
55
61
|
return this._products;
|
|
56
62
|
}
|
|
@@ -69,11 +75,13 @@ class Catalog {
|
|
|
69
75
|
getCategory(productCategoryId) {
|
|
70
76
|
return this._categoryMap.get(productCategoryId) ?? null;
|
|
71
77
|
}
|
|
78
|
+
// Null for an unknown id, and for a product that is not buyable, which this catalog does not hold
|
|
72
79
|
getProduct(productId) {
|
|
73
80
|
return this._productMap.get(productId) ?? null;
|
|
74
81
|
}
|
|
75
82
|
// 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
|
|
83
|
+
// summary lines (see Ids.isReservedPackageId), and for a package whose product was deleted or is
|
|
84
|
+
// not buyable
|
|
77
85
|
getPackage(packageId) {
|
|
78
86
|
return this._packageMap.get(packageId) ?? null;
|
|
79
87
|
}
|