@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/ProtoMap.js
CHANGED
|
@@ -62,7 +62,8 @@ import Tax from "./Tax";
|
|
|
62
62
|
import UserProfile from "./UserProfile";
|
|
63
63
|
import WorkingDays, { WorkingHours, WorkingHoursRange } from "./WorkingDays";
|
|
64
64
|
// #region A response and the blobs it is cached as
|
|
65
|
-
// Every entity of a response, in declaration order (parents before children).
|
|
65
|
+
// Every entity of a response, in declaration order (parents before children). Tombstones are
|
|
66
|
+
// included, flagged.
|
|
66
67
|
export function getBusinessResponseEntities(response) {
|
|
67
68
|
const result = [];
|
|
68
69
|
if (response.business !== undefined)
|
|
@@ -85,8 +86,8 @@ export function getBusinessResponseEntities(response) {
|
|
|
85
86
|
result.push(toItemWireEntity(price.item, proto_Price.toBinary(price)));
|
|
86
87
|
return result;
|
|
87
88
|
}
|
|
88
|
-
// Every entity of a profile
|
|
89
|
-
//
|
|
89
|
+
// Every entity of a profile response: the caller, their links, and the businesses those name. The
|
|
90
|
+
// three have separate cursors, so unlike a business response the array order does not matter.
|
|
90
91
|
export function getProfileResponseEntities(response) {
|
|
91
92
|
const result = [];
|
|
92
93
|
if (response.user !== undefined)
|
|
@@ -97,13 +98,12 @@ export function getProfileResponseEntities(response) {
|
|
|
97
98
|
result.push(toWireEntity(business.entity, proto_Business.toBinary(business)));
|
|
98
99
|
return result;
|
|
99
100
|
}
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// any entity of it.
|
|
101
|
+
// One blob per entity, tombstones included and as sent, plus the attachment base url in a blob of its
|
|
102
|
+
// own (it belongs to the business, not to an entity).
|
|
103
103
|
export function businessResponseToCacheWrite(response) {
|
|
104
104
|
const write = toCacheWrite(getBusinessResponseEntities(response));
|
|
105
|
-
// Its eTag is its expiry, which is
|
|
106
|
-
//
|
|
105
|
+
// Its eTag is its expiry, which is also its cursor: a client sends back the expiry it holds and
|
|
106
|
+
// gets a later url only if one exists.
|
|
107
107
|
const attachmentBaseUrl = response.attachmentBaseUrl;
|
|
108
108
|
if (attachmentBaseUrl !== undefined)
|
|
109
109
|
write.blobs.push({
|
|
@@ -113,43 +113,44 @@ export function businessResponseToCacheWrite(response) {
|
|
|
113
113
|
});
|
|
114
114
|
return write;
|
|
115
115
|
}
|
|
116
|
-
// The same for a profile response, which
|
|
116
|
+
// The same for a profile response, which holds only entities.
|
|
117
117
|
export function profileResponseToCacheWrite(response) {
|
|
118
118
|
return toCacheWrite(getProfileResponseEntities(response));
|
|
119
119
|
}
|
|
120
|
-
// The expiry of the attachment base url a response carried,
|
|
121
|
-
//
|
|
122
|
-
// Here rather than at a call site, because reading a field of a response is this file's job.
|
|
120
|
+
// The expiry of the attachment base url a response carried, or zero when none. The next request
|
|
121
|
+
// sends it back.
|
|
123
122
|
export function getBusinessResponseAttachmentBaseUrlExpiry(response) {
|
|
124
123
|
return response?.attachmentBaseUrl?.expiry ?? 0n;
|
|
125
124
|
}
|
|
126
|
-
// One order as its own blob, for a caller holding a message rather than a
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
export function orderToBlob(order) {
|
|
125
|
+
// One order as its own blob, for a caller holding a message rather than a response. Only the payload
|
|
126
|
+
// says what a message is, so a non-order is refused here instead of stored where a read would not
|
|
127
|
+
// find it. heldETag is the eTag the container holds the order at, or null - see messageToBlob.
|
|
128
|
+
export function orderToBlob(order, heldETag) {
|
|
130
129
|
if (order.payload.oneofKind !== "salesOrder")
|
|
131
130
|
throw new InvalidArgumentException("That message carries no sales order, so it is not an order");
|
|
132
|
-
return messageToBlob(order);
|
|
131
|
+
return messageToBlob(order, heldETag);
|
|
133
132
|
}
|
|
134
|
-
// Whether a message carries party preferences.
|
|
135
|
-
// field of a message is this file's job.
|
|
133
|
+
// Whether a message carries party preferences.
|
|
136
134
|
export function isPartyPreferencesMessage(message) {
|
|
137
135
|
return message.payload.oneofKind === "partyPreferences";
|
|
138
136
|
}
|
|
139
|
-
// When a message was last written
|
|
140
|
-
//
|
|
137
|
+
// When a message was last written. It compares two copies of one thing held at different ids, which
|
|
138
|
+
// have no shared version.
|
|
141
139
|
export function getMessageUpdatedAt(message) {
|
|
142
140
|
return getEntity(message.entity).updatedAt;
|
|
143
141
|
}
|
|
144
|
-
export function
|
|
142
|
+
export function getMessageId(message) {
|
|
143
|
+
return getEntity(message.entity).id;
|
|
144
|
+
}
|
|
145
|
+
// heldETag as for orderToBlob
|
|
146
|
+
export function partyPreferencesToBlob(partyPreferences, heldETag) {
|
|
145
147
|
if (partyPreferences.payload.oneofKind !== "partyPreferences")
|
|
146
148
|
throw new InvalidArgumentException("That message carries no party preferences");
|
|
147
|
-
return messageToBlob(partyPreferences);
|
|
149
|
+
return messageToBlob(partyPreferences, heldETag);
|
|
148
150
|
}
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// does not know is skipped rather than refused, so a newer server cannot break an older client.
|
|
151
|
+
// Rebuilds the response a business container's blobs make up, as the call returned it. Null when they
|
|
152
|
+
// hold nothing yet. The entity type in each blob id sorts them, so no type tag is stored. An unknown
|
|
153
|
+
// type is skipped, so a newer server cannot break an older client.
|
|
153
154
|
export function businessResponseFromBlobs(blobs) {
|
|
154
155
|
const response = proto_BusinessSyncOutResponse.create({});
|
|
155
156
|
let hasContent = false;
|
|
@@ -196,7 +197,7 @@ export function businessResponseFromBlobs(blobs) {
|
|
|
196
197
|
}
|
|
197
198
|
return hasContent ? response : null;
|
|
198
199
|
}
|
|
199
|
-
// The same for a profile container, whose three entity types are siblings
|
|
200
|
+
// The same for a profile container, whose three entity types are siblings.
|
|
200
201
|
export function profileResponseFromBlobs(blobs) {
|
|
201
202
|
const response = proto_ProfileSyncOutResponse.create({});
|
|
202
203
|
let hasContent = false;
|
|
@@ -220,25 +221,66 @@ export function profileResponseFromBlobs(blobs) {
|
|
|
220
221
|
}
|
|
221
222
|
return hasContent ? response : null;
|
|
222
223
|
}
|
|
223
|
-
// The
|
|
224
|
-
//
|
|
225
|
-
export function
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
224
|
+
// The tombstones a business container holds, as a response, each as stored (and so as sent). Null
|
|
225
|
+
// when it holds none. The attachment base url belongs to no entity, so it is left out.
|
|
226
|
+
export function deletedBusinessResponseFromBlobs(blobs) {
|
|
227
|
+
const held = businessResponseFromBlobs(blobs);
|
|
228
|
+
if (held === null)
|
|
229
|
+
return null;
|
|
230
|
+
const deleted = proto_BusinessSyncOutResponse.create({
|
|
231
|
+
business: held.business !== undefined && isDeletedEntity(getEntity(held.business.entity))
|
|
232
|
+
? held.business
|
|
233
|
+
: undefined,
|
|
234
|
+
attachments: held.attachments.filter((attachment) => isDeletedEntity(getEntity(attachment.entity))),
|
|
235
|
+
configurations: held.configurations.filter((configuration) => isDeletedEntity(getEntity(configuration.entity))),
|
|
236
|
+
messages: held.messages.filter((message) => isDeletedEntity(getEntity(message.entity))),
|
|
237
|
+
productCategories: held.productCategories.filter((category) => isDeletedItem(category.item)),
|
|
238
|
+
products: held.products.filter((product) => isDeletedItem(product.item)),
|
|
239
|
+
packages: held.packages.filter((pkg) => isDeletedItem(pkg.item)),
|
|
240
|
+
priceLists: held.priceLists.filter((priceList) => isDeletedItem(priceList.item)),
|
|
241
|
+
prices: held.prices.filter((price) => isDeletedItem(price.item)),
|
|
242
|
+
});
|
|
243
|
+
return businessResponseEntityCount(deleted) === 0 ? null : deleted;
|
|
231
244
|
}
|
|
232
|
-
// The
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
245
|
+
// The same for a profile container.
|
|
246
|
+
export function deletedProfileResponseFromBlobs(blobs) {
|
|
247
|
+
const held = profileResponseFromBlobs(blobs);
|
|
248
|
+
if (held === null)
|
|
249
|
+
return null;
|
|
250
|
+
const deleted = proto_ProfileSyncOutResponse.create({
|
|
251
|
+
user: held.user !== undefined && isDeletedEntity(getEntity(held.user.entity)) ? held.user : undefined,
|
|
252
|
+
businessLinks: held.businessLinks.filter((businessLink) => isDeletedEntity(getEntity(businessLink.entity))),
|
|
253
|
+
businesses: held.businesses.filter((business) => isDeletedEntity(getEntity(business.entity))),
|
|
254
|
+
});
|
|
255
|
+
return profileResponseEntityCount(deleted) === 0 ? null : deleted;
|
|
256
|
+
}
|
|
257
|
+
// The open orders a container holds, which the business has not completed. The payload says whether
|
|
258
|
+
// a message is an order or a cart. Message.messageType is not read, since the payload is typed.
|
|
259
|
+
export function orderMessagesFromBlobs(blobs) {
|
|
260
|
+
return ordersAmong(messagesFromBlobs(blobs, false));
|
|
261
|
+
}
|
|
262
|
+
// The orders the business has completed, by fulfilling or cancelling. The merchant app completes an
|
|
263
|
+
// order by deleting its message, so these are tombstones, but a server sends them whole because a
|
|
264
|
+
// customer is shown the order afterwards. A tombstone with no order (left by an older server that
|
|
265
|
+
// sent only the header) is not one.
|
|
266
|
+
export function deletedOrderMessagesFromBlobs(blobs) {
|
|
267
|
+
return ordersAmong(messagesFromBlobs(blobs, true));
|
|
268
|
+
}
|
|
269
|
+
function ordersAmong(messages) {
|
|
270
|
+
return messages.filter((message) => message.payload.oneofKind === "salesOrder");
|
|
271
|
+
}
|
|
272
|
+
// The party preferences message among these messages, or null. A customer has one per business, so
|
|
273
|
+
// two means two callers' messages share a container. The greater update number wins for want of
|
|
274
|
+
// anything better, since nothing in a message says which caller it was sent to. See THE MESSAGES OF
|
|
275
|
+
// A BUSINESS ARE NOT PUBLIC.
|
|
276
|
+
// Split from partyPreferencesMessageFromBlobs so a caller already holding the messages (such as
|
|
277
|
+
// Model.businessSyncIn folding the anonymous container in) need not read the blobs again.
|
|
278
|
+
// buildBusiness uses it too, so the cart shown, amended and carried over a sign in is one message.
|
|
239
279
|
export function partyPreferencesMessageFromMessages(messages) {
|
|
240
280
|
let result = null;
|
|
241
281
|
for (const message of messages) {
|
|
282
|
+
if (isDeletedEntity(getEntity(message.entity)))
|
|
283
|
+
continue; // A tombstone is not a cart
|
|
242
284
|
if (message.payload.oneofKind !== "partyPreferences")
|
|
243
285
|
continue;
|
|
244
286
|
if (result === null ||
|
|
@@ -254,11 +296,20 @@ export function partyPreferencesMessageFromBlobs(blobs) {
|
|
|
254
296
|
}
|
|
255
297
|
// #endregion
|
|
256
298
|
// #region Requests and credentials
|
|
257
|
-
// The request
|
|
258
|
-
//
|
|
259
|
-
|
|
299
|
+
// The request for everything changed since these cursors. A bigint business is the Business entity
|
|
300
|
+
// id, which a caller that has read it before has. A string is its url friendly businessId, which is
|
|
301
|
+
// all a first read has. attachmentBaseUrlExpiry is not a cursor but the expiry of the url already
|
|
302
|
+
// held, sent back unchanged (zero when none).
|
|
303
|
+
export function buildBusinessSyncOutRequest(business, syncInState, attachmentBaseUrlExpiry) {
|
|
304
|
+
return createBusinessSyncOutRequest(typeof business === "bigint"
|
|
305
|
+
? { oneofKind: "businessEntityId", businessEntityId: business }
|
|
306
|
+
: { oneofKind: "businessId", businessId: business }, syncInState, attachmentBaseUrlExpiry);
|
|
307
|
+
}
|
|
308
|
+
// Takes the business as the request holds it, so the request after a page names it the same way as
|
|
309
|
+
// the one before.
|
|
310
|
+
function createBusinessSyncOutRequest(business, syncInState, attachmentBaseUrlExpiry) {
|
|
260
311
|
return proto_BusinessSyncOutRequest.create({
|
|
261
|
-
|
|
312
|
+
business: business,
|
|
262
313
|
businessUpdateNumber: syncInState.getUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID),
|
|
263
314
|
attachmentUpdateNumber: syncInState.getUpdateNumber(proto_Constants.ATTACHMENT_ENTITY_TYPE_ID),
|
|
264
315
|
attachmentBaseUrlExpiry: attachmentBaseUrlExpiry,
|
|
@@ -267,40 +318,58 @@ export function buildBusinessSyncOutRequest(businessId, syncInState, attachmentB
|
|
|
267
318
|
itemUpdateNumber: syncInState.getUpdateNumber(proto_Constants.ITEM_ENTITY_TYPE_ID),
|
|
268
319
|
});
|
|
269
320
|
}
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
//
|
|
285
|
-
//
|
|
321
|
+
// How far a container has been read, as one blob per moved key at its own id - see
|
|
322
|
+
// Ids.getSyncCursorBlobId. The cursor is the eTag, so there are no bytes. A cursor only rises: the
|
|
323
|
+
// store refuses a blob that is not greater, so a stale or repeated page, or another tab writing at
|
|
324
|
+
// the same time, cannot lower it.
|
|
325
|
+
export function cursorsToBlobs(cursors, movedKeys) {
|
|
326
|
+
const blobs = [];
|
|
327
|
+
for (const key of movedKeys)
|
|
328
|
+
blobs.push({
|
|
329
|
+
id: Ids.getSyncCursorBlobId(key),
|
|
330
|
+
eTag: cursors.getUpdateNumber(key),
|
|
331
|
+
bytes: NO_BYTES,
|
|
332
|
+
});
|
|
333
|
+
return blobs;
|
|
334
|
+
}
|
|
335
|
+
// The cursors a container holds, from the blobs at the cursor ids of keys (in key order, null where
|
|
336
|
+
// none). A container never read, or filled by a version that kept no cursors, gets zeros and asks for
|
|
337
|
+
// everything again. That costs bandwidth only, since the store keeps a blob only when its eTag is
|
|
338
|
+
// greater.
|
|
339
|
+
export function cursorsFromBlobs(keys, blobs) {
|
|
340
|
+
const cursors = new SyncState(keys);
|
|
341
|
+
for (const [index, key] of keys.entries()) {
|
|
342
|
+
const eTag = blobs[index]?.eTag ?? null;
|
|
343
|
+
if (eTag !== null)
|
|
344
|
+
cursors.raiseUpdateNumber(key, eTag);
|
|
345
|
+
}
|
|
346
|
+
return cursors;
|
|
347
|
+
}
|
|
348
|
+
// Raises the cursors to what a page of a business read brought, tombstones included, and answers the
|
|
349
|
+
// keys that moved. Only a read moves them. What this client pushed does not.
|
|
350
|
+
export function raiseBusinessCursors(cursors, response) {
|
|
351
|
+
return raiseCursorsFrom(cursors, getBusinessResponseEntities(response));
|
|
352
|
+
}
|
|
353
|
+
// The request that pushes what this customer changed. A storefront may push only messages, and one
|
|
354
|
+
// request is one batch, so an order and the cart it emptied go together and a customer is never left
|
|
355
|
+
// holding the order and a still full cart. messageUpdateNumber is how far reads of this container
|
|
356
|
+
// have got with messages - see SyncCache.readMessageUpdateNumber.
|
|
286
357
|
export function buildBusinessSyncInRequest(messageUpdateNumber, messages) {
|
|
287
358
|
return proto_BusinessSyncInRequest.create({
|
|
288
359
|
messageUpdateNumber: messageUpdateNumber,
|
|
289
360
|
messages: [...messages],
|
|
290
361
|
});
|
|
291
362
|
}
|
|
292
|
-
// The messages a sync in response carried
|
|
293
|
-
//
|
|
363
|
+
// The messages a sync in response carried: this push's own, with the update numbers the server
|
|
364
|
+
// assigned. Those numbers are not how far a read has got.
|
|
294
365
|
export function getSyncInResponseMessages(response) {
|
|
295
366
|
return response.messages;
|
|
296
367
|
}
|
|
297
|
-
// The
|
|
298
|
-
// to one and has no sync in.
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// belongs to one call and not to a read, so nextProfileSyncOutRequest leaves it off the pages that
|
|
303
|
-
// follow - see there.
|
|
368
|
+
// The request for everything changed in the caller's profile since these cursors. It names no
|
|
369
|
+
// business, since a profile sync is not scoped to one and has no sync in. joinBusinessId tells the
|
|
370
|
+
// server to make the caller a customer of that business, by entity id, while answering.
|
|
371
|
+
// Constants.ENTITY_ID_NULL asks for no join and is the default, since an ordinary read joins
|
|
372
|
+
// nothing. A join belongs to one call, so nextProfileSyncOutRequest leaves it off later pages.
|
|
304
373
|
export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(proto_Constants.ENTITY_ID_NULL)) {
|
|
305
374
|
return proto_ProfileSyncOutRequest.create({
|
|
306
375
|
userUpdateNumber: syncInState.getUpdateNumber(proto_Constants.USER_ENTITY_TYPE_ID),
|
|
@@ -309,18 +378,12 @@ export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(
|
|
|
309
378
|
joinBusinessId: joinBusinessId,
|
|
310
379
|
});
|
|
311
380
|
}
|
|
312
|
-
// The same
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
cursors.raiseUpdateNumber(entity.entityTypeId, entity.updateNumber);
|
|
319
|
-
return buildProfileSyncOutRequest(cursors, joinBusinessId);
|
|
320
|
-
}
|
|
321
|
-
// The user a sign up asks the server to create, as CreateUserRequest.user. Built from the domain
|
|
322
|
-
// object rather than carrying one of its own: this user has no entity yet - the server assigns id,
|
|
323
|
-
// createdAt and updatedAt on creation - so none of EntityHeader goes out here.
|
|
381
|
+
// The same for a profile read
|
|
382
|
+
export function raiseProfileCursors(cursors, response) {
|
|
383
|
+
return raiseCursorsFrom(cursors, getProfileResponseEntities(response));
|
|
384
|
+
}
|
|
385
|
+
// The user a sign up asks the server to create, as CreateUserRequest.user. It has no entity yet (the
|
|
386
|
+
// server assigns id, createdAt and updatedAt), so none of EntityHeader goes out.
|
|
324
387
|
export function buildUser(userProfile) {
|
|
325
388
|
return proto_User.create({
|
|
326
389
|
firstName: userProfile.firstName,
|
|
@@ -330,22 +393,21 @@ export function buildUser(userProfile) {
|
|
|
330
393
|
preferredLanguageCode: stringToProto(userProfile.preferredLanguageCode),
|
|
331
394
|
});
|
|
332
395
|
}
|
|
333
|
-
// The user Service.updateUser
|
|
334
|
-
//
|
|
335
|
-
//
|
|
396
|
+
// The user Service.updateUser overwrites this customer's profile with: the fields of buildUser plus
|
|
397
|
+
// the entity id, which the call matches the caller by. The rest of EntityHeader is not read on that
|
|
398
|
+
// call, so it is not sent.
|
|
336
399
|
export function buildUpdateUser(userProfile) {
|
|
337
400
|
return proto_User.create({
|
|
338
401
|
...buildUser(userProfile),
|
|
339
402
|
entity: proto_Entity.create({ id: userProfile.id }),
|
|
340
403
|
});
|
|
341
404
|
}
|
|
342
|
-
// The two
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// business. Its linkToken is the only copy of that credential a profile response carries.
|
|
405
|
+
// The two things a join must be answered with, checked separately so a caller can report which one a
|
|
406
|
+
// server left out - see Model.requireJoinedBusiness. They take the id, since both requests that
|
|
407
|
+
// carry a joinBusinessId need them.
|
|
408
|
+
// The membership itself: a live BusinessLink naming that business. A tombstoned link does not count
|
|
409
|
+
// (see OBJECT MODEL), nor does a link to another business. Its linkToken is the only copy of that
|
|
410
|
+
// credential a profile response carries.
|
|
349
411
|
export function hasJoinedBusinessLink(joinBusinessId, response) {
|
|
350
412
|
if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
|
|
351
413
|
return false;
|
|
@@ -355,20 +417,70 @@ export function hasJoinedBusinessLink(joinBusinessId, response) {
|
|
|
355
417
|
return true;
|
|
356
418
|
return false;
|
|
357
419
|
}
|
|
358
|
-
// The business
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
//
|
|
420
|
+
// The business the link names, matched on Business.entity.id, which BusinessLink.businessId holds
|
|
421
|
+
// (Business.businessId is the url friendly string). A caller needs the business as well as the link,
|
|
422
|
+
// because every later request and container name is built from that string and the link has only
|
|
423
|
+
// the id.
|
|
362
424
|
export function hasJoinedBusiness(joinBusinessId, response) {
|
|
363
425
|
if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
|
|
364
426
|
return false;
|
|
427
|
+
// A tombstone is not the business, which is what the join is answered with
|
|
365
428
|
for (const business of response.businesses)
|
|
366
|
-
if (getEntity(business.entity).id === joinBusinessId
|
|
429
|
+
if (getEntity(business.entity).id === joinBusinessId &&
|
|
430
|
+
!isDeletedEntity(getEntity(business.entity)))
|
|
367
431
|
return true;
|
|
368
432
|
return false;
|
|
369
433
|
}
|
|
370
|
-
// The
|
|
371
|
-
//
|
|
434
|
+
// The country of the business a response holds, ISO 3166-1 numeric, or undefined when it holds none.
|
|
435
|
+
// A tombstone has no country, and a bare header carries zero, which must not be taken for one: a
|
|
436
|
+
// cart's taxes are raw codes that mean something only within a country.
|
|
437
|
+
export function getBusinessResponseCountryCode(response) {
|
|
438
|
+
const business = response?.business;
|
|
439
|
+
if (business === undefined || isDeletedEntity(getEntity(business.entity)))
|
|
440
|
+
return undefined;
|
|
441
|
+
return business.countryCode;
|
|
442
|
+
}
|
|
443
|
+
// The entity id of the business a response holds, or null when it holds none or only a tombstone.
|
|
444
|
+
// Once a business has been read, later requests name it by this id: it is stable, and the url
|
|
445
|
+
// friendly id is only how it was first found.
|
|
446
|
+
export function getBusinessResponseBusinessEntityId(response) {
|
|
447
|
+
const entity = response?.business?.entity;
|
|
448
|
+
if (entity === undefined || isDeletedEntity(entity))
|
|
449
|
+
return null;
|
|
450
|
+
return entity.id;
|
|
451
|
+
}
|
|
452
|
+
// The url friendly id of the business a response holds (the one a business is found by, not its
|
|
453
|
+
// entity id), or null when it holds none or only a tombstone.
|
|
454
|
+
export function getBusinessResponseBusinessId(response) {
|
|
455
|
+
const business = response?.business;
|
|
456
|
+
if (business === undefined || isDeletedEntity(getEntity(business.entity)))
|
|
457
|
+
return null;
|
|
458
|
+
return business.businessId;
|
|
459
|
+
}
|
|
460
|
+
// Whether the general settings a response holds turn online shopping on. Missing settings (never
|
|
461
|
+
// made, deleted, or not read yet) count as off. The server sends a business's data whether or not
|
|
462
|
+
// shopping is on, so the client decides from what it holds once it has read everything.
|
|
463
|
+
export function isBusinessResponseOnlineShoppingOn(response) {
|
|
464
|
+
if (response === null)
|
|
465
|
+
return false;
|
|
466
|
+
return getGeneralSettingsOf(response)?.onlineShoppingEnabled ?? false;
|
|
467
|
+
}
|
|
468
|
+
// The general settings a response holds, as on the wire, or undefined when none: the one public
|
|
469
|
+
// configuration that carries them, unless it is deleted. The business built from them and whether
|
|
470
|
+
// shopping is on both read from here, so they cannot be about different settings.
|
|
471
|
+
function getGeneralSettingsOf(response) {
|
|
472
|
+
let settings;
|
|
473
|
+
for (const configuration of response.configurations) {
|
|
474
|
+
if (isDeletedEntity(getEntity(configuration.entity)))
|
|
475
|
+
continue;
|
|
476
|
+
if (configuration.data.oneofKind !== "generalSettings")
|
|
477
|
+
continue; // Not a configuration this library understands
|
|
478
|
+
settings = configuration.data.generalSettings;
|
|
479
|
+
}
|
|
480
|
+
return settings;
|
|
481
|
+
}
|
|
482
|
+
// The credentials of one call, as on the wire. An absent token is left off the message, since the
|
|
483
|
+
// protocol tells absent from empty.
|
|
372
484
|
export function buildAuthTokens(authTokens) {
|
|
373
485
|
const result = proto_AuthTokens.create({
|
|
374
486
|
authenticationMethod: authTokens.authenticationMethod,
|
|
@@ -382,21 +494,19 @@ export function buildAuthTokens(authTokens) {
|
|
|
382
494
|
result.linkToken = linkToken;
|
|
383
495
|
return result;
|
|
384
496
|
}
|
|
385
|
-
// What a server issued while answering a call,
|
|
497
|
+
// What a server issued while answering a call, as this library's AuthTokens
|
|
386
498
|
export function authTokensFromProto(authTokens) {
|
|
387
499
|
return new AuthTokens(authTokens.authenticationMethod, authTokens.authenticationToken, stringFromProto(authTokens.userToken), stringFromProto(authTokens.linkToken));
|
|
388
500
|
}
|
|
389
|
-
// The authorization header of a request
|
|
390
|
-
//
|
|
501
|
+
// The encoded authorization header of a request. It lives here because this file writes the proto
|
|
502
|
+
// fields. A transport puts the bytes in the header.
|
|
391
503
|
export function buildAuthTokensBytes(authTokens) {
|
|
392
504
|
return proto_AuthTokens.toBinary(authTokens);
|
|
393
505
|
}
|
|
394
|
-
// How a container keeps the credentials it earned: the two tokens this server issued
|
|
395
|
-
// null
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
// provider, so a stored copy could only be staler than the one held in memory and would outlive
|
|
399
|
-
// the sign out that should have ended it.
|
|
506
|
+
// How a container keeps the credentials it earned: the two tokens this server issued. The eTag is
|
|
507
|
+
// null because a reissued token is not a later version of the old one, and greater-wins would refuse
|
|
508
|
+
// it. The authentication token is never stored: it belongs to the identity provider, so a copy would
|
|
509
|
+
// be staler than the one in memory and would outlive the sign out.
|
|
400
510
|
export function authTokensToBlob(authTokens) {
|
|
401
511
|
const stored = proto_AuthTokens.create({
|
|
402
512
|
authenticationMethod: authTokens.authenticationMethod,
|
|
@@ -418,8 +528,7 @@ export function authTokensFromBytes(bytes) {
|
|
|
418
528
|
}
|
|
419
529
|
// #endregion
|
|
420
530
|
// #region Paging
|
|
421
|
-
// What the server issued while answering
|
|
422
|
-
// field - the two sync outs and a sync in alike.
|
|
531
|
+
// What the server issued while answering. All three response types carry it in the same field.
|
|
423
532
|
export function getResponseAuthTokens(response) {
|
|
424
533
|
return response.authTokens;
|
|
425
534
|
}
|
|
@@ -433,13 +542,13 @@ export function refreshAuthTokens(authTokens, issued) {
|
|
|
433
542
|
result.linkToken = issued.linkToken;
|
|
434
543
|
return result;
|
|
435
544
|
}
|
|
436
|
-
// The request
|
|
437
|
-
//
|
|
438
|
-
//
|
|
439
|
-
//
|
|
545
|
+
// The request for the page after this response, or null when it was the last. A full page was cut
|
|
546
|
+
// short, so the cursors are raised from what arrived and the call repeats. A full page that raised no
|
|
547
|
+
// cursor means the server broke the ascending order it promises. The loop is unbounded, so it stops
|
|
548
|
+
// there.
|
|
440
549
|
export function nextBusinessSyncOutRequest(request, response) {
|
|
441
|
-
//
|
|
442
|
-
//
|
|
550
|
+
// A short page means the server sent everything. Counting first keeps the ordinary case to nine
|
|
551
|
+
// length reads.
|
|
443
552
|
if (businessResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
|
|
444
553
|
return null;
|
|
445
554
|
const cursors = businessSyncOutCursors(request);
|
|
@@ -470,10 +579,10 @@ export function nextBusinessSyncOutRequest(request, response) {
|
|
|
470
579
|
return null;
|
|
471
580
|
// A page that renewed the url means a later one is now held; otherwise what was sent still stands
|
|
472
581
|
const renewedExpiry = getBusinessResponseAttachmentBaseUrlExpiry(response);
|
|
473
|
-
return
|
|
582
|
+
return createBusinessSyncOutRequest(request.business, cursors, renewedExpiry === 0n ? request.attachmentBaseUrlExpiry : renewedExpiry);
|
|
474
583
|
}
|
|
475
|
-
// The same for a profile read.
|
|
476
|
-
//
|
|
584
|
+
// The same for a profile read. A joinBusinessId on the first request is left off the later ones,
|
|
585
|
+
// since the call that asked for the join does it once.
|
|
477
586
|
export function nextProfileSyncOutRequest(request, response) {
|
|
478
587
|
if (profileResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
|
|
479
588
|
return null;
|
|
@@ -487,15 +596,13 @@ export function nextProfileSyncOutRequest(request, response) {
|
|
|
487
596
|
for (const business of response.businesses)
|
|
488
597
|
if (raiseCursor(cursors, business.entity))
|
|
489
598
|
haveCursorsRisen = true;
|
|
490
|
-
// A full page that raised
|
|
491
|
-
// promises; the loop is unbounded, so it stops rather than trust it
|
|
599
|
+
// A full page that raised no cursor ends the loop, as in nextBusinessSyncOutRequest
|
|
492
600
|
if (!haveCursorsRisen)
|
|
493
601
|
return null;
|
|
494
602
|
return buildProfileSyncOutRequest(cursors);
|
|
495
603
|
}
|
|
496
|
-
// How many entities a response carried, without
|
|
497
|
-
//
|
|
498
|
-
// entity for the cache on the way past.
|
|
604
|
+
// How many entities a response carried, without encoding any. A paging loop asks this of every page,
|
|
605
|
+
// and getBusinessResponseEntities would encode each entity to answer it.
|
|
499
606
|
function businessResponseEntityCount(response) {
|
|
500
607
|
return ((response.business === undefined ? 0 : 1) +
|
|
501
608
|
response.attachments.length +
|
|
@@ -512,15 +619,24 @@ function profileResponseEntityCount(response) {
|
|
|
512
619
|
response.businessLinks.length +
|
|
513
620
|
response.businesses.length);
|
|
514
621
|
}
|
|
515
|
-
// Raises the
|
|
516
|
-
//
|
|
622
|
+
// Raises the cursors the entities of one page advance and answers the keys that moved, one per table
|
|
623
|
+
// however many entities raised it. A tombstone counts like any entity, as in raiseCursor.
|
|
624
|
+
function raiseCursorsFrom(cursors, entities) {
|
|
625
|
+
const movedKeys = new Set();
|
|
626
|
+
for (const entity of entities)
|
|
627
|
+
if (cursors.raiseUpdateNumber(entity.entityTypeId, entity.updateNumber))
|
|
628
|
+
movedKeys.add(SyncKeys.keyOf(entity.entityTypeId));
|
|
629
|
+
return movedKeys;
|
|
630
|
+
}
|
|
631
|
+
// Raises the cursor one entity advances and answers whether it moved. A tombstone counts, or the
|
|
632
|
+
// server would send it again for ever.
|
|
517
633
|
function raiseCursor(cursors, entity) {
|
|
518
634
|
if (entity === undefined)
|
|
519
635
|
return false;
|
|
520
636
|
return cursors.raiseUpdateNumber(Ids.getEntityTypeId(entity.id), entity.updateNumber);
|
|
521
637
|
}
|
|
522
|
-
// The cursors a request
|
|
523
|
-
//
|
|
638
|
+
// The cursors a request carries, as a state that can be raised: the inverse of the request builders.
|
|
639
|
+
// A paging loop uses it to carry them into the next call.
|
|
524
640
|
function businessSyncOutCursors(request) {
|
|
525
641
|
const cursors = new SyncState(SyncKeys.BUSINESS_SYNC_IN);
|
|
526
642
|
cursors.raiseUpdateNumber(proto_Constants.BUSINESS_ENTITY_TYPE_ID, request.businessUpdateNumber);
|
|
@@ -539,18 +655,20 @@ function profileSyncOutCursors(request) {
|
|
|
539
655
|
}
|
|
540
656
|
// #endregion
|
|
541
657
|
// #region A response as an object graph
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
//
|
|
545
|
-
// has yet to arrive; null when the response carries no Business at all.
|
|
658
|
+
// One business as an object graph, regrouping the flat wire entities by owner id so each class is
|
|
659
|
+
// built with its children. An entity whose parent is missing is dropped, since a paged read can hold
|
|
660
|
+
// a child before its parent arrives. Null when the response has no live Business.
|
|
546
661
|
export function buildBusiness(response) {
|
|
547
662
|
const businessProto = response.business;
|
|
548
663
|
if (businessProto === undefined || isDeletedEntity(getEntity(businessProto.entity)))
|
|
549
664
|
return null;
|
|
550
665
|
const business = businessFromProto(businessProto);
|
|
551
666
|
const updatableBusiness = business;
|
|
552
|
-
//
|
|
553
|
-
//
|
|
667
|
+
// A tax travels as a raw code that means something only within the business's country, so every tax
|
|
668
|
+
// below is read against this one
|
|
669
|
+
const countryCode = businessProto.countryCode;
|
|
670
|
+
// Resolved once and handed to every Photo built below, since a Photo takes the base url when it is
|
|
671
|
+
// built - see Photo's constructor.
|
|
554
672
|
const attachmentBaseUrlProto = response.attachmentBaseUrl;
|
|
555
673
|
const attachmentBaseUrl = attachmentBaseUrlProto === undefined ? null : attachmentBaseUrlFromProto(attachmentBaseUrlProto);
|
|
556
674
|
updatableBusiness.attachmentBaseUrl = attachmentBaseUrl;
|
|
@@ -567,19 +685,15 @@ export function buildBusiness(response) {
|
|
|
567
685
|
owned.push(attachment);
|
|
568
686
|
}
|
|
569
687
|
// The settings, unwrapped from the one public configuration that carries them
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
if (configuration.data.oneofKind !== "generalSettings")
|
|
574
|
-
continue; // Not a configuration this library understands
|
|
575
|
-
updatableBusiness.settings = generalSettingsFromProto(configuration.data.generalSettings);
|
|
576
|
-
}
|
|
688
|
+
const generalSettings = getGeneralSettingsOf(response);
|
|
689
|
+
if (generalSettings !== undefined)
|
|
690
|
+
updatableBusiness.settings = generalSettingsFromProto(generalSettings, countryCode);
|
|
577
691
|
// The photos of the store itself
|
|
578
692
|
updatableBusiness.photos = takePhotos(attachmentsByOwner, business.id, attachmentBaseUrl);
|
|
579
|
-
// Prices, by the package each applies to. Only the MeraBills Online list is this storefront's
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
693
|
+
// Prices, by the package each applies to. Only the MeraBills Online list is this storefront's. A
|
|
694
|
+
// business prices a package in several lists, and a price from another list is not what it sells at
|
|
695
|
+
// here, so it is skipped. A package has one price in that list; if a second arrives, the first is
|
|
696
|
+
// kept.
|
|
583
697
|
const pricesByPackage = new Map();
|
|
584
698
|
for (const priceProto of response.prices) {
|
|
585
699
|
if (isDeletedItem(priceProto.item))
|
|
@@ -590,10 +704,9 @@ export function buildBusiness(response) {
|
|
|
590
704
|
if (!pricesByPackage.has(price.packageId))
|
|
591
705
|
pricesByPackage.set(price.packageId, price);
|
|
592
706
|
}
|
|
593
|
-
// Packages, by the product each belongs to, built with their photos and
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
// itself, guarantee every package of an otherwise visible product is itself enabled.
|
|
707
|
+
// Packages, by the product each belongs to, built with their photos and price. A package that is not
|
|
708
|
+
// buyable (disabled, unpriced, or priced with a disabled price) is left out. A product-level filter
|
|
709
|
+
// alone would not guarantee that every package of a visible product is enabled.
|
|
597
710
|
const packagesByProduct = new Map();
|
|
598
711
|
for (const packageProto of response.packages) {
|
|
599
712
|
if (isDeletedItem(packageProto.item))
|
|
@@ -612,9 +725,9 @@ export function buildBusiness(response) {
|
|
|
612
725
|
else
|
|
613
726
|
siblings.push(pkg);
|
|
614
727
|
}
|
|
615
|
-
// Products, built with their photos and
|
|
616
|
-
//
|
|
617
|
-
// left with no buyable package by the filtering above
|
|
728
|
+
// Products, built with their photos and buyable packages, and parented to the business (their
|
|
729
|
+
// category only groups them). A product that is not buyable is left out: disabled, sold only offline
|
|
730
|
+
// (the server sends those, flagged), or left with no buyable package by the filtering above.
|
|
618
731
|
const products = [];
|
|
619
732
|
for (const productProto of response.products) {
|
|
620
733
|
if (isDeletedItem(productProto.item))
|
|
@@ -622,11 +735,11 @@ export function buildBusiness(response) {
|
|
|
622
735
|
const productId = getItemId(productProto.item);
|
|
623
736
|
const packages = packagesByProduct.get(productId) ?? [];
|
|
624
737
|
const productPhotos = takePhotos(attachmentsByOwner, productId, attachmentBaseUrl);
|
|
625
|
-
const product = productFromProto(productProto, productPhotos, Object.freeze([...packages]));
|
|
738
|
+
const product = productFromProto(productProto, productPhotos, Object.freeze([...packages]), countryCode);
|
|
626
739
|
if (!product.isBuyable)
|
|
627
740
|
continue;
|
|
628
741
|
UpdatableEntity.setParent(product, business);
|
|
629
|
-
for (const pkg of packages)
|
|
742
|
+
for (const pkg of product.packages)
|
|
630
743
|
UpdatableEntity.setParent(pkg, product);
|
|
631
744
|
products.push(product);
|
|
632
745
|
}
|
|
@@ -638,9 +751,9 @@ export function buildBusiness(response) {
|
|
|
638
751
|
UpdatableEntity.setParent(productCategory, business);
|
|
639
752
|
productCategories.push(productCategory);
|
|
640
753
|
}
|
|
641
|
-
// The one list a storefront sells from is MeraBills Online,
|
|
642
|
-
//
|
|
643
|
-
//
|
|
754
|
+
// The one list a storefront sells from is MeraBills Online, picked by id since the same sync can
|
|
755
|
+
// carry others. The catalog must answer with the list the prices above came from, or a cart would
|
|
756
|
+
// name one they are not in.
|
|
644
757
|
let priceList = null;
|
|
645
758
|
for (const priceListProto of response.priceLists) {
|
|
646
759
|
if (isDeletedItem(priceListProto.item))
|
|
@@ -652,38 +765,31 @@ export function buildBusiness(response) {
|
|
|
652
765
|
break;
|
|
653
766
|
}
|
|
654
767
|
updatableBusiness.catalog = new Catalog(productCategories, products, priceList);
|
|
655
|
-
// The orders of this customer, and their party preferences
|
|
656
|
-
//
|
|
768
|
+
// The orders of this customer, and their party preferences (the cart and the fulfillment mode to
|
|
769
|
+
// default the next order to). A message of any other payload type is dropped. The business completes
|
|
770
|
+
// an order by deleting its message, and the server sends that message whole, so the orders are the
|
|
771
|
+
// open and the completed ones, told apart by Order.isDeleted. A deleted message with no order (from
|
|
772
|
+
// an older server that sent only the header) is not an order.
|
|
657
773
|
const orders = [];
|
|
774
|
+
for (const message of response.messages) {
|
|
775
|
+
// Party preferences are one message out of possibly several, so they are settled below
|
|
776
|
+
if (message.payload.oneofKind === "salesOrder")
|
|
777
|
+
orders.push(orderFromProto(message, message.payload.salesOrder, countryCode));
|
|
778
|
+
}
|
|
779
|
+
// A customer has one party preferences message, so two means two callers' messages share a
|
|
780
|
+
// container. Take the one partyPreferencesMessageFromMessages picks, which is also what an amended or
|
|
781
|
+
// resent cart is read from. A rule of its own here would show one cart and amend another. The newer
|
|
782
|
+
// one wins even when it is the emptied one, and the older one's list is not shown.
|
|
783
|
+
const partyPreferences = partyPreferencesMessageFromMessages(response.messages);
|
|
658
784
|
let cart = null;
|
|
659
|
-
let cartUpdateNumber = 0n;
|
|
660
785
|
let defaultFulfillmentMode = proto_FulfillmentMode.DELIVERY;
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
case "partyPreferences": {
|
|
670
|
-
// One customer has one party preferences message, so two means two callers' messages are
|
|
671
|
-
// in one container - see partyPreferencesMessageFromBlobs, which breaks the tie the same way
|
|
672
|
-
const updateNumber = getEntity(message.entity).updateNumber;
|
|
673
|
-
if (cart !== null && updateNumber <= cartUpdateNumber)
|
|
674
|
-
break;
|
|
675
|
-
// An emptied cart is a message whose payload carries no shopping list, which is what
|
|
676
|
-
// placing an order leaves behind - so it answers null and not an empty list, the same as
|
|
677
|
-
// never having had a cart at all
|
|
678
|
-
cart = cartShoppingListFromProto(message.payload.partyPreferences);
|
|
679
|
-
cartUpdateNumber = updateNumber;
|
|
680
|
-
// Whatever this same message carries, DELIVERY only ever coming from the fallback above -
|
|
681
|
-
// not from a message that happens to hold UNSPECIFIED_FULFILLMENT_MODE itself
|
|
682
|
-
defaultFulfillmentMode = message.payload.partyPreferences.fulfillmentMode;
|
|
683
|
-
break;
|
|
684
|
-
}
|
|
685
|
-
// No default: MONEY_TRANSFER has no payload in this protocol, and OTHER is not ours
|
|
686
|
-
}
|
|
786
|
+
if (partyPreferences !== null && partyPreferences.payload.oneofKind === "partyPreferences") {
|
|
787
|
+
// An emptied cart is a message whose payload has no shopping list, which is what placing an order
|
|
788
|
+
// leaves. It answers null, the same as never having had a cart.
|
|
789
|
+
cart = cartShoppingListFromProto(partyPreferences.payload.partyPreferences, countryCode);
|
|
790
|
+
// Taken as the message has it. DELIVERY comes only from the fallback above, even when the message
|
|
791
|
+
// holds UNSPECIFIED_FULFILLMENT_MODE.
|
|
792
|
+
defaultFulfillmentMode = partyPreferences.payload.partyPreferences.fulfillmentMode;
|
|
687
793
|
}
|
|
688
794
|
for (const order of orders)
|
|
689
795
|
UpdatableEntity.setParent(order, business);
|
|
@@ -715,55 +821,47 @@ export function buildProfile(profileId, response) {
|
|
|
715
821
|
return null;
|
|
716
822
|
return new Profile(profileId, user, businessLinks, businesses);
|
|
717
823
|
}
|
|
718
|
-
// One order on its own, for a caller holding a message rather than a whole response
|
|
719
|
-
|
|
824
|
+
// One order on its own, for a caller holding a message rather than a whole response. The country of
|
|
825
|
+
// the business it was placed with is the caller's to give, since a tax travels as a raw code.
|
|
826
|
+
export function buildOrder(message, countryCode) {
|
|
720
827
|
if (message.payload.oneofKind !== "salesOrder")
|
|
721
828
|
return null;
|
|
722
|
-
return orderFromProto(message, message.payload.salesOrder);
|
|
829
|
+
return orderFromProto(message, message.payload.salesOrder, countryCode);
|
|
723
830
|
}
|
|
724
|
-
// The shopping list a cart message carries, empty
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
export function buildShoppingList(message) {
|
|
831
|
+
// The shopping list a cart message carries, empty when the cart was emptied. Business.cart answers
|
|
832
|
+
// null for that (see buildBusiness), but the callers here, createOrder and the visitor cart carried
|
|
833
|
+
// over on sign in, need an actual list. countryCode is the cart's business, as for buildOrder.
|
|
834
|
+
export function buildShoppingList(message, countryCode) {
|
|
729
835
|
if (message.payload.oneofKind !== "partyPreferences")
|
|
730
836
|
throw new InvalidArgumentException("That message carries no party preferences, so it is not a cart");
|
|
731
|
-
return cartShoppingListFromProto(message.payload.partyPreferences) ?? ShoppingList.EMPTY;
|
|
837
|
+
return (cartShoppingListFromProto(message.payload.partyPreferences, countryCode) ?? ShoppingList.EMPTY);
|
|
732
838
|
}
|
|
733
839
|
// #endregion
|
|
734
840
|
// #region What a customer pushes
|
|
735
841
|
//
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
//
|
|
746
|
-
//
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
// is none to amend.
|
|
750
|
-
// A list holding nothing is pushed as no list at all, the way Java serializeShoppingList answers
|
|
751
|
-
// null below one item - so emptying a cart and never having had one are one state on the wire, and
|
|
752
|
-
// placing an order is what puts a cart into it.
|
|
753
|
-
// fulfillmentMode is null to leave whatever is already there alone - the ordinary case, since a
|
|
754
|
-
// storefront otherwise treats it as the merchant app's own data - and a value only where createOrder
|
|
755
|
-
// means to set it, to the mode of the order that just emptied this cart.
|
|
842
|
+
// A storefront writes one entity type, a Message carrying the cart or an order placed from it. Two
|
|
843
|
+
// rules hold for both, so a pushed message is storable here as well as on the server. The id is the
|
|
844
|
+
// client's: a sync in answers with an update number and no id, so a message the server named could
|
|
845
|
+
// not be found here again. updatedAt is the client's too and the server keeps it, so the copy stored
|
|
846
|
+
// here has the same eTag as the one a later read brings back.
|
|
847
|
+
// This customer's party preferences as a message to push, carrying the cart and optionally the
|
|
848
|
+
// fulfillment mode. An existing message is amended, since its payload can carry fields a storefront
|
|
849
|
+
// cannot read, and dropping them would overwrite that data. Its id is kept, as the server holds the
|
|
850
|
+
// preferences under it. The derived id is used only when there is nothing to amend.
|
|
851
|
+
// A list with no items is pushed as no list, as Java serializeShoppingList answers null below one
|
|
852
|
+
// item. Emptying a cart and never having had one are one state on the wire.
|
|
853
|
+
// fulfillmentMode null leaves the existing value alone, the ordinary case: a storefront treats it as
|
|
854
|
+
// the merchant app's data. createOrder sets it to the mode of the order that just emptied the cart.
|
|
756
855
|
export function buildPartyPreferencesMessage(messageId, existing, senderId, shoppingList, fulfillmentMode, now) {
|
|
757
856
|
const existingEntity = existing === null ? null : getEntity(existing.entity);
|
|
758
857
|
const preferences = existing !== null && existing.payload.oneofKind === "partyPreferences"
|
|
759
858
|
? existing.payload.partyPreferences
|
|
760
859
|
: proto_PartyPreferencesPayload.create();
|
|
761
|
-
// Later than the copy it replaces, whatever this client's clock reads.
|
|
762
|
-
//
|
|
763
|
-
//
|
|
764
|
-
//
|
|
765
|
-
//
|
|
766
|
-
// being replaced is enough, and the server keeps whatever is sent, so both sides still agree.
|
|
860
|
+
// Later than the copy it replaces, whatever this client's clock reads. An amendment written without
|
|
861
|
+
// the server is stored at the update number of the copy it amends, so updatedAt decides. A client
|
|
862
|
+
// clock behind that copy (Constants.MAX_CLOCK_SKEW says it happens) would have its own store refuse
|
|
863
|
+
// the edit and leave the customer looking at a stale cart. One millisecond past the copy is enough,
|
|
864
|
+
// and the server keeps what is sent, so both sides agree.
|
|
767
865
|
const updatedAt = existingEntity === null || existingEntity.updatedAt < now ? now : existingEntity.updatedAt + 1n;
|
|
768
866
|
const partyPreferences = proto_PartyPreferencesPayload.create({
|
|
769
867
|
...preferences,
|
|
@@ -779,13 +877,12 @@ export function buildPartyPreferencesMessage(messageId, existing, senderId, shop
|
|
|
779
877
|
payload: { oneofKind: "partyPreferences", partyPreferences: partyPreferences },
|
|
780
878
|
});
|
|
781
879
|
}
|
|
782
|
-
// One order as a message to push. Its shopping list goes out final whatever
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
//
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
// rather than off the counts the list carries, which a caller could have set to anything.
|
|
880
|
+
// One order as a message to push. Its shopping list goes out final whatever the list says, as Java
|
|
881
|
+
// SalesOrderViewModel.saveChanges does before creating an order: the customer has finished choosing,
|
|
882
|
+
// and only a final list tracks a later change as a change instead of silently altering what was
|
|
883
|
+
// ordered. The two totals are all that remains of an order completed by an older merchant app, which
|
|
884
|
+
// moved the list out of the order. They are filled from the list being sent, not from the counts it
|
|
885
|
+
// carries, which a caller could set to anything.
|
|
789
886
|
export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMode, deliveryAddressId, deliveryAddress, note, creationDate) {
|
|
790
887
|
return proto_Message.create({
|
|
791
888
|
entity: pushedEntity(orderId, creationDate, creationDate),
|
|
@@ -808,59 +905,57 @@ export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMo
|
|
|
808
905
|
},
|
|
809
906
|
});
|
|
810
907
|
}
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
//
|
|
814
|
-
// the response already carries them as full entities, not as a bare number to reapply by hand.
|
|
908
|
+
// The messages of a sync in response as a cache write, tombstones included like any other entity
|
|
909
|
+
// array. This stores a push's own messages under the update numbers the server just assigned, with no
|
|
910
|
+
// separate stamping step, since the response carries full entities.
|
|
815
911
|
export function messagesToCacheWrite(messages) {
|
|
816
912
|
return toCacheWrite(messages.map((message) => toWireEntity(message.entity, proto_Message.toBinary(message))));
|
|
817
913
|
}
|
|
818
|
-
// The
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
const updateNumber = getEntity(message.entity).updateNumber;
|
|
826
|
-
if (updateNumber > highest)
|
|
827
|
-
highest = updateNumber;
|
|
828
|
-
}
|
|
829
|
-
return highest;
|
|
830
|
-
}
|
|
831
|
-
// The Entity of something this client is pushing. The update number goes out as zero: the protocol
|
|
832
|
-
// replaces a caller's own with the server's and answers what it assigned, so there is nothing to
|
|
833
|
-
// send and nothing to guess. updateInfo goes out as zero too, which is not the same kind of zero -
|
|
834
|
-
// it should be the id of this store, and this store has no id yet, so a read of this business will
|
|
835
|
-
// send these messages back rather than recognising them as its own. That costs bandwidth and
|
|
836
|
-
// nothing else, since the eTag of the copy stored here is the one the server kept.
|
|
914
|
+
// The Entity of something this client is pushing. The update number goes out as zero: the server
|
|
915
|
+
// replaces a caller's own with the one it assigns, so there is nothing to send. A message not pushed
|
|
916
|
+
// yet is stored holding zero, and messageToBlob versions it from the copy it amends. updateInfo is
|
|
917
|
+
// zero too, as it would be the id of the store that made the change and the server knows no store of
|
|
918
|
+
// this client. It needs none: a read sends these messages back (the server leaves no one's changes
|
|
919
|
+
// out of a storefront's read), and the eTag stored here is the one the server kept, so the read
|
|
920
|
+
// changes nothing.
|
|
837
921
|
function pushedEntity(id, createdAt, updatedAt) {
|
|
838
922
|
return proto_Entity.create({ id: id, createdAt: createdAt, updatedAt: updatedAt });
|
|
839
923
|
}
|
|
840
|
-
// Whether shoppingList differs from what stored
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
924
|
+
// Whether shoppingList differs from what stored holds, so updateCart can skip a push. Compares raw
|
|
925
|
+
// lists, since a message's updatedAt differs on every call. That is sound because the server echoes
|
|
926
|
+
// a pushed list back with the items it was sent, so stored is what was last sent.
|
|
927
|
+
// The server echoes the items in package id order, and the list's own order is each item's
|
|
928
|
+
// shoppingListOrder, compared with the item. So both lists are put in package id order first, or a
|
|
929
|
+
// cart not picked in that order would differ from its echo and be pushed again, re-stamping
|
|
930
|
+
// updatedAt for every other device.
|
|
931
|
+
// True also when stored was never pushed (updateNumber 0, left by updateCartLocalOnly), so an
|
|
932
|
+
// unsent local edit still goes out.
|
|
848
933
|
export function hasShoppingListChanged(stored, shoppingList) {
|
|
849
934
|
if (stored === null)
|
|
850
935
|
return pushedShoppingList(shoppingList, false) !== undefined;
|
|
851
936
|
if (getEntity(stored.entity).updateNumber === 0n)
|
|
852
937
|
return true;
|
|
853
|
-
return !proto_ShoppingList.equals(getCartShoppingList(stored), pushedShoppingList(shoppingList, false));
|
|
938
|
+
return !proto_ShoppingList.equals(inPackageIdOrder(getCartShoppingList(stored)), inPackageIdOrder(pushedShoppingList(shoppingList, false)));
|
|
939
|
+
}
|
|
940
|
+
// The same list with its items by package id, the order in which the server sends them
|
|
941
|
+
function inPackageIdOrder(list) {
|
|
942
|
+
if (list === undefined)
|
|
943
|
+
return undefined;
|
|
944
|
+
const byPackageId = (first, second) => first.packageId < second.packageId ? -1 : first.packageId > second.packageId ? 1 : 0;
|
|
945
|
+
return proto_ShoppingList.create({
|
|
946
|
+
...list,
|
|
947
|
+
existingItems: [...list.existingItems].sort(byPackageId),
|
|
948
|
+
deletedItems: [...list.deletedItems].sort(byPackageId),
|
|
949
|
+
});
|
|
854
950
|
}
|
|
855
|
-
// The list inside a cart message, or absent for
|
|
951
|
+
// The list inside a cart message, or absent for an emptied cart
|
|
856
952
|
function getCartShoppingList(message) {
|
|
857
953
|
return message.payload.oneofKind === "partyPreferences"
|
|
858
954
|
? message.payload.partyPreferences.shoppingList
|
|
859
955
|
: undefined;
|
|
860
956
|
}
|
|
861
|
-
// A shopping list on its way out, or absent when it holds nothing - see
|
|
862
|
-
//
|
|
863
|
-
// the list carries, for the reason the counts below are.
|
|
957
|
+
// A shopping list on its way out, or absent when it holds nothing - see buildPartyPreferencesMessage.
|
|
958
|
+
// Emptiness is judged by the items sent, not the counts, for the reason given at shoppingListToProto.
|
|
864
959
|
function pushedShoppingList(shoppingList, isFinal) {
|
|
865
960
|
if (shoppingList === null)
|
|
866
961
|
return undefined;
|
|
@@ -868,10 +963,9 @@ function pushedShoppingList(shoppingList, isFinal) {
|
|
|
868
963
|
return undefined;
|
|
869
964
|
return shoppingListToProto(shoppingList, isFinal);
|
|
870
965
|
}
|
|
871
|
-
// The counts
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
// makes it impossible to push a list whose count disagrees with its contents.
|
|
966
|
+
// The counts come from the arrays, not the list. A read takes them from the wire because the
|
|
967
|
+
// protocol sends a count even where it omits the items, but a push always sends the items. Taking the
|
|
968
|
+
// counts from the arrays means a pushed list cannot disagree with its contents.
|
|
875
969
|
function shoppingListToProto(shoppingList, isFinal) {
|
|
876
970
|
return proto_ShoppingList.create({
|
|
877
971
|
isFinal: isFinal,
|
|
@@ -887,9 +981,8 @@ function shoppingListToProto(shoppingList, isFinal) {
|
|
|
887
981
|
deletedItems: shoppingList.deletedItems.map(shoppingListItemToProto),
|
|
888
982
|
});
|
|
889
983
|
}
|
|
890
|
-
// The mirror of shoppingListItemFromProto
|
|
891
|
-
//
|
|
892
|
-
// by the server both.
|
|
984
|
+
// The mirror of shoppingListItemFromProto. Unscaled model values go back scaled, and nothing derived
|
|
985
|
+
// is sent: amount, totalTax and finalPrice are computed from these fields on both sides.
|
|
893
986
|
function shoppingListItemToProto(item) {
|
|
894
987
|
return proto_ShoppingListItem.create({
|
|
895
988
|
packageId: item.packageId,
|
|
@@ -907,42 +1000,69 @@ function shoppingListItemToProto(item) {
|
|
|
907
1000
|
});
|
|
908
1001
|
}
|
|
909
1002
|
function taxToProto(tax) {
|
|
910
|
-
return proto_Tax.create({
|
|
1003
|
+
return proto_Tax.create({ taxCode: tax.taxCode, rate: taxRateToScaledValue(tax.rate) });
|
|
911
1004
|
}
|
|
912
1005
|
// #endregion
|
|
913
1006
|
// #region Entity and item bookkeeping
|
|
914
|
-
// One pass over
|
|
915
|
-
//
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
1007
|
+
// One pass over a response's entities, each becoming a blob at its own id, so one read of a container
|
|
1008
|
+
// is enough to sort it by entity type. Every blob is versioned by entityETag, tombstones too. A
|
|
1009
|
+
// delete is a write of its own, so a tombstone has a greater update number than any copy of what it
|
|
1010
|
+
// deleted and replaces it. An older copy sent again is lesser and refused, and a later write of the
|
|
1011
|
+
// same id is greater and kept. The arrays are mutable so that businessResponseToCacheWrite can add
|
|
1012
|
+
// the attachment base url.
|
|
919
1013
|
function toCacheWrite(entities) {
|
|
920
1014
|
const blobs = [];
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1015
|
+
for (const entity of entities)
|
|
1016
|
+
blobs.push({
|
|
1017
|
+
id: entity.id,
|
|
1018
|
+
eTag: entityETag(entity.updateNumber, entity.updatedAt),
|
|
1019
|
+
bytes: entity.bytes,
|
|
1020
|
+
});
|
|
1021
|
+
return { blobs: blobs, deletedIds: [] };
|
|
1022
|
+
}
|
|
1023
|
+
// What a store versions an entity's blob by: the update number, with updatedAt below it, in one
|
|
1024
|
+
// bigint so the greater wins as the store says. The server assigns an update number to every write,
|
|
1025
|
+
// deletes included, so it rises with every change whatever any clock reads, and decides between
|
|
1026
|
+
// different versions. updatedAt decides between copies of one update number. Only a copy this client
|
|
1027
|
+
// wrote itself, at the update number of the one it amended, shares one. The value is wider than 64
|
|
1028
|
+
// bits, which a bigint holds exactly and a store must compare as one.
|
|
1029
|
+
export function entityETag(updateNumber, updatedAt) {
|
|
1030
|
+
if (updateNumber < 0n || updatedAt < 0n)
|
|
1031
|
+
throw new InvalidArgumentException("An update number and an updatedAt are never negative");
|
|
1032
|
+
return (updateNumber << UPDATED_AT_BITS) | updatedAt;
|
|
1033
|
+
}
|
|
1034
|
+
// A message this client wrote has no update number until a server assigns one, and has none in the
|
|
1035
|
+
// store while unpushed (see pushedEntity). It is versioned at the update number of the copy it
|
|
1036
|
+
// amends, which heldETag carries, so by its updatedAt it is the later copy of that version. The
|
|
1037
|
+
// server sending the amended copy again does not replace it, but a later server version with a
|
|
1038
|
+
// greater update number does. A message that amends nothing is at zero. A message with an update
|
|
1039
|
+
// number of its own is versioned by it, as any entity is.
|
|
1040
|
+
function messageToBlob(message, heldETag) {
|
|
931
1041
|
const entity = getEntity(message.entity);
|
|
1042
|
+
const updateNumber = entity.updateNumber !== 0n
|
|
1043
|
+
? entity.updateNumber
|
|
1044
|
+
: heldETag === null
|
|
1045
|
+
? 0n
|
|
1046
|
+
: heldETag >> UPDATED_AT_BITS;
|
|
932
1047
|
return {
|
|
933
1048
|
id: entity.id,
|
|
934
|
-
eTag: entity.updatedAt,
|
|
1049
|
+
eTag: entityETag(updateNumber, entity.updatedAt),
|
|
935
1050
|
bytes: proto_Message.toBinary(message),
|
|
936
1051
|
};
|
|
937
1052
|
}
|
|
938
|
-
|
|
1053
|
+
// The messages a container holds that are deleted (isDeleted) or live. Only the update number says
|
|
1054
|
+
// which, never what a message carries. A deleted message is a completed order when it carries one,
|
|
1055
|
+
// and is never a customer's cart. Each as stored.
|
|
1056
|
+
function messagesFromBlobs(blobs, isDeleted = false) {
|
|
939
1057
|
const result = [];
|
|
940
1058
|
for (const blob of blobs) {
|
|
941
1059
|
if (blob.id < 0n)
|
|
942
1060
|
continue; // A fixed blob, not an entity
|
|
943
1061
|
if (Ids.getEntityTypeId(blob.id) !== proto_Constants.MESSAGE_ENTITY_TYPE_ID)
|
|
944
1062
|
continue;
|
|
945
|
-
|
|
1063
|
+
const message = proto_Message.fromBinary(blob.bytes);
|
|
1064
|
+
if (isDeletedEntity(getEntity(message.entity)) === isDeleted)
|
|
1065
|
+
result.push(message);
|
|
946
1066
|
}
|
|
947
1067
|
return result;
|
|
948
1068
|
}
|
|
@@ -956,7 +1076,7 @@ function toWireEntity(entity, bytes) {
|
|
|
956
1076
|
updateNumber: entity.updateNumber,
|
|
957
1077
|
updatedAt: entity.updatedAt,
|
|
958
1078
|
isDeleted: isDeleted,
|
|
959
|
-
bytes:
|
|
1079
|
+
bytes: bytes,
|
|
960
1080
|
};
|
|
961
1081
|
}
|
|
962
1082
|
// A tombstone: the update type is held in the last two bits of the update number
|
|
@@ -991,28 +1111,24 @@ function getItemId(item) {
|
|
|
991
1111
|
}
|
|
992
1112
|
// #endregion
|
|
993
1113
|
// #region Values
|
|
994
|
-
// An absent optional string is null
|
|
1114
|
+
// An absent optional string is null, because Java tells absent from empty
|
|
995
1115
|
function stringFromProto(value) {
|
|
996
1116
|
return value === undefined ? null : value;
|
|
997
1117
|
}
|
|
998
|
-
// The way back. An empty string goes out absent
|
|
999
|
-
//
|
|
1118
|
+
// The way back. An empty string goes out absent too: the two mean the same, and sending one could
|
|
1119
|
+
// bring the field back as the other.
|
|
1000
1120
|
function stringToProto(value) {
|
|
1001
1121
|
return value === null || value.length === 0 ? undefined : value;
|
|
1002
1122
|
}
|
|
1003
|
-
// An absent optional bool is null - for Product.isManufacturedByBusiness that means "not said either way"
|
|
1004
|
-
function booleanFromProto(value) {
|
|
1005
|
-
return value === undefined ? null : value;
|
|
1006
|
-
}
|
|
1007
1123
|
function attachmentBaseUrlFromProto(urlAndToken) {
|
|
1008
1124
|
return new AttachmentBaseUrl(urlAndToken.resourceUrl, urlAndToken.sasToken, urlAndToken.expiry);
|
|
1009
1125
|
}
|
|
1010
1126
|
function addressFromProto(address) {
|
|
1011
1127
|
return new Address(address.id, address.addressType, stringFromProto(address.unitNumber), stringFromProto(address.community), stringFromProto(address.line2), stringFromProto(address.city), stringFromProto(address.state), stringFromProto(address.pinCode), address.countryCode);
|
|
1012
1128
|
}
|
|
1013
|
-
// The way back, for a delivery address
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1129
|
+
// The way back, for a delivery address sent directly instead of by id - see buildOrderMessage. id is
|
|
1130
|
+
// whatever Address carries, though it means nothing outside the list it was read from, and this
|
|
1131
|
+
// address is in none.
|
|
1016
1132
|
function addressToProto(address) {
|
|
1017
1133
|
return proto_Address.create({
|
|
1018
1134
|
id: address.id,
|
|
@@ -1026,7 +1142,7 @@ function addressToProto(address) {
|
|
|
1026
1142
|
countryCode: address.countryCode,
|
|
1027
1143
|
});
|
|
1028
1144
|
}
|
|
1029
|
-
// The way back, for the addresses of a user
|
|
1145
|
+
// The way back, for the addresses of a user sent to the server.
|
|
1030
1146
|
function addressListToProto(addressList) {
|
|
1031
1147
|
return proto_AddressList.create({
|
|
1032
1148
|
defaultAddressId: addressList.defaultAddressId,
|
|
@@ -1038,8 +1154,10 @@ function addressListFromProto(addressList) {
|
|
|
1038
1154
|
return AddressList.EMPTY;
|
|
1039
1155
|
return new AddressList(addressList.defaultAddressId, Object.freeze(addressList.addresses.map(addressFromProto)));
|
|
1040
1156
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1157
|
+
// The wire carries a tax's raw code, meaningful only within a country, so every caller names that of
|
|
1158
|
+
// the business the tax is on.
|
|
1159
|
+
function taxFromProto(tax, countryCode) {
|
|
1160
|
+
return Tax.fromScaledValues(countryCode, tax.taxCode, tax.rate);
|
|
1043
1161
|
}
|
|
1044
1162
|
function latLongFromProto(latLong) {
|
|
1045
1163
|
if (latLong === undefined)
|
|
@@ -1071,23 +1189,23 @@ function workingHoursRangeFromProto(range) {
|
|
|
1071
1189
|
function workingHoursFromProto(workingHours) {
|
|
1072
1190
|
return new WorkingHours(Object.freeze(workingHours.ranges.map(workingHoursRangeFromProto)));
|
|
1073
1191
|
}
|
|
1074
|
-
// An absent WorkingDays means the business works every day, all day
|
|
1192
|
+
// An absent WorkingDays means the business works every day, all day (the Java default)
|
|
1075
1193
|
function workingDaysFromProto(workingDays) {
|
|
1076
1194
|
if (workingDays === undefined)
|
|
1077
1195
|
return WorkingDays.ALL_DAYS;
|
|
1078
|
-
// Not an optional field, so an unspecified zone arrives as an empty string
|
|
1196
|
+
// Not an optional field, so an unspecified zone arrives as an empty string
|
|
1079
1197
|
return new WorkingDays(Object.freeze([...workingDays.days]), workingDays.timeZone.length === 0 ? null : workingDays.timeZone, Object.freeze(workingDays.workingHours.map(workingHoursFromProto)));
|
|
1080
1198
|
}
|
|
1081
|
-
function shoppingListItemFromProto(item) {
|
|
1082
|
-
return ShoppingListItem.fromScaledValues(item.packageId, item.quantity, item.unitPrice, item.shoppingListOrder, item.isOriginal, item.isModified, item.isStockKeepingRequired, item.priceDiscount, item.discount, Object.freeze(item.taxes.map(taxFromProto)), item.expiry, stringFromProto(item.description));
|
|
1199
|
+
function shoppingListItemFromProto(item, countryCode) {
|
|
1200
|
+
return ShoppingListItem.fromScaledValues(item.packageId, item.quantity, item.unitPrice, item.shoppingListOrder, item.isOriginal, item.isModified, item.isStockKeepingRequired, item.priceDiscount, item.discount, Object.freeze(item.taxes.map((tax) => taxFromProto(tax, countryCode))), item.expiry, stringFromProto(item.description));
|
|
1083
1201
|
}
|
|
1084
|
-
function shoppingListFromProto(shoppingList) {
|
|
1202
|
+
function shoppingListFromProto(shoppingList, countryCode) {
|
|
1085
1203
|
if (shoppingList === undefined)
|
|
1086
1204
|
return ShoppingList.EMPTY;
|
|
1087
|
-
return ShoppingList.fromScaledValues(shoppingList.isFinal, shoppingList.autoAddDeliveryCharge, shoppingList.autoAddServiceCharge, shoppingList.inventoryLocationId, shoppingList.priceListId, shoppingList.totalAmountBeforeDiscount, shoppingList.discount, shoppingList.existingItemCount, shoppingList.deletedItemCount, Object.freeze(shoppingList.existingItems.map(shoppingListItemFromProto)), Object.freeze(shoppingList.deletedItems.map(shoppingListItemFromProto)));
|
|
1205
|
+
return ShoppingList.fromScaledValues(shoppingList.isFinal, shoppingList.autoAddDeliveryCharge, shoppingList.autoAddServiceCharge, shoppingList.inventoryLocationId, shoppingList.priceListId, shoppingList.totalAmountBeforeDiscount, shoppingList.discount, shoppingList.existingItemCount, shoppingList.deletedItemCount, Object.freeze(shoppingList.existingItems.map((item) => shoppingListItemFromProto(item, countryCode))), Object.freeze(shoppingList.deletedItems.map((item) => shoppingListItemFromProto(item, countryCode))));
|
|
1088
1206
|
}
|
|
1089
|
-
function generalSettingsFromProto(settings) {
|
|
1090
|
-
return GeneralSettings.fromScaledValues(Object.freeze(settings.paymentMethodDetails.map(paymentMethodDetailFromProto)), stringFromProto(settings.customerServicePhone), settings.isWhatsAppSupported, settings.isSmsSupported, addressListFromProto(settings.businessAddresses), Object.freeze(settings.businessIdentifiers.map(businessIdentifierFromProto)), Object.freeze([...settings.fulfillmentModes]), settings.deliveryCharge, Object.freeze(settings.deliveryChargeTaxes.map(taxFromProto)), settings.freeDeliveryTotal, settings.deliveryRadiusKms, stringFromProto(settings.storeTagline), workingDaysFromProto(settings.workingDays), latLongFromProto(settings.primaryStoreLocation), stringFromProto(settings.googleBusinessCategoryId), stringFromProto(settings.googleBusinessCategoryName), stringFromProto(settings.onlineStoreThemeId));
|
|
1207
|
+
function generalSettingsFromProto(settings, countryCode) {
|
|
1208
|
+
return GeneralSettings.fromScaledValues(Object.freeze(settings.paymentMethodDetails.map(paymentMethodDetailFromProto)), stringFromProto(settings.customerServicePhone), settings.isWhatsAppSupported, settings.isSmsSupported, addressListFromProto(settings.businessAddresses), Object.freeze(settings.businessIdentifiers.map(businessIdentifierFromProto)), Object.freeze([...settings.fulfillmentModes]), settings.deliveryCharge, Object.freeze(settings.deliveryChargeTaxes.map((tax) => taxFromProto(tax, countryCode))), settings.freeDeliveryTotal, settings.deliveryRadiusKms, stringFromProto(settings.storeTagline), workingDaysFromProto(settings.workingDays), latLongFromProto(settings.primaryStoreLocation), stringFromProto(settings.googleBusinessCategoryId), stringFromProto(settings.googleBusinessCategoryName), stringFromProto(settings.onlineStoreThemeId), settings.onlineShoppingEnabled);
|
|
1091
1209
|
}
|
|
1092
1210
|
// #endregion
|
|
1093
1211
|
// #region Entities
|
|
@@ -1132,31 +1250,30 @@ function packageFromProto(packageProto, photos, price) {
|
|
|
1132
1250
|
const entity = getEntity(item.entity);
|
|
1133
1251
|
return new Package(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, packageProto.isSoldLoose, packageProto.itemCode, Object.freeze(packageProto.identifiers.map(packageIdentifierFromProto)), dimensionsFromProto(packageProto.dimensions), weightFromProto(packageProto.weight), photos, price);
|
|
1134
1252
|
}
|
|
1135
|
-
function productFromProto(productProto, photos, packages) {
|
|
1253
|
+
function productFromProto(productProto, photos, packages, countryCode) {
|
|
1136
1254
|
const item = getItem(productProto.item);
|
|
1137
1255
|
const entity = getEntity(item.entity);
|
|
1138
|
-
return new Product(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, productProto.productType, stringFromProto(productProto.brand),
|
|
1256
|
+
return new Product(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, productProto.productType, stringFromProto(productProto.brand), productProto.isManufacturedByBusiness ?? true, // Absent means true (Product.proto)
|
|
1257
|
+
stringFromProto(productProto.manufacturer), productProto.manufacturerAddress === undefined
|
|
1139
1258
|
? null
|
|
1140
|
-
: addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map(taxFromProto)), Object.freeze([...productProto.features]), photos, packages);
|
|
1259
|
+
: addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map((tax) => taxFromProto(tax, countryCode))), Object.freeze([...productProto.features]), productProto.forOfflineSaleOnly, photos, packages);
|
|
1141
1260
|
}
|
|
1142
|
-
function orderFromProto(message, salesOrder) {
|
|
1261
|
+
function orderFromProto(message, salesOrder, countryCode) {
|
|
1143
1262
|
const entity = getEntity(message.entity);
|
|
1144
|
-
return Order.fromScaledValues(toEntityHeader(entity), message.senderId, message.status, stringFromProto(message.senderName), stringFromProto(message.senderPhone), salesOrder.status, salesOrder.fulfillmentMode, shoppingListFromProto(salesOrder.shoppingList), salesOrder.shoppingListTotal, salesOrder.shoppingListCount, salesOrder.deliveryAddressId, salesOrder.deliveryAddress === undefined ? null : addressFromProto(salesOrder.deliveryAddress), salesOrder.creationDate, stringFromProto(salesOrder.note), salesOrder.paymentReceived);
|
|
1145
|
-
}
|
|
1146
|
-
// The one part of a cart message a customer is shown.
|
|
1147
|
-
//
|
|
1148
|
-
//
|
|
1149
|
-
//
|
|
1150
|
-
|
|
1151
|
-
function cartShoppingListFromProto(partyPreferences) {
|
|
1263
|
+
return Order.fromScaledValues(toEntityHeader(entity), message.senderId, message.status, stringFromProto(message.senderName), stringFromProto(message.senderPhone), salesOrder.status, salesOrder.fulfillmentMode, shoppingListFromProto(salesOrder.shoppingList, countryCode), salesOrder.shoppingListTotal, salesOrder.shoppingListCount, salesOrder.deliveryAddressId, salesOrder.deliveryAddress === undefined ? null : addressFromProto(salesOrder.deliveryAddress), salesOrder.creationDate, stringFromProto(salesOrder.note), salesOrder.paymentReceived);
|
|
1264
|
+
}
|
|
1265
|
+
// The one part of a cart message a customer is shown. Null when the cart holds nothing, so a cart
|
|
1266
|
+
// emptied by placing an order and one never created give the same answer. The rest of the message is
|
|
1267
|
+
// the server's or read elsewhere: who sent it, how far the business has read it, and the fulfillment
|
|
1268
|
+
// mode, which buildBusiness reads as defaultFulfillmentMode.
|
|
1269
|
+
function cartShoppingListFromProto(partyPreferences, countryCode) {
|
|
1152
1270
|
const shoppingList = partyPreferences.shoppingList;
|
|
1153
|
-
return shoppingList === undefined ? null : shoppingListFromProto(shoppingList);
|
|
1271
|
+
return shoppingList === undefined ? null : shoppingListFromProto(shoppingList, countryCode);
|
|
1154
1272
|
}
|
|
1155
1273
|
// #endregion
|
|
1156
1274
|
// #region Photos
|
|
1157
|
-
// The photos of one entity, in no particular order
|
|
1158
|
-
//
|
|
1159
|
-
// looking it up later.
|
|
1275
|
+
// The photos of one entity, in no particular order (a UI orders them). Each is built with the
|
|
1276
|
+
// response's base url, since a Photo takes it at construction.
|
|
1160
1277
|
function takePhotos(attachmentsByOwner, owningEntityId, attachmentBaseUrl) {
|
|
1161
1278
|
const attachments = attachmentsByOwner.get(owningEntityId);
|
|
1162
1279
|
if (attachments === undefined)
|
|
@@ -1164,5 +1281,8 @@ function takePhotos(attachmentsByOwner, owningEntityId, attachmentBaseUrl) {
|
|
|
1164
1281
|
return Object.freeze(attachments.map((attachment) => new Photo(attachment, attachmentBaseUrl)));
|
|
1165
1282
|
}
|
|
1166
1283
|
// #endregion
|
|
1167
|
-
|
|
1284
|
+
// The width of the updatedAt an eTag carries below its update number. An int64 is never wider.
|
|
1285
|
+
const UPDATED_AT_BITS = 64n;
|
|
1286
|
+
// What a blob that is only its eTag holds
|
|
1287
|
+
const NO_BYTES = new Uint8Array(0);
|
|
1168
1288
|
const NO_PHOTOS = Object.freeze([]);
|