@peabody-soft/mbs-model-ts 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Attachment.js +2 -2
- package/dist/AttachmentBaseUrl.js +4 -4
- package/dist/AuthTokens.js +7 -7
- package/dist/Business.js +11 -10
- package/dist/BusinessIdentifier.js +3 -2
- package/dist/BusinessLink.js +3 -2
- package/dist/CartEditor.js +89 -81
- package/dist/Catalog.js +4 -4
- package/dist/Constants.js +2 -2
- package/dist/ContainerNames.js +6 -6
- package/dist/Entity.js +4 -3
- package/dist/Enums.js +2 -2
- package/dist/Exceptions.js +2 -2
- package/dist/GeneralSettings.js +9 -8
- package/dist/HttpStatusCodes.js +7 -7
- package/dist/Ids.js +4 -5
- package/dist/Item.js +6 -4
- package/dist/LiveData.js +2 -2
- package/dist/Measures.js +3 -2
- package/dist/Message.js +1 -1
- package/dist/MockDatabase.js +6 -5
- package/dist/Model.js +166 -162
- package/dist/Order.js +10 -12
- package/dist/PackageIdentifier.js +2 -2
- package/dist/Price.js +6 -5
- package/dist/PriceList.js +1 -1
- package/dist/Product.js +8 -6
- package/dist/Profile.js +15 -14
- package/dist/ProtoMap.js +159 -155
- package/dist/ServiceExceptions.js +6 -6
- package/dist/ServiceProxy.js +15 -14
- package/dist/ShoppingList.js +101 -100
- package/dist/SyncCache.js +76 -68
- package/dist/SyncKeys.js +5 -5
- package/dist/SyncState.js +14 -6
- package/dist/Tax.js +11 -10
- package/dist/TaxHelper.js +9 -8
- package/dist/TaxType.js +7 -6
- package/dist/UserProfile.js +7 -7
- package/dist/WorkingDays.js +21 -18
- package/dist/index.js +11 -8
- package/package.json +1 -1
package/dist/SyncCache.js
CHANGED
|
@@ -2,18 +2,19 @@ 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
|
|
6
|
-
// everything in it came from a response, so
|
|
7
|
-
// It does not write through or sync. The caller calls the Service, does the paging and
|
|
8
|
-
// writes are reads. It holds only
|
|
9
|
-
//
|
|
10
|
-
//
|
|
5
|
+
// What the server has already sent, so the next call can ask only for what changed. A read cache:
|
|
6
|
+
// everything in it came from a response, so dropping a container costs one refetch.
|
|
7
|
+
// It does not write through or sync. The caller calls the Service, does the paging, and chooses
|
|
8
|
+
// which writes are reads. It holds only wire data, which ProtoMap reads. See SYNC LAYER in
|
|
9
|
+
// ../Prompt.txt.
|
|
10
|
+
// - One response is one write. A tombstone is a blob that no older blob replaces. The server sends
|
|
11
|
+
// one for every deleted entity, children included, so the cache never infers a deletion.
|
|
11
12
|
// - Tombstones are stored as sent, so a deleted message keeps its order (a completed order is a
|
|
12
|
-
// deleted message). readDeletedBusiness, readDeletedProfile and readDeletedOrders
|
|
13
|
+
// deleted message). readDeletedBusiness, readDeletedProfile and readDeletedOrders return them.
|
|
13
14
|
// readOrders and readPartyPreferences leave them out.
|
|
14
|
-
// - A cursor is a blob per entity type (its eTag is the cursor)
|
|
15
|
-
//
|
|
16
|
-
// read, in that page's batch, and the store keeps the greater.
|
|
15
|
+
// - A cursor is a blob per entity type (its eTag is the cursor). It is not derived from the
|
|
16
|
+
// entities, because tombstones and this client's own changes would move it. It rises only with
|
|
17
|
+
// a page of a read, in that page's batch, and the store keeps the greater.
|
|
17
18
|
export default class SyncCache {
|
|
18
19
|
constructor(_database) {
|
|
19
20
|
this._database = _database;
|
|
@@ -21,7 +22,7 @@ export default class SyncCache {
|
|
|
21
22
|
}
|
|
22
23
|
// Everything cached for one business, as the response that carried it, or null before anything is
|
|
23
24
|
// written. Tombstones are included as sent: a deleted entity carries what it was, or only its header
|
|
24
|
-
// for an item.
|
|
25
|
+
// for an item. Code that builds on this must leave tombstones out, except for the order of a message.
|
|
25
26
|
async readBusiness(containerName) {
|
|
26
27
|
return ProtoMap.businessResponseFromBlobs(await this.readAll(containerName));
|
|
27
28
|
}
|
|
@@ -35,19 +36,20 @@ export default class SyncCache {
|
|
|
35
36
|
// copy wins and a repeated page changes nothing. A tombstone wins over the copies of what it deleted,
|
|
36
37
|
// since a delete is a later write. The cursors the page raises go in the same batch, so they cannot
|
|
37
38
|
// get ahead of their entities and an interrupted read resumes from its last page.
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
39
|
+
// Returns how many blobs it stored, so a caller can tell whether the response changed anything and
|
|
40
|
+
// needs publishing. The store keeps a blob only when its eTag is later, so it can reject a
|
|
41
|
+
// response in full. A rejected response leaves the cache unchanged - see MODEL.
|
|
41
42
|
async writeBusiness(containerName, response) {
|
|
42
43
|
return this.writeRead(containerName, ProtoMap.businessResponseToCacheWrite(response), SyncKeys.BUSINESS_SYNC_IN, (cursors) => ProtoMap.raiseBusinessCursors(cursors, response));
|
|
43
44
|
}
|
|
44
|
-
// Stores
|
|
45
|
-
//
|
|
45
|
+
// Stores a business response that is not a page of a read of this container, for example one merged
|
|
46
|
+
// in from another container. It moves no cursor, because this container was not read for what the
|
|
47
|
+
// response holds. Otherwise the same as writeBusiness.
|
|
46
48
|
async storeBusiness(containerName, response) {
|
|
47
49
|
return this.write(containerName, ProtoMap.businessResponseToCacheWrite(response));
|
|
48
50
|
}
|
|
49
51
|
// 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
|
|
52
|
+
// Null when there are none. The entities a deleted entity owned are tombstones too.
|
|
51
53
|
async readDeletedBusiness(containerName) {
|
|
52
54
|
return ProtoMap.deletedBusinessResponseFromBlobs(await this.readAll(containerName));
|
|
53
55
|
}
|
|
@@ -55,7 +57,7 @@ export default class SyncCache {
|
|
|
55
57
|
async readProfile(containerName) {
|
|
56
58
|
return ProtoMap.profileResponseFromBlobs(await this.readAll(containerName));
|
|
57
59
|
}
|
|
58
|
-
// The tombstones of a profile, as readDeletedBusiness
|
|
60
|
+
// The tombstones of a profile, as readDeletedBusiness returns those of a business.
|
|
59
61
|
async readDeletedProfile(containerName) {
|
|
60
62
|
return ProtoMap.deletedProfileResponseFromBlobs(await this.readAll(containerName));
|
|
61
63
|
}
|
|
@@ -66,8 +68,9 @@ export default class SyncCache {
|
|
|
66
68
|
async writeProfile(containerName, response) {
|
|
67
69
|
return this.writeRead(containerName, ProtoMap.profileResponseToCacheWrite(response), SyncKeys.PROFILE_SYNC_IN, (cursors) => ProtoMap.raiseProfileCursors(cursors, response));
|
|
68
70
|
}
|
|
69
|
-
//
|
|
70
|
-
// made, so it moves no cursor and the next read brings it back. Otherwise the same as
|
|
71
|
+
// Stores the response to a call that is not a read (a sign up, an update). It holds a change this
|
|
72
|
+
// client made, so it moves no cursor and the next read brings it back. Otherwise the same as
|
|
73
|
+
// writeProfile.
|
|
71
74
|
async storeProfile(containerName, response) {
|
|
72
75
|
return this.write(containerName, ProtoMap.profileResponseToCacheWrite(response));
|
|
73
76
|
}
|
|
@@ -80,22 +83,22 @@ export default class SyncCache {
|
|
|
80
83
|
return ProtoMap.authTokensFromBytes(blob.bytes);
|
|
81
84
|
}
|
|
82
85
|
// Written without comparison, unlike everything else here: a reissued token is not a later version of
|
|
83
|
-
// the old one, so the
|
|
84
|
-
// for why the authentication token is dropped.
|
|
86
|
+
// the old one, so the last one written wins. Stores only what this server issued - see
|
|
87
|
+
// ProtoMap.authTokensToBlob for why the authentication token is dropped.
|
|
85
88
|
async writeAuthTokens(containerName, authTokens) {
|
|
86
89
|
await this.writeBlobs(containerName, [ProtoMap.authTokensToBlob(authTokens)]);
|
|
87
90
|
}
|
|
88
|
-
// The open orders of
|
|
89
|
-
// own messages.
|
|
90
|
-
// which readDeletedOrders
|
|
91
|
-
//
|
|
91
|
+
// The open orders of the caller whose responses were written here, because a server sends a caller
|
|
92
|
+
// only their own messages. An open order is one the business has not completed. Completed orders
|
|
93
|
+
// are deleted messages, which readDeletedOrders returns. Nothing here records which caller that was
|
|
94
|
+
// or detects a change of caller. See THE MESSAGES OF A BUSINESS ARE NOT PUBLIC.
|
|
92
95
|
async readOrders(containerName) {
|
|
93
96
|
return ProtoMap.orderMessagesFromBlobs(await this.readAll(containerName));
|
|
94
97
|
}
|
|
95
98
|
// 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
|
|
97
|
-
// customer is shown the order afterwards. Each is as stored and as sent. A tombstone with
|
|
98
|
-
//
|
|
99
|
+
// order by deleting its message, so each is a tombstone. A server sends these whole, because the
|
|
100
|
+
// customer is shown the order afterwards. Each is returned as stored and as sent. A tombstone with
|
|
101
|
+
// no order in it is not included.
|
|
99
102
|
async readDeletedOrders(containerName) {
|
|
100
103
|
return ProtoMap.deletedOrderMessagesFromBlobs(await this.readAll(containerName));
|
|
101
104
|
}
|
|
@@ -105,34 +108,37 @@ export default class SyncCache {
|
|
|
105
108
|
async readMessageUpdateNumber(containerName) {
|
|
106
109
|
return (await this.readBusinessCursors(containerName)).getUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID);
|
|
107
110
|
}
|
|
108
|
-
//
|
|
109
|
-
// other would never be read back.
|
|
111
|
+
// Throws for a message that is not an order. Only the payload shows which kind a message is, and
|
|
112
|
+
// one stored as the other kind would never be read back.
|
|
110
113
|
async writeOrder(containerName, order) {
|
|
111
114
|
return this.writeOwnMessage(containerName, order, ProtoMap.orderToBlob);
|
|
112
115
|
}
|
|
113
|
-
// The party preferences message in this container, or null when there is none.
|
|
114
|
-
//
|
|
116
|
+
// The party preferences message in this container, or null when there is none. A customer has one
|
|
117
|
+
// per business, so two would mean two callers share the container, the open question described
|
|
118
|
+
// for readOrders.
|
|
115
119
|
async readPartyPreferences(containerName) {
|
|
116
120
|
return ProtoMap.partyPreferencesMessageFromBlobs(await this.readAll(containerName));
|
|
117
121
|
}
|
|
118
122
|
async writePartyPreferences(containerName, partyPreferences) {
|
|
119
123
|
return this.writeOwnMessage(containerName, partyPreferences, ProtoMap.partyPreferencesToBlob);
|
|
120
124
|
}
|
|
121
|
-
//
|
|
122
|
-
// out to hold
|
|
123
|
-
// only
|
|
124
|
-
//
|
|
125
|
-
// would never be lowered and what the new business has below them
|
|
126
|
-
// container is dropped
|
|
125
|
+
// Drops everything cached for a business: entities, cursors and tokens. Use it when a container
|
|
126
|
+
// turns out to hold a different business than the one it is named for. That can happen because a
|
|
127
|
+
// businessId only locates a business. Only the id of its Business tells two businesses apart, and
|
|
128
|
+
// cursors do not carry over from one to the other: the store keeps a blob only when its eTag is
|
|
129
|
+
// greater, so the old cursors would never be lowered and what the new business has below them
|
|
130
|
+
// would never be requested. The container is also dropped from this cache's map of fetched
|
|
131
|
+
// containers.
|
|
127
132
|
async deleteBusiness(containerName) {
|
|
128
133
|
this._containers.delete(containerName);
|
|
129
134
|
await this._database.deleteContainer(containerName);
|
|
130
135
|
}
|
|
131
|
-
//
|
|
132
|
-
// is the
|
|
133
|
-
// the container named for the membership
|
|
134
|
-
// visitor and the next customer to sign in - see Model.businessSyncIn.
|
|
135
|
-
//
|
|
136
|
+
// Deletes the party preferences message this container holds, and returns zero when there is none.
|
|
137
|
+
// It is the only delete that no tombstone caused. A cart written before anybody signed in is handed
|
|
138
|
+
// over to the container named for the membership. The copy left behind would be read by the next
|
|
139
|
+
// anonymous visitor and the next customer to sign in - see Model.businessSyncIn. A caller deletes
|
|
140
|
+
// nothing else from a container: it all came off the wire, and anything the server has not
|
|
141
|
+
// reported as deleted stays.
|
|
136
142
|
async deletePartyPreferences(containerName) {
|
|
137
143
|
const partyPreferences = await this.readPartyPreferences(containerName);
|
|
138
144
|
if (partyPreferences === null)
|
|
@@ -142,35 +148,36 @@ export default class SyncCache {
|
|
|
142
148
|
deletedIds: [ProtoMap.getMessageId(partyPreferences)],
|
|
143
149
|
});
|
|
144
150
|
}
|
|
145
|
-
//
|
|
146
|
-
// without the party preferences message it emptied. Tombstones are included, as for any
|
|
147
|
-
// off the wire. This stores a push's own messages once the server has echoed them with
|
|
148
|
-
// numbers. It checks no payload type, unlike writeOrder and writePartyPreferences
|
|
149
|
-
// message the caller
|
|
150
|
-
// catching
|
|
151
|
-
// change and the next read brings the same messages back.
|
|
151
|
+
// Stores the messages a sync in response carried, in one batch, so the cache cannot hold a pushed
|
|
152
|
+
// order without the party preferences message it emptied. Tombstones are included, as for any
|
|
153
|
+
// entity array off the wire. This stores a push's own messages once the server has echoed them with
|
|
154
|
+
// real update numbers. It checks no payload type, unlike writeOrder and writePartyPreferences.
|
|
155
|
+
// Those take a message from the caller, where storing an order as party preferences is a mistake
|
|
156
|
+
// worth catching. Here the response is taken as correct. It moves no cursor, because this client
|
|
157
|
+
// made the change and the next read brings the same messages back.
|
|
152
158
|
async writeMessages(containerName, messages) {
|
|
153
159
|
return this.write(containerName, ProtoMap.messagesToCacheWrite(messages));
|
|
154
160
|
}
|
|
155
161
|
async readAll(containerName) {
|
|
156
162
|
return (await this.getContainer(containerName)).readAll();
|
|
157
163
|
}
|
|
158
|
-
//
|
|
159
|
-
// holds
|
|
160
|
-
// amends - see ProtoMap.messageToBlob
|
|
164
|
+
// Stores a message the caller holds, not one a response carried, versioned from the copy this
|
|
165
|
+
// container holds. A message this client amended has no update number, so it is stored at that of
|
|
166
|
+
// the copy it amends - see ProtoMap.messageToBlob
|
|
161
167
|
async writeOwnMessage(containerName, message, toBlob) {
|
|
162
168
|
const held = await (await this.getContainer(containerName)).read(ProtoMap.getMessageId(message));
|
|
163
169
|
return this.writeBlobs(containerName, [toBlob(message, held?.eTag ?? null)]);
|
|
164
170
|
}
|
|
165
|
-
// For writers that store one
|
|
166
|
-
// tombstone
|
|
171
|
+
// For writers that store one blob and delete nothing (a credential or a single message), because no
|
|
172
|
+
// tombstone comes with either
|
|
167
173
|
async writeBlobs(containerName, blobs) {
|
|
168
174
|
return this.write(containerName, { blobs: blobs, deletedIds: NO_DELETES });
|
|
169
175
|
}
|
|
170
|
-
// One transaction per call, so an interrupted write
|
|
171
|
-
// The store's count covers only what it stored: a blob
|
|
172
|
-
// tombstone too) is not stored and not counted. A delete has nothing
|
|
173
|
-
// counted instead. The only delete is a caller's own - see
|
|
176
|
+
// One transaction per call, so an interrupted write can be repeated and never leaves the cache
|
|
177
|
+
// partly written. The store's count covers only what it stored: a blob that the store already holds
|
|
178
|
+
// a copy of that is as new (a tombstone too) is not stored and not counted. A delete has nothing
|
|
179
|
+
// to compare, so the ids are counted instead. The only delete is a caller's own - see
|
|
180
|
+
// deletePartyPreferences.
|
|
174
181
|
async write(containerName, cacheWrite) {
|
|
175
182
|
const blobs = cacheWrite.blobs;
|
|
176
183
|
const deletedIds = cacheWrite.deletedIds;
|
|
@@ -179,10 +186,11 @@ export default class SyncCache {
|
|
|
179
186
|
const storedCount = await (await this.getContainer(containerName)).write(blobs, deletedIds);
|
|
180
187
|
return storedCount + deletedIds.length;
|
|
181
188
|
}
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
// when another writer moved one first. That is why the
|
|
189
|
+
// Stores a page of a read: its entities and the cursors it moved, in one batch. The count is the
|
|
190
|
+
// number of entities the page changed, so the moved cursors are subtracted from the store's count.
|
|
191
|
+
// The store keeps a cursor only if it is greater. Each moved cursor is greater than the one read,
|
|
192
|
+
// so the store keeps all of them, or fewer when another writer moved one first. That is why the
|
|
193
|
+
// count is clamped at zero.
|
|
186
194
|
async writeRead(containerName, cacheWrite, cursorKeys, raiseCursors) {
|
|
187
195
|
const cursors = await this.readCursors(containerName, cursorKeys);
|
|
188
196
|
const movedKeys = raiseCursors(cursors);
|
|
@@ -198,16 +206,16 @@ export default class SyncCache {
|
|
|
198
206
|
const blobs = await Promise.all(keys.map((key) => container.read(Ids.getSyncCursorBlobId(key))));
|
|
199
207
|
return ProtoMap.cursorsFromBlobs(keys, blobs);
|
|
200
208
|
}
|
|
201
|
-
//
|
|
202
|
-
// together share one fetch.
|
|
203
|
-
//
|
|
209
|
+
// Containers are held by name, so each is fetched once. The promise is held, so two calls arriving
|
|
210
|
+
// together share one fetch. Code that deletes a container from the store must also drop it here,
|
|
211
|
+
// because this cache cannot detect that the store no longer has it.
|
|
204
212
|
getContainer(containerName) {
|
|
205
213
|
const held = this._containers.get(containerName);
|
|
206
214
|
if (held !== undefined)
|
|
207
215
|
return held;
|
|
208
216
|
const pending = this._database.getContainer(containerName);
|
|
209
217
|
this._containers.set(containerName, pending);
|
|
210
|
-
//
|
|
218
|
+
// Forget a failed fetch, or one failure would make this name fail for as long as this cache lives
|
|
211
219
|
void pending.catch(() => {
|
|
212
220
|
if (this._containers.get(containerName) === pending)
|
|
213
221
|
this._containers.delete(containerName);
|
package/dist/SyncKeys.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
-
// Which update numbers a SyncState keeps, and which entity type counts under
|
|
3
|
-
//
|
|
4
|
-
// A state
|
|
2
|
+
// Which update numbers a SyncState keeps, and which key each entity type counts under. These are
|
|
3
|
+
// facts about the protocol, so they are kept here and not in SyncState.
|
|
4
|
+
// A state throws for keys outside its set because Business arrives from both sync outs with
|
|
5
5
|
// unrelated update numbers, which must never mix.
|
|
6
6
|
// The cursors a business sync in sends: the five cursor fields of a BusinessSyncOutRequest (the
|
|
7
7
|
// business, its attachments, configurations, messages and items).
|
|
@@ -32,8 +32,8 @@ export const ITEM_ENTITY_TYPE_IDS = Object.freeze([
|
|
|
32
32
|
proto_Constants.PRICE_ENTITY_TYPE_ID,
|
|
33
33
|
proto_Constants.PRICE_LIST_ENTITY_TYPE_ID,
|
|
34
34
|
]);
|
|
35
|
-
// The key an entity counts under
|
|
36
|
-
// is the shared item number.
|
|
35
|
+
// The key an entity type counts under. There is one update number per table, so a product's
|
|
36
|
+
// number is the shared item number.
|
|
37
37
|
export function keyOf(entityTypeId) {
|
|
38
38
|
return ITEM_ENTITY_TYPE_IDS.includes(entityTypeId)
|
|
39
39
|
? proto_Constants.ITEM_ENTITY_TYPE_ID
|
package/dist/SyncState.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import { InvalidArgumentException } from "./Exceptions";
|
|
2
2
|
import * as SyncKeys from "./SyncKeys";
|
|
3
|
-
// How far one read has got: an update number per entity type
|
|
4
|
-
//
|
|
3
|
+
// How far one read has got: an update number per entity type. It is computed from what arrives and
|
|
4
|
+
// sent with the next call, which makes a read incremental. One class serves both directions; only
|
|
5
|
+
// the keys differ.
|
|
6
|
+
// Held in memory. SyncCache persists it, one blob per entity type, so a new session asks only for
|
|
7
|
+
// what changed. A container with no stored state asks from zero and is sent what it already holds.
|
|
8
|
+
// That costs bandwidth only, because the store keeps a blob only when its eTag is greater. "In" and
|
|
9
|
+
// "out" are relative to this store, the opposite of the wire's calls.
|
|
5
10
|
export default class SyncState {
|
|
6
|
-
// The keys this state accepts
|
|
11
|
+
// The keys this state accepts. This is the only difference between a business state and a
|
|
12
|
+
// profile state. Other keys throw (see SyncKeys).
|
|
7
13
|
constructor(_keys) {
|
|
8
14
|
this._keys = _keys;
|
|
9
15
|
this._updateNumbers = new Map();
|
|
10
16
|
}
|
|
11
|
-
// Zero (an unset key)
|
|
17
|
+
// Zero (an unset key) requests everything of that type from the server. A first read sends zero
|
|
18
|
+
// for every key.
|
|
12
19
|
getUpdateNumber(entityTypeId) {
|
|
13
20
|
return this._updateNumbers.get(this.checkKey(entityTypeId)) ?? 0n;
|
|
14
21
|
}
|
|
15
|
-
// Raises the cursor for this entity type and
|
|
16
|
-
//
|
|
22
|
+
// Raises the cursor for this entity type and returns whether it moved. Tombstones count too,
|
|
23
|
+
// because skipping one would refetch it for ever.
|
|
24
|
+
// A number that is not higher is ignored as stale or redelivered.
|
|
17
25
|
raiseUpdateNumber(entityTypeId, updateNumber) {
|
|
18
26
|
const key = this.checkKey(entityTypeId);
|
|
19
27
|
if (updateNumber <= (this._updateNumbers.get(key) ?? 0n))
|
package/dist/Tax.js
CHANGED
|
@@ -5,12 +5,12 @@ import { taxRateFromScaledValue } from "./Constants";
|
|
|
5
5
|
//
|
|
6
6
|
// Taxes are included in the prices a customer sees: a product's in its package prices, the
|
|
7
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
|
|
9
|
-
// for display. A Tax holds no amount.
|
|
8
|
+
// CartEditor takes them out on the way in and ShoppingListItem.unitPriceWithTaxes adds them back
|
|
9
|
+
// for display. A Tax holds no amount. The amounts are on a line item (ShoppingListItem.taxAmounts).
|
|
10
10
|
export default class Tax {
|
|
11
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
|
|
13
|
-
//
|
|
12
|
+
// a tax as its raw code, which is unique only within a country (that of the business the tax is
|
|
13
|
+
// on), so the type is made here from the two, as Java TaxType.createId does.
|
|
14
14
|
static fromScaledValues(countryCode, taxCode, scaledRate) {
|
|
15
15
|
return new Tax(createTaxType(countryCode, taxCode), taxRateFromScaledValue(scaledRate));
|
|
16
16
|
}
|
|
@@ -22,8 +22,8 @@ export default class Tax {
|
|
|
22
22
|
return this._type;
|
|
23
23
|
}
|
|
24
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
|
|
26
|
-
//
|
|
25
|
+
// TaxType.createId(country, taxCode), with the country in its high bits, so the country can be
|
|
26
|
+
// read from the value itself. Zero for TaxType.UNSPECIFIED_TAX_TYPE, as no country's code is zero.
|
|
27
27
|
get countryCode() {
|
|
28
28
|
return countryCodeOf(this._type);
|
|
29
29
|
}
|
|
@@ -36,9 +36,9 @@ export default class Tax {
|
|
|
36
36
|
return this._rate;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
39
|
+
// The total of one type of tax across a shopping list: the line a bill shows for it, "GST 5.25".
|
|
40
|
+
// Several taxes of one type, on one item or on many, are added into one. It is already part of the
|
|
41
|
+
// list's total and is not added to it.
|
|
42
42
|
export class TaxAmount {
|
|
43
43
|
constructor(_type, _amount) {
|
|
44
44
|
this._type = _type;
|
|
@@ -47,7 +47,8 @@ export class TaxAmount {
|
|
|
47
47
|
get type() {
|
|
48
48
|
return this._type;
|
|
49
49
|
}
|
|
50
|
-
// Zero is possible (a list of free items has taxes
|
|
50
|
+
// Zero is possible (a list of free items has taxes but none are due), and a bill shows no line
|
|
51
|
+
// for it
|
|
51
52
|
get amount() {
|
|
52
53
|
return this._amount;
|
|
53
54
|
}
|
package/dist/TaxHelper.js
CHANGED
|
@@ -10,11 +10,12 @@ import { taxRateToScaledValue } from "./Constants";
|
|
|
10
10
|
// (5% is 500). Floating point would round some taxes the other way. Amounts are bigint because one
|
|
11
11
|
// can reach 2^44, and multiplying by a rate overflows 2^53.
|
|
12
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
|
|
13
|
+
// Math.round). Each tax is rounded separately, because each is shown and paid separately.
|
|
14
14
|
class TaxHelper {
|
|
15
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
|
|
17
|
-
// (getTaxCodesAndRates), so here it
|
|
16
|
+
// not greater than zero is no tax. Java rejects one (validateTaxes) and drops one that arrives
|
|
17
|
+
// (getTaxCodesAndRates), so here it becomes zero, which keeps the indexes of the taxes and their
|
|
18
|
+
// amounts aligned.
|
|
18
19
|
static scaledRatesOf(taxes) {
|
|
19
20
|
return taxes.map((tax) => Number.isFinite(tax.rate) && tax.rate > 0 ? taxRateToScaledValue(tax.rate) : 0);
|
|
20
21
|
}
|
|
@@ -43,17 +44,17 @@ class TaxHelper {
|
|
|
43
44
|
result += TaxHelper.calculateTaxAmount(taxBase, taxRate);
|
|
44
45
|
return result;
|
|
45
46
|
}
|
|
46
|
-
// The inverse of addTaxes: the amount that gives this amount with taxes. Each tax is rounded
|
|
47
|
-
//
|
|
48
|
-
// addTaxes comes closest (the lower one on a tie). So
|
|
49
|
-
// rates) is always amount.
|
|
47
|
+
// The inverse of addTaxes: the amount that gives this amount with taxes. Each tax is rounded
|
|
48
|
+
// separately, so some amounts with taxes cannot be reached. In that case this returns the amount
|
|
49
|
+
// for which addTaxes comes closest (the lower one on a tie). So
|
|
50
|
+
// removeTaxes(addTaxes(amount, rates), rates) is always amount.
|
|
50
51
|
static removeTaxes(amountWithTaxes, taxRates) {
|
|
51
52
|
if (amountWithTaxes <= 0n || taxRates.length === 0)
|
|
52
53
|
return amountWithTaxes;
|
|
53
54
|
// Ignoring rounding, the amount with taxes is the amount multiplied by (1 + the total rate), so
|
|
54
55
|
// dividing by that estimates the amount. Rounding moves each tax by at most half a unit and the
|
|
55
56
|
// estimate is rounded too, so the amount is within the number of taxes of the estimate (Java
|
|
56
|
-
// looks as far).
|
|
57
|
+
// looks as far). Check every amount within that distance.
|
|
57
58
|
const estimate = TaxHelper.divideAndRound(amountWithTaxes * TaxHelper.TAX_RATE_FACTOR, TaxHelper.TAX_RATE_FACTOR + BigInt(TaxHelper.calculateTotalTaxRate(taxRates)));
|
|
58
59
|
const reach = BigInt(taxRates.length);
|
|
59
60
|
const first = estimate > reach ? estimate - reach : 0n;
|
package/dist/TaxType.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
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
2
|
// code, unique only within a country, and ProtoMap combines it with the country of the business.
|
|
3
3
|
// The values are Java TaxType.createId(country, taxCode) = (country.getNumeric() << 8) | taxCode,
|
|
4
|
-
// so each is globally unique. countryCodeOf and taxCodeOf
|
|
5
|
-
// Copied by hand from Java's TaxType
|
|
6
|
-
// (repository/test/com/merabills/repository/entities/helpers)
|
|
7
|
-
// shift changes or a type is added. A tax added there must be added here with the same
|
|
8
|
-
// library's tests pin the same values (Values.test.ts).
|
|
4
|
+
// so each is globally unique. countryCodeOf and taxCodeOf split a value into its parts.
|
|
5
|
+
// Copied by hand from Java's TaxType. Java's TaxTypeIdsTest
|
|
6
|
+
// (repository/test/com/merabills/repository/entities/helpers) pins the values: it fails when GST or
|
|
7
|
+
// the country shift changes or a type is added. A tax added there must be added here with the same
|
|
8
|
+
// value. This library's tests pin the same values (Values.test.ts).
|
|
9
9
|
export var TaxType;
|
|
10
10
|
(function (TaxType) {
|
|
11
11
|
// No country's numeric code is 0, so this cannot collide with a real type
|
|
@@ -13,7 +13,8 @@ export var TaxType;
|
|
|
13
13
|
// India (country code 356): Goods and Services Tax (tax code 0)
|
|
14
14
|
TaxType[TaxType["GST"] = 91136] = "GST";
|
|
15
15
|
})(TaxType || (TaxType = {}));
|
|
16
|
-
//
|
|
16
|
+
// The low bits of a TaxType value that hold the raw tax code, below the country (Java
|
|
17
|
+
// TaxType.COUNTRY_CODE_SHIFT)
|
|
17
18
|
const TAX_CODE_BITS = 8;
|
|
18
19
|
const TAX_CODE_MASK = (1 << TAX_CODE_BITS) - 1;
|
|
19
20
|
// Java TaxType.createId: the country in the high bits, the raw tax code in the low eight. Not
|
package/dist/UserProfile.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Entity from "./Entity";
|
|
2
|
-
// The customer
|
|
3
|
-
//
|
|
4
|
-
// Holds the User message and no derived state.
|
|
2
|
+
// The customer, whose orders and cart the business sees. Named for the database it lives in (the
|
|
3
|
+
// profile database, not the business one), because a signed-out storefront has no user but still
|
|
4
|
+
// shows its catalog. 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 customer's delivery addresses, and which is the default.
|
|
25
|
-
//
|
|
24
|
+
// The customer's delivery addresses, and which is the default. Order.deliveryAddressId is an id
|
|
25
|
+
// into this list.
|
|
26
26
|
get addresses() {
|
|
27
27
|
return this._addresses;
|
|
28
28
|
}
|
|
@@ -30,8 +30,8 @@ export default class UserProfile extends Entity {
|
|
|
30
30
|
get preferredLanguageCode() {
|
|
31
31
|
return this._preferredLanguageCode;
|
|
32
32
|
}
|
|
33
|
-
// The
|
|
34
|
-
// Computed here
|
|
33
|
+
// The first and last names separated by a space, or the first name alone when there is no last
|
|
34
|
+
// name. Computed here, not sent.
|
|
35
35
|
get formattedName() {
|
|
36
36
|
if (this._lastName === null || this._lastName.length === 0)
|
|
37
37
|
return this._firstName;
|
package/dist/WorkingDays.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
2
|
import { InvalidArgumentException } from "./Exceptions";
|
|
3
|
-
// Days and hours a business works.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Days and hours a business works. Missing hours mean open: no WorkingDays means every day
|
|
4
|
+
// (ALL_DAYS), and a day with no hours or ranges is worked all day. A day missing from days is not
|
|
5
|
+
// worked.
|
|
6
|
+
// Hours are minutes after local midnight in timeZone, so to ask about a moment, convert it to
|
|
7
|
+
// that zone first, as a day and a minute.
|
|
7
8
|
class WorkingDays {
|
|
8
9
|
constructor(_days, _timeZone, _workingHours) {
|
|
9
10
|
this._days = _days;
|
|
@@ -17,12 +18,12 @@ class WorkingDays {
|
|
|
17
18
|
}
|
|
18
19
|
// The IANA ID of the zone the hours are in, for example "Asia/Kolkata", never an abbreviation
|
|
19
20
|
// like "IST". Null when none was specified (settings saved before the protocol carried one, and
|
|
20
|
-
// ALL_DAYS). The hours then cannot be placed on a clock
|
|
21
|
-
// business's zone can
|
|
21
|
+
// ALL_DAYS). The hours then cannot be placed on a clock. Only a caller that knows the
|
|
22
|
+
// business's zone can convert a moment to a day and a minute to compare with them.
|
|
22
23
|
get timeZone() {
|
|
23
24
|
return this._timeZone;
|
|
24
25
|
}
|
|
25
|
-
// Indexed
|
|
26
|
+
// Indexed like days. Shorter than days, or empty, is normal. See the class comment for what a
|
|
26
27
|
// missing entry means.
|
|
27
28
|
get workingHours() {
|
|
28
29
|
return this._workingHours;
|
|
@@ -37,8 +38,9 @@ class WorkingDays {
|
|
|
37
38
|
WorkingDays.checkDayOfWeek(dayOfWeek);
|
|
38
39
|
return this._days[dayOfWeek] === true;
|
|
39
40
|
}
|
|
40
|
-
// The hours of a
|
|
41
|
-
//
|
|
41
|
+
// The hours of a day, or null when the day has no hours entry or its hours cover the whole day.
|
|
42
|
+
// This does not check whether the day is worked: a UI showing opening times tests isWorkingDay
|
|
43
|
+
// first, then treats null as all day.
|
|
42
44
|
getWorkingHours(dayOfWeek) {
|
|
43
45
|
WorkingDays.checkDayOfWeek(dayOfWeek);
|
|
44
46
|
if (dayOfWeek >= this._workingHours.length)
|
|
@@ -54,8 +56,8 @@ class WorkingDays {
|
|
|
54
56
|
throw new InvalidArgumentException(`Not a day of the week: ${dayOfWeek}`);
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
+
// The value for an absent WorkingDays: the business works every day, all day long. It has no
|
|
60
|
+
// hours, so it has no time zone.
|
|
59
61
|
WorkingDays.ALL_DAYS = new WorkingDays(Object.freeze([true, true, true, true, true, true, true]), null, Object.freeze([]));
|
|
60
62
|
export default WorkingDays;
|
|
61
63
|
// The worked hours of a single day, as ranges.
|
|
@@ -63,8 +65,8 @@ export class WorkingHours {
|
|
|
63
65
|
constructor(_ranges) {
|
|
64
66
|
this._ranges = _ranges;
|
|
65
67
|
}
|
|
66
|
-
// Ordered by openMinute, non-overlapping and non-touching:
|
|
67
|
-
// opens
|
|
68
|
+
// Ordered by openMinute, non-overlapping and non-touching: ranges where one closes when the
|
|
69
|
+
// next opens arrive merged into one.
|
|
68
70
|
get ranges() {
|
|
69
71
|
return this._ranges;
|
|
70
72
|
}
|
|
@@ -93,8 +95,8 @@ export class WorkingHoursRange {
|
|
|
93
95
|
constructor(_openMinute, _closeMinute) {
|
|
94
96
|
this._openMinute = _openMinute;
|
|
95
97
|
this._closeMinute = _closeMinute;
|
|
96
|
-
// Validated on
|
|
97
|
-
// producing opening times a customer would trust
|
|
98
|
+
// Validated on construction: a payload that breaks protocol rules fails the sync instead of
|
|
99
|
+
// producing opening times that a customer would trust.
|
|
98
100
|
if (!Number.isInteger(_openMinute) ||
|
|
99
101
|
_openMinute < 0 ||
|
|
100
102
|
_openMinute >= proto_Constants.MINUTES_PER_DAY)
|
|
@@ -104,12 +106,13 @@ export class WorkingHoursRange {
|
|
|
104
106
|
_closeMinute > proto_Constants.MINUTES_PER_DAY)
|
|
105
107
|
throw new InvalidArgumentException(`Work cannot stop at minute ${_closeMinute} having started at ${_openMinute}`);
|
|
106
108
|
}
|
|
107
|
-
//
|
|
109
|
+
// The first minute that is worked, from 0 to Constants.MINUTES_PER_DAY - 1.
|
|
108
110
|
get openMinute() {
|
|
109
111
|
return this._openMinute;
|
|
110
112
|
}
|
|
111
|
-
//
|
|
112
|
-
// range never spans midnight, so overnight work is two ranges on two
|
|
113
|
+
// The first minute that is not worked. MINUTES_PER_DAY means work stops at midnight. Always
|
|
114
|
+
// greater than openMinute. A range never spans midnight, so overnight work is two ranges on two
|
|
115
|
+
// days.
|
|
113
116
|
get closeMinute() {
|
|
114
117
|
return this._closeMinute;
|
|
115
118
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
// The public surface of this library.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The public surface of this library. Not exported:
|
|
2
|
+
// - Attachment (a UI gets Photo instead) and the sync layer's internal cursors.
|
|
3
|
+
// - The auth tokens, except BusinessLink.linkToken, which the server sends to a client.
|
|
4
|
+
// - ProtoMap, the wire boundary, except buildAuthTokensBytes.
|
|
5
|
+
// - The scaled-value conversions of Constants.ts (every getter is already unscaled).
|
|
6
|
+
// - TaxHelper, which does tax arithmetic in scaled values. A UI reads a tax off a ShoppingListItem,
|
|
7
|
+
// ShoppingList or CartLine.
|
|
5
8
|
export { default as Model } from "./Model";
|
|
6
9
|
export * as ContainerNames from "./ContainerNames";
|
|
7
10
|
export { buildAuthTokensBytes } from "./ProtoMap";
|
|
@@ -45,14 +48,14 @@ export { default as ShoppingList, ItemChange, ShoppingListItem } from "./Shoppin
|
|
|
45
48
|
export { default as Tax, TaxAmount } from "./Tax";
|
|
46
49
|
export { default as WorkingDays, WorkingHours, WorkingHoursRange } from "./WorkingDays";
|
|
47
50
|
// The enums of the protocol, the limits it obeys, and how an id is read. The price list a
|
|
48
|
-
// storefront sells from is exported because a UI
|
|
49
|
-
//
|
|
51
|
+
// storefront sells from is exported because a UI that builds a cart must name it, and
|
|
52
|
+
// Catalog.priceList is null until the catalog has arrived.
|
|
50
53
|
export * from "./Enums";
|
|
51
54
|
export { Constants, MERABILLS_ONLINE_PRICE_LIST_ID, buildPriceListId } from "./Constants";
|
|
52
55
|
export { default as Ids } from "./Ids";
|
|
53
56
|
// Failures. The two in Exceptions.ts are programming errors. The rest are what a call fails with
|
|
54
|
-
// and are meant to be caught: ServiceException for
|
|
55
|
-
// NetworkConnectionException for a call that
|
|
57
|
+
// and are meant to be caught: ServiceException for an error response from the server,
|
|
58
|
+
// NetworkConnectionException for a call that got no response.
|
|
56
59
|
export { IllegalOperationException, InvalidArgumentException } from "./Exceptions";
|
|
57
60
|
export { BadRequestException, ConflictException, InternalServerException, MethodNotAllowedException, NetworkConnectionException, NotFoundException, ServiceException, ServiceUnavailableException, UnauthorizedException, UpgradeRequiredException, } from "./ServiceExceptions";
|
|
58
61
|
// The mock database: a Database over a Map, so a caller can pair it with any Service. MockService
|