@peabody-soft/mbs-model-ts 1.10.0 → 1.12.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/Catalog.d.ts +4 -0
- package/dist/Catalog.js +22 -0
- package/dist/Ids.d.ts +1 -0
- package/dist/Ids.js +5 -3
- package/dist/Message.d.ts +1 -5
- package/dist/Message.js +2 -12
- package/dist/Model.d.ts +3 -3
- package/dist/Model.js +156 -102
- package/dist/Order.d.ts +2 -2
- package/dist/Order.js +4 -4
- package/dist/ProtoMap.d.ts +13 -4
- package/dist/ProtoMap.js +139 -71
- package/dist/Service.d.ts +3 -4
- package/dist/ServiceProxy.d.ts +3 -4
- package/dist/ServiceProxy.js +4 -6
- package/dist/SyncCache.d.ts +3 -1
- package/dist/SyncCache.js +46 -31
- package/dist/SyncKeys.js +3 -2
- package/dist/generated/BusinessSyncInRequest.d.ts +30 -16
- package/dist/generated/BusinessSyncInRequest.js +9 -1
- package/dist/generated/BusinessSyncInResponse.d.ts +9 -6
- package/dist/generated/Constants.d.ts +2 -12
- package/dist/generated/Constants.js +2 -12
- package/dist/generated/Message.d.ts +5 -16
- package/dist/generated/Message.js +8 -22
- package/dist/generated/Party.d.ts +52 -0
- package/dist/generated/Party.js +68 -0
- package/dist/generated/PartyPreferencesPayload.d.ts +2 -1
- package/dist/generated/ProfileSyncOutRequest.d.ts +2 -0
- package/dist/generated/ProfileSyncOutResponse.d.ts +2 -2
- package/dist/generated/SalesOrderPayload.d.ts +9 -8
- package/dist/generated/User.d.ts +3 -1
- package/dist/generated/UserRequest.d.ts +52 -0
- package/dist/generated/{CreateUserRequest.js → UserRequest.js} +11 -11
- package/package.json +2 -2
- package/dist/generated/CreateUserRequest.d.ts +0 -43
package/dist/Catalog.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export default class Catalog {
|
|
|
16
16
|
getCategory(productCategoryId: bigint): ProductCategory | null;
|
|
17
17
|
getProduct(productId: bigint): Product | null;
|
|
18
18
|
getPackage(packageId: bigint): Package | null;
|
|
19
|
+
getProductForDisplay(productId: bigint): Product | null;
|
|
20
|
+
getPackageForDisplay(packageId: bigint): Package | null;
|
|
19
21
|
static comparePackages(left: Package, right: Package): number;
|
|
20
22
|
static compareByName(left: Item, right: Item): number;
|
|
21
23
|
private static compareNames;
|
|
@@ -27,6 +29,8 @@ export default class Catalog {
|
|
|
27
29
|
private readonly _allProducts;
|
|
28
30
|
private readonly _uncategorized;
|
|
29
31
|
private readonly _candidatePackages;
|
|
32
|
+
private readonly _displayProductMap;
|
|
33
|
+
private readonly _displayPackageMap;
|
|
30
34
|
private readonly _priceListMap;
|
|
31
35
|
private _currentSalePriceListId;
|
|
32
36
|
private _categories;
|
package/dist/Catalog.js
CHANGED
|
@@ -13,6 +13,7 @@ class Catalog {
|
|
|
13
13
|
constructor(categories, products, packages, priceLists, prices) {
|
|
14
14
|
// Every package that has a product, by that product's id, whether it can be bought or not
|
|
15
15
|
this._candidatePackages = new Map();
|
|
16
|
+
this._displayPackageMap = new Map();
|
|
16
17
|
this._priceListMap = new Map();
|
|
17
18
|
this._currentSalePriceListId = DEFAULT_SALE_PRICE_LIST_ID;
|
|
18
19
|
// What a customer can reach, rebuilt by applySalePriceList
|
|
@@ -31,11 +32,13 @@ class Catalog {
|
|
|
31
32
|
const productsById = new Map();
|
|
32
33
|
for (const product of products)
|
|
33
34
|
productsById.set(product.id, product);
|
|
35
|
+
this._displayProductMap = productsById;
|
|
34
36
|
for (const pkg of packages) {
|
|
35
37
|
const product = productsById.get(pkg.productId);
|
|
36
38
|
if (product === undefined)
|
|
37
39
|
continue;
|
|
38
40
|
UpdatableEntity.setParent(pkg, product);
|
|
41
|
+
this._displayPackageMap.set(pkg.id, pkg);
|
|
39
42
|
const productPackages = this._candidatePackages.get(product.id);
|
|
40
43
|
if (productPackages === undefined)
|
|
41
44
|
this._candidatePackages.set(product.id, [pkg]);
|
|
@@ -110,6 +113,25 @@ class Catalog {
|
|
|
110
113
|
getPackage(packageId) {
|
|
111
114
|
return this._packageMap.get(packageId) ?? null;
|
|
112
115
|
}
|
|
116
|
+
// Like getProduct, but for any product that the server sent, whether it can be bought or not: it
|
|
117
|
+
// is disabled, or it has nothing left to buy. It is for drawing a shopping list that already
|
|
118
|
+
// exists, such as an order that was placed, whose line can name a product that is no longer
|
|
119
|
+
// sold. Do not use it to offer anything for sale. Its packages are not on offer either:
|
|
120
|
+
// Product.packages keeps only the buyable ones, so it is empty for a product that cannot be
|
|
121
|
+
// bought. Null for an unknown id.
|
|
122
|
+
getProductForDisplay(productId) {
|
|
123
|
+
return this._displayProductMap.get(productId) ?? null;
|
|
124
|
+
}
|
|
125
|
+
// Like getPackage, but for any package of a product that the server sent, whether it can be
|
|
126
|
+
// bought or not: it is disabled, it has no enabled price, or its product is disabled. For
|
|
127
|
+
// drawing a shopping list that already exists (see getProductForDisplay), not for offering
|
|
128
|
+
// anything for sale. Package.price is the price in the current sale price list, null when there
|
|
129
|
+
// is none, and disabled when the package is not in that list. Null for an unknown id, and for
|
|
130
|
+
// the four reserved package ids a shopping list uses for its charge and summary lines (see
|
|
131
|
+
// Ids.isReservedPackageId).
|
|
132
|
+
getPackageForDisplay(packageId) {
|
|
133
|
+
return this._displayPackageMap.get(packageId) ?? null;
|
|
134
|
+
}
|
|
113
135
|
// Display comparator for packages, matching the merchant app's PackageComparator minus the
|
|
114
136
|
// clauses a storefront cannot exercise (disabled packages and the purchase price list, neither sent)
|
|
115
137
|
static comparePackages(left, right) {
|
package/dist/Ids.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export default class Ids {
|
|
|
13
13
|
static readonly GENERAL_SETTINGS_CONFIGURATION_ID: bigint;
|
|
14
14
|
static readonly ATTACHMENT_BASE_URL_BLOB_ID: bigint;
|
|
15
15
|
static readonly AUTH_TOKENS_BLOB_ID: bigint;
|
|
16
|
+
static readonly USER_ID_BLOB_ID: bigint;
|
|
16
17
|
private static readonly SYNC_CURSOR_BLOB_ID_BASE;
|
|
17
18
|
private static readonly UNSIGNED_64_BIT_MASK;
|
|
18
19
|
private static readonly UNSIGNED_32_BIT_MASK;
|
package/dist/Ids.js
CHANGED
|
@@ -15,8 +15,8 @@ class Ids {
|
|
|
15
15
|
return (BigInt(entityTypeId) << Ids.ENTITY_TYPE_BIT_SHIFT) | (lowPart & Ids.ID_MASK);
|
|
16
16
|
}
|
|
17
17
|
// The cart's message id: the business link id with the entity type swapped for the message one
|
|
18
|
-
// (Java Message.getPartyPreferencesMessageId).
|
|
19
|
-
//
|
|
18
|
+
// (Java Message.getPartyPreferencesMessageId). One link has one cart. The server stores a cart
|
|
19
|
+
// only under this id, and refuses an order that has it.
|
|
20
20
|
static getPartyPreferencesMessageId(businessLinkId) {
|
|
21
21
|
return Ids.buildId(proto_Constants.MESSAGE_ENTITY_TYPE_ID, Ids.getLowPart(businessLinkId));
|
|
22
22
|
}
|
|
@@ -74,7 +74,9 @@ Ids.GENERAL_SETTINGS_CONFIGURATION_ID = 0x0d10000000000000n;
|
|
|
74
74
|
Ids.ATTACHMENT_BASE_URL_BLOB_ID = -1n;
|
|
75
75
|
// Where a container keeps the credentials that sign its calls (see AuthTokens). Written with a null eTag, since a reissued token is not a later version of the earlier one. The latest wins.
|
|
76
76
|
Ids.AUTH_TOKENS_BLOB_ID = -2n;
|
|
77
|
-
//
|
|
77
|
+
// Where a profile container keeps the id that the next sign up creates the user under (see Model.createUser). Written with a null eTag, since a new id replaces the old one. It is the one blob a container holds that no response carried.
|
|
78
|
+
Ids.USER_ID_BLOB_ID = -3n;
|
|
79
|
+
// The first of a block of fixed blob ids, one per key (see getSyncCursorBlobId). A key is an entity type id, below 0x80, so the block runs from -0x100 down to -0x17F and cannot meet the fixed blobs at -1, -2 and -3.
|
|
78
80
|
Ids.SYNC_CURSOR_BLOB_ID_BASE = -0x100n;
|
|
79
81
|
Ids.UNSIGNED_64_BIT_MASK = (1n << 64n) - 1n;
|
|
80
82
|
Ids.UNSIGNED_32_BIT_MASK = (1n << 32n) - 1n;
|
package/dist/Message.d.ts
CHANGED
|
@@ -5,12 +5,8 @@ export default abstract class Message extends Entity {
|
|
|
5
5
|
private readonly _senderId;
|
|
6
6
|
private readonly _messageType;
|
|
7
7
|
private readonly _taskStatus;
|
|
8
|
-
|
|
9
|
-
private readonly _senderPhone;
|
|
10
|
-
protected constructor(header: EntityHeader, _senderId: bigint, _messageType: proto_MessageType, _taskStatus: proto_TaskStatus, _senderName: string | null, _senderPhone: string | null);
|
|
8
|
+
protected constructor(header: EntityHeader, _senderId: bigint, _messageType: proto_MessageType, _taskStatus: proto_TaskStatus);
|
|
11
9
|
get senderId(): bigint;
|
|
12
10
|
get messageType(): proto_MessageType;
|
|
13
11
|
get taskStatus(): proto_TaskStatus;
|
|
14
|
-
get senderName(): string | null;
|
|
15
|
-
get senderPhone(): string | null;
|
|
16
12
|
}
|
package/dist/Message.js
CHANGED
|
@@ -2,17 +2,14 @@ import Entity from "./Entity";
|
|
|
2
2
|
// Something a customer sent to a business: an order (sales order) or a shopping cart (party preferences), the two payloads this library understands.
|
|
3
3
|
// A business entity despite being customer-authored, since Java authorizes the message table through Right.MESSAGES_MODIFY against the business.
|
|
4
4
|
export default class Message extends Entity {
|
|
5
|
-
constructor(header, _senderId, _messageType, _taskStatus
|
|
5
|
+
constructor(header, _senderId, _messageType, _taskStatus) {
|
|
6
6
|
super(header);
|
|
7
7
|
this._senderId = _senderId;
|
|
8
8
|
this._messageType = _messageType;
|
|
9
9
|
this._taskStatus = _taskStatus;
|
|
10
|
-
this._senderName = _senderName;
|
|
11
|
-
this._senderPhone = _senderPhone;
|
|
12
10
|
}
|
|
13
11
|
// The customer's party id at the business (BusinessLink.partyId), not the id of their User. A
|
|
14
|
-
// business sync out sends a customer only messages from their own party.
|
|
15
|
-
// senderPhone identify the sender without resolving the party.
|
|
12
|
+
// business sync out sends a customer only messages from their own party.
|
|
16
13
|
get senderId() {
|
|
17
14
|
return this._senderId;
|
|
18
15
|
}
|
|
@@ -23,11 +20,4 @@ export default class Message extends Entity {
|
|
|
23
20
|
get taskStatus() {
|
|
24
21
|
return this._taskStatus;
|
|
25
22
|
}
|
|
26
|
-
get senderName() {
|
|
27
|
-
return this._senderName;
|
|
28
|
-
}
|
|
29
|
-
// E.164 formatted
|
|
30
|
-
get senderPhone() {
|
|
31
|
-
return this._senderPhone;
|
|
32
|
-
}
|
|
33
23
|
}
|
package/dist/Model.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export default class Model {
|
|
|
23
23
|
updateCartLocalOnly(shoppingList: ShoppingList): Promise<void>;
|
|
24
24
|
createOrder(orderId: bigint, fulfillmentMode: proto_FulfillmentMode, deliveryAddressId: bigint, deliveryAddress: Address | null, note: string | null): Promise<void>;
|
|
25
25
|
private beginSignIn;
|
|
26
|
-
private
|
|
26
|
+
private sendBusinessMessages;
|
|
27
27
|
private storeCartInCache;
|
|
28
28
|
private requireBusinessBeingBrowsed;
|
|
29
29
|
private requireCustomerOfBusiness;
|
|
@@ -35,15 +35,15 @@ export default class Model {
|
|
|
35
35
|
private static isBusinessNamed;
|
|
36
36
|
private requireBusinessLink;
|
|
37
37
|
private readJoinBusinessId;
|
|
38
|
+
private getVettedBusiness;
|
|
38
39
|
private static findBusinessEntity;
|
|
39
40
|
private static findBusinessLink;
|
|
40
41
|
private static getBusinessContainerName;
|
|
41
42
|
private requireBusinessAuthTokens;
|
|
42
43
|
private profileSyncIn;
|
|
43
44
|
private createProfileOnServer;
|
|
45
|
+
private readProfilePages;
|
|
44
46
|
private static requireJoinedBusiness;
|
|
45
|
-
private cacheProfileResponse;
|
|
46
|
-
private writeProfileResponse;
|
|
47
47
|
private storeIssuedTokens;
|
|
48
48
|
private getProfileContainerName;
|
|
49
49
|
private requireProfileContainerName;
|
package/dist/Model.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { Constants as proto_Constants } from "./generated/Constants";
|
|
2
|
-
import { CreateUserRequest as proto_CreateUserRequest } from "./generated/CreateUserRequest";
|
|
3
2
|
import { FulfillmentMode as proto_FulfillmentMode } from "./generated/FulfillmentMode";
|
|
4
3
|
import AuthTokens from "./AuthTokens";
|
|
5
4
|
import * as ContainerNames from "./ContainerNames";
|
|
@@ -8,7 +7,7 @@ import Ids from "./Ids";
|
|
|
8
7
|
import LiveData from "./LiveData";
|
|
9
8
|
import { canonicalizeBusinessId } from "./Profile";
|
|
10
9
|
import * as ProtoMap from "./ProtoMap";
|
|
11
|
-
import { InternalServerException, NotFoundException } from "./ServiceExceptions";
|
|
10
|
+
import { ConflictException, InternalServerException, NotFoundException } from "./ServiceExceptions";
|
|
12
11
|
import ShoppingList from "./ShoppingList";
|
|
13
12
|
import SyncCache from "./SyncCache";
|
|
14
13
|
import * as SyncKeys from "./SyncKeys";
|
|
@@ -63,7 +62,7 @@ class Model {
|
|
|
63
62
|
// Signs in and syncs this subject's profile in, for a subject the server has a profile for (use
|
|
64
63
|
// createUser otherwise). Also reads in the business being browsed, if any, as this customer.
|
|
65
64
|
//
|
|
66
|
-
// Anything thrown after beginSignIn's guard (a bad credential, a rejected
|
|
65
|
+
// Anything thrown after beginSignIn's guard (a bad credential, a rejected request while reading the
|
|
67
66
|
// business) calls signOut. profileSyncIn publishes a cached profile before it reaches the
|
|
68
67
|
// network, so a retry could otherwise find _profile set and fail the "already signed in" guard.
|
|
69
68
|
// Throws NotFoundException if the business being browsed cannot be opened. See businessSyncIn.
|
|
@@ -84,22 +83,64 @@ class Model {
|
|
|
84
83
|
// profile for (use signIn otherwise). Also reads in the business being browsed, if any, as this
|
|
85
84
|
// customer. Throws NotFoundException if that business cannot be opened (see businessSyncIn), after
|
|
86
85
|
// the User exists. The sign-up is not undone: the customer stays signed up and signed in.
|
|
86
|
+
// - The user is created under an id this model makes for the subject, not userToCreate.id. One id
|
|
87
|
+
// is made per subject and saved in the profile container before the call, and every attempt of
|
|
88
|
+
// that subject sends it until the sign up is stored or refused with a ConflictException. It is
|
|
89
|
+
// kept after a network error, a 5xx and any other error (a 400, a 401), and a successful signIn
|
|
90
|
+
// does not delete it. It is deleted once the answer is stored, and by a ConflictException.
|
|
91
|
+
// - A ConflictException means the server could not create the user: the subject has one already,
|
|
92
|
+
// or the id could not be used. It is rethrown, and this method does not sign in: the caller signs
|
|
93
|
+
// in, which reads the profile. If signIn answers UnauthorizedException the subject has no
|
|
94
|
+
// profile, and the caller creates again. The saved id is already deleted, so that sign up makes
|
|
95
|
+
// a new one.
|
|
96
|
+
// - When the business being browsed is published and was read to the end, the call joins it too,
|
|
97
|
+
// so the answer holds the link and the business and no second call is needed. If the server then
|
|
98
|
+
// answers a NotFoundException, the user was created but the join was rejected, and no answer came
|
|
99
|
+
// back: the profile is read without a join, and the business read that follows makes the join
|
|
100
|
+
// again, which throws. A NotFoundException when no join was sent is rethrown at once.
|
|
87
101
|
async createUser(authenticationMethod, subjectId, authenticationToken, userToCreate) {
|
|
88
102
|
this.beginSignIn(authenticationMethod, subjectId, authenticationToken);
|
|
89
103
|
const authTokens = await this.requireProfileAuthTokens();
|
|
90
|
-
|
|
104
|
+
const containerName = this.requireProfileContainerName();
|
|
105
|
+
const cache = Model.getCache();
|
|
106
|
+
let userId = await cache.readUserId(containerName);
|
|
107
|
+
if (userId === null) {
|
|
108
|
+
userId = Ids.getNewId(proto_Constants.USER_ENTITY_TYPE_ID);
|
|
109
|
+
await cache.writeUserId(containerName, userId);
|
|
110
|
+
}
|
|
111
|
+
// A subject with no profile has nothing synced, so every cursor is zero
|
|
112
|
+
const noCursors = new SyncState(SyncKeys.PROFILE_SYNC_IN);
|
|
113
|
+
const joinBusinessId = this.getVettedBusiness()?.id ?? BigInt(proto_Constants.ENTITY_ID_NULL);
|
|
114
|
+
try {
|
|
115
|
+
await this.createProfileOnServer(ProtoMap.buildUserRequest(ProtoMap.buildUser(userToCreate, userId), noCursors, joinBusinessId), authTokens);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
if (error instanceof ConflictException) {
|
|
119
|
+
await cache.deleteUserId(containerName);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
const isJoinRefused = error instanceof NotFoundException &&
|
|
123
|
+
joinBusinessId !== BigInt(proto_Constants.ENTITY_ID_NULL);
|
|
124
|
+
if (!isJoinRefused)
|
|
125
|
+
throw error;
|
|
126
|
+
await this.profileSyncIn(ProtoMap.buildProfileSyncOutRequest(noCursors), await this.requireProfileAuthTokens());
|
|
127
|
+
await cache.deleteUserId(containerName);
|
|
128
|
+
}
|
|
91
129
|
if (this._businessId !== null)
|
|
92
130
|
await this.businessSyncIn();
|
|
93
131
|
}
|
|
94
132
|
// Updates this customer's own profile with what userProfile holds. Service.updateUser matches the
|
|
95
133
|
// caller against the User by entity id (see ProtoMap.buildUpdateUser), so userProfile must be this
|
|
96
|
-
// customer's own, read in earlier, not built fresh. Throws if no profile has been read.
|
|
97
|
-
//
|
|
98
|
-
//
|
|
134
|
+
// customer's own, read in earlier, not built fresh. Throws if no profile has been read. The answer
|
|
135
|
+
// is cached and published as a profile read is, so the User as the server stored it replaces the
|
|
136
|
+
// held one (see readProfilePages).
|
|
99
137
|
async updateUser(userProfile) {
|
|
100
138
|
if (this._profile.value === null)
|
|
101
139
|
throw new IllegalOperationException("Nobody is signed in, so this call cannot be made");
|
|
102
|
-
|
|
140
|
+
const containerName = this.requireProfileContainerName();
|
|
141
|
+
const request = ProtoMap.buildUserRequest(ProtoMap.buildUpdateUser(userProfile), await Model.getCache().readProfileCursors(containerName));
|
|
142
|
+
const authTokens = await this.requireProfileAuthTokens();
|
|
143
|
+
await this.readProfilePages(containerName, ProtoMap.getUserRequestSyncOutRequest(request), authTokens, () => Model.getService().updateUser(request, authTokens));
|
|
103
144
|
}
|
|
104
145
|
// Discards this model. The next getInstance() builds a fresh one, signed out and browsing nothing.
|
|
105
146
|
async signOut() {
|
|
@@ -131,7 +172,7 @@ class Model {
|
|
|
131
172
|
// order placed from it. Both need a customer who is a member of the business (see
|
|
132
173
|
// requireCustomerOfBusiness), except that a signed-out visitor can keep a cart locally (see
|
|
133
174
|
// updateCartLocalOnly).
|
|
134
|
-
// An order reaches the store only after the server accepts it, so a customer never
|
|
175
|
+
// An order reaches the store only after the server accepts it, so a customer never has an order
|
|
135
176
|
// the server has not accepted, and a failed call leaves nothing to reconcile. A cart can be
|
|
136
177
|
// stored before the server has it (see updateCartLocalOnly).
|
|
137
178
|
// Creates or replaces this customer's cart, on the server and in the store. An empty list
|
|
@@ -139,9 +180,11 @@ class Model {
|
|
|
139
180
|
// Signed out, this calls updateCartLocalOnly, as the server has no cart for a visitor. Signed in,
|
|
140
181
|
// it costs a request, so an editor should write each change with updateCartLocalOnly and call this
|
|
141
182
|
// on a schedule (debounced, on blur, on a timer).
|
|
142
|
-
// A call that matches the stored copy returns without a request (hasShoppingListChanged)
|
|
143
|
-
//
|
|
144
|
-
//
|
|
183
|
+
// A call that matches the stored copy returns without a request (hasShoppingListChanged), and so
|
|
184
|
+
// reads nothing. A call that sends gets back the customer's messages that are newer than the last
|
|
185
|
+
// read, the cart among them: a change that the business made to an order comes with it. It brings
|
|
186
|
+
// nothing of the rest of the business, so it does not replace a read of it.
|
|
187
|
+
// The cart is sent as a whole and replaces the stored cart if it is dated later.
|
|
145
188
|
async updateCart(shoppingList) {
|
|
146
189
|
if (this._profile.value === null)
|
|
147
190
|
return this.updateCartLocalOnly(shoppingList);
|
|
@@ -150,7 +193,7 @@ class Model {
|
|
|
150
193
|
if (!ProtoMap.hasShoppingListChanged(stored, shoppingList))
|
|
151
194
|
return;
|
|
152
195
|
const partyPreferencesMessage = ProtoMap.buildPartyPreferencesMessage(Ids.getPartyPreferencesMessageId(target.businessLink.id), stored, target.senderId, shoppingList, null, BigInt(Date.now()));
|
|
153
|
-
await this.
|
|
196
|
+
await this.sendBusinessMessages(target, [partyPreferencesMessage]);
|
|
154
197
|
}
|
|
155
198
|
// The same, but stored locally and not sent. Call it on every edit (a request per tap of a
|
|
156
199
|
// quantity stepper is too many), and call updateCart on a schedule to send.
|
|
@@ -161,12 +204,17 @@ class Model {
|
|
|
161
204
|
// the update number of the copy it amends, buildPartyPreferencesMessage dates it past that copy,
|
|
162
205
|
// and the later copy wins.
|
|
163
206
|
async updateCartLocalOnly(shoppingList) {
|
|
164
|
-
// A signed-out visitor has no membership to derive a message id from, so a fresh id is drawn
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
if (this._profile.value === null)
|
|
169
|
-
|
|
207
|
+
// A signed-out visitor has no membership to derive a message id from, so a fresh id is drawn for
|
|
208
|
+
// the first edit and the stored cart keeps it, so later edits amend one entity. With no party, the
|
|
209
|
+
// sender is ENTITY_ID_NULL. A visitor's cart is never sent as it is: signing in sends a new cart
|
|
210
|
+
// under the id of the membership (see businessSyncIn).
|
|
211
|
+
if (this._profile.value === null) {
|
|
212
|
+
const containerName = ContainerNames.forBusiness(this.requireBusinessBeingBrowsed());
|
|
213
|
+
const stored = await Model.getCache().readPartyPreferences(containerName);
|
|
214
|
+
return this.storeCartInCache(containerName, stored === null
|
|
215
|
+
? Ids.getNewId(proto_Constants.MESSAGE_ENTITY_TYPE_ID)
|
|
216
|
+
: ProtoMap.getMessageId(stored), BigInt(proto_Constants.ENTITY_ID_NULL), shoppingList);
|
|
217
|
+
}
|
|
170
218
|
const target = this.requireCustomerOfBusiness();
|
|
171
219
|
return this.storeCartInCache(target.containerName, Ids.getPartyPreferencesMessageId(target.businessLink.id), target.senderId, shoppingList);
|
|
172
220
|
}
|
|
@@ -176,10 +224,14 @@ class Model {
|
|
|
176
224
|
// The emptied cart records this fulfillmentMode, which Business.defaultFulfillmentMode returns.
|
|
177
225
|
// The caller creates orderId with Ids.getNewId(Constants.MESSAGE_ENTITY_TYPE_ID), so it can find
|
|
178
226
|
// the placed order, and can retry a call that a NetworkConnectionException left unresolved using
|
|
179
|
-
// the same id, without placing a second order
|
|
180
|
-
//
|
|
227
|
+
// the same id, without placing a second order: the server stores an order only if no message has
|
|
228
|
+
// its id, so a retry changes nothing, even after the business has started the order. The cart has
|
|
229
|
+
// one id for the same reason (see getPartyPreferencesMessageId).
|
|
181
230
|
// A delivery must name an address: an id from this customer's list, or a full address sent
|
|
182
|
-
// directly (see requireDeliveryAddress).
|
|
231
|
+
// directly (see requireDeliveryAddress). Either way the address goes to the business with the
|
|
232
|
+
// order, in the party of the customer, so a full address has an id that is not zero.
|
|
233
|
+
// The party of the customer goes with every order: their name and phone number, and the delivery
|
|
234
|
+
// address of an order that is delivered. It is not sent with a cart.
|
|
183
235
|
async createOrder(orderId, fulfillmentMode, deliveryAddressId, deliveryAddress, note) {
|
|
184
236
|
const target = this.requireCustomerOfBusiness();
|
|
185
237
|
// Read from the store rather than the published Business, so the order matches the stored cart
|
|
@@ -190,16 +242,25 @@ class Model {
|
|
|
190
242
|
: ProtoMap.buildShoppingList(partyPreferencesMessage, target.countryCode);
|
|
191
243
|
if (shoppingList.existingItems.length === 0)
|
|
192
244
|
throw new IllegalOperationException("The cart holds nothing, so there is no order to place");
|
|
193
|
-
Model.requireDeliveryAddress(target.addresses, fulfillmentMode, deliveryAddressId, deliveryAddress);
|
|
245
|
+
Model.requireDeliveryAddress(target.user.addresses, fulfillmentMode, deliveryAddressId, deliveryAddress);
|
|
246
|
+
// A delivery is where the party of the customer gets an address, as in the merchant app, which adds
|
|
247
|
+
// the delivery address of an order to the party and none for a pick-up. It is the address of the
|
|
248
|
+
// order: the one sent whole, else the one the id names in the customer's list.
|
|
249
|
+
const partyAddress = fulfillmentMode === proto_FulfillmentMode.DELIVERY
|
|
250
|
+
? (deliveryAddress ?? target.user.addresses.getAddress(deliveryAddressId))
|
|
251
|
+
: null;
|
|
252
|
+
if (partyAddress !== null && partyAddress.id === BigInt(proto_Constants.ENTITY_ID_NULL))
|
|
253
|
+
throw new InvalidArgumentException("An address to deliver to has an id of its own, which is how the party of the customer keeps it");
|
|
194
254
|
// One timestamp for both messages: the time the order was placed
|
|
195
255
|
const now = BigInt(Date.now());
|
|
196
|
-
// One request for the order
|
|
197
|
-
// cart still full if the second failed
|
|
198
|
-
|
|
256
|
+
// One request for the order, the emptied cart and the party: two calls could leave an order placed
|
|
257
|
+
// and the cart still full if the second failed. The party goes with every order, one sent again
|
|
258
|
+
// included, and with nothing else: the server refuses a party that has no order with it.
|
|
259
|
+
await this.sendBusinessMessages(target, [
|
|
199
260
|
ProtoMap.buildOrderMessage(orderId, target.senderId, shoppingList, fulfillmentMode, deliveryAddressId, deliveryAddress, note, now),
|
|
200
261
|
// The customer's latest choice of fulfillmentMode, for defaultFulfillmentMode to offer next time
|
|
201
262
|
ProtoMap.buildPartyPreferencesMessage(Ids.getPartyPreferencesMessageId(target.businessLink.id), partyPreferencesMessage, target.senderId, null, fulfillmentMode, now),
|
|
202
|
-
]);
|
|
263
|
+
], ProtoMap.buildParty(target.user, partyAddress));
|
|
203
264
|
}
|
|
204
265
|
// #endregion
|
|
205
266
|
// Throws if a profile is already held, otherwise stores the credentials every later call signs
|
|
@@ -211,32 +272,40 @@ class Model {
|
|
|
211
272
|
this._subjectId = subjectId;
|
|
212
273
|
this._authenticationToken = authenticationToken;
|
|
213
274
|
}
|
|
214
|
-
// #region
|
|
215
|
-
//
|
|
216
|
-
// reads have reached, then stores the response.
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
|
|
275
|
+
// #region Sending
|
|
276
|
+
// Sends the messages, and the party of the customer if there is one, in one request, together with
|
|
277
|
+
// the update number this business container's reads have reached, then stores the response.
|
|
278
|
+
// The server answers with a sync out of the customer's messages, made after it writes: the stored
|
|
279
|
+
// messages that are newer than that update number, in the order of their update numbers, with the
|
|
280
|
+
// ones that were sent among them, and not in the order they were sent in. It is stored as a page of
|
|
281
|
+
// a read is, and moves the message cursor the same way. A message that was not stored (an order
|
|
282
|
+
// whose id exists, a cart dated no later than the stored one) is in it only if the stored message
|
|
283
|
+
// is newer than the cursor, and then it is the stored message. The cache has every message at or
|
|
284
|
+
// below the cursor, so what a caller needs afterwards is read from the cache by id.
|
|
285
|
+
// A full page may have more behind it, which is read as a read reads. A rejected request throws
|
|
286
|
+
// from the Service call itself, as for any bad status, so reaching the write below means the
|
|
287
|
+
// server accepted it.
|
|
288
|
+
async sendBusinessMessages(target, messages, party = null) {
|
|
222
289
|
const cache = Model.getCache();
|
|
223
290
|
const messageUpdateNumber = await cache.readMessageUpdateNumber(target.containerName);
|
|
224
291
|
const authTokens = await this.requireBusinessAuthTokens(target.containerName, target.businessLink);
|
|
225
|
-
const
|
|
292
|
+
const request = ProtoMap.buildBusinessSyncInRequest(messageUpdateNumber, messages, party);
|
|
293
|
+
const response = await Model.getService().businessSyncIn(request, ProtoMap.buildAuthTokens(authTokens));
|
|
226
294
|
await this.storeIssuedTokens(ProtoMap.getResponseAuthTokens(response), target.containerName);
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
|
|
295
|
+
let changeCount = await cache.writeMessages(target.containerName, ProtoMap.getSyncInResponseMessages(response));
|
|
296
|
+
// Read on from the cursor the page moved, with the tokens the response may have renewed
|
|
297
|
+
if (ProtoMap.isSyncInResponseFull(request, response))
|
|
298
|
+
changeCount += await this.readBusinessPages(target.containerName, target.businessLink.businessId, await cache.readBusinessCursors(target.containerName), ProtoMap.getBusinessResponseAttachmentBaseUrlExpiry(await cache.readBusiness(target.containerName)), ProtoMap.buildAuthTokens(await this.requireBusinessAuthTokens(target.containerName, target.businessLink)));
|
|
230
299
|
// What the store took decides whether anything is published - see the Publishing region
|
|
231
300
|
if (changeCount !== 0)
|
|
232
301
|
this.publishBusiness(await cache.readBusiness(target.containerName));
|
|
233
302
|
}
|
|
234
|
-
// Amends the cart containerName
|
|
303
|
+
// Amends the cart containerName stores and writes the result to the cache. Nothing is sent.
|
|
235
304
|
// The two callers differ in container, id and sender: see updateCartLocalOnly.
|
|
236
305
|
async storeCartInCache(containerName, messageId, senderId, shoppingList) {
|
|
237
306
|
const cache = Model.getCache();
|
|
238
307
|
const changeCount = await cache.writePartyPreferences(containerName, ProtoMap.buildPartyPreferencesMessage(messageId, await cache.readPartyPreferences(containerName), senderId, shoppingList, null, BigInt(Date.now())));
|
|
239
|
-
// Published on what the store took, as for every read and
|
|
308
|
+
// Published on what the store took, as for every read and sync in
|
|
240
309
|
if (changeCount !== 0)
|
|
241
310
|
this.publishBusiness(await cache.readBusiness(containerName));
|
|
242
311
|
}
|
|
@@ -268,7 +337,7 @@ class Model {
|
|
|
268
337
|
businessLink: businessLink,
|
|
269
338
|
containerName: Model.getBusinessContainerName(businessLink),
|
|
270
339
|
senderId: businessLink.partyId,
|
|
271
|
-
|
|
340
|
+
user: user,
|
|
272
341
|
countryCode: business.countryCode,
|
|
273
342
|
};
|
|
274
343
|
}
|
|
@@ -292,7 +361,8 @@ class Model {
|
|
|
292
361
|
// #region The four calls of Service, each as this model makes it
|
|
293
362
|
//
|
|
294
363
|
// Each caches the response (a read caches every page as it arrives), files the tokens the server
|
|
295
|
-
// issued, and publishes what the cache then holds. A read is paged
|
|
364
|
+
// issued, and publishes what the cache then holds. A read is paged, and the answer to a sign up or
|
|
365
|
+
// an update is the first page of a profile read.
|
|
296
366
|
// Reads in the business being browsed: an anonymous visitor under its businessId, a signed-in
|
|
297
367
|
// customer under their own container (see getBusinessContainerName), joining as a customer first
|
|
298
368
|
// if not linked (see requireBusinessLink).
|
|
@@ -529,8 +599,8 @@ class Model {
|
|
|
529
599
|
// A business published from the cache by a read that has not ended is not known to be open or to
|
|
530
600
|
// be the one the businessId names, so a customer is not joined to it.
|
|
531
601
|
async readJoinBusinessId(businessId) {
|
|
532
|
-
const alreadyHeld = this.
|
|
533
|
-
if (alreadyHeld !== null
|
|
602
|
+
const alreadyHeld = this.getVettedBusiness();
|
|
603
|
+
if (alreadyHeld !== null)
|
|
534
604
|
return alreadyHeld.id;
|
|
535
605
|
await this.syncAnonymousBusiness(businessId);
|
|
536
606
|
// A missing business is a NotFoundException from the read above. Reaching here with none
|
|
@@ -540,6 +610,12 @@ class Model {
|
|
|
540
610
|
throw new InternalServerException(`The read of business ${businessId} answered with no Business`);
|
|
541
611
|
return business.id;
|
|
542
612
|
}
|
|
613
|
+
// The business being browsed, if it is published and has been read to the end and found open. A
|
|
614
|
+
// business published from the cache by a read that has not ended is not known to be either, so
|
|
615
|
+
// this is null for it.
|
|
616
|
+
getVettedBusiness() {
|
|
617
|
+
return this._isPublishedBusinessVetted ? this._business.value : null;
|
|
618
|
+
}
|
|
543
619
|
static findBusinessEntity(profile, businessId) {
|
|
544
620
|
const wanted = canonicalizeBusinessId(businessId);
|
|
545
621
|
for (const business of profile.businesses)
|
|
@@ -600,9 +676,26 @@ class Model {
|
|
|
600
676
|
async profileSyncIn(firstRequest, firstAuthTokens) {
|
|
601
677
|
const containerName = this.getProfileContainerName();
|
|
602
678
|
if (containerName === null)
|
|
603
|
-
return
|
|
604
|
-
|
|
605
|
-
this.
|
|
679
|
+
return;
|
|
680
|
+
this.publishProfile(await Model.getCache().readProfile(containerName));
|
|
681
|
+
await this.readProfilePages(containerName, firstRequest, firstAuthTokens, () => Model.getService().profileSyncOut(firstRequest, firstAuthTokens));
|
|
682
|
+
}
|
|
683
|
+
// The sign-up call. Its answer is the first page of a profile read, which readProfilePages
|
|
684
|
+
// continues if the page is full. The id saved for the sign up is deleted once that page is stored.
|
|
685
|
+
async createProfileOnServer(request, authTokens) {
|
|
686
|
+
const containerName = this.requireProfileContainerName();
|
|
687
|
+
await this.readProfilePages(containerName, ProtoMap.getUserRequestSyncOutRequest(request), authTokens, () => Model.getService().createUser(request, authTokens), () => Model.getCache().deleteUserId(containerName));
|
|
688
|
+
}
|
|
689
|
+
// Caches and publishes a profile read of the pages that firstRequest starts. sendFirstPage makes
|
|
690
|
+
// the first call: profileSyncOut for a read, createUser or updateUser for a write, which answers
|
|
691
|
+
// with the first page of the read that its profileSyncOutRequest describes. Later pages are read
|
|
692
|
+
// with profileSyncOut, because a write cannot be sent again.
|
|
693
|
+
// Each page is cached, moving the cursors, and the tokens it reissues are stored. afterFirstPage
|
|
694
|
+
// runs once the first page and its tokens are stored. The profile is published once, and only if
|
|
695
|
+
// the store took something. The join is checked last, so everything that arrived is cached and
|
|
696
|
+
// published before it can throw: the pages are valid even when the join is not.
|
|
697
|
+
async readProfilePages(containerName, firstRequest, firstAuthTokens, sendFirstPage, afterFirstPage = null) {
|
|
698
|
+
const cache = Model.getCache();
|
|
606
699
|
let request = firstRequest;
|
|
607
700
|
let authTokens = firstAuthTokens;
|
|
608
701
|
let changeCount = 0;
|
|
@@ -611,9 +704,14 @@ class Model {
|
|
|
611
704
|
const joinBusinessId = firstRequest.joinBusinessId;
|
|
612
705
|
let hasJoinedLink = false;
|
|
613
706
|
let hasJoinedBusiness = false;
|
|
707
|
+
let page = await sendFirstPage();
|
|
614
708
|
for (;;) {
|
|
615
|
-
|
|
616
|
-
|
|
709
|
+
changeCount += await cache.writeProfile(containerName, page);
|
|
710
|
+
await this.storeIssuedTokens(ProtoMap.getResponseAuthTokens(page), containerName);
|
|
711
|
+
if (afterFirstPage !== null) {
|
|
712
|
+
await afterFirstPage();
|
|
713
|
+
afterFirstPage = null;
|
|
714
|
+
}
|
|
617
715
|
if (ProtoMap.hasJoinedBusinessLink(joinBusinessId, page))
|
|
618
716
|
hasJoinedLink = true;
|
|
619
717
|
if (ProtoMap.hasJoinedBusiness(joinBusinessId, page))
|
|
@@ -625,36 +723,19 @@ class Model {
|
|
|
625
723
|
// Each call after the first is signed with what the previous page reissued, since a token can
|
|
626
724
|
// expire part way through a long read
|
|
627
725
|
authTokens = ProtoMap.refreshAuthTokens(authTokens, ProtoMap.getResponseAuthTokens(page));
|
|
726
|
+
page = await Model.getService().profileSyncOut(request, authTokens);
|
|
628
727
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
this.publishProfile(response);
|
|
634
|
-
}
|
|
635
|
-
// Checked last, so everything that arrived is cached and published before this can throw. The
|
|
636
|
-
// pages are valid even when the join is not.
|
|
728
|
+
if (changeCount !== 0)
|
|
729
|
+
this.publishProfile(await cache.readProfile(containerName));
|
|
730
|
+
// After a sign up the User exists whatever became of the join, so what came back is kept and
|
|
731
|
+
// published before this throws
|
|
637
732
|
Model.requireJoinedBusiness(joinBusinessId, hasJoinedLink, hasJoinedBusiness);
|
|
638
|
-
return response;
|
|
639
|
-
}
|
|
640
|
-
// The sign-up call. Not paged: resending it would create a second user, and a new caller has at
|
|
641
|
-
// most their User, one BusinessLink and the Business it names. It can join a business in the same
|
|
642
|
-
// call, with the same join requirements as a profile read, in one response.
|
|
643
|
-
async createProfileOnServer(request, authTokens) {
|
|
644
|
-
const response = await this.writeProfileResponse(await Model.getService().createUser(request, authTokens));
|
|
645
|
-
const joinBusinessId = request.joinBusinessId;
|
|
646
|
-
// Raised after the response is cached and published, as on a profile read. It matters more
|
|
647
|
-
// here: the User exists regardless of the join, so discarding the response and signing up
|
|
648
|
-
// again would create a second one
|
|
649
|
-
Model.requireJoinedBusiness(joinBusinessId, ProtoMap.hasJoinedBusinessLink(joinBusinessId, response), ProtoMap.hasJoinedBusiness(joinBusinessId, response));
|
|
650
|
-
return response;
|
|
651
733
|
}
|
|
652
734
|
// #endregion
|
|
653
735
|
// #region Driving one call
|
|
654
736
|
// Throws when a requested join is not answered with both the BusinessLink (the membership, whose
|
|
655
737
|
// token signs calls) and the Business it names (whose businessId everything else is built from).
|
|
656
|
-
// Without both, the caller is a customer of something it cannot reach.
|
|
657
|
-
// that can carry a joinBusinessId.
|
|
738
|
+
// Without both, the caller is a customer of something it cannot reach.
|
|
658
739
|
//
|
|
659
740
|
// Throws a ServiceException, since the fault is the server's and not the caller's: the server
|
|
660
741
|
// answered 200 and left out what was asked for. The 500 status means a retry is worthwhile.
|
|
@@ -673,34 +754,6 @@ class Model {
|
|
|
673
754
|
: "BusinessLink";
|
|
674
755
|
throw new InternalServerException(`A join of business ${joinBusinessId} was answered with no ${missing}`);
|
|
675
756
|
}
|
|
676
|
-
// Caches the entities of a profile response and files the tokens it reissued under the same
|
|
677
|
-
// container. Returns how much the store accepted, which decides whether anything is published.
|
|
678
|
-
// The three calls that receive a profile response (a page of a read, a sign up, an update) share
|
|
679
|
-
// this, so they cache and file tokens the same way. It does not publish: a read publishes once
|
|
680
|
-
// after all its pages, the other two once each.
|
|
681
|
-
// Only a page of a read moves the container's cursors. A sign up and an update are changes this
|
|
682
|
-
// client made and the next read brings them back, so they are stored without moving any.
|
|
683
|
-
async cacheProfileResponse(containerName, response, isPageOfRead) {
|
|
684
|
-
const cache = Model.getCache();
|
|
685
|
-
const changeCount = isPageOfRead
|
|
686
|
-
? await cache.writeProfile(containerName, response)
|
|
687
|
-
: await cache.storeProfile(containerName, response);
|
|
688
|
-
await this.storeIssuedTokens(ProtoMap.getResponseAuthTokens(response), containerName);
|
|
689
|
-
return changeCount;
|
|
690
|
-
}
|
|
691
|
-
// Handles the response of a profile call other than a read: caches and publishes it, as for a
|
|
692
|
-
// paged read without the paging. Like a read, a sign up and an update publish only when the store
|
|
693
|
-
// accepted something. An update publishes nothing today because the server puts no entities in
|
|
694
|
-
// its response, only reissued tokens. If it ever answers with the stored User, whose update
|
|
695
|
-
// number and updated time a caller cannot know, this publishes it as the server sent it.
|
|
696
|
-
async writeProfileResponse(response) {
|
|
697
|
-
const containerName = this.getProfileContainerName();
|
|
698
|
-
if (containerName === null)
|
|
699
|
-
return response;
|
|
700
|
-
if ((await this.cacheProfileResponse(containerName, response, false)) !== 0)
|
|
701
|
-
this.publishProfile(await Model.getCache().readProfile(containerName));
|
|
702
|
-
return response;
|
|
703
|
-
}
|
|
704
757
|
// Stores the tokens a call received in the container the call was about: a profile call's tokens
|
|
705
758
|
// in the subject's profile container, a business call's in that business's container. The caller
|
|
706
759
|
// picks the container, because a token does not record what it is for. A business sync out that
|
|
@@ -765,7 +818,8 @@ class Model {
|
|
|
765
818
|
// over every page), whatever the response carried. The store keeps a blob only when its eTag is
|
|
766
819
|
// later, so it can reject a response in full (a cursor that moved without its eTag resends an
|
|
767
820
|
// entity already cached). Publishing then would notify every observer of an identical value.
|
|
768
|
-
//
|
|
821
|
+
// A sign up and an update follow this too, because their answer is the first page of a profile read
|
|
822
|
+
// (see readProfilePages).
|
|
769
823
|
//
|
|
770
824
|
// A business with online shopping off is not published, even from the cache, or a customer could
|
|
771
825
|
// be joined to it. A read that finds the published business is no longer open, or that the server
|
package/dist/Order.d.ts
CHANGED
|
@@ -15,8 +15,8 @@ export default class Order extends Message {
|
|
|
15
15
|
private readonly _deliveryAddress;
|
|
16
16
|
private readonly _note;
|
|
17
17
|
private readonly _paymentReceived;
|
|
18
|
-
static fromScaledValues(header: EntityHeader, senderId: bigint, taskStatus: proto_TaskStatus,
|
|
19
|
-
constructor(header: EntityHeader, senderId: bigint, taskStatus: proto_TaskStatus,
|
|
18
|
+
static fromScaledValues(header: EntityHeader, senderId: bigint, taskStatus: proto_TaskStatus, status: proto_OrderStatus, fulfillmentMode: proto_FulfillmentMode, shoppingList: ShoppingList, scaledShoppingListTotal: bigint, shoppingListCount: number, deliveryAddressId: bigint, deliveryAddress: Address | null, creationDate: bigint, note: string | null, scaledPaymentReceived: bigint | undefined): Order;
|
|
19
|
+
constructor(header: EntityHeader, senderId: bigint, taskStatus: proto_TaskStatus, _status: proto_OrderStatus, _fulfillmentMode: proto_FulfillmentMode, _shoppingList: ShoppingList, _shoppingListTotal: number, _shoppingListCount: number, _deliveryAddressId: bigint, _deliveryAddress: Address | null, creationDate: bigint, _note: string | null, _paymentReceived: number | null);
|
|
20
20
|
get status(): proto_OrderStatus;
|
|
21
21
|
get fulfillmentMode(): proto_FulfillmentMode;
|
|
22
22
|
get shoppingList(): ShoppingList;
|