@pisell/pisellos 0.10.57 → 0.10.59
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/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/inventory/index.d.ts +92 -0
- package/dist/inventory/index.js +500 -0
- package/dist/modules/Order/index.d.ts +20 -3
- package/dist/modules/Order/index.js +1369 -1021
- package/dist/modules/Order/types.d.ts +5 -0
- package/dist/modules/Order/utils.d.ts +5 -3
- package/dist/modules/Order/utils.js +32 -11
- package/dist/modules/ProductList/index.js +40 -17
- package/dist/solution/BaseSales/index.d.ts +8 -0
- package/dist/solution/BaseSales/index.js +1382 -1207
- package/dist/solution/BookingByStep/index.d.ts +1 -1
- package/dist/solution/UnifiedBookingSales/index.js +58 -38
- package/lib/index.d.ts +1 -0
- package/lib/index.js +12 -0
- package/lib/inventory/index.d.ts +92 -0
- package/lib/inventory/index.js +280 -0
- package/lib/modules/Order/index.d.ts +20 -3
- package/lib/modules/Order/index.js +111 -19
- package/lib/modules/Order/types.d.ts +5 -0
- package/lib/modules/Order/utils.d.ts +5 -3
- package/lib/modules/Order/utils.js +29 -10
- package/lib/modules/ProductList/index.js +21 -4
- package/lib/solution/BaseSales/index.d.ts +8 -0
- package/lib/solution/BaseSales/index.js +82 -7
- package/lib/solution/BookingByStep/index.d.ts +1 -1
- package/lib/solution/UnifiedBookingSales/index.js +11 -0
- package/package.json +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.salesGuardError = exports.InventoryController = void 0;
|
|
7
|
+
/** Optional, instance-scoped sales inventory. Never stored in ProductData or order payloads. */
|
|
8
|
+
|
|
9
|
+
const salesGuardError = message => Object.assign(new Error(message), {
|
|
10
|
+
code: 'SALES_GUARD_REJECTED'
|
|
11
|
+
});
|
|
12
|
+
exports.salesGuardError = salesGuardError;
|
|
13
|
+
const cancelled = () => Object.assign(new Error('Inventory request cancelled'), {
|
|
14
|
+
name: 'AbortError'
|
|
15
|
+
});
|
|
16
|
+
const variantIdentity = (productId, variantId) => JSON.stringify([String(productId), String(variantId ?? 0)]);
|
|
17
|
+
class InventoryController {
|
|
18
|
+
identity = Symbol('sales-inventory-instance');
|
|
19
|
+
scopeKey;
|
|
20
|
+
entries = new Map();
|
|
21
|
+
lastQuantities = new Map();
|
|
22
|
+
products = new Map();
|
|
23
|
+
keys = new Map();
|
|
24
|
+
catalogProducts = new Map();
|
|
25
|
+
listeners = new Set();
|
|
26
|
+
revision = 0;
|
|
27
|
+
state = 'stale';
|
|
28
|
+
pending;
|
|
29
|
+
disposed = false;
|
|
30
|
+
constructor(source) {
|
|
31
|
+
this.source = source;
|
|
32
|
+
this.scopeKey = source.scopeKey;
|
|
33
|
+
}
|
|
34
|
+
getRevision = () => this.revision;
|
|
35
|
+
subscribe = listener => {
|
|
36
|
+
this.listeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
this.listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
publish() {
|
|
42
|
+
this.revision += 1;
|
|
43
|
+
this.listeners.forEach(listener => {
|
|
44
|
+
try {
|
|
45
|
+
listener();
|
|
46
|
+
} catch {/* A view cannot interrupt a stock update. */}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
assertCurrent(signal) {
|
|
50
|
+
if (this.disposed || signal?.aborted) throw cancelled();
|
|
51
|
+
this.source.assertCurrent();
|
|
52
|
+
}
|
|
53
|
+
activate() {
|
|
54
|
+
this.disposed = false;
|
|
55
|
+
}
|
|
56
|
+
dispose() {
|
|
57
|
+
this.disposed = true;
|
|
58
|
+
this.invalidate();
|
|
59
|
+
}
|
|
60
|
+
invalidate() {
|
|
61
|
+
this.pending?.controller.abort();
|
|
62
|
+
this.pending = undefined;
|
|
63
|
+
this.state = 'stale';
|
|
64
|
+
this.publish();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Catalog replaces identities; partial detail only merges its variants, never its prices. */
|
|
68
|
+
registerProducts(products, complete = false) {
|
|
69
|
+
this.assertCurrent();
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const product of products) {
|
|
72
|
+
const id = String(product.id ?? product.product_id);
|
|
73
|
+
if (complete && this.catalogProducts.get(id) === product) continue;
|
|
74
|
+
if (complete) this.catalogProducts.set(id, product);
|
|
75
|
+
const previous = this.products.get(id);
|
|
76
|
+
if (complete) for (const variantId of previous?.keys() || []) this.keys.delete(variantIdentity(id, variantId));
|
|
77
|
+
const variants = complete ? new Map() : new Map(previous);
|
|
78
|
+
const list = product.variant?.length ? product.variant : [{
|
|
79
|
+
id: 0,
|
|
80
|
+
code: product.code
|
|
81
|
+
}];
|
|
82
|
+
for (const variant of list) {
|
|
83
|
+
const variantId = String(variant.id ?? variant.product_variant_id ?? 0);
|
|
84
|
+
const key = this.source.keyOf(product, variant) ?? previous?.get(variantId);
|
|
85
|
+
variants.set(variantId, key);
|
|
86
|
+
this.keys.set(variantIdentity(id, variantId), key);
|
|
87
|
+
}
|
|
88
|
+
if (!previous || previous.size !== variants.size || [...variants].some(([id, key]) => previous.get(id) !== key)) changed = true;
|
|
89
|
+
this.products.set(id, variants);
|
|
90
|
+
}
|
|
91
|
+
if (changed) this.publish();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Consumers share I/O; cancelling one detail does not cancel another consumer. */
|
|
95
|
+
async refresh(context) {
|
|
96
|
+
this.assertCurrent(context.signal);
|
|
97
|
+
if (context.force) this.invalidate();
|
|
98
|
+
let read = this.pending;
|
|
99
|
+
if (!read) {
|
|
100
|
+
read = {
|
|
101
|
+
controller: new AbortController(),
|
|
102
|
+
consumers: 0,
|
|
103
|
+
promise: Promise.resolve()
|
|
104
|
+
};
|
|
105
|
+
const current = read;
|
|
106
|
+
this.pending = current;
|
|
107
|
+
this.state = 'loading';
|
|
108
|
+
this.publish();
|
|
109
|
+
current.promise = (async () => {
|
|
110
|
+
try {
|
|
111
|
+
const entries = await this.source.read({
|
|
112
|
+
reason: context.reason,
|
|
113
|
+
signal: current.controller.signal
|
|
114
|
+
});
|
|
115
|
+
this.assertCurrent(current.controller.signal);
|
|
116
|
+
if (this.pending !== current) throw cancelled();
|
|
117
|
+
this.entries = entries;
|
|
118
|
+
for (const [key, stock] of entries) if (stock.quantity !== null && Number.isSafeInteger(stock.quantity) && stock.quantity >= 0) this.lastQuantities.set(key, stock.quantity);
|
|
119
|
+
this.state = 'ready';
|
|
120
|
+
} catch (error) {
|
|
121
|
+
if (this.pending !== current || current.controller.signal.aborted) throw cancelled();
|
|
122
|
+
this.state = 'error';
|
|
123
|
+
throw error;
|
|
124
|
+
} finally {
|
|
125
|
+
if (this.pending === current) {
|
|
126
|
+
this.pending = undefined;
|
|
127
|
+
this.publish();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
}
|
|
132
|
+
read.consumers += 1;
|
|
133
|
+
try {
|
|
134
|
+
if (!context.signal) await read.promise;else await new Promise((resolve, reject) => {
|
|
135
|
+
const signal = context.signal;
|
|
136
|
+
const abort = () => reject(cancelled());
|
|
137
|
+
signal.addEventListener('abort', abort, {
|
|
138
|
+
once: true
|
|
139
|
+
});
|
|
140
|
+
read.promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
|
|
141
|
+
if (signal.aborted) abort();
|
|
142
|
+
});
|
|
143
|
+
this.assertCurrent(context.signal);
|
|
144
|
+
} finally {
|
|
145
|
+
read.consumers -= 1;
|
|
146
|
+
if (this.pending === read && read.consumers === 0) this.invalidate();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
availabilityForKey(key) {
|
|
150
|
+
const stock = key ? this.entries.get(key) : undefined;
|
|
151
|
+
const quantity = key ? this.lastQuantities.get(key) : undefined;
|
|
152
|
+
if (this.state !== 'ready') return {
|
|
153
|
+
status: this.state,
|
|
154
|
+
quantity,
|
|
155
|
+
message: this.source.message('unavailable')
|
|
156
|
+
};
|
|
157
|
+
if (!stock) return {
|
|
158
|
+
status: 'unmapped',
|
|
159
|
+
quantity,
|
|
160
|
+
message: this.source.message('unmapped')
|
|
161
|
+
};
|
|
162
|
+
if (stock.quantity === null || !Number.isSafeInteger(stock.quantity) || stock.quantity < 0) return {
|
|
163
|
+
status: 'error',
|
|
164
|
+
quantity,
|
|
165
|
+
message: stock.message || this.source.message('unavailable')
|
|
166
|
+
};
|
|
167
|
+
return {
|
|
168
|
+
status: 'ready',
|
|
169
|
+
quantity: stock.quantity
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
availability(product, variantId) {
|
|
173
|
+
const id = product.id ?? product.product_id;
|
|
174
|
+
if (variantId !== undefined && variantId !== null) return this.availabilityForKey(this.keys.get(variantIdentity(id, variantId)));
|
|
175
|
+
const keys = [...new Set(this.products.get(String(id))?.values() || [])];
|
|
176
|
+
const states = keys.map(key => this.availabilityForKey(key));
|
|
177
|
+
if (!states.length) return this.availabilityForKey();
|
|
178
|
+
const quantity = states.every(entry => entry.quantity !== undefined) ? states.reduce((sum, entry) => sum + entry.quantity, 0) : undefined;
|
|
179
|
+
const failed = states.find(entry => entry.status !== 'ready');
|
|
180
|
+
if (quantity !== undefined && !Number.isSafeInteger(quantity)) return {
|
|
181
|
+
status: 'error',
|
|
182
|
+
message: this.source.message('unavailable')
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
status: failed?.status || 'ready',
|
|
186
|
+
quantity,
|
|
187
|
+
message: failed?.message
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
keyForLine(item) {
|
|
191
|
+
return this.keys.get(variantIdentity(item.product_id, item.product_variant_id ?? item.product_sku?.product_variant_id));
|
|
192
|
+
}
|
|
193
|
+
/** Advisory UI limit only. The serialized cart guard remains authoritative. */
|
|
194
|
+
selectionAvailability(product, variantId, items, editingItem) {
|
|
195
|
+
const identity = variantIdentity(product.id ?? product.product_id, variantId);
|
|
196
|
+
const key = this.keys.get(identity);
|
|
197
|
+
let availability = this.availabilityForKey(key);
|
|
198
|
+
let reserved = 0;
|
|
199
|
+
let retainedQuantity = 0;
|
|
200
|
+
for (const item of items) {
|
|
201
|
+
const matches = key ? this.keyForLine(item) === key : variantIdentity(item.product_id, item.product_variant_id ?? item.product_sku?.product_variant_id) === identity;
|
|
202
|
+
if (!matches) continue;
|
|
203
|
+
const count = Number(item.num ?? item.quantity);
|
|
204
|
+
if (!Number.isSafeInteger(count) || count < 0) {
|
|
205
|
+
availability = {
|
|
206
|
+
...availability,
|
|
207
|
+
status: 'error',
|
|
208
|
+
message: this.source.message('unavailable')
|
|
209
|
+
};
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (item === editingItem) retainedQuantity = count;else reserved += count;
|
|
213
|
+
}
|
|
214
|
+
if (!Number.isSafeInteger(reserved)) {
|
|
215
|
+
availability = {
|
|
216
|
+
...availability,
|
|
217
|
+
status: 'error',
|
|
218
|
+
message: this.source.message('unavailable')
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const availableQuantity = availability.status === 'ready' ? Math.max(0, availability.quantity - reserved) : 0;
|
|
222
|
+
return {
|
|
223
|
+
availability,
|
|
224
|
+
availableQuantity,
|
|
225
|
+
retainedQuantity,
|
|
226
|
+
maxQuantity: Math.max(availableQuantity, retainedQuantity)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
totals(items) {
|
|
230
|
+
const totals = new Map();
|
|
231
|
+
for (const item of items) {
|
|
232
|
+
const key = this.keyForLine(item);
|
|
233
|
+
const identity = key ? JSON.stringify(['inventory', key]) : variantIdentity(item.product_id, item.product_variant_id);
|
|
234
|
+
const quantity = Number(item.num ?? item.quantity);
|
|
235
|
+
if (!Number.isSafeInteger(quantity) || quantity < 0) throw salesGuardError(this.source.message('unavailable'));
|
|
236
|
+
if (!quantity) continue;
|
|
237
|
+
const total = (totals.get(identity)?.quantity || 0) + quantity;
|
|
238
|
+
if (!Number.isSafeInteger(total)) throw salesGuardError(this.source.message('unavailable'));
|
|
239
|
+
totals.set(identity, {
|
|
240
|
+
key,
|
|
241
|
+
quantity: total
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return totals;
|
|
245
|
+
}
|
|
246
|
+
validate(proposed, current = [], allowReductions = true) {
|
|
247
|
+
this.assertCurrent();
|
|
248
|
+
const previous = this.totals(current);
|
|
249
|
+
for (const [identity, {
|
|
250
|
+
key,
|
|
251
|
+
quantity
|
|
252
|
+
}] of this.totals(proposed)) {
|
|
253
|
+
if (allowReductions && quantity <= (previous.get(identity)?.quantity || 0)) continue;
|
|
254
|
+
const stock = this.availabilityForKey(key);
|
|
255
|
+
if (stock.status !== 'ready') throw salesGuardError(stock.message || this.source.message('unavailable'));
|
|
256
|
+
if (quantity > stock.quantity) throw salesGuardError(this.source.message('insufficient', {
|
|
257
|
+
key: key,
|
|
258
|
+
quantity: stock.quantity
|
|
259
|
+
}));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
freezeCheckout(items) {
|
|
263
|
+
if (!items.length || items.some(item => Number(item.num ?? item.quantity) <= 0)) {
|
|
264
|
+
throw salesGuardError(this.source.message('unavailable'));
|
|
265
|
+
}
|
|
266
|
+
this.validate(items, [], false);
|
|
267
|
+
return Object.freeze({
|
|
268
|
+
scopeKey: this.scopeKey,
|
|
269
|
+
revision: this.revision,
|
|
270
|
+
lines: Object.freeze(items.map((item, index) => Object.freeze({
|
|
271
|
+
lineId: String(item.metadata?.unique_identification_number ?? item.unique_identification_number ?? item.order_detail_id ?? index),
|
|
272
|
+
productId: item.product_id,
|
|
273
|
+
variantId: item.product_variant_id ?? item.product_sku?.product_variant_id ?? 0,
|
|
274
|
+
quantity: Number(item.num ?? item.quantity),
|
|
275
|
+
inventoryKey: this.keyForLine(item)
|
|
276
|
+
})))
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.InventoryController = InventoryController;
|
|
@@ -4,7 +4,7 @@ import { OrderModuleAPI, CommitOrderParams, UpdateOrderBookingParams, UpdateOrde
|
|
|
4
4
|
import { CartItem } from '../Cart/types';
|
|
5
5
|
import { type SubmitPayloadEnhancer } from './utils';
|
|
6
6
|
import { type PickupTimingBooking, type PickupTimingBookingInput } from './pickupTiming';
|
|
7
|
-
import type { OrderAmountSnapshot, AddProductBookingInput, AddProductToOrderOptions, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, OrderCartIntentCommitContext, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, PointCardSnapshotMergeContext, PointCardSnapshotItem, ShopServiceData, ShopServiceType } from './types';
|
|
7
|
+
import type { OrderAmountSnapshot, AddProductBookingInput, AddProductToOrderOptions, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, OrderHooksConfig, OrderCartIntentCommitContext, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, PointCardSnapshotMergeContext, PointCardSnapshotItem, ShopServiceData, ShopServiceType } from './types';
|
|
8
8
|
import type { ICustomer } from '../AccountList/types';
|
|
9
9
|
import type { Discount } from '../Discount/types';
|
|
10
10
|
import { UnavailableReason } from '../Rules/types';
|
|
@@ -29,6 +29,12 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
29
29
|
private rulesHooksOverride?;
|
|
30
30
|
private moduleHooksOverride?;
|
|
31
31
|
private orderHooks?;
|
|
32
|
+
private cartCommandQueue;
|
|
33
|
+
private queueCartCommand;
|
|
34
|
+
/** Use the exact Order normalizers/merge rules on a private draft, not a second projection. */
|
|
35
|
+
private beginCartMutation;
|
|
36
|
+
/** Serializes checkout preparation with local cart commands; hydration remains outside this guard. */
|
|
37
|
+
prepareCheckout<T>(prepare: (items: OrderProduct[]) => Promise<T>): Promise<T>;
|
|
32
38
|
private otherParams?;
|
|
33
39
|
/** 评估器引用;由 SalesSdkProvider 注入,未注入时 applyPromotion 静默跳过 */
|
|
34
40
|
private promotionEvaluator;
|
|
@@ -66,7 +72,11 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
66
72
|
* @example
|
|
67
73
|
* orderModule.updateOtherParams({ cacheId: 'cache-2', channel: 'pos' });
|
|
68
74
|
*/
|
|
69
|
-
updateOtherParams(params: Record<string, any
|
|
75
|
+
updateOtherParams(params: Record<string, any>, options?: {
|
|
76
|
+
hooks?: OrderHooksConfig;
|
|
77
|
+
changedParams?: Record<string, any>;
|
|
78
|
+
cover?: boolean;
|
|
79
|
+
}): void;
|
|
70
80
|
/** Internal intent boundary shared with asynchronous scanner/detail flows. */
|
|
71
81
|
createCartIntentCommitGuard(action: OrderCartIntentCommitContext['action'], tempOrder?: OrderTempOrder | null): () => void;
|
|
72
82
|
/** PC/H5 属于在线店铺;其它或缺失平台统一沿用 POS 行为。 */
|
|
@@ -481,6 +491,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
481
491
|
* // → tempOrder.products 新增 3 行,每行 num=1,bookings 3 条
|
|
482
492
|
*/
|
|
483
493
|
addProductToOrder(product: Partial<OrderProduct> & OrderProductIdentity, booking?: AddProductBookingInput, options?: AddProductToOrderOptions): Promise<OrderProduct[]>;
|
|
494
|
+
private addProductToOrderDraft;
|
|
484
495
|
/**
|
|
485
496
|
* 批量添加商品行,所有行追加完成后只触发一次促销、折扣、summary 重算与持久化。
|
|
486
497
|
*
|
|
@@ -496,6 +507,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
496
507
|
}>, options?: {
|
|
497
508
|
skipEditDiscountConfigRefresh?: boolean;
|
|
498
509
|
}): Promise<OrderProduct[]>;
|
|
510
|
+
private addProductsToOrderDraft;
|
|
499
511
|
private getFullCoveragePassDiscounts;
|
|
500
512
|
private hasEntitlementPassDiscount;
|
|
501
513
|
private hasGoodPassDiscount;
|
|
@@ -507,8 +519,11 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
507
519
|
private isRestrictedCartProductUpdate;
|
|
508
520
|
private applyOrderProductUpdateToTempOrder;
|
|
509
521
|
updateOrderProduct(params: UpdateOrderProductParams): Promise<OrderProduct[]>;
|
|
522
|
+
private updateOrderProductDraft;
|
|
510
523
|
updateOrderProducts(paramsList: UpdateOrderProductParams[]): Promise<OrderProduct[]>;
|
|
524
|
+
private updateOrderProductsDraft;
|
|
511
525
|
updateOrderProductQuantity(params: UpdateOrderProductQuantityParams): Promise<OrderProduct[]>;
|
|
526
|
+
private updateOrderProductQuantityDraft;
|
|
512
527
|
updateOrderBooking(params: UpdateOrderBookingParams): any[];
|
|
513
528
|
/**
|
|
514
529
|
* 商品行写路径收尾:促销 → 折扣 → 清洗 → summary → 持久化。
|
|
@@ -525,12 +540,14 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
525
540
|
* await order.removeProductsFromOrder([identityA, identityB]);
|
|
526
541
|
*/
|
|
527
542
|
removeProductsFromOrder(identities: OrderProductIdentity[], options?: RemoveProductsFromOrderOptions): Promise<OrderProduct[]>;
|
|
543
|
+
private removeProductsFromOrderDraft;
|
|
528
544
|
removeProductFromOrder(identity: OrderProductIdentity, options?: RemoveProductsFromOrderOptions): Promise<OrderProduct[]>;
|
|
529
545
|
/**
|
|
530
546
|
* 仅清空购物车行(products / bookings),不重置整单。
|
|
531
547
|
* 普通整单 note 保持不变;以商品行为主体的 Sales Notes 同步删除。
|
|
532
548
|
*/
|
|
533
549
|
clearOrderCartLines(): Promise<OrderTempOrder>;
|
|
550
|
+
private clearOrderCartLinesDraft;
|
|
534
551
|
/**
|
|
535
552
|
* 切换购物车结算单位时清理所有会影响新币种的订单状态,但保留当前客户。
|
|
536
553
|
*/
|
|
@@ -592,7 +609,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
|
|
|
592
609
|
generateIdempotencyToken(): string;
|
|
593
610
|
syncPaymentsToOrder<T = any>(params: SyncPaymentsToOrderParams): Promise<SyncPaymentsToOrderResult<T>>;
|
|
594
611
|
createOrder(params: CommitOrderParams['query']): {
|
|
595
|
-
type: "
|
|
612
|
+
type: "appointment_booking" | "virtual";
|
|
596
613
|
platform: string;
|
|
597
614
|
sales_channel: string;
|
|
598
615
|
order_sales_channel: string;
|
|
@@ -145,6 +145,63 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
145
145
|
rulesHooksOverride;
|
|
146
146
|
moduleHooksOverride;
|
|
147
147
|
orderHooks;
|
|
148
|
+
cartCommandQueue = Promise.resolve();
|
|
149
|
+
queueCartCommand(run) {
|
|
150
|
+
if (!this.orderHooks?.onBeforeCartCommit) return run();
|
|
151
|
+
const pending = this.cartCommandQueue.then(run, run);
|
|
152
|
+
this.cartCommandQueue = pending.catch(() => undefined);
|
|
153
|
+
return pending;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Use the exact Order normalizers/merge rules on a private draft, not a second projection. */
|
|
157
|
+
beginCartMutation() {
|
|
158
|
+
const current = this.ensureTempOrder();
|
|
159
|
+
const guard = this.orderHooks?.onBeforeCartCommit;
|
|
160
|
+
if (!guard) return {
|
|
161
|
+
current,
|
|
162
|
+
draft: current,
|
|
163
|
+
commit: undefined
|
|
164
|
+
};
|
|
165
|
+
const before = (0, _lodashEs.cloneDeep)(current);
|
|
166
|
+
const draft = (0, _lodashEs.cloneDeep)(current);
|
|
167
|
+
return {
|
|
168
|
+
current,
|
|
169
|
+
draft,
|
|
170
|
+
commit: async beforeCommit => {
|
|
171
|
+
await guard({
|
|
172
|
+
currentItems: (0, _lodashEs.cloneDeep)(before.products),
|
|
173
|
+
proposedItems: (0, _lodashEs.cloneDeep)(draft.products)
|
|
174
|
+
});
|
|
175
|
+
if (this.store.tempOrder !== current || guard !== this.orderHooks?.onBeforeCartCommit || !(0, _lodashEs.isEqual)(current, before)) {
|
|
176
|
+
throw Object.assign(new Error('The cart changed. Please try again.'), {
|
|
177
|
+
code: 'SALES_GUARD_REJECTED'
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Recheck the accepted intent's scope after asynchronous inventory validation.
|
|
181
|
+
beforeCommit?.();
|
|
182
|
+
Object.assign(current, draft);
|
|
183
|
+
return current;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Serializes checkout preparation with local cart commands; hydration remains outside this guard. */
|
|
189
|
+
async prepareCheckout(prepare) {
|
|
190
|
+
const run = async () => {
|
|
191
|
+
const order = this.ensureTempOrder();
|
|
192
|
+
const items = (0, _lodashEs.cloneDeep)(order.products);
|
|
193
|
+
const result = await prepare(items);
|
|
194
|
+
if (this.store.tempOrder !== order || !(0, _lodashEs.isEqual)(order.products, items)) {
|
|
195
|
+
throw Object.assign(new Error('The cart changed. Please try again.'), {
|
|
196
|
+
code: 'SALES_GUARD_REJECTED'
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return result;
|
|
200
|
+
};
|
|
201
|
+
const pending = this.cartCommandQueue.then(run, run);
|
|
202
|
+
this.cartCommandQueue = pending.catch(() => undefined);
|
|
203
|
+
return pending;
|
|
204
|
+
}
|
|
148
205
|
otherParams;
|
|
149
206
|
// ─── 促销/赠品 ────────────────────────────────────
|
|
150
207
|
/** 评估器引用;由 SalesSdkProvider 注入,未注入时 applyPromotion 静默跳过 */
|
|
@@ -432,8 +489,11 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
432
489
|
* @example
|
|
433
490
|
* orderModule.updateOtherParams({ cacheId: 'cache-2', channel: 'pos' });
|
|
434
491
|
*/
|
|
435
|
-
updateOtherParams(params) {
|
|
492
|
+
updateOtherParams(params, options) {
|
|
436
493
|
const otherParams = params || {};
|
|
494
|
+
if (options && Object.prototype.hasOwnProperty.call(options, 'hooks')) {
|
|
495
|
+
this.moduleHooksOverride = options.hooks;
|
|
496
|
+
}
|
|
437
497
|
this.syncSessionStorageOptions(otherParams);
|
|
438
498
|
this.salesSummaryModuleName = otherParams.salesSummaryModuleName;
|
|
439
499
|
this.rulesHooksOverride = otherParams.rules?.hooks;
|
|
@@ -3704,8 +3764,12 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3704
3764
|
* // → tempOrder.products 新增 3 行,每行 num=1,bookings 3 条
|
|
3705
3765
|
*/
|
|
3706
3766
|
async addProductToOrder(product, booking, options) {
|
|
3707
|
-
|
|
3708
|
-
|
|
3767
|
+
return this.queueCartCommand(() => this.addProductToOrderDraft(product, booking, options));
|
|
3768
|
+
}
|
|
3769
|
+
async addProductToOrderDraft(product, booking, options) {
|
|
3770
|
+
const mutation = this.beginCartMutation();
|
|
3771
|
+
let tempOrder = mutation.draft;
|
|
3772
|
+
const beforeCommit = this.createCartIntentCommitGuard('add_product', mutation.current);
|
|
3709
3773
|
const salesNoteDrafts = (options?.salesNotes || []).map(draft => ({
|
|
3710
3774
|
note_type: String(draft.note_type || '').trim(),
|
|
3711
3775
|
text: {
|
|
@@ -3802,6 +3866,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3802
3866
|
rollbackProductAndNotesMutation();
|
|
3803
3867
|
throw error;
|
|
3804
3868
|
}
|
|
3869
|
+
if (mutation.commit) tempOrder = await mutation.commit(beforeCommit);
|
|
3805
3870
|
await this.finalizeProductOrderMutation(tempOrder);
|
|
3806
3871
|
this.logInfo('addProductToOrder success', {
|
|
3807
3872
|
product_id: product.product_id,
|
|
@@ -3820,9 +3885,13 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3820
3885
|
* ]);
|
|
3821
3886
|
*/
|
|
3822
3887
|
async addProductsToOrder(items, options) {
|
|
3823
|
-
|
|
3888
|
+
return this.queueCartCommand(() => this.addProductsToOrderDraft(items, options));
|
|
3889
|
+
}
|
|
3890
|
+
async addProductsToOrderDraft(items, options) {
|
|
3891
|
+
const mutation = this.beginCartMutation();
|
|
3892
|
+
let tempOrder = mutation.draft;
|
|
3824
3893
|
if (!Array.isArray(items) || items.length === 0) return tempOrder.products;
|
|
3825
|
-
const beforeCommit = this.createCartIntentCommitGuard('add_product',
|
|
3894
|
+
const beforeCommit = this.createCartIntentCommitGuard('add_product', mutation.current);
|
|
3826
3895
|
for (const item of items || []) {
|
|
3827
3896
|
const {
|
|
3828
3897
|
product,
|
|
@@ -3848,6 +3917,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
3848
3917
|
});
|
|
3849
3918
|
if (appendResult) await appendResult;
|
|
3850
3919
|
}
|
|
3920
|
+
if (mutation.commit) tempOrder = await mutation.commit(beforeCommit);
|
|
3851
3921
|
await this.finalizeProductOrderMutation(tempOrder, options);
|
|
3852
3922
|
this.logInfo('addProductsToOrder success', {
|
|
3853
3923
|
itemsCount: items.length
|
|
@@ -4190,12 +4260,16 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4190
4260
|
return tempOrder.products[productIndex].metadata?.unique_identification_number;
|
|
4191
4261
|
}
|
|
4192
4262
|
async updateOrderProduct(params) {
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4263
|
+
return this.queueCartCommand(() => this.updateOrderProductDraft(params));
|
|
4264
|
+
}
|
|
4265
|
+
async updateOrderProductDraft(params) {
|
|
4266
|
+
const mutation = this.beginCartMutation();
|
|
4267
|
+
let tempOrder = mutation.draft;
|
|
4268
|
+
const beforeCommit = this.isRestrictedCartProductUpdate(tempOrder, params) ? this.createCartIntentCommitGuard('update_product', mutation.current) : undefined;
|
|
4269
|
+
beforeCommit?.();
|
|
4197
4270
|
const reapplyWeighedDiscountProductUids = [];
|
|
4198
4271
|
const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params, reapplyWeighedDiscountProductUids);
|
|
4272
|
+
if (mutation.commit) tempOrder = await mutation.commit(beforeCommit);
|
|
4199
4273
|
const promotionPayload = await this.evaluatePromotion();
|
|
4200
4274
|
this.applyDiscount({
|
|
4201
4275
|
reapplyWeighedDiscountProductUids,
|
|
@@ -4213,13 +4287,16 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4213
4287
|
return tempOrder.products;
|
|
4214
4288
|
}
|
|
4215
4289
|
async updateOrderProducts(paramsList) {
|
|
4216
|
-
|
|
4290
|
+
return this.queueCartCommand(() => this.updateOrderProductsDraft(paramsList));
|
|
4291
|
+
}
|
|
4292
|
+
async updateOrderProductsDraft(paramsList) {
|
|
4293
|
+
const mutation = this.beginCartMutation();
|
|
4294
|
+
let tempOrder = mutation.draft;
|
|
4217
4295
|
if (!paramsList.length) {
|
|
4218
4296
|
return tempOrder.products;
|
|
4219
4297
|
}
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
}
|
|
4298
|
+
const beforeCommit = paramsList.some(params => this.isRestrictedCartProductUpdate(tempOrder, params)) ? this.createCartIntentCommitGuard('update_product', mutation.current) : undefined;
|
|
4299
|
+
beforeCommit?.();
|
|
4223
4300
|
const reapplyWeighedDiscountProductUids = [];
|
|
4224
4301
|
const reapplyBookingDiscountProductUids = paramsList.reduce((acc, params) => {
|
|
4225
4302
|
const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params, reapplyWeighedDiscountProductUids);
|
|
@@ -4229,6 +4306,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4229
4306
|
}
|
|
4230
4307
|
return acc;
|
|
4231
4308
|
}, []);
|
|
4309
|
+
if (mutation.commit) tempOrder = await mutation.commit(beforeCommit);
|
|
4232
4310
|
await this.finalizeAfterProductsMutation(tempOrder, {
|
|
4233
4311
|
reapplyWeighedDiscountProductUids,
|
|
4234
4312
|
reapplyBookingDiscountProductUids: reapplyBookingDiscountProductUids.length ? reapplyBookingDiscountProductUids : undefined
|
|
@@ -4239,6 +4317,9 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4239
4317
|
return tempOrder.products;
|
|
4240
4318
|
}
|
|
4241
4319
|
async updateOrderProductQuantity(params) {
|
|
4320
|
+
return this.queueCartCommand(() => this.updateOrderProductQuantityDraft(params));
|
|
4321
|
+
}
|
|
4322
|
+
async updateOrderProductQuantityDraft(params) {
|
|
4242
4323
|
const {
|
|
4243
4324
|
product_id,
|
|
4244
4325
|
product_variant_id,
|
|
@@ -4248,7 +4329,8 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4248
4329
|
product_sku,
|
|
4249
4330
|
product_bundle
|
|
4250
4331
|
} = params;
|
|
4251
|
-
const
|
|
4332
|
+
const mutation = this.beginCartMutation();
|
|
4333
|
+
let tempOrder = mutation.draft;
|
|
4252
4334
|
this.repairDuplicateProductLineIdentities(tempOrder);
|
|
4253
4335
|
const identityLookup = {
|
|
4254
4336
|
product_id,
|
|
@@ -4304,10 +4386,10 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4304
4386
|
throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
|
|
4305
4387
|
}
|
|
4306
4388
|
}
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
}
|
|
4389
|
+
const beforeCommit = normalizedProduct.num > targetProduct.num ? this.createCartIntentCommitGuard('increase_quantity', mutation.current) : undefined;
|
|
4390
|
+
beforeCommit?.();
|
|
4310
4391
|
tempOrder.products[productIndex] = normalizedProduct;
|
|
4392
|
+
if (mutation.commit) tempOrder = await mutation.commit(beforeCommit);
|
|
4311
4393
|
this.prepareVirtualSettlementTempOrder(tempOrder);
|
|
4312
4394
|
const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
|
|
4313
4395
|
const promotionPayload = !isVirtualSettlement ? await this.evaluatePromotion() : null;
|
|
@@ -4384,7 +4466,11 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4384
4466
|
* await order.removeProductsFromOrder([identityA, identityB]);
|
|
4385
4467
|
*/
|
|
4386
4468
|
async removeProductsFromOrder(identities, options) {
|
|
4387
|
-
|
|
4469
|
+
return this.queueCartCommand(() => this.removeProductsFromOrderDraft(identities, options));
|
|
4470
|
+
}
|
|
4471
|
+
async removeProductsFromOrderDraft(identities, options) {
|
|
4472
|
+
const mutation = this.beginCartMutation();
|
|
4473
|
+
let tempOrder = mutation.draft;
|
|
4388
4474
|
if (!identities.length) {
|
|
4389
4475
|
return tempOrder.products;
|
|
4390
4476
|
}
|
|
@@ -4451,6 +4537,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4451
4537
|
}
|
|
4452
4538
|
}
|
|
4453
4539
|
this.removeSalesNotesForMissingProductLines(tempOrder);
|
|
4540
|
+
if (mutation.commit) tempOrder = await mutation.commit();
|
|
4454
4541
|
await this.finalizeAfterProductsMutation(tempOrder);
|
|
4455
4542
|
this.logInfo('removeProductsFromOrder success', {
|
|
4456
4543
|
identities
|
|
@@ -4466,7 +4553,11 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4466
4553
|
* 普通整单 note 保持不变;以商品行为主体的 Sales Notes 同步删除。
|
|
4467
4554
|
*/
|
|
4468
4555
|
async clearOrderCartLines() {
|
|
4469
|
-
|
|
4556
|
+
return this.queueCartCommand(() => this.clearOrderCartLinesDraft());
|
|
4557
|
+
}
|
|
4558
|
+
async clearOrderCartLinesDraft() {
|
|
4559
|
+
const mutation = this.beginCartMutation();
|
|
4560
|
+
let tempOrder = mutation.draft;
|
|
4470
4561
|
this.ensureSalesNotesLoadedForMutation(tempOrder);
|
|
4471
4562
|
tempOrder.products = [];
|
|
4472
4563
|
tempOrder.bookings = [];
|
|
@@ -4479,6 +4570,7 @@ class OrderModule extends _BaseModule.BaseModule {
|
|
|
4479
4570
|
productsByUid: {}
|
|
4480
4571
|
};
|
|
4481
4572
|
}
|
|
4573
|
+
if (mutation.commit) tempOrder = await mutation.commit();
|
|
4482
4574
|
const promotionPayload = await this.evaluatePromotion();
|
|
4483
4575
|
this.applyDiscount();
|
|
4484
4576
|
this.sanitizeTempOrderProducts(tempOrder);
|
|
@@ -526,6 +526,11 @@ export interface OrderHooksConfig {
|
|
|
526
526
|
beforeCartIntentCommit?: (context: OrderCartIntentCommitContext) => void;
|
|
527
527
|
/** Captures tenant/business runtime identity for asynchronous cart intents. */
|
|
528
528
|
getCartIntentScope?: () => unknown;
|
|
529
|
+
/** Awaited before a local cart draft is published. Reject to leave the live order unchanged. */
|
|
530
|
+
onBeforeCartCommit?: (context: {
|
|
531
|
+
currentItems: readonly OrderProduct[];
|
|
532
|
+
proposedItems: readonly OrderProduct[];
|
|
533
|
+
}) => void | Promise<void>;
|
|
529
534
|
onBeforeMergeProductToOrder?: (context: OrderProductMergeDecisionContext) => OrderProductMergeDecisionResult | Promise<OrderProductMergeDecisionResult>;
|
|
530
535
|
}
|
|
531
536
|
/**
|
|
@@ -121,8 +121,9 @@ export declare function getBundleSellingMagnitude(bundle: Record<string, any> |
|
|
|
121
121
|
* = `metadata.main_product_original_price` + Σ(bundle 原价 × num)。
|
|
122
122
|
* - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),不含 option、
|
|
123
123
|
* 不含折扣。**权威源**。
|
|
124
|
-
* - `
|
|
125
|
-
* - `metadata.
|
|
124
|
+
* - `product_sku.option[].add_price` 保留单规格折前单价;Rules 的 option.price 只用于本次计算。
|
|
125
|
+
* - `metadata.main_product_original_price` = source + Σ(option.add_price × option.num)(含 option、不含折扣)。
|
|
126
|
+
* - `metadata.main_product_selling_price` = main_original − 行内 per-unit amount(含主商品及 option 优惠)。
|
|
126
127
|
* - `payment_price`:出站 payload 中为行 composite 已减整单折扣均摊价(无折扣时与 `selling_price` 同义)。
|
|
127
128
|
*
|
|
128
129
|
* Rules 钩子契约(hook_adapt 方案,Rules 内部保持不变):
|
|
@@ -131,8 +132,9 @@ export declare function getBundleSellingMagnitude(bundle: Record<string, any> |
|
|
|
131
132
|
* `getProductOriginTotalPrice` 会自行叠加 option + bundle,喂 source 可避免 option 双加。
|
|
132
133
|
* - `getProduct.total` = `selling_price × num`(行 composite × num),与 Rules 内部产出的
|
|
133
134
|
* total(source + option + bundle)对齐。
|
|
135
|
+
* - `getProduct.options` 用单规格原价参与重算;主商品折后价从 main_selling 减去 option 折后合计反推。
|
|
134
136
|
* - `setProduct` 反向流程:把 Rules 返回的 source-level `main_product_selling_price` / `price`
|
|
135
|
-
*
|
|
137
|
+
* 加回单规格原价合计,再减去 optionDiscountAmount;保留订单单规格原价供取消折扣及后续重算。
|
|
136
138
|
* 优先级:
|
|
137
139
|
* 1. `values.main_product_selling_price`(券应用分支,per-unit 券后 source-level 主价)
|
|
138
140
|
* 2. `values.price`(还原分支 `restoredPrice`、good_pass 归零)
|