@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.
Files changed (61) hide show
  1. package/dist/CartEditor.d.ts +10 -5
  2. package/dist/CartEditor.js +232 -125
  3. package/dist/Catalog.js +26 -18
  4. package/dist/Enums.d.ts +1 -1
  5. package/dist/Enums.js +2 -1
  6. package/dist/GeneralSettings.d.ts +4 -2
  7. package/dist/GeneralSettings.js +18 -7
  8. package/dist/Ids.d.ts +2 -0
  9. package/dist/Ids.js +10 -0
  10. package/dist/Message.js +3 -2
  11. package/dist/Model.d.ts +5 -0
  12. package/dist/Model.js +263 -50
  13. package/dist/Package.js +2 -2
  14. package/dist/Price.js +6 -2
  15. package/dist/Product.d.ts +5 -3
  16. package/dist/Product.js +30 -18
  17. package/dist/Profile.d.ts +1 -0
  18. package/dist/Profile.js +91 -4
  19. package/dist/ProtoMap.d.ts +15 -8
  20. package/dist/ProtoMap.js +265 -139
  21. package/dist/ShoppingList.d.ts +30 -1
  22. package/dist/ShoppingList.js +345 -35
  23. package/dist/SyncCache.d.ts +9 -0
  24. package/dist/SyncCache.js +93 -30
  25. package/dist/SyncState.js +1 -1
  26. package/dist/Tax.d.ts +12 -4
  27. package/dist/Tax.js +33 -4
  28. package/dist/TaxHelper.d.ts +12 -0
  29. package/dist/TaxHelper.js +93 -0
  30. package/dist/TaxType.d.ts +7 -0
  31. package/dist/TaxType.js +32 -0
  32. package/dist/generated/AttachmentType.d.ts +19 -1
  33. package/dist/generated/AttachmentType.js +18 -0
  34. package/dist/generated/AuthTokens.d.ts +3 -1
  35. package/dist/generated/BusinessLink.d.ts +4 -1
  36. package/dist/generated/BusinessSyncInRequest.d.ts +11 -3
  37. package/dist/generated/BusinessSyncInResponse.d.ts +5 -4
  38. package/dist/generated/BusinessSyncOutRequest.d.ts +37 -10
  39. package/dist/generated/BusinessSyncOutRequest.js +42 -29
  40. package/dist/generated/BusinessSyncOutResponse.d.ts +14 -1
  41. package/dist/generated/Configuration.d.ts +2 -1
  42. package/dist/generated/CreateUserRequest.d.ts +8 -2
  43. package/dist/generated/Entity.d.ts +7 -3
  44. package/dist/generated/GeneralSettings.d.ts +14 -4
  45. package/dist/generated/GeneralSettings.js +9 -1
  46. package/dist/generated/Message.d.ts +4 -2
  47. package/dist/generated/Product.d.ts +10 -1
  48. package/dist/generated/Product.js +9 -1
  49. package/dist/generated/ProfileSyncOutRequest.d.ts +17 -1
  50. package/dist/generated/ProfileSyncOutResponse.d.ts +8 -3
  51. package/dist/generated/SalesOrderPayload.d.ts +7 -2
  52. package/dist/generated/ShoppingList.d.ts +6 -3
  53. package/dist/generated/ShoppingListItem.d.ts +21 -6
  54. package/dist/generated/Tax.d.ts +4 -4
  55. package/dist/generated/Tax.js +7 -8
  56. package/dist/generated/WorkingHoursRange.d.ts +2 -2
  57. package/dist/index.d.ts +2 -2
  58. package/dist/index.js +4 -3
  59. package/package.json +55 -55
  60. package/dist/generated/TaxType.d.ts +0 -25
  61. 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 cursors are the caller's. It holds the wire and only the wire, and ProtoMap reads every field. See SYNC LAYER in ../Prompt.txt.
5
- // One response is one write: the entities it brought are stored and the entities its tombstones
6
- // named are deleted, in a single batch. There is nothing to work out beyond that, because the
7
- // server sends a tombstone for every entity it deleted, children included - so this cache never
8
- // infers a deletion from another one, and a write costs one transaction whether or not the page
9
- // happened to carry a tombstone.
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
- // Takes in what one business sync out returned, one blob per entity versioned by Entity.updatedAt, so the later of two copies wins and a page that arrives twice changes nothing the second time.
20
- // What its tombstones named goes in the same batch, deleted rather than stored: a page brings what
21
- // has changed and says what has gone, and applying half of that would leave the cache holding an
22
- // entity the server has already reported dead.
23
- // Answers how much of the page it kept - blobs stored plus ids deleted, which is what the container
24
- // now differs by. That is how a caller tells a response that changed something from one that
25
- // changed nothing, and what it publishes on: the store keeps a blob only when its eTag is later, so
26
- // a response can be rejected in full, and one that was cannot change what a later read of this
27
- // container answers - see MODEL.
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 highest update number among this customer's messages already
56
- // cached here, party preferences and orders alike. Not persisted, like every cursor in this
57
- // library - read off the store fresh each time rather than kept anywhere.
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 ProtoMap.highestCachedMessageUpdateNumber(await this.readAll(containerName));
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.writeBlobs(containerName, [ProtoMap.orderToBlob(order)]);
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.writeBlobs(containerName, [ProtoMap.partyPreferencesToBlob(partyPreferences)]);
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.partyPreferencesToBlob(partyPreferences).id],
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, and it is also how anything else of this customer's that the response
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 delete is unconditional, so
110
- // there is nothing for it to compare and nothing for it to report. Counting the ids instead can
111
- // therefore call a page that deleted nothing a page that changed something, when a tombstone
112
- // names an entity this container never held - which is the harmless direction, since a caller
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
- // Held in memory by whoever is reading and stored nowhere, so a new session asks from zero and is sent what it already holds: that 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.
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 as proto_TaxType } from "./generated/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(type: proto_TaxType, scaledRate: number): Tax;
6
- constructor(_type: proto_TaxType, _rate: number);
7
- get type(): proto_TaxType;
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
- static fromScaledValues(type, scaledRate) {
10
- return new Tax(type, taxRateFromScaledValue(scaledRate));
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 >> 8;
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;
@@ -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. Not issued or renewed by this server; refreshed with the identity provider.
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. Not resolvable by a storefront - Party is not generated by this protocol.
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, named the same way a BusinessSyncOutRequest names it.
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 BusinessSyncInResponse.
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
- * The answer to a BusinessSyncInRequest: the update number the server assigned to the pushed
11
- * entities, one per entity type. Raise the matching cursor to this value so the next sync out does
12
- * not fetch back what was just sent; 0 means nothing of that type was accepted. Carries no entities
13
- * - sync out again to see the server's view of what was pushed.
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
  */