@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.
Files changed (64) hide show
  1. package/dist/Attachment.js +2 -2
  2. package/dist/AttachmentBaseUrl.js +4 -4
  3. package/dist/AuthTokens.js +7 -7
  4. package/dist/Business.js +11 -10
  5. package/dist/BusinessIdentifier.js +3 -2
  6. package/dist/BusinessLink.js +3 -2
  7. package/dist/CartEditor.js +89 -81
  8. package/dist/Catalog.d.ts +19 -7
  9. package/dist/Catalog.js +121 -41
  10. package/dist/Constants.d.ts +1 -0
  11. package/dist/Constants.js +7 -5
  12. package/dist/ContainerNames.js +6 -6
  13. package/dist/Entity.js +4 -3
  14. package/dist/Enums.d.ts +1 -0
  15. package/dist/Enums.js +3 -2
  16. package/dist/Exceptions.js +2 -2
  17. package/dist/GeneralSettings.d.ts +5 -4
  18. package/dist/GeneralSettings.js +17 -15
  19. package/dist/HttpStatusCodes.js +7 -7
  20. package/dist/Ids.js +4 -5
  21. package/dist/Item.js +8 -5
  22. package/dist/LiveData.js +2 -2
  23. package/dist/Measures.js +3 -2
  24. package/dist/Message.js +1 -1
  25. package/dist/MockDatabase.js +6 -5
  26. package/dist/Model.js +166 -162
  27. package/dist/Order.js +10 -12
  28. package/dist/Package.d.ts +6 -2
  29. package/dist/Package.js +10 -5
  30. package/dist/PackageIdentifier.js +2 -2
  31. package/dist/Price.js +6 -5
  32. package/dist/PriceList.d.ts +13 -0
  33. package/dist/PriceList.js +34 -2
  34. package/dist/Product.d.ts +6 -2
  35. package/dist/Product.js +15 -10
  36. package/dist/Profile.js +15 -14
  37. package/dist/ProtoMap.js +189 -206
  38. package/dist/ServiceExceptions.js +6 -6
  39. package/dist/ServiceProxy.js +15 -14
  40. package/dist/ShoppingList.js +102 -100
  41. package/dist/SyncCache.js +76 -68
  42. package/dist/SyncKeys.js +5 -5
  43. package/dist/SyncState.js +14 -6
  44. package/dist/Tax.js +11 -10
  45. package/dist/TaxHelper.js +9 -8
  46. package/dist/TaxType.js +7 -6
  47. package/dist/UserProfile.js +7 -7
  48. package/dist/WorkingDays.js +21 -18
  49. package/dist/generated/BusinessSyncOutResponse.d.ts +4 -1
  50. package/dist/generated/Constants.d.ts +0 -4
  51. package/dist/generated/Constants.js +0 -4
  52. package/dist/generated/GeneralSettings.d.ts +5 -4
  53. package/dist/generated/GeneralSettings.js +8 -6
  54. package/dist/generated/Item.d.ts +4 -3
  55. package/dist/generated/Message.d.ts +5 -0
  56. package/dist/generated/OnlineStoreTheme.d.ts +33 -0
  57. package/dist/generated/OnlineStoreTheme.js +38 -0
  58. package/dist/generated/PriceList.d.ts +3 -2
  59. package/dist/generated/ProfileSyncOutRequest.d.ts +16 -15
  60. package/dist/generated/ProfileSyncOutResponse.d.ts +4 -6
  61. package/dist/generated/SalesOrderPayload.d.ts +6 -3
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +11 -8
  64. 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 in a blob of its
102
- // own (it belongs to the business, not to an entity).
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
- // 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.
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 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.
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. It compares two copies of one thing held at different ids, which
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 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.
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, 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.
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 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.
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 a sign in is one message.
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 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
+ // 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 and asks for
337
- // everything again. That costs bandwidth only, since the store keeps a blob only when its eTag is
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 answers the
349
- // keys that moved. Only a read moves them. What this client pushed does not.
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, and one
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
- // holding the order and a still full cart. messageUpdateNumber is how far reads of this container
356
- // have got with messages - see SyncCache.readMessageUpdateNumber.
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 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.
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 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.
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 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.
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 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 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 tombstone is not the business, which is what the join is answered with
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, which must not be taken for one: a
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: it is stable, and the url
445
- // friendly id is only how it was first found.
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: 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
+ // 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 tells absent from empty.
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
- // 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.
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 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.
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
- // length reads.
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
- // A page that renewed the url means a later one is now held; otherwise what was sent still stands
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 that asked for the join does it once.
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 asks this of every page,
605
- // and getBusinessResponseEntities would encode each entity to answer it.
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 answers the keys that moved, one per table
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 answers whether it moved. A tombstone counts, or the
632
- // server would send it again for ever.
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
- // 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.
697
- const pricesByPackage = new Map();
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
- const price = priceFromProto(priceProto);
702
- if (price.priceListId !== MERABILLS_ONLINE_PRICE_LIST_ID)
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
- if (!pricesByPackage.has(price.packageId))
705
- pricesByPackage.set(price.packageId, price);
710
+ const priceList = priceListFromProto(priceListProto);
711
+ UpdatableEntity.setParent(priceList, business);
712
+ priceLists.push(priceList);
706
713
  }
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.
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 packageId = getItemId(packageProto.item);
715
- const packagePhotos = takePhotos(attachmentsByOwner, packageId, attachmentBaseUrl);
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
- // 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.
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 productId = getItemId(productProto.item);
736
- const packages = packagesByProduct.get(productId) ?? [];
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
- // 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.
757
- let priceList = null;
758
- for (const priceListProto of response.priceLists) {
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 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.
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 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.
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. It answers null, the same as never having had a cart.
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 country of
825
- // the business it was placed with is the caller's to give, since a tax travels as a raw code.
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 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.
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 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.
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 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.
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, 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.
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 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.
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 just assigned, with no
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: 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.
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 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.
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 in which the server sends them
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 on its way out, or absent when it holds nothing - see buildPartyPreferencesMessage.
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 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.
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 go back scaled, and nothing derived
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. 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.
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 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.
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 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.
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. 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.
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 tells absent from empty
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 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.
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 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.
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 way back, for the addresses of a user sent to the server.
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 names that of
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), stringFromProto(settings.onlineStoreThemeId), settings.onlineShoppingEnabled);
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, price) {
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, price);
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, packages, countryCode) {
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, packages);
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 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
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
- // What a blob that is only its eTag holds
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([]);