@pisell/pisellos 2.3.150 → 2.3.152
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/model/strategy/adapter/itemRule/adapter.d.ts +3 -1
- package/dist/model/strategy/adapter/itemRule/adapter.js +74 -7
- package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +3 -1
- package/dist/model/strategy/adapter/itemRule/evaluator.js +13 -0
- package/dist/model/strategy/adapter/itemRule/type.d.ts +40 -1
- package/dist/model/strategy/adapter/itemRule/type.js +10 -0
- package/dist/model/strategy/adapter/promotion/index.js +9 -0
- package/dist/modules/OpenData/types.d.ts +2 -0
- package/dist/modules/Order/index.d.ts +3 -1
- package/dist/modules/Order/index.js +115 -67
- package/dist/modules/Order/types.d.ts +7 -1
- package/dist/modules/Product/index.d.ts +1 -1
- package/dist/solution/BaseSales/fulfillmentItemRules.d.ts +27 -0
- package/dist/solution/BaseSales/fulfillmentItemRules.js +750 -0
- package/dist/solution/BaseSales/index.d.ts +5 -1
- package/dist/solution/BaseSales/index.js +1127 -1021
- package/dist/solution/BaseSales/pickupTiming.d.ts +2 -0
- package/dist/solution/BaseSales/pickupTiming.js +16 -5
- package/dist/solution/BookingByStep/index.d.ts +2 -2
- package/dist/solution/BookingTicket/index.d.ts +1 -0
- package/dist/solution/BookingTicket/index.js +444 -425
- package/lib/model/strategy/adapter/itemRule/adapter.d.ts +3 -1
- package/lib/model/strategy/adapter/itemRule/adapter.js +54 -4
- package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +3 -1
- package/lib/model/strategy/adapter/itemRule/evaluator.js +9 -0
- package/lib/model/strategy/adapter/itemRule/type.d.ts +40 -1
- package/lib/model/strategy/adapter/itemRule/type.js +10 -0
- package/lib/model/strategy/adapter/promotion/index.js +46 -1
- package/lib/modules/OpenData/types.d.ts +2 -0
- package/lib/modules/Order/index.d.ts +3 -1
- package/lib/modules/Order/index.js +33 -14
- package/lib/modules/Order/types.d.ts +7 -1
- package/lib/modules/Product/index.d.ts +1 -1
- package/lib/solution/BaseSales/fulfillmentItemRules.d.ts +27 -0
- package/lib/solution/BaseSales/fulfillmentItemRules.js +259 -0
- package/lib/solution/BaseSales/index.d.ts +5 -1
- package/lib/solution/BaseSales/index.js +43 -2
- package/lib/solution/BaseSales/pickupTiming.d.ts +2 -0
- package/lib/solution/BaseSales/pickupTiming.js +5 -1
- package/lib/solution/BookingByStep/index.d.ts +2 -2
- package/lib/solution/BookingTicket/index.d.ts +1 -0
- package/lib/solution/BookingTicket/index.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.FulfillmentItemRulesController = void 0;
|
|
7
|
+
var _dayjs = _interopRequireDefault(require("dayjs"));
|
|
8
|
+
var _itemRule = require("../../model/strategy/adapter/itemRule");
|
|
9
|
+
var _transformBaseProductToOrderProduct = require("./utils/transformBaseProductToOrderProduct");
|
|
10
|
+
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
11
|
+
const ownershipField = 'fulfillment_item_rule';
|
|
12
|
+
function positiveIds(value) {
|
|
13
|
+
if (!Array.isArray(value)) return [];
|
|
14
|
+
return [...new Set(value.map(Number).filter(id => Number.isInteger(id) && id > 0))];
|
|
15
|
+
}
|
|
16
|
+
function lineIdentity(line) {
|
|
17
|
+
return {
|
|
18
|
+
product_id: line.product_id,
|
|
19
|
+
product_variant_id: line.product_variant_id,
|
|
20
|
+
unique_identification_number: line.metadata?.unique_identification_number || line.unique_identification_number,
|
|
21
|
+
product_sku: line.product_sku,
|
|
22
|
+
product_bundle: line.product_bundle
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function cartFingerprint(lines) {
|
|
26
|
+
return JSON.stringify(lines.map(line => [lineIdentity(line), line.num, line.metadata?.[ownershipField]]));
|
|
27
|
+
}
|
|
28
|
+
function selectionFingerprint(lines, excludedIds) {
|
|
29
|
+
const quantities = new Map();
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (excludedIds.has(Number(line.product_id)) || line.metadata?._giftInfo) continue;
|
|
32
|
+
const key = JSON.stringify([Number(line.product_id), Number(line.product_variant_id) || 0]);
|
|
33
|
+
quantities.set(key, (quantities.get(key) || 0) + Number(line.num || 0));
|
|
34
|
+
}
|
|
35
|
+
return JSON.stringify([...quantities].sort(([left], [right]) => left.localeCompare(right)));
|
|
36
|
+
}
|
|
37
|
+
function ownerKey(action, product) {
|
|
38
|
+
return JSON.stringify([action.strategyId, action.ruleId, product.product_id, product.product_variant_id ?? 0]);
|
|
39
|
+
}
|
|
40
|
+
function ownedLines(lines, key) {
|
|
41
|
+
return lines.filter(line => line.metadata?.[ownershipField] === key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Executes item rules at the sales boundary, independent of the calling template. */
|
|
45
|
+
class FulfillmentItemRulesController {
|
|
46
|
+
sequence = 0;
|
|
47
|
+
pending = Promise.resolve();
|
|
48
|
+
evaluator = new _itemRule.ItemRuleEvaluator();
|
|
49
|
+
constructor(host) {
|
|
50
|
+
this.host = host;
|
|
51
|
+
}
|
|
52
|
+
invalidate() {
|
|
53
|
+
this.sequence += 1;
|
|
54
|
+
}
|
|
55
|
+
apply(serviceType) {
|
|
56
|
+
const sequence = ++this.sequence;
|
|
57
|
+
// Even an unsupported mode supersedes any pending takeaway operation.
|
|
58
|
+
if (serviceType !== 'takeaway' && serviceType !== 'dine_in') return Promise.resolve();
|
|
59
|
+
const order = this.host.getOrder();
|
|
60
|
+
const tempOrder = order.getTempOrder?.();
|
|
61
|
+
const scope = this.host.getScope();
|
|
62
|
+
const identity = JSON.stringify([tempOrder?.order_id, tempOrder?.external_sale_number, tempOrder?.customer_id]);
|
|
63
|
+
const initialProducts = tempOrder?.products;
|
|
64
|
+
const initialFingerprint = cartFingerprint(initialProducts || []);
|
|
65
|
+
const isCurrent = () => sequence === this.sequence && scope === this.host.getScope() && this.host.getOrder() === order && order.getTempOrder?.() === tempOrder && identity === JSON.stringify([tempOrder?.order_id, tempOrder?.external_sale_number, tempOrder?.customer_id]) && tempOrder?.metadata?.shop_service_data?.service_type === serviceType;
|
|
66
|
+
const run = async () => {
|
|
67
|
+
const openData = await this.host.loadOpenData();
|
|
68
|
+
const ids = positiveIds(openData?.[`fulfillment.${serviceType}_fees_rules`]);
|
|
69
|
+
if (!ids.length || !isCurrent()) return;
|
|
70
|
+
if (!tempOrder) return;
|
|
71
|
+
const assertSnapshot = () => {
|
|
72
|
+
if (!isCurrent() || tempOrder.products !== initialProducts || cartFingerprint(tempOrder.products) !== initialFingerprint) {
|
|
73
|
+
throw new Error('The order changed while preparing fulfillment item rules');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
assertSnapshot();
|
|
77
|
+
const response = await this.host.fetchStrategyModels(ids);
|
|
78
|
+
assertSnapshot();
|
|
79
|
+
if (response?.code !== 200 || !Array.isArray(response?.data?.list)) {
|
|
80
|
+
throw new Error('Unable to load fulfillment item rules');
|
|
81
|
+
}
|
|
82
|
+
// Response order need not match the configured order (the latter breaks priority ties).
|
|
83
|
+
const configs = ids.map(id => {
|
|
84
|
+
const config = response.data.list.find(item => Number(item?.id) === id)?.metadata;
|
|
85
|
+
if (config?.metadata?.type !== 'item_rule' || !Array.isArray(config.actions) || !config.conditions) {
|
|
86
|
+
throw new Error(`Invalid or missing fulfillment item rule ${id}`);
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
});
|
|
90
|
+
const runtime = this.host.getOtherParams();
|
|
91
|
+
const pax = runtime.itemRule?.pax || {};
|
|
92
|
+
const businessData = {
|
|
93
|
+
executionScope: 'set_handover',
|
|
94
|
+
serviceType,
|
|
95
|
+
channel: String(runtime.channel ?? runtime.platform ?? ''),
|
|
96
|
+
pax: {
|
|
97
|
+
total: Number(pax.total) || 0,
|
|
98
|
+
adult: Number(pax.adult) || 0,
|
|
99
|
+
child: Number(pax.child) || 0
|
|
100
|
+
},
|
|
101
|
+
cartItems: tempOrder.products.map(line => ({
|
|
102
|
+
product_id: Number(line.product_id),
|
|
103
|
+
product_variant_id: Number(line.product_variant_id) || 0,
|
|
104
|
+
quantity: Number(line.num) || 0
|
|
105
|
+
})),
|
|
106
|
+
strategyConfigs: configs,
|
|
107
|
+
custom: runtime.itemRule?.custom
|
|
108
|
+
};
|
|
109
|
+
const initialEvaluation = this.evaluator.evaluate(businessData);
|
|
110
|
+
const requiresMainQuantity = initialEvaluation.rawResults.some(result => result.applicable && result.matchedActions.some(action => action.type === 'PREFILL_CART' && action.target === 'cart' && action.config?.scope === 'set_handover' && action.config.products?.some(product => product.quantityFrom === 'main_product_number')));
|
|
111
|
+
const actions = requiresMainQuantity ? this.evaluator.evaluate({
|
|
112
|
+
...businessData,
|
|
113
|
+
mainProductNumber: await this.getMainProductNumber(tempOrder.products, configs)
|
|
114
|
+
}).cartActions : initialEvaluation.cartActions;
|
|
115
|
+
assertSnapshot();
|
|
116
|
+
// Prepare every addition before changing the cart; missing products must not leave a half-loaded batch.
|
|
117
|
+
const sources = await this.loadPrefillSources(actions, tempOrder.products);
|
|
118
|
+
assertSnapshot();
|
|
119
|
+
const targetIds = new Set(actions.flatMap(action => action.products.map(product => Number(product.product_id))));
|
|
120
|
+
const originalSelection = selectionFingerprint(tempOrder.products, targetIds);
|
|
121
|
+
// Promotion may add gifts or split/merge rows. Only a change to the purchased
|
|
122
|
+
// quantities invalidates this selection, not promotion's own line reshaping.
|
|
123
|
+
const targetIsCurrent = () => isCurrent() && selectionFingerprint(tempOrder.products, targetIds) === originalSelection;
|
|
124
|
+
for (const action of actions) {
|
|
125
|
+
if (!targetIsCurrent()) return;
|
|
126
|
+
if (action.type === 'DELETE_PRODUCT') {
|
|
127
|
+
const deletedIds = new Set(action.products.map(product => Number(product.product_id)));
|
|
128
|
+
const lines = tempOrder.products.filter(line => deletedIds.has(Number(line.product_id)));
|
|
129
|
+
if (lines.length) {
|
|
130
|
+
await order.removeProductsFromOrder(lines.map(lineIdentity), {
|
|
131
|
+
isCurrentOrder: targetIsCurrent
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
for (const product of action.products) {
|
|
137
|
+
if (!targetIsCurrent()) return;
|
|
138
|
+
const key = ownerKey(action, product);
|
|
139
|
+
const existing = ownedLines(tempOrder.products, key);
|
|
140
|
+
const quantity = Math.max(0, Math.floor(Number(product.quantity) || 0));
|
|
141
|
+
if (product.repeat_skip && existing.length) {
|
|
142
|
+
if (quantity === 0) {
|
|
143
|
+
await order.removeProductsFromOrder(existing.map(lineIdentity), {
|
|
144
|
+
isCurrentOrder: targetIsCurrent
|
|
145
|
+
});
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
// Keep one owned row and its pricing; never absorb independently added products.
|
|
149
|
+
if (existing.length > 1) {
|
|
150
|
+
await order.removeProductsFromOrder(existing.slice(1).map(lineIdentity), {
|
|
151
|
+
isCurrentOrder: targetIsCurrent
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!targetIsCurrent()) return;
|
|
155
|
+
if (Number(existing[0].num) !== quantity) {
|
|
156
|
+
await order.updateOrderProductQuantity({
|
|
157
|
+
...lineIdentity(existing[0]),
|
|
158
|
+
num: quantity
|
|
159
|
+
}, {
|
|
160
|
+
isCurrentOrder: targetIsCurrent
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (quantity === 0) continue;
|
|
166
|
+
const source = sources.get(Number(product.product_id));
|
|
167
|
+
const line = (0, _transformBaseProductToOrderProduct.transformBaseProductToOrderProduct)({
|
|
168
|
+
sourceProduct: source,
|
|
169
|
+
payload: {
|
|
170
|
+
product_id: product.product_id,
|
|
171
|
+
product_variant_id: product.product_variant_id ?? 0,
|
|
172
|
+
quantity
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
if (!line) throw new Error(`Unable to prepare fulfillment product ${product.product_id}`);
|
|
176
|
+
line.metadata = {
|
|
177
|
+
...line.metadata,
|
|
178
|
+
[ownershipField]: key
|
|
179
|
+
};
|
|
180
|
+
const beforeProducts = tempOrder.products;
|
|
181
|
+
const beforeFingerprint = cartFingerprint(beforeProducts);
|
|
182
|
+
await order.addProductToOrder(line, undefined, {
|
|
183
|
+
insertAtStart: true,
|
|
184
|
+
disableMerge: true,
|
|
185
|
+
availabilityCommitGuard: () => {
|
|
186
|
+
if (!targetIsCurrent() || tempOrder.products !== beforeProducts || cartFingerprint(tempOrder.products) !== beforeFingerprint) {
|
|
187
|
+
throw new Error('The order changed while adding a fulfillment product');
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
availabilityCommitTargetGuard: () => targetIsCurrent() && ownedLines(tempOrder.products, key).length > 0
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
// Serialize writes; a newer choice invalidates the old request before its next mutation.
|
|
196
|
+
const task = this.pending.then(run).catch(error => {
|
|
197
|
+
if (sequence === this.sequence) this.host.reportError(error);
|
|
198
|
+
// Fees are best effort by product decision: Next must remain usable on failure.
|
|
199
|
+
});
|
|
200
|
+
this.pending = task;
|
|
201
|
+
return task;
|
|
202
|
+
}
|
|
203
|
+
async loadProducts(ids) {
|
|
204
|
+
if (!ids.length) return [];
|
|
205
|
+
const products = this.host.getProducts();
|
|
206
|
+
if (!products) throw new Error('products module is unavailable for fulfillment item rules');
|
|
207
|
+
const runtime = this.host.getOtherParams();
|
|
208
|
+
const now = (0, _dayjs.default)();
|
|
209
|
+
return products.loadProducts({
|
|
210
|
+
product_ids: ids,
|
|
211
|
+
cacheId: runtime.cacheId,
|
|
212
|
+
customer_id: this.host.getOrder().getTempOrder()?.customer_id ?? undefined,
|
|
213
|
+
schedule_date: now.format('YYYY-MM-DD'),
|
|
214
|
+
schedule_datetime: now.format('YYYY-MM-DD HH:mm:ss')
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
async getMainProductNumber(lines, configs) {
|
|
218
|
+
const feeIds = new Set(configs.flatMap(config => config.actions.flatMap(action => action.config?.scope === 'set_handover' && ['PREFILL_CART', 'DELETE_PRODUCT'].includes(action.type) ? (action.config.products || []).map(product => Number(product.product_id)) : [])));
|
|
219
|
+
const candidates = lines.filter(line => !line.metadata?.[ownershipField] && !feeIds.has(Number(line.product_id)));
|
|
220
|
+
if (!candidates.length) return 0;
|
|
221
|
+
const ids = positiveIds(candidates.map(line => line.product_id));
|
|
222
|
+
const cached = (await this.host.getProducts()?.queryProducts({
|
|
223
|
+
includeIds: ids
|
|
224
|
+
})) || [];
|
|
225
|
+
const sources = new Map();
|
|
226
|
+
for (const line of candidates) {
|
|
227
|
+
const cachedSource = cached.find(product => Number(product.id) === Number(line.product_id));
|
|
228
|
+
const source = Array.isArray(cachedSource?.data_variants) ? cachedSource : line._origin;
|
|
229
|
+
if (Array.isArray(source?.data_variants)) sources.set(Number(line.product_id), source);
|
|
230
|
+
}
|
|
231
|
+
const missing = ids.filter(id => !sources.has(id));
|
|
232
|
+
for (const source of await this.loadProducts(missing)) sources.set(Number(source.id ?? source.product_id), source);
|
|
233
|
+
if (ids.some(id => !sources.has(id))) throw new Error('Unable to identify fulfillment main products');
|
|
234
|
+
return candidates.reduce((sum, line) => {
|
|
235
|
+
const source = sources.get(Number(line.product_id));
|
|
236
|
+
const isMain = source?.data_variants?.some(variant => variant?.data?.client_meta?.is_main_product === 1);
|
|
237
|
+
return sum + (isMain ? Math.max(0, Math.floor(Number(line.num) || 0)) : 0);
|
|
238
|
+
}, 0);
|
|
239
|
+
}
|
|
240
|
+
async loadPrefillSources(actions, lines) {
|
|
241
|
+
const ids = positiveIds(actions.flatMap(action => action.type === 'PREFILL_CART' ? action.products.filter(product => product.quantity > 0).map(product => product.product_id) : []));
|
|
242
|
+
const cached = ids.length ? (await this.host.getProducts()?.queryProducts({
|
|
243
|
+
includeIds: ids
|
|
244
|
+
})) || [] : [];
|
|
245
|
+
const sources = new Map(cached.map(product => [Number(product.id), product]));
|
|
246
|
+
// Existing owned rows can supply their original product if the catalog no longer contains it.
|
|
247
|
+
for (const line of lines) {
|
|
248
|
+
if (line.metadata?.[ownershipField] && line._origin && !sources.has(Number(line.product_id))) {
|
|
249
|
+
sources.set(Number(line.product_id), line._origin);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
for (const source of await this.loadProducts(ids.filter(id => !sources.has(id)))) {
|
|
253
|
+
sources.set(Number(source.id ?? source.product_id), source);
|
|
254
|
+
}
|
|
255
|
+
if (ids.some(id => !sources.has(id))) throw new Error('Unable to load fulfillment fee products');
|
|
256
|
+
return sources;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
exports.FulfillmentItemRulesController = FulfillmentItemRulesController;
|
|
@@ -18,6 +18,7 @@ import { QuotationModule } from '../../modules/Quotation';
|
|
|
18
18
|
import type { ISalesDetail, ISalesDetailHeader, ISalesBooking, ISalesPayment, ISalesSurcharge } from './utils/parseSalesResponse';
|
|
19
19
|
import { AppPlugin } from '../../plugins/app';
|
|
20
20
|
import { type TransformBaseProductToOrderProductParams } from './utils/transformBaseProductToOrderProduct';
|
|
21
|
+
import type { OpenDataConfig } from '../../modules/OpenData/types';
|
|
21
22
|
export interface BaseSalesState {
|
|
22
23
|
order?: OrderModule;
|
|
23
24
|
products?: ProductList;
|
|
@@ -80,6 +81,7 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
|
|
|
80
81
|
private serverOrderChangeSyncEnabled;
|
|
81
82
|
protected pickupTimingEnabled: boolean;
|
|
82
83
|
private pickupTimingController?;
|
|
84
|
+
private fulfillmentItemRulesController?;
|
|
83
85
|
constructor(name?: string, version?: string);
|
|
84
86
|
/**
|
|
85
87
|
* 子类可覆盖此方法以追加/收敛要注册的子模块。
|
|
@@ -159,6 +161,8 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
|
|
|
159
161
|
protected getSubmitOrderSalesChannel(): string | undefined;
|
|
160
162
|
/** Only solutions opting into Pickup Timing supply effective OpenData. */
|
|
161
163
|
protected loadPickupTimingOpenData(_force: boolean): Promise<Record<string, unknown>>;
|
|
164
|
+
protected loadFulfillmentItemRuleOpenData(): Promise<OpenDataConfig | null>;
|
|
165
|
+
private getFulfillmentItemRulesController;
|
|
162
166
|
protected isPickupTimingEnabled(): boolean;
|
|
163
167
|
protected getPickupTimingContextData(key: string): unknown;
|
|
164
168
|
protected getPickupTimingScope(): string;
|
|
@@ -270,7 +274,7 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
|
|
|
270
274
|
updateTempOrderShopDiscount(amount: string | number): string;
|
|
271
275
|
updateTempOrderDepositAmount(amount: string | number): OrderSummary;
|
|
272
276
|
setPickupReferenceMode(mode: PickupReferenceMode): ShopServiceData;
|
|
273
|
-
setServiceType(serviceType: ShopServiceType): ShopServiceData
|
|
277
|
+
setServiceType(serviceType: ShopServiceType): Promise<ShopServiceData>;
|
|
274
278
|
getPickupTimingBooking(): import("../../modules/Order").PickupTimingBooking | null;
|
|
275
279
|
setPickupTimingBooking(input: PickupTimingBookingInput): import("../../modules/Order").PickupTimingBooking;
|
|
276
280
|
clearPickupTimingBooking(): void;
|
|
@@ -23,6 +23,7 @@ Object.keys(_types).forEach(function (key) {
|
|
|
23
23
|
var _Order = require("../../modules/Order");
|
|
24
24
|
var _pickupTiming = require("../../modules/Order/pickupTiming");
|
|
25
25
|
var _pickupTiming2 = require("./pickupTiming");
|
|
26
|
+
var _fulfillmentItemRules = require("./fulfillmentItemRules");
|
|
26
27
|
var _decimal = _interopRequireDefault(require("decimal.js"));
|
|
27
28
|
var _lodashEs = require("lodash-es");
|
|
28
29
|
var _types2 = require("../BookingByStep/types");
|
|
@@ -503,6 +504,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
503
504
|
serverOrderChangeSyncEnabled = false;
|
|
504
505
|
pickupTimingEnabled = false;
|
|
505
506
|
pickupTimingController;
|
|
507
|
+
fulfillmentItemRulesController;
|
|
506
508
|
constructor(name, version) {
|
|
507
509
|
super(name, version);
|
|
508
510
|
}
|
|
@@ -1165,6 +1167,34 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
1165
1167
|
async loadPickupTimingOpenData(_force) {
|
|
1166
1168
|
return {};
|
|
1167
1169
|
}
|
|
1170
|
+
async loadFulfillmentItemRuleOpenData() {
|
|
1171
|
+
const openData = this.otherParams?.openData ?? this.otherParams?.dineInConfig;
|
|
1172
|
+
return openData && typeof openData === 'object' && !Array.isArray(openData) ? openData : null;
|
|
1173
|
+
}
|
|
1174
|
+
getFulfillmentItemRulesController() {
|
|
1175
|
+
if (!this.fulfillmentItemRulesController) {
|
|
1176
|
+
this.fulfillmentItemRulesController = new _fulfillmentItemRules.FulfillmentItemRulesController({
|
|
1177
|
+
getOrder: () => {
|
|
1178
|
+
if (!this.store.order) throw new Error('order 模块未初始化');
|
|
1179
|
+
return this.store.order;
|
|
1180
|
+
},
|
|
1181
|
+
getProducts: () => this.store.products,
|
|
1182
|
+
loadOpenData: () => this.loadFulfillmentItemRuleOpenData(),
|
|
1183
|
+
getScope: () => this.getPickupTimingScope(),
|
|
1184
|
+
getOtherParams: () => this.otherParams,
|
|
1185
|
+
fetchStrategyModels: ids => this.request.get('/promotion', {
|
|
1186
|
+
skip: 1,
|
|
1187
|
+
num: 99,
|
|
1188
|
+
status: 'active',
|
|
1189
|
+
ids
|
|
1190
|
+
}),
|
|
1191
|
+
reportError: error => this.logWarning('履约费用策略执行失败,保留服务类型设置', {
|
|
1192
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1193
|
+
})
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
return this.fulfillmentItemRulesController;
|
|
1197
|
+
}
|
|
1168
1198
|
isPickupTimingEnabled() {
|
|
1169
1199
|
return this.pickupTimingEnabled && this.otherParams?.enablePickupTiming !== false;
|
|
1170
1200
|
}
|
|
@@ -1193,6 +1223,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
1193
1223
|
if (!this.store.order) throw new Error('order 模块未初始化');
|
|
1194
1224
|
return this.store.order;
|
|
1195
1225
|
},
|
|
1226
|
+
setServiceType: serviceType => this.setServiceType(serviceType),
|
|
1196
1227
|
getSchedule: () => this.store.schedule,
|
|
1197
1228
|
loadOpenData: force => this.loadPickupTimingOpenData(force),
|
|
1198
1229
|
getTimezone: () => String(this.getPickupTimingContextData('timezone') ?? ''),
|
|
@@ -1559,6 +1590,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
1559
1590
|
}
|
|
1560
1591
|
async destroy() {
|
|
1561
1592
|
this.resetPickupTimingCache();
|
|
1593
|
+
this.fulfillmentItemRulesController?.invalidate();
|
|
1562
1594
|
this.serverOrderChangeUnsubscribe?.();
|
|
1563
1595
|
this.serverOrderChangeUnsubscribe = null;
|
|
1564
1596
|
this.paymentMethodsChangedUnsubscribe?.();
|
|
@@ -1776,9 +1808,16 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
1776
1808
|
if (!this.store.order) throw new Error('order 模块未初始化');
|
|
1777
1809
|
return this.store.order.updateTempOrderPickupReferenceMode(mode);
|
|
1778
1810
|
}
|
|
1779
|
-
setServiceType(serviceType) {
|
|
1811
|
+
async setServiceType(serviceType) {
|
|
1780
1812
|
if (!this.store.order) throw new Error('order 模块未初始化');
|
|
1781
|
-
|
|
1813
|
+
const serviceData = this.store.order.updateTempOrderServiceType(serviceType);
|
|
1814
|
+
await this.getFulfillmentItemRulesController().apply(serviceType);
|
|
1815
|
+
// 费用规则可能增删商品;即使没有普通促销,也要同步页面的购物车和金额快照。
|
|
1816
|
+
const tempOrder = this.getTempOrder();
|
|
1817
|
+
if (tempOrder) await this.effectsEmit('onTempOrderReplaced', {
|
|
1818
|
+
tempOrder
|
|
1819
|
+
});
|
|
1820
|
+
return serviceData;
|
|
1782
1821
|
}
|
|
1783
1822
|
getPickupTimingBooking() {
|
|
1784
1823
|
return this.store.order?.getPickupTimingBooking() || null;
|
|
@@ -1870,6 +1909,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
1870
1909
|
async clearOrderCartLines() {
|
|
1871
1910
|
try {
|
|
1872
1911
|
if (!this.store.order) throw new Error('order 模块未初始化');
|
|
1912
|
+
this.fulfillmentItemRulesController?.invalidate();
|
|
1873
1913
|
const tempOrder = await this.store.order.clearOrderCartLines();
|
|
1874
1914
|
if (this.currentSalesDetail) {
|
|
1875
1915
|
this.currentSalesDetail = {
|
|
@@ -1897,6 +1937,7 @@ class BaseSalesImpl extends _BaseModule.BaseModule {
|
|
|
1897
1937
|
/** 切换法币 / WalletPass 单位时清空商品、折扣与支付状态,保留客户。 */
|
|
1898
1938
|
async clearOrderForSettlementSwitch() {
|
|
1899
1939
|
if (!this.store.order) throw new Error('order 模块未初始化');
|
|
1940
|
+
this.fulfillmentItemRulesController?.invalidate();
|
|
1900
1941
|
const tempOrder = await this.store.order.clearOrderForSettlementSwitch();
|
|
1901
1942
|
if (this.currentSalesDetail) {
|
|
1902
1943
|
this.currentSalesDetail = {
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { OrderModule } from '../../modules/Order';
|
|
2
|
+
import type { ShopServiceType } from '../../modules/Order/types';
|
|
2
3
|
import type { ScheduleModule } from '../../modules/Schedule';
|
|
3
4
|
import { type PickupTimingAvailability, type PickupTimingSelection, type PickupTimingValidationResult } from '../../modules/FulfillmentTiming';
|
|
4
5
|
interface PickupTimingHost {
|
|
5
6
|
getOrder: () => OrderModule;
|
|
7
|
+
setServiceType?: (serviceType: ShopServiceType) => unknown | Promise<unknown>;
|
|
6
8
|
getSchedule: () => ScheduleModule | undefined;
|
|
7
9
|
loadOpenData: (force: boolean) => Promise<Record<string, unknown>>;
|
|
8
10
|
getTimezone: () => string;
|
|
@@ -219,7 +219,11 @@ class PickupTimingController {
|
|
|
219
219
|
return this.getSelection();
|
|
220
220
|
}
|
|
221
221
|
if (selection.serviceType !== 'pick_up') {
|
|
222
|
-
|
|
222
|
+
if (this.host.setServiceType) {
|
|
223
|
+
await this.host.setServiceType(selection.serviceType);
|
|
224
|
+
} else {
|
|
225
|
+
order.updateTempOrderServiceType(selection.serviceType);
|
|
226
|
+
}
|
|
223
227
|
return this.getSelection();
|
|
224
228
|
}
|
|
225
229
|
const slot = selection.mode === 'asap' ? (0, _FulfillmentTiming.getAsapPickupTiming)({
|
|
@@ -328,7 +328,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
328
328
|
date: string;
|
|
329
329
|
status: string;
|
|
330
330
|
week: string;
|
|
331
|
-
weekNum: 0 | 2 | 1 |
|
|
331
|
+
weekNum: 0 | 2 | 1 | 6 | 4 | 5 | 3;
|
|
332
332
|
}[]>;
|
|
333
333
|
submitTimeSlot(timeSlots: TimeSliceItem): void;
|
|
334
334
|
private getScheduleDataByIds;
|
|
@@ -375,7 +375,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
375
375
|
};
|
|
376
376
|
setOtherData(key: string, value: any): void;
|
|
377
377
|
getOtherData(key: string): any;
|
|
378
|
-
getProductTypeById(id: number): Promise<"
|
|
378
|
+
getProductTypeById(id: number): Promise<"normal" | "duration" | "session">;
|
|
379
379
|
/**
|
|
380
380
|
* 提供给 UI 的方法,减轻 UI 层的计算压力,UI 层只需要传递 cartItemId 和 resourceCode 即返回对应的 renderList
|
|
381
381
|
*
|
|
@@ -58,6 +58,7 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
|
|
|
58
58
|
private getIdGeneratorPlugin;
|
|
59
59
|
protected loadOpenDataConfig(): Promise<OpenDataConfig | null>;
|
|
60
60
|
getOpenData(): Promise<OpenDataConfig | null>;
|
|
61
|
+
protected loadFulfillmentItemRuleOpenData(): Promise<OpenDataConfig | null>;
|
|
61
62
|
getInvoiceLayoutConfig(): Promise<InvoiceLayoutConfig>;
|
|
62
63
|
getInvoiceTemplateSettings(): Promise<InvoiceTemplateSettings>;
|
|
63
64
|
protected isInvoiceOpenDataCacheCurrent(): boolean;
|
|
@@ -289,6 +289,9 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
|
|
|
289
289
|
async getOpenData() {
|
|
290
290
|
return this.loadOpenDataConfig();
|
|
291
291
|
}
|
|
292
|
+
async loadFulfillmentItemRuleOpenData() {
|
|
293
|
+
return this.store.openData ? this.getOpenData() : super.loadFulfillmentItemRuleOpenData();
|
|
294
|
+
}
|
|
292
295
|
async getInvoiceLayoutConfig() {
|
|
293
296
|
const openData = await this.loadOpenDataConfig();
|
|
294
297
|
if (!openData) return (0, _OpenData.getDefaultInvoiceLayoutConfig)();
|