@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/CartEditor.js
CHANGED
|
@@ -1,53 +1,58 @@
|
|
|
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";
|
|
8
|
-
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// Business.createCartEditor, edit it, then hand build() to Model.updateCartLocalOnly.
|
|
7
|
+
import ShoppingList, { ShoppingListItem, validatePricing } from "./ShoppingList";
|
|
8
|
+
import TaxHelper from "./TaxHelper";
|
|
9
|
+
// Builds the cart a customer is filling, in the terms a UI has to hand: packages, quantities as
|
|
10
|
+
// the customer means them (not multiplied by Constants.QUANTITY_FACTOR), prices from the catalog.
|
|
11
|
+
// Get one from Business.createCartEditor, edit it, then hand build() to Model.updateCartLocalOnly.
|
|
12
12
|
//
|
|
13
|
-
// Mutable, and
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
13
|
+
// Mutable, and the only mutable thing in this object model. It is a scratch copy: editing it
|
|
14
|
+
// changes no Business, store or server until a built list is handed to Model. It is also a
|
|
15
|
+
// snapshot, read once at construction, so a sync mid-edit does not move prices under the customer.
|
|
16
|
+
// Make a fresh one when the Business republishes.
|
|
17
|
+
//
|
|
18
|
+
// EVERY PRICE HERE INCLUDES TAXES: a package's includes its product's taxes, the delivery charge
|
|
19
|
+
// the taxes the settings name for it, so a customer pays what the catalog shows. A shopping list
|
|
20
|
+
// item stores a price WITHOUT taxes, so building one takes them out, as Java
|
|
21
|
+
// ShoppingListItem.setDetailsFromTaxInclusivePrices does (to the unit, see TaxHelper). Taxes are
|
|
22
|
+
// shown as what a total includes (taxes), never added to it.
|
|
18
23
|
class CartEditor {
|
|
19
|
-
// Business.createCartEditor is how a UI gets one
|
|
20
|
-
//
|
|
21
|
-
// that are not the ones currently published.
|
|
24
|
+
// Business.createCartEditor is how a UI gets one. It takes the catalog, settings and cart, not
|
|
25
|
+
// the Business, so a caller can build a cart against ones that are not currently published.
|
|
22
26
|
//
|
|
23
|
-
// Every line is priced off the catalog
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// withdrawn, disabled
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
27
|
+
// Every line is priced off the catalog, not off what the cart held: the merchant may have
|
|
28
|
+
// repriced since, and an order freezes the price when it is placed. A line goes to
|
|
29
|
+
// unavailableItems, for a UI to tell the customer about, when the catalog can no longer price it
|
|
30
|
+
// (package withdrawn, disabled or without a price) or setQuantity would refuse it (a cart that
|
|
31
|
+
// was read is not known to be one this class could have made): a zero or invalid quantity, a
|
|
32
|
+
// fraction of a package not sold loose, more than the wire or the protocol carries at today's
|
|
33
|
+
// price, or a line past the most a cart holds (the first lines are kept, in pick order).
|
|
34
|
+
// The charge lines of the starting list (see ShoppingListItem.isReservedPackage) are dropped, not
|
|
35
|
+
// listed, since build() puts back what the settings now say.
|
|
30
36
|
constructor(_catalog, _settings, cart) {
|
|
31
37
|
this._catalog = _catalog;
|
|
32
38
|
this._settings = _settings;
|
|
33
|
-
// Insertion ordered
|
|
34
|
-
//
|
|
39
|
+
// Insertion ordered: keeps the cart in the order the customer filled it and gives each line its
|
|
40
|
+
// shoppingListOrder
|
|
35
41
|
this._quantities = new Map();
|
|
36
42
|
this._inventoryLocationId = cart?.inventoryLocationId ?? 0;
|
|
43
|
+
// goodsItems is in the order the customer picked. existingItems is not, the server sending it
|
|
44
|
+
// by package id, so a cart read back would reorder itself
|
|
37
45
|
const unavailableItems = [];
|
|
38
|
-
for (const item of cart?.
|
|
46
|
+
for (const item of cart?.goodsItems ?? []) {
|
|
39
47
|
if (item.isReservedPackage)
|
|
40
48
|
continue;
|
|
41
|
-
|
|
42
|
-
if (buyable === null)
|
|
49
|
+
if (!this.tryKeep(item))
|
|
43
50
|
unavailableItems.push(item);
|
|
44
|
-
else
|
|
45
|
-
this._quantities.set(buyable.id, item.quantity);
|
|
46
51
|
}
|
|
47
52
|
this._unavailableItems = Object.freeze(unavailableItems);
|
|
48
53
|
}
|
|
49
|
-
// What the customer has picked, in
|
|
50
|
-
//
|
|
54
|
+
// What the customer has picked, in pick order, each line priced as it would be bought now.
|
|
55
|
+
// Rebuilt on every read, so a line held across an edit is stale.
|
|
51
56
|
get lines() {
|
|
52
57
|
const lines = [];
|
|
53
58
|
for (const [packageId, quantity] of this._quantities) {
|
|
@@ -58,15 +63,15 @@ class CartEditor {
|
|
|
58
63
|
}
|
|
59
64
|
return Object.freeze(lines);
|
|
60
65
|
}
|
|
61
|
-
// The lines of the cart
|
|
62
|
-
//
|
|
66
|
+
// The lines of the starting cart that the catalog can no longer price (see the constructor).
|
|
67
|
+
// Empty for a cart built from nothing, and no edit adds to it.
|
|
63
68
|
get unavailableItems() {
|
|
64
69
|
return this._unavailableItems;
|
|
65
70
|
}
|
|
66
71
|
get isEmpty() {
|
|
67
72
|
return this._quantities.size === 0;
|
|
68
73
|
}
|
|
69
|
-
//
|
|
74
|
+
// The number of lines, which is what a cart badge counts
|
|
70
75
|
get lineCount() {
|
|
71
76
|
return this._quantities.size;
|
|
72
77
|
}
|
|
@@ -81,33 +86,24 @@ class CartEditor {
|
|
|
81
86
|
getQuantity(item) {
|
|
82
87
|
return this._quantities.get(item.id) ?? 0;
|
|
83
88
|
}
|
|
84
|
-
// Sets how much of one package the cart holds; zero takes it out.
|
|
85
|
-
// buy
|
|
86
|
-
//
|
|
87
|
-
//
|
|
89
|
+
// Sets how much of one package the cart holds; zero takes it out. Refuses a package a customer
|
|
90
|
+
// cannot buy (disabled, or priced in no list this storefront sells from), a fractional quantity
|
|
91
|
+
// of a package that is not sold loose, and a quantity past what the wire can carry. Sold loose is
|
|
92
|
+
// read from the catalog's package, since the Package passed in may predate a catalog change.
|
|
93
|
+
// Returns this, so edits chain.
|
|
88
94
|
setQuantity(item, quantity) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
const wireQuantity = CartEditor.toWireQuantity(quantity);
|
|
96
|
+
// Less than half a thousandth rounds to zero on the wire, a line that costs nothing, so it
|
|
97
|
+
// takes the line out as zero does
|
|
98
|
+
if (wireQuantity === 0) {
|
|
92
99
|
this._quantities.delete(item.id);
|
|
93
100
|
return this;
|
|
94
101
|
}
|
|
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);
|
|
102
|
+
this._quantities.set(item.id, this.checkQuantity(item.id, quantity, wireQuantity));
|
|
107
103
|
return this;
|
|
108
104
|
}
|
|
109
|
-
// Adds to what the cart
|
|
110
|
-
//
|
|
105
|
+
// Adds to what the cart holds of this package, as a quantity stepper does. A negative amount
|
|
106
|
+
// takes away, down to zero at the lowest.
|
|
111
107
|
add(item, quantity = 1) {
|
|
112
108
|
return this.setQuantity(item, Math.max(0, this.getQuantity(item) + quantity));
|
|
113
109
|
}
|
|
@@ -118,207 +114,289 @@ class CartEditor {
|
|
|
118
114
|
this._quantities.clear();
|
|
119
115
|
return this;
|
|
120
116
|
}
|
|
121
|
-
// What the packages
|
|
117
|
+
// What the packages come to, taxes included and offers taken off: the sum of the lines' final
|
|
118
|
+
// prices, which a customer sees as the price of the cart. Delivery is not in it.
|
|
122
119
|
get itemsTotal() {
|
|
123
|
-
|
|
124
|
-
for (const line of this.lines)
|
|
125
|
-
result += line.amount;
|
|
126
|
-
return result;
|
|
120
|
+
return ShoppingList.sumOfFinalPrices(this.lines.map((line) => line.item));
|
|
127
121
|
}
|
|
128
|
-
// What the offers took off,
|
|
122
|
+
// What the offers took off the packages, shown on a bill as savings: the total at regular prices
|
|
123
|
+
// less the total. Zero when nothing is on offer. Taxes are included on both sides.
|
|
129
124
|
get savings() {
|
|
130
|
-
|
|
131
|
-
for (const line of this.lines)
|
|
132
|
-
result += line.savings;
|
|
133
|
-
return result;
|
|
125
|
+
return this.build().savings;
|
|
134
126
|
}
|
|
127
|
+
// The taxes already inside itemsTotal, all types together. See taxes for the split by type.
|
|
135
128
|
get itemsTax() {
|
|
136
|
-
|
|
137
|
-
for (const line of this.lines)
|
|
138
|
-
result += line.totalTax;
|
|
139
|
-
return result;
|
|
129
|
+
return this.build().computedTotalTax;
|
|
140
130
|
}
|
|
141
|
-
// The
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
result += line.finalPrice;
|
|
147
|
-
return result;
|
|
131
|
+
// The taxes the cart's total includes, one entry per TYPE of tax. A bill shows a line for each
|
|
132
|
+
// that is not zero. withDeliveryCharge is as for total: the delivery charge's taxes count only
|
|
133
|
+
// when it is true.
|
|
134
|
+
taxes(withDeliveryCharge = false) {
|
|
135
|
+
return this.build(withDeliveryCharge).computedTaxes;
|
|
148
136
|
}
|
|
149
|
-
// True when this business delivers
|
|
150
|
-
//
|
|
137
|
+
// True when this business delivers. When false there is no delivery charge for build() to add,
|
|
138
|
+
// and no fulfillment choice for a UI to offer.
|
|
151
139
|
get isDeliveryOffered() {
|
|
152
140
|
return this._settings.fulfillmentModes.includes(proto_FulfillmentMode.DELIVERY);
|
|
153
141
|
}
|
|
154
|
-
// What delivering this cart
|
|
155
|
-
// has
|
|
156
|
-
//
|
|
142
|
+
// What delivering this cart costs, taxes included: the business's flat charge, or zero when the
|
|
143
|
+
// cart has reached freeDeliveryTotal or the business delivers free. Zero when the business does
|
|
144
|
+
// not deliver.
|
|
157
145
|
get deliveryCharge() {
|
|
158
|
-
return this.getDeliveryCharge(this.
|
|
146
|
+
return this.buildDeliveryChargeItem(this.getDeliveryCharge(this.itemsTotal), 0)?.finalPrice ?? 0;
|
|
159
147
|
}
|
|
160
|
-
// What delivery costs before any waiver
|
|
161
|
-
//
|
|
148
|
+
// What delivery costs before any waiver, which a bill strikes through when it reads FREE. Zero
|
|
149
|
+
// where the business delivers free, so there is nothing to strike through.
|
|
162
150
|
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);
|
|
151
|
+
return this.buildDeliveryChargeItem(this.regularCharge, 0)?.finalPrice ?? 0;
|
|
167
152
|
}
|
|
168
|
-
// True when delivering this cart costs nothing,
|
|
169
|
-
//
|
|
153
|
+
// True when delivering this cart costs nothing, because the business never charges or the cart
|
|
154
|
+
// has reached the threshold
|
|
170
155
|
get isDeliveryFree() {
|
|
171
156
|
return this.isDeliveryOffered && this.deliveryCharge === 0;
|
|
172
157
|
}
|
|
173
|
-
// How much more the customer
|
|
174
|
-
//
|
|
175
|
-
//
|
|
158
|
+
// How much more the customer must add for free delivery, for the "add X more for free delivery"
|
|
159
|
+
// note. Zero when delivery is already free. Null when the business does not deliver or has set no
|
|
160
|
+
// threshold, so no amount would make it free.
|
|
176
161
|
get amountToFreeDelivery() {
|
|
177
162
|
if (!this.isDeliveryOffered)
|
|
178
163
|
return null;
|
|
179
|
-
if (this.
|
|
164
|
+
if (this.regularCharge === 0)
|
|
180
165
|
return 0; // Already free, and free at any total
|
|
181
166
|
const freeDeliveryTotal = this._settings.freeDeliveryTotal;
|
|
182
167
|
if (freeDeliveryTotal <= 0)
|
|
183
168
|
return null; // No threshold: adding more changes nothing
|
|
184
|
-
|
|
169
|
+
const remaining = amountToScaledValue(freeDeliveryTotal) - amountToScaledValue(this.itemsTotal);
|
|
170
|
+
return remaining > 0n ? amountFromScaledValue(remaining) : 0;
|
|
185
171
|
}
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
//
|
|
172
|
+
// The tax inside deliveryCharge, from GeneralSettings.deliveryChargeTaxes. Those are the
|
|
173
|
+
// business's own, unrelated to a product's. Included in the charge, not added to it. Zero when
|
|
174
|
+
// delivery is free.
|
|
189
175
|
get deliveryTax() {
|
|
190
|
-
return
|
|
176
|
+
return this.buildDeliveryChargeItem(this.getDeliveryCharge(this.itemsTotal), 0)?.totalTax ?? 0;
|
|
191
177
|
}
|
|
192
|
-
// What the cart comes to. Delivery is left out unless asked for
|
|
193
|
-
// out and a checkout page puts it in
|
|
178
|
+
// What the cart comes to, taxes included. Delivery is left out unless asked for: a cart page
|
|
179
|
+
// leaves it out and a checkout page puts it in, as in build(). It is the total of the list
|
|
180
|
+
// build() answers, and so of an order placed from it.
|
|
194
181
|
total(withDeliveryCharge = false) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return total + charge + CartEditor.getTax(charge, this._settings.deliveryChargeTaxes);
|
|
202
|
-
}
|
|
203
|
-
// The cart as a list to write: what Model.updateCartLocalOnly and Model.updateCart take, and
|
|
204
|
-
// what a later Model.createOrder turns into an order.
|
|
205
|
-
//
|
|
206
|
-
// withDeliveryCharge decides whether the delivery charge rides along as a line of its own,
|
|
207
|
-
// carrying ReservedPackage.DELIVERY_CHARGE_PACKAGE and flagged by
|
|
208
|
-
// ShoppingList.autoAddDeliveryCharge. A cart page has no fulfillment chosen yet and so leaves it
|
|
209
|
-
// out, which is the default; a checkout page that has settled on delivery asks for it. The line
|
|
210
|
-
// is added even when the charge has been waived - at zero, with the regular charge as its
|
|
211
|
-
// priceDiscount - so a bill can show FREE with the original struck through, which is what Java
|
|
212
|
-
// ShoppingCartViewModel.setDeliveryCharge writes too.
|
|
182
|
+
return this.build(withDeliveryCharge).totalAmount;
|
|
183
|
+
}
|
|
184
|
+
// The cart as a list, to write (Model.updateCartLocalOnly, Model.updateCart, Model.createOrder)
|
|
185
|
+
// and to SHOW: every price, total and tax a cart or checkout page displays can be read off it.
|
|
186
|
+
// Each line stores its price without taxes and is flagged original, as Java ShoppingList.addItem
|
|
187
|
+
// does before a list is final. The list comes back not final: Model.createOrder makes it so.
|
|
213
188
|
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
189
|
+
// withDeliveryCharge adds the delivery charge as a line (ReservedPackage.DELIVERY_CHARGE_PACKAGE)
|
|
190
|
+
// and sets ShoppingList.autoAddDeliveryCharge. A cart page leaves it out (the default), a checkout
|
|
191
|
+
// page that has settled on delivery asks for it. The line is added even when the charge is
|
|
192
|
+
// waived: at zero, with the regular charge (less its taxes) as its priceDiscount, so a bill can
|
|
193
|
+
// show FREE with the original struck through (Java ShoppingCartViewModel.setDeliveryCharge).
|
|
194
|
+
// An empty cart gets no line: a list holding only a delivery charge is not an order, and Java
|
|
195
|
+
// removes that line when it is all that is left.
|
|
218
196
|
build(withDeliveryCharge = false) {
|
|
219
197
|
const items = [];
|
|
220
198
|
for (const line of this.lines)
|
|
221
199
|
items.push(line.toShoppingListItem(items.length));
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
200
|
+
const delivery = withDeliveryCharge && items.length > 0
|
|
201
|
+
? this.buildDeliveryChargeItem(this.getDeliveryCharge(ShoppingList.sumOfFinalPrices(items)), items.length)
|
|
202
|
+
: null;
|
|
203
|
+
if (delivery !== null)
|
|
204
|
+
items.push(delivery);
|
|
205
|
+
// The total is the sum of the final prices of the lines sent, tax included, as ShoppingList.proto
|
|
206
|
+
// defines it. There is no list-wide discount: an offer comes off a line, and a storefront gives
|
|
207
|
+
// no other.
|
|
208
|
+
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([]));
|
|
209
|
+
}
|
|
210
|
+
// The delivery charge as a line: one unit at what it costs now (charge: the regular charge, or
|
|
211
|
+
// zero once waived), with the waived amount as its price discount. That is the shape of a package
|
|
212
|
+
// bought on offer, so a bill renders it with no special case beyond its label. Null when the
|
|
213
|
+
// business does not deliver. The settings' charge includes the delivery charge taxes, so they
|
|
214
|
+
// come out of it like those of any other price.
|
|
215
|
+
buildDeliveryChargeItem(charge, shoppingListOrder) {
|
|
216
|
+
if (!this.isDeliveryOffered)
|
|
217
|
+
return null;
|
|
218
|
+
return buildItem(Ids.buildId(proto_Constants.PACKAGE_ENTITY_TYPE_ID, BigInt(proto_ReservedPackage.DELIVERY_CHARGE_PACKAGE)), 1, charge, this.regularCharge, this._settings.deliveryChargeTaxes, shoppingListOrder);
|
|
219
|
+
}
|
|
220
|
+
// The charge before any waiver. Java clamps a negative charge to zero (no refund), and so does
|
|
221
|
+
// this. Zero when the business does not deliver.
|
|
222
|
+
get regularCharge() {
|
|
223
|
+
if (!this.isDeliveryOffered)
|
|
224
|
+
return 0;
|
|
225
|
+
return Math.max(0, this._settings.deliveryCharge);
|
|
226
|
+
}
|
|
227
|
+
// The charge for a cart whose goods come to itemsTotal: the regular charge, or nothing once the
|
|
228
|
+
// goods reach the threshold. Java ShoppingCartViewModel.calculateDeliveryCharge compares the same
|
|
229
|
+
// total, taxes included and the delivery line left out.
|
|
230
|
+
getDeliveryCharge(itemsTotal) {
|
|
231
|
+
const charge = this.regularCharge;
|
|
244
232
|
if (charge === 0)
|
|
245
233
|
return 0; // Free for everyone, or not delivered at all
|
|
246
234
|
const freeDeliveryTotal = this._settings.freeDeliveryTotal;
|
|
247
235
|
if (freeDeliveryTotal <= 0)
|
|
248
|
-
return charge; // Never free,
|
|
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;
|
|
236
|
+
return charge; // Never free, at any total
|
|
237
|
+
return itemsTotal >= freeDeliveryTotal ? 0 : charge;
|
|
256
238
|
}
|
|
257
239
|
// The package this id names, and null unless a customer could actually buy it right now
|
|
258
240
|
getBuyablePackage(packageId) {
|
|
259
241
|
const item = this._catalog.getPackage(packageId);
|
|
260
242
|
return item !== null && item.isBuyable ? item : null;
|
|
261
243
|
}
|
|
244
|
+
// Keeps a line of a cart that was read, as setQuantity would, and says whether it could. A line
|
|
245
|
+
// that cannot be kept goes to unavailableItems, as it is not the caller's mistake.
|
|
246
|
+
tryKeep(item) {
|
|
247
|
+
try {
|
|
248
|
+
const wireQuantity = CartEditor.toWireQuantity(item.quantity);
|
|
249
|
+
if (wireQuantity === 0)
|
|
250
|
+
return false; // A line of nothing is no line
|
|
251
|
+
this._quantities.set(item.packageId, this.checkQuantity(item.packageId, item.quantity, wireQuantity));
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
if (error instanceof InvalidArgumentException)
|
|
256
|
+
return false;
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// A quantity travels as an int32 multiplied by Constants.QUANTITY_FACTOR (Java rounds to it with
|
|
261
|
+
// NumberHelper.floatToInt), so the least is a thousandth. This converts a quantity to that. What
|
|
262
|
+
// is kept is the quantity the wire holds, so getQuantity never reports one the order will not
|
|
263
|
+
// have.
|
|
264
|
+
static toWireQuantity(quantity) {
|
|
265
|
+
if (!Number.isFinite(quantity) || quantity < 0)
|
|
266
|
+
throw new InvalidArgumentException(`${quantity} is not a quantity`);
|
|
267
|
+
return Math.round(quantity * proto_Constants.QUANTITY_FACTOR);
|
|
268
|
+
}
|
|
269
|
+
// The quantity to keep for this much of a package, as the wire holds it, or an
|
|
270
|
+
// InvalidArgumentException saying why the cart cannot have it: the catalog does not sell the
|
|
271
|
+
// package, it is not sold loose and the quantity is a fraction, the quantity or what it comes to
|
|
272
|
+
// is more than the protocol carries, or the cart is full. All of it is read off the catalog's
|
|
273
|
+
// package, which the order is priced from.
|
|
274
|
+
checkQuantity(packageId, quantity, wireQuantity) {
|
|
275
|
+
const buyable = this.getBuyablePackage(packageId);
|
|
276
|
+
if (buyable === null)
|
|
277
|
+
throw new InvalidArgumentException(`Package ${Ids.toStorageHexString(packageId)} cannot be bought from this catalog`);
|
|
278
|
+
if (!buyable.isSoldLoose && !Number.isInteger(quantity))
|
|
279
|
+
throw new InvalidArgumentException(`${buyable.name} is not sold loose, so it cannot be bought ${quantity} at a time`);
|
|
280
|
+
// The most of one package a cart can hold. Refused here so it does not wrap silently on the wire
|
|
281
|
+
if (wireQuantity > CartEditor.MAX_WIRE_QUANTITY)
|
|
282
|
+
throw new InvalidArgumentException(`${quantity} is more than the ${CartEditor.MAX_WIRE_QUANTITY / proto_Constants.QUANTITY_FACTOR} the protocol can carry`);
|
|
283
|
+
// A list holds at most Constants.SHOPPING_LIST_MAX_ITEMS items, and the delivery charge is one
|
|
284
|
+
// (Java ShoppingList.addItem counts it like any other, and a server refuses a list one line
|
|
285
|
+
// over). Where the business delivers, one place is kept for it: a cart filled to the limit
|
|
286
|
+
// could not be ordered for delivery.
|
|
287
|
+
const maxLines = proto_Constants.SHOPPING_LIST_MAX_ITEMS - (this.isDeliveryOffered ? 1 : 0);
|
|
288
|
+
if (!this._quantities.has(packageId) && this._quantities.size >= maxLines)
|
|
289
|
+
throw new InvalidArgumentException(`A cart holds at most ${maxLines} lines`);
|
|
290
|
+
// Java refuses an amount past Transaction.MAX_AMOUNT (validateAmount) in the item that comes to
|
|
291
|
+
// it, and so does pricing an item here. It is priced before the line is kept, so the failure
|
|
292
|
+
// does not wait until a list is written.
|
|
293
|
+
const stored = wireQuantity / proto_Constants.QUANTITY_FACTOR;
|
|
294
|
+
try {
|
|
295
|
+
validatePricing(new CartLine(buyable, this._catalog.getProduct(buyable.productId), stored).item);
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
if (error instanceof InvalidArgumentException)
|
|
299
|
+
throw new InvalidArgumentException(`${quantity} of ${buyable.name} comes to more than the protocol can carry (${error.message})`);
|
|
300
|
+
throw error;
|
|
301
|
+
}
|
|
302
|
+
return stored;
|
|
303
|
+
}
|
|
262
304
|
}
|
|
263
|
-
//
|
|
264
|
-
CartEditor.
|
|
305
|
+
// The most an int32 holds, which bounds a quantity multiplied by Constants.QUANTITY_FACTOR
|
|
306
|
+
CartEditor.MAX_WIRE_QUANTITY = 0x7fffffff;
|
|
265
307
|
export default CartEditor;
|
|
266
|
-
// One line of a cart being edited: the package, the product it is a variant of, and how much of it
|
|
267
|
-
//
|
|
268
|
-
// on every read of CartEditor.lines
|
|
308
|
+
// One line of a cart being edited: the package, the product it is a variant of, and how much of it,
|
|
309
|
+
// with the prices the catalog gives it now, taxes included. A read-only view over what CartEditor
|
|
310
|
+
// holds, rebuilt on every read of CartEditor.lines.
|
|
311
|
+
//
|
|
312
|
+
// Everything is read off item, the line as a shopping list would store it, so the cart page shows
|
|
313
|
+
// what the order will and no price is worked out twice.
|
|
269
314
|
export class CartLine {
|
|
270
315
|
constructor(_package, _product, _quantity) {
|
|
271
316
|
this._package = _package;
|
|
272
317
|
this._product = _product;
|
|
273
318
|
this._quantity = _quantity;
|
|
319
|
+
this._item = null;
|
|
274
320
|
}
|
|
275
321
|
get package() {
|
|
276
322
|
return this._package;
|
|
277
323
|
}
|
|
278
|
-
// Null
|
|
324
|
+
// Null when the package's product is not in the catalog, which a buyable package never has
|
|
279
325
|
get product() {
|
|
280
326
|
return this._product;
|
|
281
327
|
}
|
|
282
328
|
get quantity() {
|
|
283
329
|
return this._quantity;
|
|
284
330
|
}
|
|
285
|
-
//
|
|
286
|
-
//
|
|
331
|
+
// This line as a shopping list holds it: priced without taxes, with the taxes beside it. Show
|
|
332
|
+
// this to show the line as an order does. Its place in the list means nothing here.
|
|
333
|
+
get item() {
|
|
334
|
+
this._item ?? (this._item = this.toShoppingListItem(0));
|
|
335
|
+
return this._item;
|
|
336
|
+
}
|
|
337
|
+
// The taxes of the package's product, which its price includes. A rate not greater than zero is
|
|
338
|
+
// no tax, as in Java, and is left out.
|
|
339
|
+
get taxes() {
|
|
340
|
+
return includedTaxes(this._product?.taxes ?? []);
|
|
341
|
+
}
|
|
342
|
+
// What one unit costs, taxes included: the offer price where the package is on offer, else the
|
|
343
|
+
// regular price. This is the catalog's price to within tax rounding (a thousandth or two, which
|
|
344
|
+
// no one is shown). A buyable package always has a price, so nothing is guessed.
|
|
287
345
|
get unitPrice() {
|
|
288
|
-
return this.
|
|
346
|
+
return this.item.unitPriceWithTaxes;
|
|
289
347
|
}
|
|
348
|
+
// The regular price of a unit, taxes included. Struck through when it is on offer
|
|
290
349
|
get regularUnitPrice() {
|
|
291
|
-
return this.
|
|
350
|
+
return this.item.regularUnitPriceWithTaxes;
|
|
292
351
|
}
|
|
293
352
|
get isOnOffer() {
|
|
294
353
|
return this._package.price?.isOnOffer ?? false;
|
|
295
354
|
}
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
return this._product?.taxes ?? EMPTY_TAXES;
|
|
299
|
-
}
|
|
300
|
-
// What this line costs before tax
|
|
355
|
+
// What this line costs before its taxes: its tax base. A bill that shows prices with taxes shows
|
|
356
|
+
// finalPrice.
|
|
301
357
|
get amount() {
|
|
302
|
-
return this.
|
|
358
|
+
return this.item.amount;
|
|
303
359
|
}
|
|
304
|
-
// What the offer takes off this line
|
|
360
|
+
// What the offer takes off this line, taxes included
|
|
305
361
|
get savings() {
|
|
306
|
-
|
|
362
|
+
const saved = amountToScaledValue(this.item.finalPriceWithoutDiscounts) -
|
|
363
|
+
amountToScaledValue(this.item.finalPrice);
|
|
364
|
+
return saved > 0n ? amountFromScaledValue(saved) : 0;
|
|
307
365
|
}
|
|
366
|
+
// The taxes inside finalPrice
|
|
308
367
|
get totalTax() {
|
|
309
|
-
|
|
310
|
-
for (const tax of this.taxes)
|
|
311
|
-
result += (this.amount * tax.rate) / 100;
|
|
312
|
-
return result;
|
|
368
|
+
return this.item.totalTax;
|
|
313
369
|
}
|
|
314
|
-
// What this line adds to the cart
|
|
370
|
+
// What this line adds to the cart and a customer pays for it: the unit price times the quantity,
|
|
371
|
+
// with its taxes already in it
|
|
315
372
|
get finalPrice() {
|
|
316
|
-
return this.
|
|
373
|
+
return this.item.finalPrice;
|
|
317
374
|
}
|
|
318
|
-
// This line as the protocol carries it.
|
|
319
|
-
// the
|
|
375
|
+
// This line as the protocol carries it. CartEditor.build calls it and assigns the order. Taxes
|
|
376
|
+
// come out of the price on the way (see buildItem).
|
|
320
377
|
toShoppingListItem(shoppingListOrder) {
|
|
321
|
-
|
|
378
|
+
const price = this._package.price;
|
|
379
|
+
return buildItem(this._package.id, this._quantity, price?.finalPrice ?? 0, price?.regularPrice ?? 0, this._product?.taxes ?? [], shoppingListOrder);
|
|
322
380
|
}
|
|
323
381
|
}
|
|
324
|
-
|
|
382
|
+
// The taxes a price includes, as an item carries them: only those with a rate, as Java does with a
|
|
383
|
+
// product's taxes (TaxHelper.getTaxCodesAndRates drops the rest).
|
|
384
|
+
function includedTaxes(taxes) {
|
|
385
|
+
return Object.freeze(taxes.filter((tax) => Number.isFinite(tax.rate) && tax.rate > 0));
|
|
386
|
+
}
|
|
387
|
+
// Where a price with taxes in it becomes a shopping list item, and so where CartEditor takes a tax
|
|
388
|
+
// out of a price, as Java ShoppingListItem.setDetailsFromTaxInclusivePrices does. The offer price
|
|
389
|
+
// and the regular price each have their taxes removed, and the price discount is the difference of
|
|
390
|
+
// those two, which is not the difference of the prices with taxes. The rest is as Java's
|
|
391
|
+
// ProductSelectionCard leaves a new item: no discount of its own, no description (that is for
|
|
392
|
+
// lines with no package), no stock keeping or expiry (a storefront is not told). It is flagged
|
|
393
|
+
// original, as in build.
|
|
394
|
+
// A price is a whole number of thousandths and each tax is rounded, so some prices cannot be
|
|
395
|
+
// reached. The item then has the nearest price (TaxHelper.removeTaxes).
|
|
396
|
+
function buildItem(packageId, quantity, offerPriceWithTaxes, regularPriceWithTaxes, taxes, shoppingListOrder) {
|
|
397
|
+
const included = includedTaxes(taxes);
|
|
398
|
+
const rates = TaxHelper.scaledRatesOf(included);
|
|
399
|
+
const offerPrice = TaxHelper.removeTaxes(amountToScaledValue(offerPriceWithTaxes), rates);
|
|
400
|
+
const regularPrice = TaxHelper.removeTaxes(amountToScaledValue(regularPriceWithTaxes), rates);
|
|
401
|
+
return new ShoppingListItem(packageId, quantity, amountFromScaledValue(offerPrice), shoppingListOrder, true, false, false, amountFromScaledValue(regularPrice > offerPrice ? regularPrice - offerPrice : 0n), 0, included, 0, null);
|
|
402
|
+
}
|