@peabody-soft/mbs-model-ts 1.4.0 → 1.5.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/CartEditor.d.ts +10 -5
- package/dist/CartEditor.js +232 -125
- package/dist/Catalog.js +26 -18
- package/dist/Enums.d.ts +1 -1
- package/dist/Enums.js +2 -1
- package/dist/GeneralSettings.d.ts +4 -2
- package/dist/GeneralSettings.js +18 -7
- package/dist/Ids.d.ts +2 -0
- package/dist/Ids.js +10 -0
- package/dist/Message.js +3 -2
- package/dist/Model.d.ts +5 -0
- package/dist/Model.js +263 -50
- package/dist/Package.js +2 -2
- package/dist/Price.js +6 -2
- package/dist/Product.d.ts +5 -3
- package/dist/Product.js +30 -18
- package/dist/Profile.d.ts +1 -0
- package/dist/Profile.js +91 -4
- package/dist/ProtoMap.d.ts +15 -8
- package/dist/ProtoMap.js +265 -139
- package/dist/ShoppingList.d.ts +30 -1
- package/dist/ShoppingList.js +345 -35
- package/dist/SyncCache.d.ts +9 -0
- package/dist/SyncCache.js +93 -30
- package/dist/SyncState.js +1 -1
- package/dist/Tax.d.ts +12 -4
- package/dist/Tax.js +33 -4
- package/dist/TaxHelper.d.ts +12 -0
- package/dist/TaxHelper.js +93 -0
- package/dist/TaxType.d.ts +7 -0
- package/dist/TaxType.js +32 -0
- package/dist/generated/AttachmentType.d.ts +19 -1
- package/dist/generated/AttachmentType.js +18 -0
- package/dist/generated/AuthTokens.d.ts +3 -1
- package/dist/generated/BusinessLink.d.ts +4 -1
- package/dist/generated/BusinessSyncInRequest.d.ts +11 -3
- package/dist/generated/BusinessSyncInResponse.d.ts +5 -4
- package/dist/generated/BusinessSyncOutRequest.d.ts +37 -10
- package/dist/generated/BusinessSyncOutRequest.js +42 -29
- package/dist/generated/BusinessSyncOutResponse.d.ts +14 -1
- package/dist/generated/Configuration.d.ts +2 -1
- package/dist/generated/CreateUserRequest.d.ts +8 -2
- package/dist/generated/Entity.d.ts +7 -3
- package/dist/generated/GeneralSettings.d.ts +14 -4
- package/dist/generated/GeneralSettings.js +9 -1
- package/dist/generated/Message.d.ts +4 -2
- package/dist/generated/Product.d.ts +10 -1
- package/dist/generated/Product.js +9 -1
- package/dist/generated/ProfileSyncOutRequest.d.ts +17 -1
- package/dist/generated/ProfileSyncOutResponse.d.ts +8 -3
- package/dist/generated/SalesOrderPayload.d.ts +7 -2
- package/dist/generated/ShoppingList.d.ts +6 -3
- package/dist/generated/ShoppingListItem.d.ts +21 -6
- package/dist/generated/Tax.d.ts +4 -4
- package/dist/generated/Tax.js +7 -8
- package/dist/generated/WorkingHoursRange.d.ts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +4 -3
- package/package.json +55 -55
- package/dist/generated/TaxType.d.ts +0 -25
- package/dist/generated/TaxType.js +0 -30
package/dist/SyncCache.js
CHANGED
|
@@ -1,38 +1,64 @@
|
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
1
2
|
import Ids from "./Ids";
|
|
2
3
|
import * as ProtoMap from "./ProtoMap";
|
|
4
|
+
import * as SyncKeys from "./SyncKeys";
|
|
3
5
|
// What the server has already said, so that the next call need only ask for what has changed since. A read cache and nothing else: everything in it came out of a response, so giving a container up costs one refetch.
|
|
4
|
-
// Not write through, not an identity (it stamps nothing) and not a sync - a caller calls the Service and hands the response in, which is why the
|
|
5
|
-
// One response is one write:
|
|
6
|
-
//
|
|
7
|
-
// server sends a tombstone for every entity it deleted, children included - so
|
|
8
|
-
// infers a deletion from another one
|
|
9
|
-
//
|
|
6
|
+
// Not write through, not an identity (it stamps nothing) and not a sync - a caller calls the Service and hands the response in, which is why the paging, and which writes are reads, are the caller's. It holds the wire and only the wire, and ProtoMap reads every field. See SYNC LAYER in ../Prompt.txt.
|
|
7
|
+
// One response is one write: every entity it brought, and every tombstone, is stored in a single
|
|
8
|
+
// batch. A tombstone is kept, as a blob that nothing older replaces, and there is nothing to work out
|
|
9
|
+
// beyond that, because the server sends a tombstone for every entity it deleted, children included - so
|
|
10
|
+
// this cache never infers a deletion from another one.
|
|
11
|
+
// How far a container has been read is kept too, as a blob of its own for each entity type and not
|
|
12
|
+
// worked out from the entities: a read that came back with tombstones, or changes made by this
|
|
13
|
+
// client, would otherwise move it. It moves only with a page of a read, in the batch of the page that
|
|
14
|
+
// moved it, and only upwards: each blob is versioned by its cursor, so the store keeps the greater.
|
|
10
15
|
export default class SyncCache {
|
|
11
16
|
constructor(_database) {
|
|
12
17
|
this._database = _database;
|
|
13
18
|
this._containers = new Map();
|
|
14
19
|
}
|
|
15
20
|
// Everything cached for one business, as the response that carried it, or null before anything has been written for it.
|
|
21
|
+
// Tombstones are among it, as the entities they are of, and whatever builds on this has to leave them out.
|
|
16
22
|
async readBusiness(containerName) {
|
|
17
23
|
return ProtoMap.businessResponseFromBlobs(await this.readAll(containerName));
|
|
18
24
|
}
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
// The cursors the next read of this business sends, which is how far the reads of it have got: the
|
|
26
|
+
// highest update number of every entity type that a page of one brought, tombstones included, and zero for a
|
|
27
|
+
// type that none has. Nothing else moves them, so, what this client pushes is read back by the next read.
|
|
28
|
+
async readBusinessCursors(containerName) {
|
|
29
|
+
return this.readCursors(containerName, SyncKeys.BUSINESS_SYNC_IN);
|
|
30
|
+
}
|
|
31
|
+
// Takes in one page of a business read, one blob per entity versioned by ProtoMap.entityETag, so the later of two copies wins and a page that arrives twice changes nothing the second time.
|
|
32
|
+
// A tombstone is stored the same way, and wins over the copies of what it is of, a delete being a later write than any of them. The cursors are raised
|
|
33
|
+
// by the page, tombstones included, in the same batch: they cannot get ahead of the entities they
|
|
34
|
+
// describe, and an interrupted read repeats itself from where its last page got to.
|
|
35
|
+
// Answers how much of the page it kept - the blobs stored, which is what the container now differs
|
|
36
|
+
// by. That is how a caller tells a response that changed something from one that changed nothing,
|
|
37
|
+
// and what it publishes on: the store keeps a blob only when its eTag is later, so a response can be
|
|
38
|
+
// rejected in full, and one that was cannot change what a later read of this container answers - see MODEL.
|
|
28
39
|
async writeBusiness(containerName, response) {
|
|
40
|
+
return this.writeRead(containerName, ProtoMap.businessResponseToCacheWrite(response), SyncKeys.BUSINESS_SYNC_IN, (cursors) => ProtoMap.raiseBusinessCursors(cursors, response));
|
|
41
|
+
}
|
|
42
|
+
// Takes in what a business response brought that was not read, which moves no cursor: it is how
|
|
43
|
+
// far the container has been read that this does not change, and what is merged in here is not
|
|
44
|
+
// something this container was read for. Otherwise the same as writeBusiness.
|
|
45
|
+
async storeBusiness(containerName, response) {
|
|
29
46
|
return this.write(containerName, ProtoMap.businessResponseToCacheWrite(response));
|
|
30
47
|
}
|
|
31
48
|
// The same for one profile: the customer, the businesses they may reach, and the link to each.
|
|
32
49
|
async readProfile(containerName) {
|
|
33
50
|
return ProtoMap.profileResponseFromBlobs(await this.readAll(containerName));
|
|
34
51
|
}
|
|
52
|
+
async readProfileCursors(containerName) {
|
|
53
|
+
return this.readCursors(containerName, SyncKeys.PROFILE_SYNC_IN);
|
|
54
|
+
}
|
|
55
|
+
// One page of a profile read - see writeBusiness
|
|
35
56
|
async writeProfile(containerName, response) {
|
|
57
|
+
return this.writeRead(containerName, ProtoMap.profileResponseToCacheWrite(response), SyncKeys.PROFILE_SYNC_IN, (cursors) => ProtoMap.raiseProfileCursors(cursors, response));
|
|
58
|
+
}
|
|
59
|
+
// What a call that is not a read answered with - a sign up, an update - which is a change this client
|
|
60
|
+
// made, and so, moves no cursor: the next read brings it back. Otherwise the same as writeProfile.
|
|
61
|
+
async storeProfile(containerName, response) {
|
|
36
62
|
return this.write(containerName, ProtoMap.profileResponseToCacheWrite(response));
|
|
37
63
|
}
|
|
38
64
|
// The credentials that sign calls about this container - the user token in a profile container, the link token in a business one - and null before the server has issued either.
|
|
@@ -52,15 +78,15 @@ export default class SyncCache {
|
|
|
52
78
|
async readOrders(containerName) {
|
|
53
79
|
return ProtoMap.orderMessagesFromBlobs(await this.readAll(containerName));
|
|
54
80
|
}
|
|
55
|
-
// The cursor a push sends: the
|
|
56
|
-
//
|
|
57
|
-
//
|
|
81
|
+
// The cursor a push sends: how far the reads of this container have got with this customer's
|
|
82
|
+
// messages, party preferences and orders alike - the cursor the next read sends for them. What a push
|
|
83
|
+
// stores here does not move it, so that a change made while it was being made is not skipped.
|
|
58
84
|
async readMessageUpdateNumber(containerName) {
|
|
59
|
-
return
|
|
85
|
+
return (await this.readBusinessCursors(containerName)).getUpdateNumber(proto_Constants.MESSAGE_ENTITY_TYPE_ID);
|
|
60
86
|
}
|
|
61
87
|
// Refuses a message that is not an order, since only the payload says which it is: one stored as the other would never be read back.
|
|
62
88
|
async writeOrder(containerName, order) {
|
|
63
|
-
return this.
|
|
89
|
+
return this.writeOwnMessage(containerName, order, ProtoMap.orderToBlob);
|
|
64
90
|
}
|
|
65
91
|
// The party preferences message in this container, or null when there is none. Two mean two
|
|
66
92
|
// callers, which is the same open question as the orders above.
|
|
@@ -68,7 +94,18 @@ export default class SyncCache {
|
|
|
68
94
|
return ProtoMap.partyPreferencesMessageFromBlobs(await this.readAll(containerName));
|
|
69
95
|
}
|
|
70
96
|
async writePartyPreferences(containerName, partyPreferences) {
|
|
71
|
-
return this.
|
|
97
|
+
return this.writeOwnMessage(containerName, partyPreferences, ProtoMap.partyPreferencesToBlob);
|
|
98
|
+
}
|
|
99
|
+
// Gives up everything cached for a business: the entities, the cursors that say how far they were read, and the tokens
|
|
100
|
+
// that were filed with them. For when what a container holds turns out to be of another business than the one it is
|
|
101
|
+
// named for - which a businessId, being only how a business is found, can come to be. It cannot be told apart from the
|
|
102
|
+
// business that the same name now finds by anything but the id of its Business, and a cursor of the one is no cursor
|
|
103
|
+
// of the other: this cache keeps a blob only when its eTag is the greater, so, the cursors of the old would never be
|
|
104
|
+
// lowered to the numbers of the new, and what the new has below them would never be asked for.
|
|
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.
|
|
106
|
+
async deleteBusiness(containerName) {
|
|
107
|
+
this._containers.delete(containerName);
|
|
108
|
+
await this._database.deleteContainer(containerName);
|
|
72
109
|
}
|
|
73
110
|
// Drops the party preferences message this container holds, and answers zero when it holds none.
|
|
74
111
|
// The one delete here that no tombstone asked for, and the only reason it exists: one written
|
|
@@ -82,35 +119,41 @@ export default class SyncCache {
|
|
|
82
119
|
return 0;
|
|
83
120
|
return this.write(containerName, {
|
|
84
121
|
blobs: NO_BLOBS,
|
|
85
|
-
deletedIds: [ProtoMap.
|
|
122
|
+
deletedIds: [ProtoMap.getMessageId(partyPreferences)],
|
|
86
123
|
});
|
|
87
124
|
}
|
|
88
125
|
// The messages a sync in response carried, in a single batch - a pushed order and the party
|
|
89
126
|
// preferences message it emptied belong in one write, so that this cache cannot come to hold the
|
|
90
127
|
// order without the emptying. Tombstones included, the same as any other entity array read off
|
|
91
128
|
// the wire: this is what a push's own messages are stored as once the server has echoed them back
|
|
92
|
-
// with real update numbers
|
|
93
|
-
// carried lands here. Neither is checked against a payload type, unlike the two writers above -
|
|
129
|
+
// with real update numbers. Neither is checked against a payload type, unlike the two writers above -
|
|
94
130
|
// those are for a caller handing in a message it was given, where storing an order as party
|
|
95
131
|
// preferences would be a mistake worth catching, and this is for messages the response itself is
|
|
96
|
-
// the authority on.
|
|
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.
|
|
97
134
|
async writeMessages(containerName, messages) {
|
|
98
135
|
return this.write(containerName, ProtoMap.messagesToCacheWrite(messages));
|
|
99
136
|
}
|
|
100
137
|
async readAll(containerName) {
|
|
101
138
|
return (await this.getContainer(containerName)).readAll();
|
|
102
139
|
}
|
|
140
|
+
// A message the caller holds rather than one a response carried, versioned from what this container
|
|
141
|
+
// holds of it: one this client amended has no update number of its own, and is stored at the update
|
|
142
|
+
// number of the copy it amends - see ProtoMap.messageToBlob
|
|
143
|
+
async writeOwnMessage(containerName, message, toBlob) {
|
|
144
|
+
const held = await (await this.getContainer(containerName)).read(ProtoMap.getMessageId(message));
|
|
145
|
+
return this.writeBlobs(containerName, [toBlob(message, held?.eTag ?? null)]);
|
|
146
|
+
}
|
|
103
147
|
// For the writers that store one thing and delete nothing - a credential or a single message,
|
|
104
148
|
// none of which a tombstone ever arrives for on its own
|
|
105
149
|
async writeBlobs(containerName, blobs) {
|
|
106
150
|
return this.write(containerName, { blobs: blobs, deletedIds: NO_DELETES });
|
|
107
151
|
}
|
|
108
152
|
// One transaction per call, which is what makes an interrupted write repeat itself rather than leave the cache part written.
|
|
109
|
-
// The count the store answers with covers only what it stored: a
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// republishes once for nothing rather than going on showing an entity the server has said is gone.
|
|
153
|
+
// The count the store answers with covers only what it stored: a blob it already holds a copy of that
|
|
154
|
+
// is as new, a tombstone it already holds included, is not stored, and is not counted. A delete is
|
|
155
|
+
// unconditional, so there is nothing for it to compare, and the ids are counted instead: the only
|
|
156
|
+
// delete there is is a caller's own - see deletePartyPreferences.
|
|
114
157
|
async write(containerName, cacheWrite) {
|
|
115
158
|
const blobs = cacheWrite.blobs;
|
|
116
159
|
const deletedIds = cacheWrite.deletedIds;
|
|
@@ -119,6 +162,26 @@ export default class SyncCache {
|
|
|
119
162
|
const storedCount = await (await this.getContainer(containerName)).write(blobs, deletedIds);
|
|
120
163
|
return storedCount + deletedIds.length;
|
|
121
164
|
}
|
|
165
|
+
// A page of a read: what it brought, and the cursors it moved, in one batch. The cursors are not what the
|
|
166
|
+
// page brought, so they are not counted: the count is what the entities of it changed. The store
|
|
167
|
+
// keeps a cursor only where it is the greater, which it is of the one that was read, so what it
|
|
168
|
+
// stored of them is the cursors that moved - or fewer, where another writer moved one first, which
|
|
169
|
+
// is why the count cannot go below zero.
|
|
170
|
+
async writeRead(containerName, cacheWrite, cursorKeys, raiseCursors) {
|
|
171
|
+
const cursors = await this.readCursors(containerName, cursorKeys);
|
|
172
|
+
const movedKeys = raiseCursors(cursors);
|
|
173
|
+
const blobs = [...cacheWrite.blobs, ...ProtoMap.cursorsToBlobs(cursors, movedKeys)];
|
|
174
|
+
const deletedIds = cacheWrite.deletedIds;
|
|
175
|
+
if (blobs.length === 0 && deletedIds.length === 0)
|
|
176
|
+
return 0;
|
|
177
|
+
const storedCount = await (await this.getContainer(containerName)).write(blobs, deletedIds);
|
|
178
|
+
return Math.max(0, storedCount - movedKeys.size) + deletedIds.length;
|
|
179
|
+
}
|
|
180
|
+
async readCursors(containerName, keys) {
|
|
181
|
+
const container = await this.getContainer(containerName);
|
|
182
|
+
const blobs = await Promise.all(keys.map((key) => container.read(Ids.getSyncCursorBlobId(key))));
|
|
183
|
+
return ProtoMap.cursorsFromBlobs(keys, blobs);
|
|
184
|
+
}
|
|
122
185
|
// Held against their names, so each container is fetched once however often it is used. The promise is held rather than the container, so two calls arriving together share one fetch.
|
|
123
186
|
// A caller that deletes a container from the store gives up this cache with it: nothing here can tell that what it holds is no longer what the store would hand out.
|
|
124
187
|
getContainer(containerName) {
|
package/dist/SyncState.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InvalidArgumentException } from "./Exceptions";
|
|
2
2
|
import * as SyncKeys from "./SyncKeys";
|
|
3
3
|
// How far one read has got, as an update number per entity type computed from what arrives and sent back on the next call, which is what makes a read incremental. One class for both directions - only the keys differ.
|
|
4
|
-
//
|
|
4
|
+
// A value held in memory: SyncCache keeps it between sessions, one blob per entity type, so that a new session asks only for what changed. A container that holds none asks from zero and is sent what it already holds, which costs bandwidth alone, since the store keeps a blob only when its eTag is the greater. "In" and "out" name the direction relative to this store, the opposite way round to the calls of the wire.
|
|
5
5
|
export default class SyncState {
|
|
6
6
|
// The keys this state accepts, which is the whole difference between a business state and a profile one. Anything else is refused, because a Business arrives from both calls with an unrelated update number in each.
|
|
7
7
|
constructor(_keys) {
|
package/dist/Tax.d.ts
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import { TaxType
|
|
1
|
+
import { TaxType } from "./TaxType";
|
|
2
2
|
export default class Tax {
|
|
3
3
|
private readonly _type;
|
|
4
4
|
private readonly _rate;
|
|
5
|
-
static fromScaledValues(
|
|
6
|
-
constructor(_type:
|
|
7
|
-
get type():
|
|
5
|
+
static fromScaledValues(countryCode: number, taxCode: number, scaledRate: number): Tax;
|
|
6
|
+
constructor(_type: TaxType, _rate: number);
|
|
7
|
+
get type(): TaxType;
|
|
8
8
|
get countryCode(): number;
|
|
9
|
+
get taxCode(): number;
|
|
9
10
|
get rate(): number;
|
|
10
11
|
}
|
|
12
|
+
export declare class TaxAmount {
|
|
13
|
+
private readonly _type;
|
|
14
|
+
private readonly _amount;
|
|
15
|
+
constructor(_type: TaxType, _amount: number);
|
|
16
|
+
get type(): TaxType;
|
|
17
|
+
get amount(): number;
|
|
18
|
+
}
|
package/dist/Tax.js
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
+
import { countryCodeOf, createTaxType, taxCodeOf } from "./TaxType";
|
|
1
2
|
import { taxRateFromScaledValue } from "./Constants";
|
|
2
3
|
// A tax and its rate, applying to a product or a shopping-list line. Rate is exposed as a
|
|
3
4
|
// percentage (18 for 18%), though the wire carries it scaled by Constants.TAX_RATE_FACTOR.
|
|
5
|
+
//
|
|
6
|
+
// The taxes of a product are INCLUDED in the prices of its packages, and the taxes of the delivery
|
|
7
|
+
// charge in the charge: nothing is added to a price a customer is shown. A shopping list item is
|
|
8
|
+
// the other way round - it stores its price WITHOUT taxes and works its taxes out on top - which is
|
|
9
|
+
// why a price has its taxes taken out on the way into an item (see CartEditor) and put back for
|
|
10
|
+
// display (see ShoppingListItem.unitPriceWithTaxes).
|
|
4
11
|
// Deliberately doesn't compute tax on an amount - only a line item knows the amount, and does
|
|
5
12
|
// that itself.
|
|
6
13
|
export default class Tax {
|
|
7
14
|
// Only ProtoMap calls this: it is the one place that sees a rate still multiplied by
|
|
8
|
-
// Constants.TAX_RATE_FACTOR.
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
// Constants.TAX_RATE_FACTOR. The wire carries a tax as its raw code, which means something only
|
|
16
|
+
// within a country - that of the business the tax is on - so the type is made here from the two,
|
|
17
|
+
// the same way Java TaxType.createId does.
|
|
18
|
+
static fromScaledValues(countryCode, taxCode, scaledRate) {
|
|
19
|
+
return new Tax(createTaxType(countryCode, taxCode), taxRateFromScaledValue(scaledRate));
|
|
11
20
|
}
|
|
12
21
|
constructor(_type, _rate) {
|
|
13
22
|
this._type = _type;
|
|
@@ -21,10 +30,30 @@ export default class Tax {
|
|
|
21
30
|
// country comes back out of the value itself rather than having to be carried alongside it.
|
|
22
31
|
// Zero for TaxType.UNSPECIFIED_TAX_TYPE, no country's code being zero.
|
|
23
32
|
get countryCode() {
|
|
24
|
-
return this._type
|
|
33
|
+
return countryCodeOf(this._type);
|
|
34
|
+
}
|
|
35
|
+
// The raw tax code, which is all the wire carries of a tax: the low eight bits of the type
|
|
36
|
+
get taxCode() {
|
|
37
|
+
return taxCodeOf(this._type);
|
|
25
38
|
}
|
|
26
39
|
// A percentage: 18 means 18%
|
|
27
40
|
get rate() {
|
|
28
41
|
return this._rate;
|
|
29
42
|
}
|
|
30
43
|
}
|
|
44
|
+
// How much one TYPE of tax comes to across a shopping list - the line a bill shows for it, "GST
|
|
45
|
+
// 5.25". Several taxes of one type, on one item or on many, are added into one. Always a tax that
|
|
46
|
+
// is included in the total the list comes to, never an extra on top of it.
|
|
47
|
+
export class TaxAmount {
|
|
48
|
+
constructor(_type, _amount) {
|
|
49
|
+
this._type = _type;
|
|
50
|
+
this._amount = _amount;
|
|
51
|
+
}
|
|
52
|
+
get type() {
|
|
53
|
+
return this._type;
|
|
54
|
+
}
|
|
55
|
+
// Zero is possible - a list of free items has taxes and owes none - and a bill shows no line for it
|
|
56
|
+
get amount() {
|
|
57
|
+
return this._amount;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import Tax from "./Tax";
|
|
2
|
+
export default class TaxHelper {
|
|
3
|
+
static scaledRatesOf(taxes: ReadonlyArray<Tax>): ReadonlyArray<number>;
|
|
4
|
+
static calculateTaxAmount(taxBase: bigint, taxRate: number): bigint;
|
|
5
|
+
static calculateTaxes(taxBase: bigint, taxRates: ReadonlyArray<number>): Array<bigint>;
|
|
6
|
+
static calculateTotalTaxRate(taxRates: ReadonlyArray<number>): number;
|
|
7
|
+
static addTaxes(taxBase: bigint, taxRates: ReadonlyArray<number>): bigint;
|
|
8
|
+
static removeTaxes(amountWithTaxes: bigint, taxRates: ReadonlyArray<number>): bigint;
|
|
9
|
+
private static divideAndRound;
|
|
10
|
+
private static abs;
|
|
11
|
+
private static readonly TAX_RATE_FACTOR;
|
|
12
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
+
import { taxRateToScaledValue } from "./Constants";
|
|
3
|
+
// The arithmetic of taxes, which is Java com.merabills.repository.entities.helpers.TaxHelper and
|
|
4
|
+
// is meant to give the same answer to the unit. It is the only place in this library where a tax is
|
|
5
|
+
// worked out, added to an amount or taken out of one.
|
|
6
|
+
//
|
|
7
|
+
// It works on SCALED values, not the unscaled numbers the rest of this library speaks: an amount is
|
|
8
|
+
// a bigint multiplied by Constants.AMOUNT_FACTOR, and a rate is a number multiplied by
|
|
9
|
+
// Constants.TAX_RATE_FACTOR, so 5% is 500. Java does the same in longs and ints, and rounding a
|
|
10
|
+
// tax in floating point instead would round some of them the other way, which is the difference
|
|
11
|
+
// between a bill that agrees with the merchant's and one that is a paisa out. A bigint rather than
|
|
12
|
+
// a number because an amount can be as large as 2^44 and a product with a rate overflows 2^53.
|
|
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.
|
|
20
|
+
class TaxHelper {
|
|
21
|
+
// The rates of these taxes the way the arithmetic below takes them: scaled, and in the same
|
|
22
|
+
// order. A rate that is not greater than zero is no tax at all - Java refuses one (validateTaxes)
|
|
23
|
+
// and drops one that arrives (getTaxCodesAndRates) - so it is a zero here, adding and removing
|
|
24
|
+
// nothing, and the taxes stay in step with the amounts worked out for them.
|
|
25
|
+
static scaledRatesOf(taxes) {
|
|
26
|
+
return taxes.map((tax) => Number.isFinite(tax.rate) && tax.rate > 0 ? taxRateToScaledValue(tax.rate) : 0);
|
|
27
|
+
}
|
|
28
|
+
// One tax on a tax base (the amount before taxes)
|
|
29
|
+
static calculateTaxAmount(taxBase, taxRate) {
|
|
30
|
+
return TaxHelper.divideAndRound(taxBase * BigInt(taxRate), TaxHelper.TAX_RATE_FACTOR);
|
|
31
|
+
}
|
|
32
|
+
// Each of the taxes with these rates on a tax base, in the order of the rates
|
|
33
|
+
static calculateTaxes(taxBase, taxRates) {
|
|
34
|
+
return taxRates.map((taxRate) => TaxHelper.calculateTaxAmount(taxBase, taxRate));
|
|
35
|
+
}
|
|
36
|
+
// The sum of these rates (zero when there are none), scaled
|
|
37
|
+
static calculateTotalTaxRate(taxRates) {
|
|
38
|
+
let result = 0;
|
|
39
|
+
for (const taxRate of taxRates)
|
|
40
|
+
result += taxRate;
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
// The tax base plus the taxes with these rates on it. Taxes only apply to a tax base greater
|
|
44
|
+
// than zero.
|
|
45
|
+
static addTaxes(taxBase, taxRates) {
|
|
46
|
+
if (taxBase <= 0n)
|
|
47
|
+
return taxBase;
|
|
48
|
+
let result = taxBase;
|
|
49
|
+
for (const taxRate of taxRates)
|
|
50
|
+
result += TaxHelper.calculateTaxAmount(taxBase, taxRate);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
// The inverse of addTaxes: the amount the taxes with these rates have to be added to for the
|
|
54
|
+
// result to be this amount with taxes. Each tax is rounded on its own, so some amounts with taxes
|
|
55
|
+
// cannot be reached; this then answers the amount for which addTaxes comes closest (the lower
|
|
56
|
+
// one, when two are equally close). So removeTaxes(addTaxes(amount, rates), rates) is always
|
|
57
|
+
// amount.
|
|
58
|
+
static removeTaxes(amountWithTaxes, taxRates) {
|
|
59
|
+
if (amountWithTaxes <= 0n || taxRates.length === 0)
|
|
60
|
+
return amountWithTaxes;
|
|
61
|
+
// Ignoring rounding, the amount with taxes is the amount multiplied by (1 + the total rate), so
|
|
62
|
+
// estimate the amount by dividing by that. Rounding moves each tax by at most half a unit, and
|
|
63
|
+
// the estimate is itself rounded, so the amount looked for is within the number of taxes of the
|
|
64
|
+
// estimate (Java looks as far): look at every amount that close.
|
|
65
|
+
const estimate = TaxHelper.divideAndRound(amountWithTaxes * TaxHelper.TAX_RATE_FACTOR, TaxHelper.TAX_RATE_FACTOR + BigInt(TaxHelper.calculateTotalTaxRate(taxRates)));
|
|
66
|
+
const reach = BigInt(taxRates.length);
|
|
67
|
+
const first = estimate > reach ? estimate - reach : 0n;
|
|
68
|
+
let result = estimate;
|
|
69
|
+
let smallestDifference = null;
|
|
70
|
+
for (let candidate = first; candidate <= estimate + reach; ++candidate) {
|
|
71
|
+
const difference = TaxHelper.abs(TaxHelper.addTaxes(candidate, taxRates) - amountWithTaxes);
|
|
72
|
+
// Strictly smaller, so of two equally close the lower amount (reached first) is kept
|
|
73
|
+
if (smallestDifference === null || difference < smallestDifference) {
|
|
74
|
+
smallestDifference = difference;
|
|
75
|
+
result = candidate;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
}
|
|
80
|
+
// Divides by a positive denominator and rounds half up, exactly as Math.round does. A bigint
|
|
81
|
+
// division truncates towards zero, so the floor is taken by hand.
|
|
82
|
+
static divideAndRound(numerator, denominator) {
|
|
83
|
+
const doubled = numerator * 2n + denominator;
|
|
84
|
+
const divisor = denominator * 2n;
|
|
85
|
+
const quotient = doubled / divisor;
|
|
86
|
+
return doubled % divisor !== 0n && doubled < 0n ? quotient - 1n : quotient;
|
|
87
|
+
}
|
|
88
|
+
static abs(value) {
|
|
89
|
+
return value < 0n ? -value : value;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
TaxHelper.TAX_RATE_FACTOR = BigInt(proto_Constants.TAX_RATE_FACTOR);
|
|
93
|
+
export default TaxHelper;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare enum TaxType {
|
|
2
|
+
UNSPECIFIED_TAX_TYPE = 0,
|
|
3
|
+
GST = 91136
|
|
4
|
+
}
|
|
5
|
+
export declare function createTaxType(countryCode: number, taxCode: number): TaxType;
|
|
6
|
+
export declare function countryCodeOf(type: TaxType): number;
|
|
7
|
+
export declare function taxCodeOf(type: TaxType): number;
|
package/dist/TaxType.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
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, which is unique only within a country, and ProtoMap combines it with the country of the
|
|
3
|
+
// business the tax is on. The values are Java TaxType.createId(country, taxCode) =
|
|
4
|
+
// (country.getNumeric() << 8) | taxCode, so each is globally unique; the country is recovered with
|
|
5
|
+
// countryCodeOf and the raw tax code with taxCodeOf. They are copied from Java's TaxType by hand, and
|
|
6
|
+
// pinned there by TaxTypeIdsTest (repository/test/com/merabills/repository/entities/helpers), which
|
|
7
|
+
// fails when GST or the shift of the country changes, or a type is added, until this file has
|
|
8
|
+
// followed - so a tax added there must be added here with the same value. The tests of this
|
|
9
|
+
// library pin the same values (Values.test.ts).
|
|
10
|
+
export var TaxType;
|
|
11
|
+
(function (TaxType) {
|
|
12
|
+
// No country's numeric code is ever 0, so this can never collide with a real type
|
|
13
|
+
TaxType[TaxType["UNSPECIFIED_TAX_TYPE"] = 0] = "UNSPECIFIED_TAX_TYPE";
|
|
14
|
+
// India (country code 356): Goods and Services Tax (tax code 0)
|
|
15
|
+
TaxType[TaxType["GST"] = 91136] = "GST";
|
|
16
|
+
})(TaxType || (TaxType = {}));
|
|
17
|
+
// Bits of a TaxType value the raw tax code takes, below the country - Java TaxType.COUNTRY_CODE_SHIFT
|
|
18
|
+
const TAX_CODE_BITS = 8;
|
|
19
|
+
const TAX_CODE_MASK = (1 << TAX_CODE_BITS) - 1;
|
|
20
|
+
// Java TaxType.createId: the country in the high bits, the raw tax code in the low eight. Not
|
|
21
|
+
// restricted to the TaxType members, since a country's tax this library has no name for is still a tax.
|
|
22
|
+
export function createTaxType(countryCode, taxCode) {
|
|
23
|
+
return ((countryCode << TAX_CODE_BITS) | taxCode);
|
|
24
|
+
}
|
|
25
|
+
// The country a type belongs to, ISO 3166-1 numeric; zero for UNSPECIFIED_TAX_TYPE
|
|
26
|
+
export function countryCodeOf(type) {
|
|
27
|
+
return type >> TAX_CODE_BITS;
|
|
28
|
+
}
|
|
29
|
+
// The raw tax code of a type: the low eight bits
|
|
30
|
+
export function taxCodeOf(type) {
|
|
31
|
+
return type & TAX_CODE_MASK;
|
|
32
|
+
}
|
|
@@ -154,5 +154,23 @@ export declare enum AttachmentType {
|
|
|
154
154
|
/**
|
|
155
155
|
* @generated from protobuf enum value: TRANSACTION_3 = 83;
|
|
156
156
|
*/
|
|
157
|
-
TRANSACTION_3 = 83
|
|
157
|
+
TRANSACTION_3 = 83,
|
|
158
|
+
/**
|
|
159
|
+
* Other kinds of image that an attachment can be
|
|
160
|
+
*
|
|
161
|
+
* @generated from protobuf enum value: AI_GENERATED_PHOTO = 90;
|
|
162
|
+
*/
|
|
163
|
+
AI_GENERATED_PHOTO = 90,
|
|
164
|
+
/**
|
|
165
|
+
* @generated from protobuf enum value: CAMERA_CAPTURE_PHOTO = 91;
|
|
166
|
+
*/
|
|
167
|
+
CAMERA_CAPTURE_PHOTO = 91,
|
|
168
|
+
/**
|
|
169
|
+
* @generated from protobuf enum value: BUSINESS_CARD_IMAGE = 92;
|
|
170
|
+
*/
|
|
171
|
+
BUSINESS_CARD_IMAGE = 92,
|
|
172
|
+
/**
|
|
173
|
+
* @generated from protobuf enum value: GREETING_IMAGE = 93;
|
|
174
|
+
*/
|
|
175
|
+
GREETING_IMAGE = 93
|
|
158
176
|
}
|
|
@@ -160,4 +160,22 @@ export var AttachmentType;
|
|
|
160
160
|
* @generated from protobuf enum value: TRANSACTION_3 = 83;
|
|
161
161
|
*/
|
|
162
162
|
AttachmentType[AttachmentType["TRANSACTION_3"] = 83] = "TRANSACTION_3";
|
|
163
|
+
/**
|
|
164
|
+
* Other kinds of image that an attachment can be
|
|
165
|
+
*
|
|
166
|
+
* @generated from protobuf enum value: AI_GENERATED_PHOTO = 90;
|
|
167
|
+
*/
|
|
168
|
+
AttachmentType[AttachmentType["AI_GENERATED_PHOTO"] = 90] = "AI_GENERATED_PHOTO";
|
|
169
|
+
/**
|
|
170
|
+
* @generated from protobuf enum value: CAMERA_CAPTURE_PHOTO = 91;
|
|
171
|
+
*/
|
|
172
|
+
AttachmentType[AttachmentType["CAMERA_CAPTURE_PHOTO"] = 91] = "CAMERA_CAPTURE_PHOTO";
|
|
173
|
+
/**
|
|
174
|
+
* @generated from protobuf enum value: BUSINESS_CARD_IMAGE = 92;
|
|
175
|
+
*/
|
|
176
|
+
AttachmentType[AttachmentType["BUSINESS_CARD_IMAGE"] = 92] = "BUSINESS_CARD_IMAGE";
|
|
177
|
+
/**
|
|
178
|
+
* @generated from protobuf enum value: GREETING_IMAGE = 93;
|
|
179
|
+
*/
|
|
180
|
+
AttachmentType[AttachmentType["GREETING_IMAGE"] = 93] = "GREETING_IMAGE";
|
|
163
181
|
})(AttachmentType || (AttachmentType = {}));
|
|
@@ -23,7 +23,9 @@ export interface AuthTokens {
|
|
|
23
23
|
authenticationMethod: AuthenticationMethod;
|
|
24
24
|
/**
|
|
25
25
|
* The credential from that identity provider - a Firebase id token when the method is FIREBASE.
|
|
26
|
-
* Always set.
|
|
26
|
+
* Always set in a request. Blank in a response, which carries the tokens that the server renewed:
|
|
27
|
+
* it never echoes this back, as it belongs to the identity provider, so, a client keeps the one that it
|
|
28
|
+
* holds. Not issued or renewed by this server; refreshed with the identity provider.
|
|
27
29
|
*
|
|
28
30
|
* @generated from protobuf field: string authenticationToken = 2
|
|
29
31
|
*/
|
|
@@ -43,7 +43,10 @@ export interface BusinessLink {
|
|
|
43
43
|
role: Role;
|
|
44
44
|
/**
|
|
45
45
|
* Id of the party record of the linked entity within that business, or Constants.ENTITY_ID_NULL
|
|
46
|
-
* when there is none.
|
|
46
|
+
* when there is none. A link that has been blocked has the invalid party id that Java names
|
|
47
|
+
* Party.INVALID_PARTY_ID instead, which is not zero (it is the id of a party whose low part is
|
|
48
|
+
* zero, 0x0800000000000000), so, a partyId that is not zero is not always a party.
|
|
49
|
+
* Not resolvable by a storefront - Party is not generated by this protocol.
|
|
47
50
|
*
|
|
48
51
|
* @generated from protobuf field: int64 partyId = 5
|
|
49
52
|
*/
|
|
@@ -7,16 +7,24 @@ import { MessageType } from "@protobuf-ts/runtime";
|
|
|
7
7
|
import { Message } from "./Message";
|
|
8
8
|
/**
|
|
9
9
|
* Pushes the business entities a customer has changed - their shopping cart and the orders placed
|
|
10
|
-
* from it - to one business database
|
|
10
|
+
* from it - to one business database. The request names no business: the link token in the
|
|
11
|
+
* AuthTokens of the authorization header does, as a link names exactly one.
|
|
12
|
+
* Not implemented yet: the route (shopBusinessSyncIn) answers 501, and what is said here and in the
|
|
13
|
+
* BusinessSyncInResponse is the contract that it is to meet, and not behaviour that the server has.
|
|
14
|
+
* ProtoMap.fromProto accepts the paymentReceived of an order, and the senderId and status of a message,
|
|
15
|
+
* as they are sent, so, sync in has to enforce who may set each of them when it is written.
|
|
11
16
|
* An entity being synced in carries the id it already has, or Constants.ENTITY_ID_NULL if the
|
|
12
17
|
* caller is creating it and the server is to assign one; incoming update numbers are the caller's
|
|
13
|
-
* own and are replaced by the server, which returns the new ones in the
|
|
18
|
+
* own and are replaced by the server, which returns the messages bearing the new ones in the
|
|
19
|
+
* BusinessSyncInResponse.
|
|
14
20
|
*
|
|
15
21
|
* @generated from protobuf message com.merabills.storefront.BusinessSyncInRequest
|
|
16
22
|
*/
|
|
17
23
|
export interface BusinessSyncInRequest {
|
|
18
24
|
/**
|
|
19
|
-
* Client-side cursor for the customer's orders and shopping cart (Constants.MESSAGE_ENTITY_TYPE_ID)
|
|
25
|
+
* Client-side cursor for the customer's orders and shopping cart (Constants.MESSAGE_ENTITY_TYPE_ID):
|
|
26
|
+
* how far the client's sync outs of them have got, which is not moved by a push. The answer to a push
|
|
27
|
+
* is only the messages that were pushed, so, it does not depend on this.
|
|
20
28
|
*
|
|
21
29
|
* @generated from protobuf field: int64 messageUpdateNumber = 1
|
|
22
30
|
*/
|
|
@@ -7,10 +7,11 @@ import { MessageType } from "@protobuf-ts/runtime";
|
|
|
7
7
|
import { Message } from "./Message";
|
|
8
8
|
import { AuthTokens } from "./AuthTokens";
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* Not implemented yet (the route answers 501): this is the contract that it is to meet.
|
|
11
|
+
* The answer to a BusinessSyncInRequest: the messages the server now holds for what was pushed,
|
|
12
|
+
* which bear the update numbers it assigned, and any tokens it renewed. A push is not a read, so, do
|
|
13
|
+
* not move the message cursor with these update numbers: a change somebody else made, with an update
|
|
14
|
+
* number below them, would then never be sent. The next sync out returns what was pushed as well.
|
|
14
15
|
*
|
|
15
16
|
* @generated from protobuf message com.merabills.storefront.BusinessSyncInResponse
|
|
16
17
|
*/
|