@peabody-soft/mbs-model-ts 1.7.0 → 1.8.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/Attachment.js +2 -2
- package/dist/AttachmentBaseUrl.js +4 -4
- package/dist/AuthTokens.js +7 -7
- package/dist/Business.js +11 -10
- package/dist/BusinessIdentifier.js +3 -2
- package/dist/BusinessLink.js +3 -2
- package/dist/CartEditor.js +89 -81
- package/dist/Catalog.d.ts +19 -7
- package/dist/Catalog.js +121 -41
- package/dist/Constants.d.ts +1 -0
- package/dist/Constants.js +7 -5
- package/dist/ContainerNames.js +6 -6
- package/dist/Entity.js +4 -3
- package/dist/Enums.d.ts +1 -0
- package/dist/Enums.js +3 -2
- package/dist/Exceptions.js +2 -2
- package/dist/GeneralSettings.d.ts +5 -4
- package/dist/GeneralSettings.js +17 -15
- package/dist/HttpStatusCodes.js +7 -7
- package/dist/Ids.js +4 -5
- package/dist/Item.js +8 -5
- package/dist/LiveData.js +2 -2
- package/dist/Measures.js +3 -2
- package/dist/Message.js +1 -1
- package/dist/MockDatabase.js +6 -5
- package/dist/Model.js +166 -162
- package/dist/Order.js +10 -12
- package/dist/Package.d.ts +6 -2
- package/dist/Package.js +10 -5
- package/dist/PackageIdentifier.js +2 -2
- package/dist/Price.js +6 -5
- package/dist/PriceList.d.ts +13 -0
- package/dist/PriceList.js +34 -2
- package/dist/Product.d.ts +6 -2
- package/dist/Product.js +15 -10
- package/dist/Profile.js +15 -14
- package/dist/ProtoMap.js +189 -206
- package/dist/ServiceExceptions.js +6 -6
- package/dist/ServiceProxy.js +15 -14
- package/dist/ShoppingList.js +102 -100
- package/dist/SyncCache.js +76 -68
- package/dist/SyncKeys.js +5 -5
- package/dist/SyncState.js +14 -6
- package/dist/Tax.js +11 -10
- package/dist/TaxHelper.js +9 -8
- package/dist/TaxType.js +7 -6
- package/dist/UserProfile.js +7 -7
- package/dist/WorkingDays.js +21 -18
- package/dist/generated/BusinessSyncOutResponse.d.ts +4 -1
- package/dist/generated/Constants.d.ts +0 -4
- package/dist/generated/Constants.js +0 -4
- package/dist/generated/GeneralSettings.d.ts +5 -4
- package/dist/generated/GeneralSettings.js +8 -6
- package/dist/generated/Item.d.ts +4 -3
- package/dist/generated/Message.d.ts +5 -0
- package/dist/generated/OnlineStoreTheme.d.ts +33 -0
- package/dist/generated/OnlineStoreTheme.js +38 -0
- package/dist/generated/PriceList.d.ts +3 -2
- package/dist/generated/ProfileSyncOutRequest.d.ts +16 -15
- package/dist/generated/ProfileSyncOutResponse.d.ts +4 -6
- package/dist/generated/SalesOrderPayload.d.ts +6 -3
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -8
- package/package.json +2 -2
package/dist/ProtoMap.js
CHANGED
|
@@ -13,6 +13,7 @@ import { Entity as proto_Entity } from "./generated/Entity";
|
|
|
13
13
|
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
14
14
|
import { Message as proto_Message } from "./generated/Message";
|
|
15
15
|
import { MessageType as proto_MessageType } from "./generated/MessageType";
|
|
16
|
+
import { OnlineStoreTheme as proto_OnlineStoreTheme } from "./generated/OnlineStoreTheme";
|
|
16
17
|
import { OrderStatus as proto_OrderStatus } from "./generated/OrderStatus";
|
|
17
18
|
import { Package as proto_Package } from "./generated/Package";
|
|
18
19
|
import { PartyPreferencesPayload as proto_PartyPreferencesPayload } from "./generated/PartyPreferencesPayload";
|
|
@@ -98,12 +99,12 @@ export function getProfileResponseEntities(response) {
|
|
|
98
99
|
result.push(toWireEntity(business.entity, proto_Business.toBinary(business)));
|
|
99
100
|
return result;
|
|
100
101
|
}
|
|
101
|
-
// One blob per entity, tombstones included and as sent, plus the attachment base url
|
|
102
|
-
//
|
|
102
|
+
// One blob per entity, tombstones included and as sent, plus a blob for the attachment base url,
|
|
103
|
+
// which belongs to the business and has no entity.
|
|
103
104
|
export function businessResponseToCacheWrite(response) {
|
|
104
105
|
const write = toCacheWrite(getBusinessResponseEntities(response));
|
|
105
|
-
//
|
|
106
|
-
// gets a later url only if one exists.
|
|
106
|
+
// The url's eTag is its expiry, which is also its cursor: a client sends back the expiry it holds
|
|
107
|
+
// and gets a later url only if one exists.
|
|
107
108
|
const attachmentBaseUrl = response.attachmentBaseUrl;
|
|
108
109
|
if (attachmentBaseUrl !== undefined)
|
|
109
110
|
write.blobs.push({
|
|
@@ -122,9 +123,10 @@ export function profileResponseToCacheWrite(response) {
|
|
|
122
123
|
export function getBusinessResponseAttachmentBaseUrlExpiry(response) {
|
|
123
124
|
return response?.attachmentBaseUrl?.expiry ?? 0n;
|
|
124
125
|
}
|
|
125
|
-
// One order as
|
|
126
|
-
//
|
|
127
|
-
// find it. heldETag is the eTag the container holds the order at, or
|
|
126
|
+
// One order as a blob, for a caller holding a message rather than a response. Only the payload says
|
|
127
|
+
// whether a message is an order, so a message without a sales order throws here instead of being
|
|
128
|
+
// stored where a read would not find it. heldETag is the eTag the container holds the order at, or
|
|
129
|
+
// null - see messageToBlob.
|
|
128
130
|
export function orderToBlob(order, heldETag) {
|
|
129
131
|
if (order.payload.oneofKind !== "salesOrder")
|
|
130
132
|
throw new InvalidArgumentException("That message carries no sales order, so it is not an order");
|
|
@@ -134,8 +136,8 @@ export function orderToBlob(order, heldETag) {
|
|
|
134
136
|
export function isPartyPreferencesMessage(message) {
|
|
135
137
|
return message.payload.oneofKind === "partyPreferences";
|
|
136
138
|
}
|
|
137
|
-
// When a message was last written.
|
|
138
|
-
// have no shared version.
|
|
139
|
+
// When a message was last written. Use it to order two copies of one thing held at different ids,
|
|
140
|
+
// which have no shared version.
|
|
139
141
|
export function getMessageUpdatedAt(message) {
|
|
140
142
|
return getEntity(message.entity).updatedAt;
|
|
141
143
|
}
|
|
@@ -149,8 +151,8 @@ export function partyPreferencesToBlob(partyPreferences, heldETag) {
|
|
|
149
151
|
return messageToBlob(partyPreferences, heldETag);
|
|
150
152
|
}
|
|
151
153
|
// 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
|
|
153
|
-
// type is skipped, so a newer server cannot break an older client.
|
|
154
|
+
// hold nothing yet. The entity type is part of each blob id and picks the array a blob goes in, so no
|
|
155
|
+
// type tag is stored. An unknown type is skipped, so a newer server cannot break an older client.
|
|
154
156
|
export function businessResponseFromBlobs(blobs) {
|
|
155
157
|
const response = proto_BusinessSyncOutResponse.create({});
|
|
156
158
|
let hasContent = false;
|
|
@@ -260,9 +262,9 @@ export function orderMessagesFromBlobs(blobs) {
|
|
|
260
262
|
return ordersAmong(messagesFromBlobs(blobs, false));
|
|
261
263
|
}
|
|
262
264
|
// 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
|
|
264
|
-
// customer is shown the order afterwards. A tombstone
|
|
265
|
-
// sent only the header) is
|
|
265
|
+
// order by deleting its message, so these are tombstones. The server sends them whole because the
|
|
266
|
+
// customer is shown the order afterwards. A tombstone without an order (from an older server that
|
|
267
|
+
// sent only the header) is skipped.
|
|
266
268
|
export function deletedOrderMessagesFromBlobs(blobs) {
|
|
267
269
|
return ordersAmong(messagesFromBlobs(blobs, true));
|
|
268
270
|
}
|
|
@@ -270,12 +272,13 @@ function ordersAmong(messages) {
|
|
|
270
272
|
return messages.filter((message) => message.payload.oneofKind === "salesOrder");
|
|
271
273
|
}
|
|
272
274
|
// The party preferences message among these messages, or null. A customer has one per business, so
|
|
273
|
-
// two
|
|
274
|
-
//
|
|
275
|
-
// A BUSINESS ARE NOT PUBLIC.
|
|
275
|
+
// two mean that two callers' messages share a container. The one with the greater update number is
|
|
276
|
+
// returned, an arbitrary choice since nothing in a message says which caller it was sent to.
|
|
277
|
+
// See THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
|
|
276
278
|
// Split from partyPreferencesMessageFromBlobs so a caller already holding the messages (such as
|
|
277
279
|
// 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
|
|
280
|
+
// buildBusiness uses it too, so the cart that is shown, amended and carried over at sign in always
|
|
281
|
+
// comes from the same message.
|
|
279
282
|
export function partyPreferencesMessageFromMessages(messages) {
|
|
280
283
|
let result = null;
|
|
281
284
|
for (const message of messages) {
|
|
@@ -297,9 +300,9 @@ export function partyPreferencesMessageFromBlobs(blobs) {
|
|
|
297
300
|
// #endregion
|
|
298
301
|
// #region Requests and credentials
|
|
299
302
|
// The request for everything changed since these cursors. A bigint business is the Business entity
|
|
300
|
-
// id, which a caller that has read
|
|
301
|
-
//
|
|
302
|
-
// held, sent back unchanged (zero when none).
|
|
303
|
+
// id, which a caller that has read the business before holds. A string is its url friendly
|
|
304
|
+
// businessId, the only identifier a first read has. attachmentBaseUrlExpiry is not one of the
|
|
305
|
+
// cursors. It is the expiry of the url already held, sent back unchanged (zero when none).
|
|
303
306
|
export function buildBusinessSyncOutRequest(business, syncInState, attachmentBaseUrlExpiry) {
|
|
304
307
|
return createBusinessSyncOutRequest(typeof business === "bigint"
|
|
305
308
|
? { oneofKind: "businessEntityId", businessEntityId: business }
|
|
@@ -333,9 +336,9 @@ export function cursorsToBlobs(cursors, movedKeys) {
|
|
|
333
336
|
return blobs;
|
|
334
337
|
}
|
|
335
338
|
// 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
|
|
337
|
-
// everything again. That costs bandwidth
|
|
338
|
-
// greater.
|
|
339
|
+
// none). A container never read, or filled by a version that kept no cursors, gets zeros, so the
|
|
340
|
+
// next read fetches everything again. That costs only bandwidth, since the store keeps a blob only
|
|
341
|
+
// when its eTag is greater.
|
|
339
342
|
export function cursorsFromBlobs(keys, blobs) {
|
|
340
343
|
const cursors = new SyncState(keys);
|
|
341
344
|
for (const [index, key] of keys.entries()) {
|
|
@@ -345,15 +348,15 @@ export function cursorsFromBlobs(keys, blobs) {
|
|
|
345
348
|
}
|
|
346
349
|
return cursors;
|
|
347
350
|
}
|
|
348
|
-
// Raises the cursors to what a page of a business read brought, tombstones included, and
|
|
349
|
-
// keys that moved. Only a read moves them.
|
|
351
|
+
// Raises the cursors to what a page of a business read brought, tombstones included, and returns the
|
|
352
|
+
// keys that moved. Only a read moves them. A push does not.
|
|
350
353
|
export function raiseBusinessCursors(cursors, response) {
|
|
351
354
|
return raiseCursorsFrom(cursors, getBusinessResponseEntities(response));
|
|
352
355
|
}
|
|
353
|
-
// The request that pushes what this customer changed. A storefront may push only messages
|
|
356
|
+
// The request that pushes what this customer changed. A storefront may push only messages. One
|
|
354
357
|
// request is one batch, so an order and the cart it emptied go together and a customer is never left
|
|
355
|
-
//
|
|
356
|
-
//
|
|
358
|
+
// with the order and a still full cart. messageUpdateNumber is how far reads of this container have
|
|
359
|
+
// got with messages - see SyncCache.readMessageUpdateNumber.
|
|
357
360
|
export function buildBusinessSyncInRequest(messageUpdateNumber, messages) {
|
|
358
361
|
return proto_BusinessSyncInRequest.create({
|
|
359
362
|
messageUpdateNumber: messageUpdateNumber,
|
|
@@ -366,10 +369,10 @@ export function getSyncInResponseMessages(response) {
|
|
|
366
369
|
return response.messages;
|
|
367
370
|
}
|
|
368
371
|
// 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
|
|
370
|
-
//
|
|
371
|
-
// Constants.ENTITY_ID_NULL
|
|
372
|
-
//
|
|
372
|
+
// business, since a profile sync is not scoped to one and has no sync in. joinBusinessId is the
|
|
373
|
+
// entity id of a business to make the caller a customer of while the server answers.
|
|
374
|
+
// Constants.ENTITY_ID_NULL means no join and is the default, since an ordinary read joins nothing.
|
|
375
|
+
// A join belongs to one call, so nextProfileSyncOutRequest leaves it off later pages.
|
|
373
376
|
export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(proto_Constants.ENTITY_ID_NULL)) {
|
|
374
377
|
return proto_ProfileSyncOutRequest.create({
|
|
375
378
|
userUpdateNumber: syncInState.getUpdateNumber(proto_Constants.USER_ENTITY_TYPE_ID),
|
|
@@ -382,8 +385,8 @@ export function buildProfileSyncOutRequest(syncInState, joinBusinessId = BigInt(
|
|
|
382
385
|
export function raiseProfileCursors(cursors, response) {
|
|
383
386
|
return raiseCursorsFrom(cursors, getProfileResponseEntities(response));
|
|
384
387
|
}
|
|
385
|
-
// The user a sign up
|
|
386
|
-
// server assigns id, createdAt and updatedAt), so none of EntityHeader
|
|
388
|
+
// The user a sign up sends as CreateUserRequest.user, for the server to create. It has no entity yet
|
|
389
|
+
// (the server assigns id, createdAt and updatedAt), so none of EntityHeader is sent.
|
|
387
390
|
export function buildUser(userProfile) {
|
|
388
391
|
return proto_User.create({
|
|
389
392
|
firstName: userProfile.firstName,
|
|
@@ -393,18 +396,18 @@ export function buildUser(userProfile) {
|
|
|
393
396
|
preferredLanguageCode: stringToProto(userProfile.preferredLanguageCode),
|
|
394
397
|
});
|
|
395
398
|
}
|
|
396
|
-
// The user Service.updateUser
|
|
397
|
-
// the entity id, which the
|
|
398
|
-
// call, so it is not sent.
|
|
399
|
+
// The user that Service.updateUser writes over this customer's profile: the fields of buildUser plus
|
|
400
|
+
// the entity id, which the server matches to the caller. The rest of EntityHeader is not read on
|
|
401
|
+
// that call, so it is not sent.
|
|
399
402
|
export function buildUpdateUser(userProfile) {
|
|
400
403
|
return proto_User.create({
|
|
401
404
|
...buildUser(userProfile),
|
|
402
405
|
entity: proto_Entity.create({ id: userProfile.id }),
|
|
403
406
|
});
|
|
404
407
|
}
|
|
405
|
-
// The two things a join must
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
+
// The two things a response to a join must hold, checked separately so a caller can report which one
|
|
409
|
+
// is missing - see Model.requireJoinedBusiness. Both take the id, since both requests that carry a
|
|
410
|
+
// joinBusinessId need them.
|
|
408
411
|
// The membership itself: a live BusinessLink naming that business. A tombstoned link does not count
|
|
409
412
|
// (see OBJECT MODEL), nor does a link to another business. Its linkToken is the only copy of that
|
|
410
413
|
// credential a profile response carries.
|
|
@@ -424,7 +427,7 @@ export function hasJoinedBusinessLink(joinBusinessId, response) {
|
|
|
424
427
|
export function hasJoinedBusiness(joinBusinessId, response) {
|
|
425
428
|
if (joinBusinessId === BigInt(proto_Constants.ENTITY_ID_NULL))
|
|
426
429
|
return false;
|
|
427
|
-
// A
|
|
430
|
+
// A tombstoned business does not count
|
|
428
431
|
for (const business of response.businesses)
|
|
429
432
|
if (getEntity(business.entity).id === joinBusinessId &&
|
|
430
433
|
!isDeletedEntity(getEntity(business.entity)))
|
|
@@ -432,8 +435,8 @@ export function hasJoinedBusiness(joinBusinessId, response) {
|
|
|
432
435
|
return false;
|
|
433
436
|
}
|
|
434
437
|
// 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
|
|
436
|
-
// cart's taxes are raw codes that mean something only within a country.
|
|
438
|
+
// A tombstone has no country, and a bare header carries zero. Zero must not be taken for a country,
|
|
439
|
+
// because a cart's taxes are raw codes that mean something only within a country.
|
|
437
440
|
export function getBusinessResponseCountryCode(response) {
|
|
438
441
|
const business = response?.business;
|
|
439
442
|
if (business === undefined || isDeletedEntity(getEntity(business.entity)))
|
|
@@ -441,8 +444,8 @@ export function getBusinessResponseCountryCode(response) {
|
|
|
441
444
|
return business.countryCode;
|
|
442
445
|
}
|
|
443
446
|
// 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
|
|
445
|
-
// friendly id is only
|
|
447
|
+
// Once a business has been read, later requests name it by this id, which is stable. The url
|
|
448
|
+
// friendly id is used only to find the business the first time.
|
|
446
449
|
export function getBusinessResponseBusinessEntityId(response) {
|
|
447
450
|
const entity = response?.business?.entity;
|
|
448
451
|
if (entity === undefined || isDeletedEntity(entity))
|
|
@@ -465,9 +468,9 @@ export function isBusinessResponseOnlineShoppingOn(response) {
|
|
|
465
468
|
return false;
|
|
466
469
|
return getGeneralSettingsOf(response)?.onlineShoppingEnabled ?? false;
|
|
467
470
|
}
|
|
468
|
-
// The general settings a response holds, as on the wire, or undefined when none
|
|
469
|
-
// configuration that carries them, unless it is deleted.
|
|
470
|
-
//
|
|
471
|
+
// The general settings a response holds, as on the wire, or undefined when none. They come from the
|
|
472
|
+
// one public configuration that carries them, unless it is deleted. buildBusiness and
|
|
473
|
+
// isBusinessResponseOnlineShoppingOn both read them here, so both use the same settings.
|
|
471
474
|
function getGeneralSettingsOf(response) {
|
|
472
475
|
let settings;
|
|
473
476
|
for (const configuration of response.configurations) {
|
|
@@ -480,7 +483,7 @@ function getGeneralSettingsOf(response) {
|
|
|
480
483
|
return settings;
|
|
481
484
|
}
|
|
482
485
|
// The credentials of one call, as on the wire. An absent token is left off the message, since the
|
|
483
|
-
// protocol
|
|
486
|
+
// protocol distinguishes absent from empty.
|
|
484
487
|
export function buildAuthTokens(authTokens) {
|
|
485
488
|
const result = proto_AuthTokens.create({
|
|
486
489
|
authenticationMethod: authTokens.authenticationMethod,
|
|
@@ -503,10 +506,10 @@ export function authTokensFromProto(authTokens) {
|
|
|
503
506
|
export function buildAuthTokensBytes(authTokens) {
|
|
504
507
|
return proto_AuthTokens.toBinary(authTokens);
|
|
505
508
|
}
|
|
506
|
-
//
|
|
507
|
-
// null because a reissued token is not a later version of the old
|
|
508
|
-
// it. The authentication token is never stored: it belongs to the
|
|
509
|
-
// be staler than the one in memory and would outlive the sign out.
|
|
509
|
+
// The credentials as a container stores them: the two tokens this server issued, and the
|
|
510
|
+
// authentication method. The eTag is null because a reissued token is not a later version of the old
|
|
511
|
+
// one, and greater-wins would refuse it. The authentication token is never stored: it belongs to the
|
|
512
|
+
// identity provider, so a copy would be staler than the one in memory and would outlive the sign out.
|
|
510
513
|
export function authTokensToBlob(authTokens) {
|
|
511
514
|
const stored = proto_AuthTokens.create({
|
|
512
515
|
authenticationMethod: authTokens.authenticationMethod,
|
|
@@ -542,13 +545,13 @@ export function refreshAuthTokens(authTokens, issued) {
|
|
|
542
545
|
result.linkToken = issued.linkToken;
|
|
543
546
|
return result;
|
|
544
547
|
}
|
|
545
|
-
// The request for the page after this response, or null when it was the last. A full page
|
|
546
|
-
// short, so the cursors are raised from what arrived and the call repeats. A full page that
|
|
547
|
-
// cursor means the server broke the ascending order it promises. The loop
|
|
548
|
-
// there.
|
|
548
|
+
// The request for the page after this response, or null when it was the last. A full page may have
|
|
549
|
+
// been cut short, so the cursors are raised from what arrived and the call repeats. A full page that
|
|
550
|
+
// raised no cursor means the server broke the ascending order it promises. The loop has no other
|
|
551
|
+
// bound, so it ends there.
|
|
549
552
|
export function nextBusinessSyncOutRequest(request, response) {
|
|
550
553
|
// A short page means the server sent everything. Counting first keeps the ordinary case to nine
|
|
551
|
-
//
|
|
554
|
+
// field reads.
|
|
552
555
|
if (businessResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
|
|
553
556
|
return null;
|
|
554
557
|
const cursors = businessSyncOutCursors(request);
|
|
@@ -577,12 +580,12 @@ export function nextBusinessSyncOutRequest(request, response) {
|
|
|
577
580
|
raise(getItem(price.item).entity);
|
|
578
581
|
if (!haveCursorsRisen)
|
|
579
582
|
return null;
|
|
580
|
-
//
|
|
583
|
+
// Use the expiry of a renewed url if this page carried one, else the expiry already sent
|
|
581
584
|
const renewedExpiry = getBusinessResponseAttachmentBaseUrlExpiry(response);
|
|
582
585
|
return createBusinessSyncOutRequest(request.business, cursors, renewedExpiry === 0n ? request.attachmentBaseUrlExpiry : renewedExpiry);
|
|
583
586
|
}
|
|
584
587
|
// The same for a profile read. A joinBusinessId on the first request is left off the later ones,
|
|
585
|
-
// since the call
|
|
588
|
+
// since the first call makes the join.
|
|
586
589
|
export function nextProfileSyncOutRequest(request, response) {
|
|
587
590
|
if (profileResponseEntityCount(response) < proto_Constants.SYNC_OUT_LIMIT)
|
|
588
591
|
return null;
|
|
@@ -601,8 +604,8 @@ export function nextProfileSyncOutRequest(request, response) {
|
|
|
601
604
|
return null;
|
|
602
605
|
return buildProfileSyncOutRequest(cursors);
|
|
603
606
|
}
|
|
604
|
-
// How many entities a response carried, without encoding any. A paging loop
|
|
605
|
-
// and getBusinessResponseEntities would encode each entity to
|
|
607
|
+
// How many entities a response carried, counted without encoding any. A paging loop needs the count
|
|
608
|
+
// for every page, and getBusinessResponseEntities would encode each entity to produce it.
|
|
606
609
|
function businessResponseEntityCount(response) {
|
|
607
610
|
return ((response.business === undefined ? 0 : 1) +
|
|
608
611
|
response.attachments.length +
|
|
@@ -619,7 +622,7 @@ function profileResponseEntityCount(response) {
|
|
|
619
622
|
response.businessLinks.length +
|
|
620
623
|
response.businesses.length);
|
|
621
624
|
}
|
|
622
|
-
// Raises the cursors the entities of one page advance and
|
|
625
|
+
// Raises the cursors the entities of one page advance and returns the keys that moved, each once
|
|
623
626
|
// however many entities raised it. A tombstone counts like any entity, as in raiseCursor.
|
|
624
627
|
function raiseCursorsFrom(cursors, entities) {
|
|
625
628
|
const movedKeys = new Set();
|
|
@@ -628,8 +631,8 @@ function raiseCursorsFrom(cursors, entities) {
|
|
|
628
631
|
movedKeys.add(SyncKeys.keyOf(entity.entityTypeId));
|
|
629
632
|
return movedKeys;
|
|
630
633
|
}
|
|
631
|
-
// Raises the cursor one entity advances and
|
|
632
|
-
// server would send it again
|
|
634
|
+
// Raises the cursor one entity advances and returns whether it moved. A tombstone counts, because
|
|
635
|
+
// otherwise the server would send it again forever.
|
|
633
636
|
function raiseCursor(cursors, entity) {
|
|
634
637
|
if (entity === undefined)
|
|
635
638
|
return false;
|
|
@@ -690,57 +693,39 @@ export function buildBusiness(response) {
|
|
|
690
693
|
updatableBusiness.settings = generalSettingsFromProto(generalSettings, countryCode);
|
|
691
694
|
// The photos of the store itself
|
|
692
695
|
updatableBusiness.photos = takePhotos(attachmentsByOwner, business.id, attachmentBaseUrl);
|
|
693
|
-
//
|
|
694
|
-
//
|
|
695
|
-
// here
|
|
696
|
-
//
|
|
697
|
-
const
|
|
696
|
+
// The catalog's items, each built alone. Catalog links them and drops what it cannot (a package
|
|
697
|
+
// with no product, a price with no list) and what cannot be bought. Photos go to packages and
|
|
698
|
+
// products here. A tombstone is skipped, and the business is the containment parent of the items
|
|
699
|
+
// that sit directly under it. Catalog parents the rest.
|
|
700
|
+
const prices = [];
|
|
698
701
|
for (const priceProto of response.prices) {
|
|
699
702
|
if (isDeletedItem(priceProto.item))
|
|
700
703
|
continue;
|
|
701
|
-
|
|
702
|
-
|
|
704
|
+
prices.push(priceFromProto(priceProto));
|
|
705
|
+
}
|
|
706
|
+
const priceLists = [];
|
|
707
|
+
for (const priceListProto of response.priceLists) {
|
|
708
|
+
if (isDeletedItem(priceListProto.item))
|
|
703
709
|
continue;
|
|
704
|
-
|
|
705
|
-
|
|
710
|
+
const priceList = priceListFromProto(priceListProto);
|
|
711
|
+
UpdatableEntity.setParent(priceList, business);
|
|
712
|
+
priceLists.push(priceList);
|
|
706
713
|
}
|
|
707
|
-
|
|
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.
|
|
710
|
-
const packagesByProduct = new Map();
|
|
714
|
+
const packages = [];
|
|
711
715
|
for (const packageProto of response.packages) {
|
|
712
716
|
if (isDeletedItem(packageProto.item))
|
|
713
717
|
continue;
|
|
714
|
-
const
|
|
715
|
-
|
|
716
|
-
const pkg = packageFromProto(packageProto, packagePhotos, pricesByPackage.get(packageId) ?? null);
|
|
717
|
-
if (!pkg.isBuyable)
|
|
718
|
-
continue;
|
|
719
|
-
const price = pkg.price;
|
|
720
|
-
if (price !== null)
|
|
721
|
-
UpdatableEntity.setParent(price, pkg);
|
|
722
|
-
const siblings = packagesByProduct.get(pkg.productId);
|
|
723
|
-
if (siblings === undefined)
|
|
724
|
-
packagesByProduct.set(pkg.productId, [pkg]);
|
|
725
|
-
else
|
|
726
|
-
siblings.push(pkg);
|
|
718
|
+
const packagePhotos = takePhotos(attachmentsByOwner, getItemId(packageProto.item), attachmentBaseUrl);
|
|
719
|
+
packages.push(packageFromProto(packageProto, packagePhotos));
|
|
727
720
|
}
|
|
728
|
-
//
|
|
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.
|
|
721
|
+
// A product's category only groups it, so its containment parent is the business
|
|
731
722
|
const products = [];
|
|
732
723
|
for (const productProto of response.products) {
|
|
733
724
|
if (isDeletedItem(productProto.item))
|
|
734
725
|
continue;
|
|
735
|
-
const
|
|
736
|
-
const
|
|
737
|
-
const productPhotos = takePhotos(attachmentsByOwner, productId, attachmentBaseUrl);
|
|
738
|
-
const product = productFromProto(productProto, productPhotos, Object.freeze([...packages]), countryCode);
|
|
739
|
-
if (!product.isBuyable)
|
|
740
|
-
continue;
|
|
726
|
+
const productPhotos = takePhotos(attachmentsByOwner, getItemId(productProto.item), attachmentBaseUrl);
|
|
727
|
+
const product = productFromProto(productProto, productPhotos, countryCode);
|
|
741
728
|
UpdatableEntity.setParent(product, business);
|
|
742
|
-
for (const pkg of product.packages)
|
|
743
|
-
UpdatableEntity.setParent(pkg, product);
|
|
744
729
|
products.push(product);
|
|
745
730
|
}
|
|
746
731
|
const productCategories = [];
|
|
@@ -751,41 +736,32 @@ export function buildBusiness(response) {
|
|
|
751
736
|
UpdatableEntity.setParent(productCategory, business);
|
|
752
737
|
productCategories.push(productCategory);
|
|
753
738
|
}
|
|
754
|
-
//
|
|
755
|
-
//
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if (isDeletedItem(priceListProto.item))
|
|
760
|
-
continue;
|
|
761
|
-
if (getItemId(priceListProto.item) !== MERABILLS_ONLINE_PRICE_LIST_ID)
|
|
762
|
-
continue;
|
|
763
|
-
priceList = priceListFromProto(priceListProto);
|
|
764
|
-
UpdatableEntity.setParent(priceList, business);
|
|
765
|
-
break;
|
|
766
|
-
}
|
|
767
|
-
updatableBusiness.catalog = new Catalog(productCategories, products, priceList);
|
|
739
|
+
// A storefront sells from the MeraBills Online list, picked by id because a response can carry
|
|
740
|
+
// other public lists. A package with no price in that list is priced from the list's parent.
|
|
741
|
+
const catalog = new Catalog(productCategories, products, packages, priceLists, prices);
|
|
742
|
+
catalog.currentSalePriceListId = MERABILLS_ONLINE_PRICE_LIST_ID;
|
|
743
|
+
updatableBusiness.catalog = catalog;
|
|
768
744
|
// The orders of this customer, and their party preferences (the cart and the fulfillment mode to
|
|
769
745
|
// 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
|
|
771
|
-
// open and the completed ones
|
|
772
|
-
// an older server that sent only the header) is
|
|
746
|
+
// an order by deleting its message, and the server sends that message whole, so the orders include
|
|
747
|
+
// the open and the completed ones. Order.isDeleted tells them apart. A deleted message without an
|
|
748
|
+
// order (from an older server that sent only the header) is skipped.
|
|
773
749
|
const orders = [];
|
|
774
750
|
for (const message of response.messages) {
|
|
775
751
|
// Party preferences are one message out of possibly several, so they are settled below
|
|
776
752
|
if (message.payload.oneofKind === "salesOrder")
|
|
777
753
|
orders.push(orderFromProto(message, message.payload.salesOrder, countryCode));
|
|
778
754
|
}
|
|
779
|
-
// A customer has one party preferences message, so two
|
|
780
|
-
// container.
|
|
781
|
-
// resent cart is read from. A rule
|
|
782
|
-
//
|
|
755
|
+
// A customer has one party preferences message, so two mean two callers' messages share a
|
|
756
|
+
// container. Use the one partyPreferencesMessageFromMessages picks, which is also the one an amended
|
|
757
|
+
// or resent cart is read from. A separate rule here would show one cart and amend another. The newer
|
|
758
|
+
// message wins even when its cart is empty, and the older one's list is not shown.
|
|
783
759
|
const partyPreferences = partyPreferencesMessageFromMessages(response.messages);
|
|
784
760
|
let cart = null;
|
|
785
761
|
let defaultFulfillmentMode = proto_FulfillmentMode.DELIVERY;
|
|
786
762
|
if (partyPreferences !== null && partyPreferences.payload.oneofKind === "partyPreferences") {
|
|
787
763
|
// An emptied cart is a message whose payload has no shopping list, which is what placing an order
|
|
788
|
-
// leaves.
|
|
764
|
+
// leaves. The cart is null then, the same as for a customer who never had one.
|
|
789
765
|
cart = cartShoppingListFromProto(partyPreferences.payload.partyPreferences, countryCode);
|
|
790
766
|
// Taken as the message has it. DELIVERY comes only from the fallback above, even when the message
|
|
791
767
|
// holds UNSPECIFIED_FULFILLMENT_MODE.
|
|
@@ -821,16 +797,16 @@ export function buildProfile(profileId, response) {
|
|
|
821
797
|
return null;
|
|
822
798
|
return new Profile(profileId, user, businessLinks, businesses);
|
|
823
799
|
}
|
|
824
|
-
// One order on its own, for a caller holding a message rather than a whole response. The
|
|
825
|
-
// the business
|
|
800
|
+
// One order on its own, for a caller holding a message rather than a whole response. The caller
|
|
801
|
+
// passes the country of the business the order was placed with, since a tax travels as a raw code.
|
|
826
802
|
export function buildOrder(message, countryCode) {
|
|
827
803
|
if (message.payload.oneofKind !== "salesOrder")
|
|
828
804
|
return null;
|
|
829
805
|
return orderFromProto(message, message.payload.salesOrder, countryCode);
|
|
830
806
|
}
|
|
831
|
-
// The shopping list a cart message carries, empty when the cart was emptied. Business.cart
|
|
832
|
-
//
|
|
833
|
-
//
|
|
807
|
+
// The shopping list a cart message carries, empty when the cart was emptied. Business.cart is null
|
|
808
|
+
// then (see buildBusiness), but the callers here, createOrder and the visitor cart carried over on
|
|
809
|
+
// sign in, need a list. countryCode is the country of the cart's business, as for buildOrder.
|
|
834
810
|
export function buildShoppingList(message, countryCode) {
|
|
835
811
|
if (message.payload.oneofKind !== "partyPreferences")
|
|
836
812
|
throw new InvalidArgumentException("That message carries no party preferences, so it is not a cart");
|
|
@@ -840,28 +816,29 @@ export function buildShoppingList(message, countryCode) {
|
|
|
840
816
|
// #region What a customer pushes
|
|
841
817
|
//
|
|
842
818
|
// 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
|
|
844
|
-
//
|
|
845
|
-
// not be found here again.
|
|
846
|
-
// here has the same eTag as the one a later read brings back.
|
|
819
|
+
// rules hold for both, so a pushed message is storable here as well as on the server. The client
|
|
820
|
+
// chooses the id: a sync in returns an update number and no id, so a message the server named could
|
|
821
|
+
// not be found here again. The client also sets updatedAt and the server keeps it, so the copy
|
|
822
|
+
// stored here has the same eTag as the one a later read brings back.
|
|
847
823
|
// This customer's party preferences as a message to push, carrying the cart and optionally the
|
|
848
824
|
// fulfillment mode. An existing message is amended, since its payload can carry fields a storefront
|
|
849
825
|
// cannot read, and dropping them would overwrite that data. Its id is kept, as the server holds the
|
|
850
826
|
// 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
|
|
852
|
-
// item.
|
|
853
|
-
// fulfillmentMode null
|
|
854
|
-
// the merchant app's data. createOrder sets it to the mode of the order that
|
|
827
|
+
// A list with no items is pushed as no list, as Java serializeShoppingList returns null below one
|
|
828
|
+
// item. An emptied cart and a cart never created look the same on the wire.
|
|
829
|
+
// fulfillmentMode null keeps the existing value, the ordinary case: a storefront treats that field
|
|
830
|
+
// as the merchant app's data and leaves it alone. createOrder sets it to the mode of the order that
|
|
831
|
+
// emptied the cart.
|
|
855
832
|
export function buildPartyPreferencesMessage(messageId, existing, senderId, shoppingList, fulfillmentMode, now) {
|
|
856
833
|
const existingEntity = existing === null ? null : getEntity(existing.entity);
|
|
857
834
|
const preferences = existing !== null && existing.payload.oneofKind === "partyPreferences"
|
|
858
835
|
? existing.payload.partyPreferences
|
|
859
836
|
: proto_PartyPreferencesPayload.create();
|
|
860
|
-
// Later than the copy it replaces,
|
|
861
|
-
// the server is stored at the update number of the copy it amends, so updatedAt decides. A
|
|
862
|
-
// clock behind that copy (Constants.MAX_CLOCK_SKEW
|
|
863
|
-
// the edit and leave the customer looking at a stale cart. One millisecond past the
|
|
864
|
-
// and the server keeps what is sent, so both sides agree.
|
|
837
|
+
// Later than the copy it replaces, even if this client's clock is behind it. An amendment written
|
|
838
|
+
// without the server is stored at the update number of the copy it amends, so updatedAt decides. A
|
|
839
|
+
// client clock behind that copy (clocks do differ, see Constants.MAX_CLOCK_SKEW) would make its own
|
|
840
|
+
// store refuse the edit and leave the customer looking at a stale cart. One millisecond past the
|
|
841
|
+
// copy is enough, and the server keeps what is sent, so both sides agree.
|
|
865
842
|
const updatedAt = existingEntity === null || existingEntity.updatedAt < now ? now : existingEntity.updatedAt + 1n;
|
|
866
843
|
const partyPreferences = proto_PartyPreferencesPayload.create({
|
|
867
844
|
...preferences,
|
|
@@ -877,12 +854,12 @@ export function buildPartyPreferencesMessage(messageId, existing, senderId, shop
|
|
|
877
854
|
payload: { oneofKind: "partyPreferences", partyPreferences: partyPreferences },
|
|
878
855
|
});
|
|
879
856
|
}
|
|
880
|
-
// One order as a message to push. Its shopping list
|
|
881
|
-
// SalesOrderViewModel.saveChanges does before creating an order: the customer has
|
|
882
|
-
// and only a final list
|
|
883
|
-
// ordered. The two totals are all that remains of an order completed by an older merchant app,
|
|
884
|
-
// moved the list out of the order. They are filled from the list being sent, not from the
|
|
885
|
-
// carries, which a caller could set to anything.
|
|
857
|
+
// One order as a message to push. Its shopping list is sent as final regardless of the list's
|
|
858
|
+
// isFinal, as Java SalesOrderViewModel.saveChanges does before creating an order: the customer has
|
|
859
|
+
// finished choosing, and only a final list records a later edit as a change instead of altering what
|
|
860
|
+
// was ordered. The two totals are all that remains of an order completed by an older merchant app,
|
|
861
|
+
// which moved the list out of the order. They are filled from the list being sent, not from the
|
|
862
|
+
// counts it carries, which a caller could set to anything.
|
|
886
863
|
export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMode, deliveryAddressId, deliveryAddress, note, creationDate) {
|
|
887
864
|
return proto_Message.create({
|
|
888
865
|
entity: pushedEntity(orderId, creationDate, creationDate),
|
|
@@ -906,30 +883,30 @@ export function buildOrderMessage(orderId, senderId, shoppingList, fulfillmentMo
|
|
|
906
883
|
});
|
|
907
884
|
}
|
|
908
885
|
// 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
|
|
886
|
+
// array. This stores a push's own messages under the update numbers the server assigned, with no
|
|
910
887
|
// separate stamping step, since the response carries full entities.
|
|
911
888
|
export function messagesToCacheWrite(messages) {
|
|
912
889
|
return toCacheWrite(messages.map((message) => toWireEntity(message.entity, proto_Message.toBinary(message))));
|
|
913
890
|
}
|
|
914
|
-
// The Entity of something this client is pushing. The update number goes out as zero
|
|
915
|
-
// replaces a caller's own with the one it assigns
|
|
916
|
-
//
|
|
917
|
-
//
|
|
918
|
-
//
|
|
919
|
-
//
|
|
920
|
-
// changes nothing.
|
|
891
|
+
// The Entity of something this client is pushing. The update number goes out as zero, since the
|
|
892
|
+
// server replaces a caller's own with the one it assigns. A message not pushed yet is stored holding
|
|
893
|
+
// zero, and messageToBlob versions it from the copy it amends. updateInfo is zero too: it would be
|
|
894
|
+
// the id of the store that made the change, and the server knows no store of this client. This
|
|
895
|
+
// client does not need it: a read sends these messages back (the server leaves no one's changes out
|
|
896
|
+
// of a storefront's read), and the eTag stored here is the one the server kept, so storing what the
|
|
897
|
+
// read brings back changes nothing.
|
|
921
898
|
function pushedEntity(id, createdAt, updatedAt) {
|
|
922
899
|
return proto_Entity.create({ id: id, createdAt: createdAt, updatedAt: updatedAt });
|
|
923
900
|
}
|
|
924
901
|
// 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
|
|
926
|
-
//
|
|
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
|
|
929
|
-
// cart not picked in that order would differ from its echo and be pushed again,
|
|
930
|
-
// updatedAt for every other device.
|
|
931
|
-
//
|
|
932
|
-
// unsent local edit still
|
|
902
|
+
// lists, since a message's updatedAt differs on every call. That works because the server echoes a
|
|
903
|
+
// pushed list back with the items it was sent, so stored is what was last sent.
|
|
904
|
+
// The server echoes the items in package id order, and the list's own order is kept in each item's
|
|
905
|
+
// shoppingListOrder, which is compared with the item. So both lists are put in package id order
|
|
906
|
+
// first. Otherwise a cart not picked in that order would differ from its echo and be pushed again,
|
|
907
|
+
// which re-stamps updatedAt for every other device.
|
|
908
|
+
// Also true when stored was never pushed (updateNumber 0, left by updateCartLocalOnly), so an
|
|
909
|
+
// unsent local edit is still pushed.
|
|
933
910
|
export function hasShoppingListChanged(stored, shoppingList) {
|
|
934
911
|
if (stored === null)
|
|
935
912
|
return pushedShoppingList(shoppingList, false) !== undefined;
|
|
@@ -937,7 +914,7 @@ export function hasShoppingListChanged(stored, shoppingList) {
|
|
|
937
914
|
return true;
|
|
938
915
|
return !proto_ShoppingList.equals(inPackageIdOrder(getCartShoppingList(stored)), inPackageIdOrder(pushedShoppingList(shoppingList, false)));
|
|
939
916
|
}
|
|
940
|
-
// The same list with its items by package id, the order
|
|
917
|
+
// The same list with its items sorted by package id, the order the server sends them in
|
|
941
918
|
function inPackageIdOrder(list) {
|
|
942
919
|
if (list === undefined)
|
|
943
920
|
return undefined;
|
|
@@ -954,7 +931,7 @@ function getCartShoppingList(message) {
|
|
|
954
931
|
? message.payload.partyPreferences.shoppingList
|
|
955
932
|
: undefined;
|
|
956
933
|
}
|
|
957
|
-
// A shopping list
|
|
934
|
+
// A shopping list to push, or absent when it holds nothing - see buildPartyPreferencesMessage.
|
|
958
935
|
// Emptiness is judged by the items sent, not the counts, for the reason given at shoppingListToProto.
|
|
959
936
|
function pushedShoppingList(shoppingList, isFinal) {
|
|
960
937
|
if (shoppingList === null)
|
|
@@ -963,9 +940,9 @@ function pushedShoppingList(shoppingList, isFinal) {
|
|
|
963
940
|
return undefined;
|
|
964
941
|
return shoppingListToProto(shoppingList, isFinal);
|
|
965
942
|
}
|
|
966
|
-
// The counts
|
|
967
|
-
// protocol sends a count even where it omits the items, but a push always sends
|
|
968
|
-
//
|
|
943
|
+
// The counts are the lengths of the item arrays, not the list's own counts. A read takes them from
|
|
944
|
+
// the wire because the protocol sends a count even where it omits the items, but a push always sends
|
|
945
|
+
// the items. Counting the arrays means a pushed list cannot disagree with its contents.
|
|
969
946
|
function shoppingListToProto(shoppingList, isFinal) {
|
|
970
947
|
return proto_ShoppingList.create({
|
|
971
948
|
isFinal: isFinal,
|
|
@@ -981,8 +958,8 @@ function shoppingListToProto(shoppingList, isFinal) {
|
|
|
981
958
|
deletedItems: shoppingList.deletedItems.map(shoppingListItemToProto),
|
|
982
959
|
});
|
|
983
960
|
}
|
|
984
|
-
// The mirror of shoppingListItemFromProto. Unscaled model values
|
|
985
|
-
// is sent: amount, totalTax and finalPrice are computed from these fields on both sides.
|
|
961
|
+
// The mirror of shoppingListItemFromProto. Unscaled model values are scaled again, and nothing
|
|
962
|
+
// derived is sent: amount, totalTax and finalPrice are computed from these fields on both sides.
|
|
986
963
|
function shoppingListItemToProto(item) {
|
|
987
964
|
return proto_ShoppingListItem.create({
|
|
988
965
|
packageId: item.packageId,
|
|
@@ -1005,11 +982,11 @@ function taxToProto(tax) {
|
|
|
1005
982
|
// #endregion
|
|
1006
983
|
// #region Entity and item bookkeeping
|
|
1007
984
|
// 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.
|
|
1009
|
-
//
|
|
1010
|
-
// deleted and replaces it. An older copy sent again is lesser and
|
|
1011
|
-
// same id is greater and kept. The arrays are mutable so that
|
|
1012
|
-
// the attachment base url.
|
|
985
|
+
// is enough to sort it by entity type. Every blob is versioned by entityETag, tombstones too. The
|
|
986
|
+
// server gives a delete an update number like any other write, so a tombstone has a greater update
|
|
987
|
+
// number than any copy of what it deleted and replaces it. An older copy sent again is lesser and
|
|
988
|
+
// refused, and a later write of the same id is greater and kept. The arrays are mutable so that
|
|
989
|
+
// businessResponseToCacheWrite can add the attachment base url.
|
|
1013
990
|
function toCacheWrite(entities) {
|
|
1014
991
|
const blobs = [];
|
|
1015
992
|
for (const entity of entities)
|
|
@@ -1021,11 +998,12 @@ function toCacheWrite(entities) {
|
|
|
1021
998
|
return { blobs: blobs, deletedIds: [] };
|
|
1022
999
|
}
|
|
1023
1000
|
// What a store versions an entity's blob by: the update number, with updatedAt below it, in one
|
|
1024
|
-
// bigint so the greater
|
|
1025
|
-
// deletes included, so it rises with every change
|
|
1026
|
-
// different versions. updatedAt decides between copies of one update
|
|
1027
|
-
// wrote itself, at the update number of the one it amended, shares
|
|
1028
|
-
// bits
|
|
1001
|
+
// bigint so that the store's greater-wins rule orders versions correctly. The server assigns an
|
|
1002
|
+
// update number to every write, deletes included, so it rises with every change regardless of any
|
|
1003
|
+
// clock, and decides between different versions. updatedAt decides between copies of one update
|
|
1004
|
+
// number. Only a copy this client wrote itself, at the update number of the one it amended, shares
|
|
1005
|
+
// one. The value is wider than 64 bits. A bigint holds it exactly, and a store must compare it as a
|
|
1006
|
+
// bigint.
|
|
1029
1007
|
export function entityETag(updateNumber, updatedAt) {
|
|
1030
1008
|
if (updateNumber < 0n || updatedAt < 0n)
|
|
1031
1009
|
throw new InvalidArgumentException("An update number and an updatedAt are never negative");
|
|
@@ -1033,10 +1011,10 @@ export function entityETag(updateNumber, updatedAt) {
|
|
|
1033
1011
|
}
|
|
1034
1012
|
// A message this client wrote has no update number until a server assigns one, and has none in the
|
|
1035
1013
|
// store while unpushed (see pushedEntity). It is versioned at the update number of the copy it
|
|
1036
|
-
// amends, which heldETag carries, so
|
|
1037
|
-
// server
|
|
1038
|
-
// greater update number does. A message that amends nothing is at zero. A message
|
|
1039
|
-
// number
|
|
1014
|
+
// amends, which heldETag carries, so its updatedAt makes it the later copy of that version. If the
|
|
1015
|
+
// server sends the amended copy again, that copy does not replace it, but a later server version
|
|
1016
|
+
// with a greater update number does. A message that amends nothing is at zero. A message that has an
|
|
1017
|
+
// update number is versioned by it, as any entity is.
|
|
1040
1018
|
function messageToBlob(message, heldETag) {
|
|
1041
1019
|
const entity = getEntity(message.entity);
|
|
1042
1020
|
const updateNumber = entity.updateNumber !== 0n
|
|
@@ -1050,9 +1028,9 @@ function messageToBlob(message, heldETag) {
|
|
|
1050
1028
|
bytes: proto_Message.toBinary(message),
|
|
1051
1029
|
};
|
|
1052
1030
|
}
|
|
1053
|
-
// The messages a container holds that are deleted (isDeleted) or live.
|
|
1054
|
-
//
|
|
1055
|
-
// and is never a customer's cart. Each as stored.
|
|
1031
|
+
// The messages a container holds that are deleted (isDeleted true) or live (false). A message is
|
|
1032
|
+
// deleted according to its update number alone, regardless of its payload. A deleted message is a
|
|
1033
|
+
// completed order when it carries one, and is never a customer's cart. Each as stored.
|
|
1056
1034
|
function messagesFromBlobs(blobs, isDeleted = false) {
|
|
1057
1035
|
const result = [];
|
|
1058
1036
|
for (const blob of blobs) {
|
|
@@ -1111,12 +1089,17 @@ function getItemId(item) {
|
|
|
1111
1089
|
}
|
|
1112
1090
|
// #endregion
|
|
1113
1091
|
// #region Values
|
|
1114
|
-
// An absent optional string is null, because Java
|
|
1092
|
+
// An absent optional string is null, because Java distinguishes absent from empty
|
|
1115
1093
|
function stringFromProto(value) {
|
|
1116
1094
|
return value === undefined ? null : value;
|
|
1117
1095
|
}
|
|
1118
|
-
// The
|
|
1119
|
-
//
|
|
1096
|
+
// The enum's zero means no theme was picked. A number the enum has no entry for (a theme that a newer
|
|
1097
|
+
// app added) is kept as it came, so the caller decides what to show.
|
|
1098
|
+
function onlineStoreThemeFromProto(value) {
|
|
1099
|
+
return value === proto_OnlineStoreTheme.UNSPECIFIED_ONLINE_STORE_THEME ? null : value;
|
|
1100
|
+
}
|
|
1101
|
+
// The reverse of stringFromProto. An empty string goes out absent too: the two mean the same, and
|
|
1102
|
+
// sending one could bring the field back as the other.
|
|
1120
1103
|
function stringToProto(value) {
|
|
1121
1104
|
return value === null || value.length === 0 ? undefined : value;
|
|
1122
1105
|
}
|
|
@@ -1126,9 +1109,9 @@ function attachmentBaseUrlFromProto(urlAndToken) {
|
|
|
1126
1109
|
function addressFromProto(address) {
|
|
1127
1110
|
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);
|
|
1128
1111
|
}
|
|
1129
|
-
// The
|
|
1130
|
-
//
|
|
1131
|
-
// address is in none.
|
|
1112
|
+
// The reverse of addressFromProto, for a delivery address sent directly instead of by id - see
|
|
1113
|
+
// buildOrderMessage. id is copied from Address, though it means nothing outside the list the address
|
|
1114
|
+
// was read from, and this address is in none.
|
|
1132
1115
|
function addressToProto(address) {
|
|
1133
1116
|
return proto_Address.create({
|
|
1134
1117
|
id: address.id,
|
|
@@ -1142,7 +1125,7 @@ function addressToProto(address) {
|
|
|
1142
1125
|
countryCode: address.countryCode,
|
|
1143
1126
|
});
|
|
1144
1127
|
}
|
|
1145
|
-
// The
|
|
1128
|
+
// The reverse of addressListFromProto, for the addresses of a user sent to the server.
|
|
1146
1129
|
function addressListToProto(addressList) {
|
|
1147
1130
|
return proto_AddressList.create({
|
|
1148
1131
|
defaultAddressId: addressList.defaultAddressId,
|
|
@@ -1154,8 +1137,8 @@ function addressListFromProto(addressList) {
|
|
|
1154
1137
|
return AddressList.EMPTY;
|
|
1155
1138
|
return new AddressList(addressList.defaultAddressId, Object.freeze(addressList.addresses.map(addressFromProto)));
|
|
1156
1139
|
}
|
|
1157
|
-
// The wire carries a tax's raw code, meaningful only within a country, so every caller
|
|
1158
|
-
// the business the tax is on.
|
|
1140
|
+
// The wire carries a tax's raw code, meaningful only within a country, so every caller passes the
|
|
1141
|
+
// country of the business the tax is on.
|
|
1159
1142
|
function taxFromProto(tax, countryCode) {
|
|
1160
1143
|
return Tax.fromScaledValues(countryCode, tax.taxCode, tax.rate);
|
|
1161
1144
|
}
|
|
@@ -1205,7 +1188,7 @@ function shoppingListFromProto(shoppingList, countryCode) {
|
|
|
1205
1188
|
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))));
|
|
1206
1189
|
}
|
|
1207
1190
|
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),
|
|
1191
|
+
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), onlineStoreThemeFromProto(settings.onlineStoreTheme), settings.onlineShoppingEnabled);
|
|
1209
1192
|
}
|
|
1210
1193
|
// #endregion
|
|
1211
1194
|
// #region Entities
|
|
@@ -1245,26 +1228,26 @@ function priceFromProto(priceProto) {
|
|
|
1245
1228
|
const entity = getEntity(item.entity);
|
|
1246
1229
|
return Price.fromScaledValues(toEntityHeader(entity), item.name, stringFromProto(item.description), item.parentItemId, item.isDisabled, priceProto.packageId, priceProto.regularPrice, priceProto.offerPrice);
|
|
1247
1230
|
}
|
|
1248
|
-
function packageFromProto(packageProto, photos
|
|
1231
|
+
function packageFromProto(packageProto, photos) {
|
|
1249
1232
|
const item = getItem(packageProto.item);
|
|
1250
1233
|
const entity = getEntity(item.entity);
|
|
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
|
|
1234
|
+
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);
|
|
1252
1235
|
}
|
|
1253
|
-
function productFromProto(productProto, photos,
|
|
1236
|
+
function productFromProto(productProto, photos, countryCode) {
|
|
1254
1237
|
const item = getItem(productProto.item);
|
|
1255
1238
|
const entity = getEntity(item.entity);
|
|
1256
1239
|
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
1240
|
stringFromProto(productProto.manufacturer), productProto.manufacturerAddress === undefined
|
|
1258
1241
|
? null
|
|
1259
|
-
: addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map((tax) => taxFromProto(tax, countryCode))), Object.freeze([...productProto.features]), productProto.forOfflineSaleOnly, photos
|
|
1242
|
+
: addressFromProto(productProto.manufacturerAddress), Object.freeze(productProto.taxes.map((tax) => taxFromProto(tax, countryCode))), Object.freeze([...productProto.features]), productProto.forOfflineSaleOnly, photos);
|
|
1260
1243
|
}
|
|
1261
1244
|
function orderFromProto(message, salesOrder, countryCode) {
|
|
1262
1245
|
const entity = getEntity(message.entity);
|
|
1263
1246
|
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
1247
|
}
|
|
1265
1248
|
// 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
|
|
1267
|
-
// the server
|
|
1249
|
+
// emptied by placing an order and one never created both give null. The rest of the message is set by
|
|
1250
|
+
// the server or read elsewhere: who sent it, how far the business has read it, and the fulfillment
|
|
1268
1251
|
// mode, which buildBusiness reads as defaultFulfillmentMode.
|
|
1269
1252
|
function cartShoppingListFromProto(partyPreferences, countryCode) {
|
|
1270
1253
|
const shoppingList = partyPreferences.shoppingList;
|
|
@@ -1283,6 +1266,6 @@ function takePhotos(attachmentsByOwner, owningEntityId, attachmentBaseUrl) {
|
|
|
1283
1266
|
// #endregion
|
|
1284
1267
|
// The width of the updatedAt an eTag carries below its update number. An int64 is never wider.
|
|
1285
1268
|
const UPDATED_AT_BITS = 64n;
|
|
1286
|
-
//
|
|
1269
|
+
// The bytes of a blob that exists only for its eTag
|
|
1287
1270
|
const NO_BYTES = new Uint8Array(0);
|
|
1288
1271
|
const NO_PHOTOS = Object.freeze([]);
|