@peabody-soft/mbs-model-ts 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Address.js +8 -11
- package/dist/Attachment.js +7 -8
- package/dist/AttachmentBaseUrl.js +10 -10
- package/dist/AuthTokens.js +12 -7
- package/dist/Business.js +19 -19
- package/dist/BusinessIdentifier.js +2 -2
- package/dist/BusinessLink.js +16 -16
- package/dist/BusinessProfile.js +4 -4
- package/dist/CartEditor.js +137 -166
- package/dist/Catalog.js +19 -25
- package/dist/Constants.js +10 -10
- package/dist/ContainerNames.js +10 -11
- package/dist/Entity.js +11 -9
- package/dist/Enums.js +2 -2
- package/dist/Exceptions.js +2 -2
- package/dist/GeneralSettings.js +23 -21
- package/dist/HttpStatusCodes.js +10 -11
- package/dist/Ids.js +24 -30
- package/dist/Item.js +6 -7
- package/dist/LatLong.js +3 -3
- package/dist/LiveData.js +6 -6
- package/dist/Measures.js +2 -2
- package/dist/Message.js +6 -6
- package/dist/MockDatabase.js +2 -2
- package/dist/Model.js +294 -365
- package/dist/Order.js +20 -17
- package/dist/Package.js +7 -8
- package/dist/PackageIdentifier.js +2 -2
- package/dist/PaymentMethodDetail.js +4 -4
- package/dist/Photo.js +6 -7
- package/dist/Price.js +8 -9
- package/dist/PriceList.js +2 -2
- package/dist/Product.js +17 -22
- package/dist/ProductCategory.js +3 -4
- package/dist/Profile.js +26 -27
- package/dist/ProtoMap.d.ts +3 -0
- package/dist/ProtoMap.js +288 -294
- package/dist/Routes.js +3 -3
- package/dist/ServiceExceptions.js +11 -12
- package/dist/ServiceProxy.js +19 -23
- package/dist/ShoppingList.js +112 -130
- package/dist/SyncCache.d.ts +3 -0
- package/dist/SyncCache.js +92 -75
- package/dist/SyncKeys.js +8 -8
- package/dist/SyncState.js +5 -5
- package/dist/Tax.js +16 -21
- package/dist/TaxHelper.js +22 -30
- package/dist/TaxType.js +8 -9
- package/dist/UserProfile.js +4 -4
- package/dist/WorkingDays.js +15 -15
- package/dist/index.js +9 -9
- package/package.json +1 -1
package/dist/SyncCache.js
CHANGED
|
@@ -2,53 +2,63 @@ import { Constants as proto_Constants } from "./generated/Constants";
|
|
|
2
2
|
import Ids from "./Ids";
|
|
3
3
|
import * as ProtoMap from "./ProtoMap";
|
|
4
4
|
import * as SyncKeys from "./SyncKeys";
|
|
5
|
-
// What the server has already said, so
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
5
|
+
// What the server has already said, so the next call asks only for what changed. A read cache:
|
|
6
|
+
// everything in it came from a response, so giving up a container costs one refetch.
|
|
7
|
+
// It does not write through or sync. The caller calls the Service, does the paging and says which
|
|
8
|
+
// writes are reads. It holds only the wire, which ProtoMap reads. See SYNC LAYER in ../Prompt.txt.
|
|
9
|
+
// - One response is one write. A tombstone is a blob nothing older replaces. The server sends one
|
|
10
|
+
// for every deleted entity, children included, so the cache never infers a deletion.
|
|
11
|
+
// - Tombstones are stored as sent, so a deleted message keeps its order (a completed order is a
|
|
12
|
+
// deleted message). readDeletedBusiness, readDeletedProfile and readDeletedOrders answer them.
|
|
13
|
+
// readOrders and readPartyPreferences leave them out.
|
|
14
|
+
// - A cursor is a blob per entity type (its eTag is the cursor), not derived from the entities,
|
|
15
|
+
// since tombstones and this client's own changes would move it. It rises only with a page of a
|
|
16
|
+
// read, in that page's batch, and the store keeps the greater.
|
|
15
17
|
export default class SyncCache {
|
|
16
18
|
constructor(_database) {
|
|
17
19
|
this._database = _database;
|
|
18
20
|
this._containers = new Map();
|
|
19
21
|
}
|
|
20
|
-
// Everything cached for one business, as the response that carried it, or null before anything
|
|
21
|
-
// Tombstones are
|
|
22
|
+
// Everything cached for one business, as the response that carried it, or null before anything is
|
|
23
|
+
// written. Tombstones are included as sent: a deleted entity carries what it was, or only its header
|
|
24
|
+
// for an item. Whatever builds on this must leave tombstones out, except for the order of a message.
|
|
22
25
|
async readBusiness(containerName) {
|
|
23
26
|
return ProtoMap.businessResponseFromBlobs(await this.readAll(containerName));
|
|
24
27
|
}
|
|
25
|
-
// The cursors the next read
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
+
// The cursors the next read sends: the highest update number of each entity type that a page of a
|
|
29
|
+
// read brought, tombstones included, or zero for a type none has brought. Only a read moves them, so
|
|
30
|
+
// the next read brings back what this client pushed.
|
|
28
31
|
async readBusinessCursors(containerName) {
|
|
29
32
|
return this.readCursors(containerName, SyncKeys.BUSINESS_SYNC_IN);
|
|
30
33
|
}
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
// Answers how
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// rejected in full, and one that was cannot change what a later read of this container answers - see MODEL.
|
|
34
|
+
// Stores one page of a business read, a blob per entity versioned by ProtoMap.entityETag, so the later
|
|
35
|
+
// copy wins and a repeated page changes nothing. A tombstone wins over the copies of what it deleted,
|
|
36
|
+
// since a delete is a later write. The cursors the page raises go in the same batch, so they cannot
|
|
37
|
+
// get ahead of their entities and an interrupted read resumes from its last page.
|
|
38
|
+
// Answers how many blobs it stored, which tells a caller whether the response changed anything and
|
|
39
|
+
// what to publish on. The store keeps a blob only when its eTag is later, so a response can be
|
|
40
|
+
// rejected in full, and a rejected one cannot change what a later read answers - see MODEL.
|
|
39
41
|
async writeBusiness(containerName, response) {
|
|
40
42
|
return this.writeRead(containerName, ProtoMap.businessResponseToCacheWrite(response), SyncKeys.BUSINESS_SYNC_IN, (cursors) => ProtoMap.raiseBusinessCursors(cursors, response));
|
|
41
43
|
}
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
// something this container was read for. Otherwise the same as writeBusiness.
|
|
44
|
+
// Stores what a business response brought that was not read. It moves no cursor, since what is merged
|
|
45
|
+
// here is not something this container was read for. Otherwise the same as writeBusiness.
|
|
45
46
|
async storeBusiness(containerName, response) {
|
|
46
47
|
return this.write(containerName, ProtoMap.businessResponseToCacheWrite(response));
|
|
47
48
|
}
|
|
49
|
+
// The tombstones cached for one business, as a response, each as stored and as the server sent it.
|
|
50
|
+
// Null when there are none. The entities a deleted entity owned are tombstones of their own.
|
|
51
|
+
async readDeletedBusiness(containerName) {
|
|
52
|
+
return ProtoMap.deletedBusinessResponseFromBlobs(await this.readAll(containerName));
|
|
53
|
+
}
|
|
48
54
|
// The same for one profile: the customer, the businesses they may reach, and the link to each.
|
|
49
55
|
async readProfile(containerName) {
|
|
50
56
|
return ProtoMap.profileResponseFromBlobs(await this.readAll(containerName));
|
|
51
57
|
}
|
|
58
|
+
// The tombstones of a profile, as readDeletedBusiness answers those of a business.
|
|
59
|
+
async readDeletedProfile(containerName) {
|
|
60
|
+
return ProtoMap.deletedProfileResponseFromBlobs(await this.readAll(containerName));
|
|
61
|
+
}
|
|
52
62
|
async readProfileCursors(containerName) {
|
|
53
63
|
return this.readCursors(containerName, SyncKeys.PROFILE_SYNC_IN);
|
|
54
64
|
}
|
|
@@ -56,35 +66,47 @@ export default class SyncCache {
|
|
|
56
66
|
async writeProfile(containerName, response) {
|
|
57
67
|
return this.writeRead(containerName, ProtoMap.profileResponseToCacheWrite(response), SyncKeys.PROFILE_SYNC_IN, (cursors) => ProtoMap.raiseProfileCursors(cursors, response));
|
|
58
68
|
}
|
|
59
|
-
// What a call that is not a read answered with
|
|
60
|
-
// made,
|
|
69
|
+
// What a call that is not a read answered with (a sign up, an update). It is a change this client
|
|
70
|
+
// made, so it moves no cursor and the next read brings it back. Otherwise the same as writeProfile.
|
|
61
71
|
async storeProfile(containerName, response) {
|
|
62
72
|
return this.write(containerName, ProtoMap.profileResponseToCacheWrite(response));
|
|
63
73
|
}
|
|
64
|
-
// The credentials that sign calls about this container
|
|
74
|
+
// The credentials that sign calls about this container (the user token in a profile container, the
|
|
75
|
+
// link token in a business one), or null before the server issues either.
|
|
65
76
|
async readAuthTokens(containerName) {
|
|
66
77
|
const blob = await (await this.getContainer(containerName)).read(Ids.AUTH_TOKENS_BLOB_ID);
|
|
67
78
|
if (blob === null)
|
|
68
79
|
return null;
|
|
69
80
|
return ProtoMap.authTokensFromBytes(blob.bytes);
|
|
70
81
|
}
|
|
71
|
-
//
|
|
72
|
-
// Keeps only what this server issued - see ProtoMap.authTokensToBlob
|
|
82
|
+
// Written without comparison, unlike everything else here: a reissued token is not a later version of
|
|
83
|
+
// the old one, so the latest wins. Keeps only what this server issued - see ProtoMap.authTokensToBlob
|
|
84
|
+
// for why the authentication token is dropped.
|
|
73
85
|
async writeAuthTokens(containerName, authTokens) {
|
|
74
86
|
await this.writeBlobs(containerName, [ProtoMap.authTokensToBlob(authTokens)]);
|
|
75
87
|
}
|
|
76
|
-
// The orders of whoever the
|
|
77
|
-
//
|
|
88
|
+
// The open orders of whoever the written responses answered, since a server sends a caller only their
|
|
89
|
+
// own messages. Open means the business has not completed them; completed ones are deleted messages,
|
|
90
|
+
// which readDeletedOrders answers. Nothing here records which caller that was or notices when it
|
|
91
|
+
// changes. See THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
|
|
78
92
|
async readOrders(containerName) {
|
|
79
93
|
return ProtoMap.orderMessagesFromBlobs(await this.readAll(containerName));
|
|
80
94
|
}
|
|
81
|
-
// The
|
|
82
|
-
//
|
|
83
|
-
//
|
|
95
|
+
// The orders the business has completed, by fulfilling or cancelling. The merchant app completes an
|
|
96
|
+
// order by deleting its message, so each is a tombstone, but one a server sends whole because the
|
|
97
|
+
// customer is shown the order afterwards. Each is as stored and as sent. A tombstone with no order is
|
|
98
|
+
// not one of them.
|
|
99
|
+
async readDeletedOrders(containerName) {
|
|
100
|
+
return ProtoMap.deletedOrderMessagesFromBlobs(await this.readAll(containerName));
|
|
101
|
+
}
|
|
102
|
+
// The cursor a push sends: how far reads of this container have got with this customer's messages,
|
|
103
|
+
// party preferences and orders alike. It is the cursor the next read sends. What a push stores here
|
|
104
|
+
// does not move it, so a change made during the push is not skipped.
|
|
84
105
|
async readMessageUpdateNumber(containerName) {
|
|
85
106
|
return (await this.readBusinessCursors(containerName)).getUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID);
|
|
86
107
|
}
|
|
87
|
-
// Refuses a message that is not an order
|
|
108
|
+
// Refuses a message that is not an order. Only the payload says which it is, and one stored as the
|
|
109
|
+
// other would never be read back.
|
|
88
110
|
async writeOrder(containerName, order) {
|
|
89
111
|
return this.writeOwnMessage(containerName, order, ProtoMap.orderToBlob);
|
|
90
112
|
}
|
|
@@ -96,23 +118,21 @@ export default class SyncCache {
|
|
|
96
118
|
async writePartyPreferences(containerName, partyPreferences) {
|
|
97
119
|
return this.writeOwnMessage(containerName, partyPreferences, ProtoMap.partyPreferencesToBlob);
|
|
98
120
|
}
|
|
99
|
-
// Gives up everything cached for a business:
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
// The container is let go of here as well as in the store, as this cache holds each one it has fetched against its name.
|
|
121
|
+
// Gives up everything cached for a business: entities, cursors and tokens. For when a container turns
|
|
122
|
+
// out to hold another business than the one it is named for, which can happen because a businessId is
|
|
123
|
+
// only how a business is found. Only the id of its Business tells them apart, and a cursor of one is
|
|
124
|
+
// no cursor of the other: the store keeps a blob only when its eTag is greater, so the old cursors
|
|
125
|
+
// would never be lowered and what the new business has below them would never be requested. The
|
|
126
|
+
// container is dropped here too, since this cache holds each fetched container by name.
|
|
106
127
|
async deleteBusiness(containerName) {
|
|
107
128
|
this._containers.delete(containerName);
|
|
108
129
|
await this._database.deleteContainer(containerName);
|
|
109
130
|
}
|
|
110
|
-
// Drops the party preferences message this container holds, and answers zero when
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// to
|
|
115
|
-
// everything in one came off the wire, and what the server has not said is gone stays.
|
|
131
|
+
// Drops the party preferences message this container holds, and answers zero when there is none. It
|
|
132
|
+
// is the one delete no tombstone asked for. A cart written before anybody signed in is handed over to
|
|
133
|
+
// the container named for the membership, and the copy left behind would be read by the next anonymous
|
|
134
|
+
// visitor and the next customer to sign in - see Model.businessSyncIn. Nothing else in a container is
|
|
135
|
+
// a caller's to remove: it all came off the wire, and anything the server has not said is gone stays.
|
|
116
136
|
async deletePartyPreferences(containerName) {
|
|
117
137
|
const partyPreferences = await this.readPartyPreferences(containerName);
|
|
118
138
|
if (partyPreferences === null)
|
|
@@ -122,15 +142,13 @@ export default class SyncCache {
|
|
|
122
142
|
deletedIds: [ProtoMap.getMessageId(partyPreferences)],
|
|
123
143
|
});
|
|
124
144
|
}
|
|
125
|
-
// The messages a sync in response carried, in
|
|
126
|
-
// preferences message it emptied
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
// the authority on. It moves no cursor, as it is a change that this client made: the next read
|
|
133
|
-
// brings the messages back, and what is stored here is the same, so, that changes nothing.
|
|
145
|
+
// The messages a sync in response carried, in one batch, so the cache cannot hold a pushed order
|
|
146
|
+
// without the party preferences message it emptied. Tombstones are included, as for any entity array
|
|
147
|
+
// off the wire. This stores a push's own messages once the server has echoed them with real update
|
|
148
|
+
// numbers. It checks no payload type, unlike writeOrder and writePartyPreferences: those take a
|
|
149
|
+
// message the caller was given, where an order stored as party preferences is a mistake worth
|
|
150
|
+
// catching, and here the response is the authority. It moves no cursor, since this client made the
|
|
151
|
+
// change and the next read brings the same messages back.
|
|
134
152
|
async writeMessages(containerName, messages) {
|
|
135
153
|
return this.write(containerName, ProtoMap.messagesToCacheWrite(messages));
|
|
136
154
|
}
|
|
@@ -138,22 +156,21 @@ export default class SyncCache {
|
|
|
138
156
|
return (await this.getContainer(containerName)).readAll();
|
|
139
157
|
}
|
|
140
158
|
// A message the caller holds rather than one a response carried, versioned from what this container
|
|
141
|
-
// holds of it
|
|
142
|
-
//
|
|
159
|
+
// holds of it. One this client amended has no update number, so it is stored at that of the copy it
|
|
160
|
+
// amends - see ProtoMap.messageToBlob
|
|
143
161
|
async writeOwnMessage(containerName, message, toBlob) {
|
|
144
162
|
const held = await (await this.getContainer(containerName)).read(ProtoMap.getMessageId(message));
|
|
145
163
|
return this.writeBlobs(containerName, [toBlob(message, held?.eTag ?? null)]);
|
|
146
164
|
}
|
|
147
|
-
// For
|
|
148
|
-
//
|
|
165
|
+
// For writers that store one thing and delete nothing (a credential or a single message), since no
|
|
166
|
+
// tombstone arrives for either on its own
|
|
149
167
|
async writeBlobs(containerName, blobs) {
|
|
150
168
|
return this.write(containerName, { blobs: blobs, deletedIds: NO_DELETES });
|
|
151
169
|
}
|
|
152
|
-
// One transaction per call,
|
|
153
|
-
// The
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// delete there is is a caller's own - see deletePartyPreferences.
|
|
170
|
+
// One transaction per call, so an interrupted write repeats instead of leaving the cache part written.
|
|
171
|
+
// The store's count covers only what it stored: a blob it already holds a copy of that is as new (a
|
|
172
|
+
// tombstone too) is not stored and not counted. A delete has nothing to compare, so the ids are
|
|
173
|
+
// counted instead. The only delete is a caller's own - see deletePartyPreferences.
|
|
157
174
|
async write(containerName, cacheWrite) {
|
|
158
175
|
const blobs = cacheWrite.blobs;
|
|
159
176
|
const deletedIds = cacheWrite.deletedIds;
|
|
@@ -162,11 +179,10 @@ export default class SyncCache {
|
|
|
162
179
|
const storedCount = await (await this.getContainer(containerName)).write(blobs, deletedIds);
|
|
163
180
|
return storedCount + deletedIds.length;
|
|
164
181
|
}
|
|
165
|
-
// A page of a read: what it brought
|
|
166
|
-
// page
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
// is why the count cannot go below zero.
|
|
182
|
+
// A page of a read: what it brought and the cursors it moved, in one batch. The count is what the
|
|
183
|
+
// page's entities changed, so the cursors are subtracted. The store keeps a cursor only where it is
|
|
184
|
+
// greater, and each moved one is greater than the one read, so it stores the moved cursors or fewer,
|
|
185
|
+
// when another writer moved one first. That is why the count is clamped at zero.
|
|
170
186
|
async writeRead(containerName, cacheWrite, cursorKeys, raiseCursors) {
|
|
171
187
|
const cursors = await this.readCursors(containerName, cursorKeys);
|
|
172
188
|
const movedKeys = raiseCursors(cursors);
|
|
@@ -182,8 +198,9 @@ export default class SyncCache {
|
|
|
182
198
|
const blobs = await Promise.all(keys.map((key) => container.read(Ids.getSyncCursorBlobId(key))));
|
|
183
199
|
return ProtoMap.cursorsFromBlobs(keys, blobs);
|
|
184
200
|
}
|
|
185
|
-
// Held
|
|
186
|
-
//
|
|
201
|
+
// Held by name, so each container is fetched once. The promise is held, so two calls arriving
|
|
202
|
+
// together share one fetch. Whoever deletes a container from the store must drop it here too, since
|
|
203
|
+
// nothing here can tell that it no longer matches the store.
|
|
187
204
|
getContainer(containerName) {
|
|
188
205
|
const held = this._containers.get(containerName);
|
|
189
206
|
if (held !== undefined)
|
package/dist/SyncKeys.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
-
// Which update numbers a SyncState keeps, and
|
|
3
|
-
//
|
|
4
|
-
// A state refuses keys outside its set
|
|
5
|
-
// unrelated update numbers,
|
|
6
|
-
// The cursors a business sync in sends: the
|
|
7
|
-
// its messages and
|
|
2
|
+
// Which update numbers a SyncState keeps, and which entity type counts under which key. Kept here
|
|
3
|
+
// since these are facts about the protocol, not about counting.
|
|
4
|
+
// A state refuses keys outside its set because Business arrives from both sync outs with
|
|
5
|
+
// unrelated update numbers, which must never mix.
|
|
6
|
+
// The cursors a business sync in sends: the five cursor fields of a BusinessSyncOutRequest (the
|
|
7
|
+
// business, its attachments, configurations, messages and items).
|
|
8
8
|
export const BUSINESS_SYNC_IN = Object.freeze([
|
|
9
9
|
proto_Constants.BUSINESS_ENTITY_TYPE_ID,
|
|
10
10
|
proto_Constants.ATTACHMENT_ENTITY_TYPE_ID,
|
|
@@ -12,8 +12,8 @@ export const BUSINESS_SYNC_IN = Object.freeze([
|
|
|
12
12
|
proto_Constants.MESSAGE_ENTITY_TYPE_ID,
|
|
13
13
|
proto_Constants.ITEM_ENTITY_TYPE_ID,
|
|
14
14
|
]);
|
|
15
|
-
// The cursors a profile sync in sends
|
|
16
|
-
// BUSINESS also appears above
|
|
15
|
+
// The cursors a profile sync in sends: the three entity types Java keeps in a profile database.
|
|
16
|
+
// BUSINESS also appears above with an unrelated number, so never mix the two.
|
|
17
17
|
export const PROFILE_SYNC_IN = Object.freeze([
|
|
18
18
|
proto_Constants.USER_ENTITY_TYPE_ID,
|
|
19
19
|
proto_Constants.BUSINESS_ENTITY_TYPE_ID,
|
package/dist/SyncState.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { InvalidArgumentException } from "./Exceptions";
|
|
2
2
|
import * as SyncKeys from "./SyncKeys";
|
|
3
|
-
// How far one read has got
|
|
4
|
-
//
|
|
3
|
+
// How far one read has got: an update number per entity type, computed from what arrives and sent back on the next call, which makes a read incremental. One class for both directions; only the keys differ.
|
|
4
|
+
// Held in memory. SyncCache persists it, one blob per entity type, so a new session asks only for what changed. A container with none asks from zero and is sent what it already holds, which costs bandwidth only, since the store keeps a blob only when its eTag is greater. "In" and "out" are relative to this store, the opposite of the wire's calls.
|
|
5
5
|
export default class SyncState {
|
|
6
|
-
// The keys this state accepts, which is the whole difference between a business state and a profile one.
|
|
6
|
+
// The keys this state accepts, which is the whole difference between a business state and a profile one. Others are refused (see SyncKeys).
|
|
7
7
|
constructor(_keys) {
|
|
8
8
|
this._keys = _keys;
|
|
9
9
|
this._updateNumbers = new Map();
|
|
@@ -12,8 +12,8 @@ export default class SyncState {
|
|
|
12
12
|
getUpdateNumber(entityTypeId) {
|
|
13
13
|
return this._updateNumbers.get(this.checkKey(entityTypeId)) ?? 0n;
|
|
14
14
|
}
|
|
15
|
-
// Raises the cursor for this entity type
|
|
16
|
-
//
|
|
15
|
+
// Raises the cursor for this entity type and reports whether it moved. Tombstones count too, since skipping one would refetch it for ever.
|
|
16
|
+
// A lower number is ignored as stale or redelivered.
|
|
17
17
|
raiseUpdateNumber(entityTypeId, updateNumber) {
|
|
18
18
|
const key = this.checkKey(entityTypeId);
|
|
19
19
|
if (updateNumber <= (this._updateNumbers.get(key) ?? 0n))
|
package/dist/Tax.js
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { countryCodeOf, createTaxType, taxCodeOf } from "./TaxType";
|
|
2
2
|
import { taxRateFromScaledValue } from "./Constants";
|
|
3
|
-
// A tax and its rate,
|
|
4
|
-
//
|
|
3
|
+
// A tax and its rate, on a product or a shopping-list line. The rate is a percentage (18 for 18%),
|
|
4
|
+
// though the wire carries it scaled by Constants.TAX_RATE_FACTOR.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// charge in the charge
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// display (see ShoppingListItem.unitPriceWithTaxes).
|
|
11
|
-
// Deliberately doesn't compute tax on an amount - only a line item knows the amount, and does
|
|
12
|
-
// that itself.
|
|
6
|
+
// Taxes are included in the prices a customer sees: a product's in its package prices, the
|
|
7
|
+
// delivery charge's in the charge. A shopping list item stores its price without taxes, so
|
|
8
|
+
// CartEditor takes them out on the way in and ShoppingListItem.unitPriceWithTaxes puts them back
|
|
9
|
+
// for display. A Tax holds no amount. Only a line item knows one.
|
|
13
10
|
export default class Tax {
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// the same way Java TaxType.createId does.
|
|
11
|
+
// For ProtoMap, which sees a rate still multiplied by Constants.TAX_RATE_FACTOR. The wire carries
|
|
12
|
+
// a tax as its raw code, which means something only within a country (that of the business the
|
|
13
|
+
// tax is on), so the type is made here from the two, as Java TaxType.createId does.
|
|
18
14
|
static fromScaledValues(countryCode, taxCode, scaledRate) {
|
|
19
15
|
return new Tax(createTaxType(countryCode, taxCode), taxRateFromScaledValue(scaledRate));
|
|
20
16
|
}
|
|
@@ -25,10 +21,9 @@ export default class Tax {
|
|
|
25
21
|
get type() {
|
|
26
22
|
return this._type;
|
|
27
23
|
}
|
|
28
|
-
// The country this tax belongs to, ISO 3166-1 numeric. A TaxType value is
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
// Zero for TaxType.UNSPECIFIED_TAX_TYPE, no country's code being zero.
|
|
24
|
+
// The country this tax belongs to, ISO 3166-1 numeric. A TaxType value is Java
|
|
25
|
+
// TaxType.createId(country, taxCode), with the country in its high bits, so the country comes
|
|
26
|
+
// out of the value itself. Zero for TaxType.UNSPECIFIED_TAX_TYPE, as no country's code is zero.
|
|
32
27
|
get countryCode() {
|
|
33
28
|
return countryCodeOf(this._type);
|
|
34
29
|
}
|
|
@@ -41,9 +36,9 @@ export default class Tax {
|
|
|
41
36
|
return this._rate;
|
|
42
37
|
}
|
|
43
38
|
}
|
|
44
|
-
// How much one
|
|
45
|
-
// 5.25". Several taxes of one type, on one item or on many, are added into one. Always
|
|
46
|
-
//
|
|
39
|
+
// How much one type of tax comes to across a shopping list: the line a bill shows for it, "GST
|
|
40
|
+
// 5.25". Several taxes of one type, on one item or on many, are added into one. Always included in
|
|
41
|
+
// the list's total, never an extra on top.
|
|
47
42
|
export class TaxAmount {
|
|
48
43
|
constructor(_type, _amount) {
|
|
49
44
|
this._type = _type;
|
|
@@ -52,7 +47,7 @@ export class TaxAmount {
|
|
|
52
47
|
get type() {
|
|
53
48
|
return this._type;
|
|
54
49
|
}
|
|
55
|
-
// Zero is possible
|
|
50
|
+
// Zero is possible (a list of free items has taxes and owes none), and a bill shows no line for it
|
|
56
51
|
get amount() {
|
|
57
52
|
return this._amount;
|
|
58
53
|
}
|
package/dist/TaxHelper.js
CHANGED
|
@@ -1,27 +1,20 @@
|
|
|
1
1
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
2
|
import { taxRateToScaledValue } from "./Constants";
|
|
3
|
-
// The arithmetic of taxes,
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// The arithmetic of taxes, ported from Java com.merabills.repository.entities.helpers.TaxHelper to
|
|
4
|
+
// give the same answer to the unit. The only place in this library that works out a tax, adds it to
|
|
5
|
+
// an amount or takes it out. Not on the public surface (see index.ts): a UI reads unscaled taxes
|
|
6
|
+
// off a ShoppingListItem, ShoppingList or CartLine.
|
|
6
7
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// Only integer arithmetic is used: a tax is the tax base multiplied by the rate, and the division
|
|
15
|
-
// by TAX_RATE_FACTOR and the rounding (half up, as Math.round) are the last steps. Each tax is
|
|
16
|
-
// rounded on its own, as each is shown (and paid) on its own.
|
|
17
|
-
//
|
|
18
|
-
// Not on the public surface (see index.ts): a UI reads a tax off a ShoppingListItem, a ShoppingList
|
|
19
|
-
// or a CartLine, which have done this arithmetic already, in unscaled numbers.
|
|
8
|
+
// Unlike the rest of this library it works on scaled integers, as Java does: an amount is a bigint
|
|
9
|
+
// multiplied by Constants.AMOUNT_FACTOR, a rate a number multiplied by Constants.TAX_RATE_FACTOR
|
|
10
|
+
// (5% is 500). Floating point would round some taxes the other way. Amounts are bigint because one
|
|
11
|
+
// can reach 2^44, and multiplying by a rate overflows 2^53.
|
|
12
|
+
// A tax is the tax base times the rate, divided by TAX_RATE_FACTOR and rounded half up (as
|
|
13
|
+
// Math.round). Each tax is rounded on its own, as each is shown and paid on its own.
|
|
20
14
|
class TaxHelper {
|
|
21
|
-
// The rates of these taxes
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// nothing, and the taxes stay in step with the amounts worked out for them.
|
|
15
|
+
// The rates of these taxes as the arithmetic below takes them: scaled, in the same order. A rate
|
|
16
|
+
// not greater than zero is no tax. Java refuses one (validateTaxes) and drops one that arrives
|
|
17
|
+
// (getTaxCodesAndRates), so here it is zero, which keeps the taxes in step with their amounts.
|
|
25
18
|
static scaledRatesOf(taxes) {
|
|
26
19
|
return taxes.map((tax) => Number.isFinite(tax.rate) && tax.rate > 0 ? taxRateToScaledValue(tax.rate) : 0);
|
|
27
20
|
}
|
|
@@ -50,18 +43,17 @@ class TaxHelper {
|
|
|
50
43
|
result += TaxHelper.calculateTaxAmount(taxBase, taxRate);
|
|
51
44
|
return result;
|
|
52
45
|
}
|
|
53
|
-
// The inverse of addTaxes: the amount
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// amount.
|
|
46
|
+
// The inverse of addTaxes: the amount that gives this amount with taxes. Each tax is rounded on
|
|
47
|
+
// its own, so some amounts with taxes cannot be reached. Then this answers the amount for which
|
|
48
|
+
// addTaxes comes closest (the lower one on a tie). So removeTaxes(addTaxes(amount, rates),
|
|
49
|
+
// rates) is always amount.
|
|
58
50
|
static removeTaxes(amountWithTaxes, taxRates) {
|
|
59
51
|
if (amountWithTaxes <= 0n || taxRates.length === 0)
|
|
60
52
|
return amountWithTaxes;
|
|
61
53
|
// Ignoring rounding, the amount with taxes is the amount multiplied by (1 + the total rate), so
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
54
|
+
// dividing by that estimates the amount. Rounding moves each tax by at most half a unit and the
|
|
55
|
+
// estimate is rounded too, so the amount is within the number of taxes of the estimate (Java
|
|
56
|
+
// looks as far). Look at every amount that close.
|
|
65
57
|
const estimate = TaxHelper.divideAndRound(amountWithTaxes * TaxHelper.TAX_RATE_FACTOR, TaxHelper.TAX_RATE_FACTOR + BigInt(TaxHelper.calculateTotalTaxRate(taxRates)));
|
|
66
58
|
const reach = BigInt(taxRates.length);
|
|
67
59
|
const first = estimate > reach ? estimate - reach : 0n;
|
|
@@ -77,8 +69,8 @@ class TaxHelper {
|
|
|
77
69
|
}
|
|
78
70
|
return result;
|
|
79
71
|
}
|
|
80
|
-
// Divides by a positive denominator and rounds half up,
|
|
81
|
-
//
|
|
72
|
+
// Divides by a positive denominator and rounds half up, as Math.round does. A bigint division
|
|
73
|
+
// truncates towards zero, so the floor is taken by hand.
|
|
82
74
|
static divideAndRound(numerator, denominator) {
|
|
83
75
|
const doubled = numerator * 2n + denominator;
|
|
84
76
|
const divisor = denominator * 2n;
|
package/dist/TaxType.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
// The name of a tax, for a UI to show. Not part of the wire protocol: a Tax is sent as its raw tax
|
|
2
|
-
// code,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// library pin the same values (Values.test.ts).
|
|
2
|
+
// code, unique only within a country, and ProtoMap combines it with the country of the business.
|
|
3
|
+
// The values are Java TaxType.createId(country, taxCode) = (country.getNumeric() << 8) | taxCode,
|
|
4
|
+
// so each is globally unique. countryCodeOf and taxCodeOf take one apart.
|
|
5
|
+
// Copied by hand from Java's TaxType and pinned there by TaxTypeIdsTest
|
|
6
|
+
// (repository/test/com/merabills/repository/entities/helpers), which fails when GST or the country
|
|
7
|
+
// shift changes or a type is added. A tax added there must be added here with the same value. This
|
|
8
|
+
// library's tests pin the same values (Values.test.ts).
|
|
10
9
|
export var TaxType;
|
|
11
10
|
(function (TaxType) {
|
|
12
|
-
// No country's numeric code is
|
|
11
|
+
// No country's numeric code is 0, so this cannot collide with a real type
|
|
13
12
|
TaxType[TaxType["UNSPECIFIED_TAX_TYPE"] = 0] = "UNSPECIFIED_TAX_TYPE";
|
|
14
13
|
// India (country code 356): Goods and Services Tax (tax code 0)
|
|
15
14
|
TaxType[TaxType["GST"] = 91136] = "GST";
|
package/dist/UserProfile.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Entity from "./Entity";
|
|
2
2
|
// The customer: whose orders and cart the business sees. Named for the database it lives in
|
|
3
|
-
// (profile, not business)
|
|
4
|
-
//
|
|
3
|
+
// (profile, not business), since a signed-out storefront has no user but still shows its catalog.
|
|
4
|
+
// Holds the User message and no derived state.
|
|
5
5
|
export default class UserProfile extends Entity {
|
|
6
6
|
constructor(header, _firstName, _lastName, _mobilePhoneNumber, _addresses, _preferredLanguageCode) {
|
|
7
7
|
super(header);
|
|
@@ -21,8 +21,8 @@ export default class UserProfile extends Entity {
|
|
|
21
21
|
get mobilePhoneNumber() {
|
|
22
22
|
return this._mobilePhoneNumber;
|
|
23
23
|
}
|
|
24
|
-
// The delivery addresses
|
|
25
|
-
//
|
|
24
|
+
// The customer's delivery addresses, and which is the default. Also resolves
|
|
25
|
+
// Order.deliveryAddressId, an id into this list.
|
|
26
26
|
get addresses() {
|
|
27
27
|
return this._addresses;
|
|
28
28
|
}
|