@pisell/pisellos 0.10.50 → 0.10.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  2. package/dist/modules/FulfillmentTiming/index.d.ts +11 -0
  3. package/dist/modules/FulfillmentTiming/index.js +407 -0
  4. package/dist/modules/FulfillmentTiming/types.d.ts +67 -0
  5. package/dist/modules/FulfillmentTiming/types.js +1 -0
  6. package/dist/modules/OpenData/types.d.ts +6 -0
  7. package/dist/modules/Order/index.d.ts +18 -1
  8. package/dist/modules/Order/index.js +322 -195
  9. package/dist/modules/Order/pickupTiming.d.ts +46 -0
  10. package/dist/modules/Order/pickupTiming.js +53 -0
  11. package/dist/modules/Order/types.d.ts +11 -1
  12. package/dist/modules/Order/utils.d.ts +10 -0
  13. package/dist/modules/Order/utils.js +145 -85
  14. package/dist/modules/Rules/index.js +3 -2
  15. package/dist/modules/Schedule/index.d.ts +8 -0
  16. package/dist/modules/Schedule/index.js +134 -37
  17. package/dist/modules/index.d.ts +1 -0
  18. package/dist/modules/index.js +1 -0
  19. package/dist/server/modules/order/index.d.ts +8 -0
  20. package/dist/server/modules/order/index.js +506 -453
  21. package/dist/server/modules/order/types.d.ts +1 -0
  22. package/dist/server/utils/small-ticket.js +165 -94
  23. package/dist/solution/BaseSales/index.d.ts +24 -0
  24. package/dist/solution/BaseSales/index.js +1231 -1037
  25. package/dist/solution/BaseSales/pickupTiming.d.ts +43 -0
  26. package/dist/solution/BaseSales/pickupTiming.js +589 -0
  27. package/dist/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  28. package/dist/solution/BookingByStep/index.d.ts +1 -1
  29. package/dist/solution/BookingTicket/index.d.ts +2 -2
  30. package/dist/solution/BookingTicket/utils/cartView.js +3 -1
  31. package/dist/solution/UnifiedBookingSales/index.d.ts +13 -4
  32. package/dist/solution/UnifiedBookingSales/index.js +873 -708
  33. package/lib/modules/FulfillmentTiming/index.d.ts +11 -0
  34. package/lib/modules/FulfillmentTiming/index.js +359 -0
  35. package/lib/modules/FulfillmentTiming/types.d.ts +67 -0
  36. package/lib/modules/FulfillmentTiming/types.js +5 -0
  37. package/lib/modules/OpenData/types.d.ts +6 -0
  38. package/lib/modules/Order/index.d.ts +18 -1
  39. package/lib/modules/Order/index.js +101 -3
  40. package/lib/modules/Order/pickupTiming.d.ts +46 -0
  41. package/lib/modules/Order/pickupTiming.js +61 -0
  42. package/lib/modules/Order/types.d.ts +11 -1
  43. package/lib/modules/Order/utils.d.ts +10 -0
  44. package/lib/modules/Order/utils.js +58 -4
  45. package/lib/modules/Rules/index.js +3 -2
  46. package/lib/modules/Schedule/index.d.ts +8 -0
  47. package/lib/modules/Schedule/index.js +64 -11
  48. package/lib/modules/index.d.ts +1 -0
  49. package/lib/modules/index.js +11 -0
  50. package/lib/server/modules/order/index.d.ts +8 -0
  51. package/lib/server/modules/order/index.js +19 -0
  52. package/lib/server/modules/order/types.d.ts +1 -0
  53. package/lib/server/utils/small-ticket.js +86 -22
  54. package/lib/solution/BaseSales/index.d.ts +24 -0
  55. package/lib/solution/BaseSales/index.js +109 -1
  56. package/lib/solution/BaseSales/pickupTiming.d.ts +43 -0
  57. package/lib/solution/BaseSales/pickupTiming.js +269 -0
  58. package/lib/solution/BaseSales/utils/parseSalesResponse.js +3 -0
  59. package/lib/solution/BookingByStep/index.d.ts +1 -1
  60. package/lib/solution/BookingTicket/index.d.ts +2 -2
  61. package/lib/solution/BookingTicket/utils/cartView.js +3 -1
  62. package/lib/solution/UnifiedBookingSales/index.d.ts +13 -4
  63. package/lib/solution/UnifiedBookingSales/index.js +130 -46
  64. package/package.json +1 -1
@@ -0,0 +1,269 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PickupTimingController = void 0;
7
+ var _lodashEs = require("lodash-es");
8
+ var _FulfillmentTiming = require("../../modules/FulfillmentTiming");
9
+ async function withTimeout(request) {
10
+ let timer;
11
+ try {
12
+ return await Promise.race([request, new Promise((_, reject) => {
13
+ timer = setTimeout(() => reject(new Error('Unable to load pickup times. Please try again.')), 30000);
14
+ })]);
15
+ } finally {
16
+ if (timer !== undefined) clearTimeout(timer);
17
+ }
18
+ }
19
+
20
+ /** Reuses runtime configuration and schedules; every calculation still uses the live clock. */
21
+ class PickupTimingController {
22
+ latest;
23
+ runtime;
24
+ writeSequence = 0;
25
+ constructor(host) {
26
+ this.host = host;
27
+ }
28
+ now() {
29
+ return this.host.now?.() ?? new Date();
30
+ }
31
+
32
+ /** A new solution context or an explicit settings refresh invalidates pending work too. */
33
+ invalidate() {
34
+ this.runtime = undefined;
35
+ this.latest = undefined;
36
+ this.writeSequence += 1;
37
+ }
38
+ assertCurrentRuntime(runtime) {
39
+ if (this.runtime !== runtime || runtime.scope !== this.host.getScope()) {
40
+ throw new Error('Store settings changed. Please try again.');
41
+ }
42
+ }
43
+ getRuntime() {
44
+ const scope = this.host.getScope();
45
+ if (this.runtime?.scope !== scope) {
46
+ this.latest = undefined;
47
+ this.runtime = {
48
+ scope
49
+ };
50
+ }
51
+ return this.runtime;
52
+ }
53
+ async loadConfig(runtime) {
54
+ if (runtime.config) return runtime.config;
55
+ if (!runtime.configRequest) {
56
+ const request = (async () => {
57
+ const config = (0, _FulfillmentTiming.normalizePickupTimingConfig)(await withTimeout(this.host.loadOpenData(false)));
58
+ this.assertCurrentRuntime(runtime);
59
+ runtime.config = config;
60
+ return config;
61
+ })();
62
+ runtime.configRequest = request;
63
+ }
64
+ const request = runtime.configRequest;
65
+ try {
66
+ return await request;
67
+ } finally {
68
+ if (runtime.configRequest === request) runtime.configRequest = undefined;
69
+ }
70
+ }
71
+ async loadSchedules(runtime, config) {
72
+ if (runtime.schedules) return runtime.schedules;
73
+ if (!runtime.scheduleRequest) {
74
+ const request = (async () => {
75
+ const schedule = this.host.getSchedule();
76
+ if (!schedule) throw new Error('Pickup schedule is unavailable. Please try again.');
77
+ // Reuse the catalog loaded by solution initialization, including its in-flight request.
78
+ await withTimeout(schedule.loadAllSchedule({
79
+ useCache: true
80
+ }));
81
+ this.assertCurrentRuntime(runtime);
82
+ const ids = [...new Set([...config.scheduleIds, ...(config.operatingHoursScheduleIds || [])])];
83
+ const schedules = (0, _lodashEs.cloneDeep)(schedule.getScheduleListByIds(ids) || []);
84
+ runtime.schedules = schedules;
85
+ return schedules;
86
+ })();
87
+ runtime.scheduleRequest = request;
88
+ }
89
+ const request = runtime.scheduleRequest;
90
+ try {
91
+ return await request;
92
+ } finally {
93
+ if (runtime.scheduleRequest === request) runtime.scheduleRequest = undefined;
94
+ }
95
+ }
96
+ async snapshot(includeSchedule, tolerateScheduleError = false) {
97
+ const runtime = this.getRuntime();
98
+ const config = await this.loadConfig(runtime);
99
+ this.assertCurrentRuntime(runtime);
100
+ let schedules = [];
101
+ let error;
102
+ if (includeSchedule && config.handoverModes.includes('pick_up') && config.timingMode !== 'asap_only' && config.scheduleIds.length) {
103
+ try {
104
+ schedules = await this.loadSchedules(runtime, config);
105
+ } catch (reason) {
106
+ if (!tolerateScheduleError) throw reason;
107
+ error = reason instanceof Error ? reason.message : 'Pickup schedule is unavailable. Please try again.';
108
+ }
109
+ }
110
+ this.assertCurrentRuntime(runtime);
111
+ const result = {
112
+ config,
113
+ timezone: this.host.getTimezone(),
114
+ schedules,
115
+ scope: runtime.scope,
116
+ error
117
+ };
118
+ this.latest = result;
119
+ return result;
120
+ }
121
+ async getAvailability(params = {}) {
122
+ // `force` remains accepted for older callers; refresh recomputes time using the runtime catalog.
123
+ const snapshot = await this.snapshot(true, true);
124
+ const now = this.now();
125
+ try {
126
+ return {
127
+ ...(0, _FulfillmentTiming.computePickupTimingAvailability)({
128
+ ...snapshot,
129
+ date: params.date,
130
+ now
131
+ }),
132
+ error: snapshot.error
133
+ };
134
+ } catch (reason) {
135
+ // Keep the handover selector usable when only Pickup's timezone is unavailable.
136
+ return {
137
+ config: snapshot.config,
138
+ timezone: snapshot.timezone,
139
+ now: now.toISOString(),
140
+ today: '',
141
+ maxDate: '',
142
+ date: params.date || '',
143
+ dates: [],
144
+ slots: [],
145
+ error: reason instanceof Error ? reason.message : 'Pickup times are unavailable. Please try again.'
146
+ };
147
+ }
148
+ }
149
+ getSelection() {
150
+ const order = this.host.getOrder();
151
+ const tempOrder = order.getTempOrder();
152
+ const booking = order.getPickupTimingBooking();
153
+ const config = this.latest?.scope === this.host.getScope() ? this.latest.config : undefined;
154
+ const storedServiceType = String(tempOrder?.metadata?.shop_service_data?.service_type || '');
155
+ const serviceType = config?.handoverModes.length ? config.handoverModes.includes(storedServiceType) ? storedServiceType : config.handoverModes[0] : storedServiceType;
156
+ const details = serviceType === 'pick_up' ? booking?.metadata.fulfillment_timing : undefined;
157
+ const mode = config?.timingMode === 'asap_only' ? 'asap' : config?.timingMode === 'scheduled_only' ? 'scheduled' : details?.mode ?? 'asap';
158
+ const sameMode = details?.mode === mode;
159
+ return {
160
+ serviceType,
161
+ mode,
162
+ source: mode === 'asap' ? 'asap' : sameMode && details?.source || 'schedule',
163
+ ...(sameMode && booking ? {
164
+ date: booking.start_date,
165
+ time: booking.start_time,
166
+ instant: details?.instant
167
+ } : {})
168
+ };
169
+ }
170
+ async validateSnapshot(selection) {
171
+ const snapshot = await this.snapshot(selection.serviceType === 'pick_up' && selection.mode === 'scheduled' && selection.source !== 'pos_manual');
172
+ const now = this.now();
173
+ const result = (0, _FulfillmentTiming.validatePickupTimingSelection)(selection, {
174
+ ...snapshot,
175
+ now,
176
+ allowManualOverride: this.host.isPos()
177
+ });
178
+ return {
179
+ snapshot,
180
+ now,
181
+ result
182
+ };
183
+ }
184
+ async validate(selection) {
185
+ return (await this.validateSnapshot(selection)).result;
186
+ }
187
+ async setSelection(selection, options = {}) {
188
+ const order = this.host.getOrder();
189
+ const tempOrder = order.ensureTempOrder();
190
+ const sequence = ++this.writeSequence;
191
+ const {
192
+ snapshot,
193
+ now,
194
+ result
195
+ } = await this.validateSnapshot(selection);
196
+ if (sequence !== this.writeSequence || order.getTempOrder() !== tempOrder || snapshot.scope !== this.host.getScope()) {
197
+ throw new Error('The order changed. Please choose the pickup time again.');
198
+ }
199
+ if (!result.valid) throw new Error(result.message || 'Please choose a valid pickup time.');
200
+ if (!snapshot.config.handoverModes.length) {
201
+ order.clearPickupTimingBooking();
202
+ return this.getSelection();
203
+ }
204
+ if (selection.serviceType !== 'pick_up') {
205
+ order.updateTempOrderServiceType(selection.serviceType);
206
+ return this.getSelection();
207
+ }
208
+ const slot = selection.mode === 'asap' ? (0, _FulfillmentTiming.getAsapPickupTiming)({
209
+ ...snapshot,
210
+ now
211
+ }) : result.slot;
212
+ if (!slot) throw new Error('Please choose a pickup time.');
213
+ // Validate everything before either field is changed; notify once after the combined update.
214
+ tempOrder.metadata = {
215
+ ...(tempOrder.metadata || {}),
216
+ shop_service_data: {
217
+ ...(tempOrder.metadata?.shop_service_data || {}),
218
+ service_type: 'pick_up'
219
+ }
220
+ };
221
+ order.setPickupTimingBooking({
222
+ mode: selection.mode,
223
+ source: selection.mode === 'asap' ? 'asap' : selection.source === 'pos_manual' ? 'pos_manual' : 'schedule',
224
+ date: slot.date,
225
+ time: slot.time,
226
+ instant: slot.instant,
227
+ timezone: snapshot.timezone,
228
+ frozen: options.frozen ?? false
229
+ });
230
+ return this.getSelection();
231
+ }
232
+
233
+ /** Called before a new checkout/payment; payment callbacks keep their already prepared appointment. */
234
+ async prepare(options = {}) {
235
+ const order = this.host.getOrder();
236
+ const tempOrder = order.getTempOrder();
237
+ const sequence = this.writeSequence;
238
+ if (order.getOrderIdentity()?.isSubmitted) return this.getSelection();
239
+ const booking = order.getPickupTimingBooking();
240
+ if (booking?.metadata.fulfillment_timing?.frozen) return this.getSelection();
241
+ // An actual payment callback must never revalidate or move the time after funds have been taken.
242
+ const paymentStatus = order.getTempOrder()?.payment_status;
243
+ if (paymentStatus === 'paid' || paymentStatus === 'partially_paid') {
244
+ if (booking?.metadata.fulfillment_timing) {
245
+ const detail = booking.metadata.fulfillment_timing;
246
+ order.setPickupTimingBooking({
247
+ ...detail,
248
+ date: booking.start_date,
249
+ time: booking.start_time,
250
+ frozen: true
251
+ });
252
+ }
253
+ return this.getSelection();
254
+ }
255
+ const snapshot = await this.snapshot(false);
256
+ if (order !== this.host.getOrder() || order.getTempOrder() !== tempOrder || sequence !== this.writeSequence) {
257
+ throw new Error('The order changed. Please choose the pickup time again.');
258
+ }
259
+ const selection = this.getSelection();
260
+ if (!snapshot.config.handoverModes.length) {
261
+ order.clearPickupTimingBooking();
262
+ return selection;
263
+ }
264
+ return this.setSelection(selection, {
265
+ frozen: options.freeze ?? false
266
+ });
267
+ }
268
+ }
269
+ exports.PickupTimingController = PickupTimingController;
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.parseSalesResponse = parseSalesResponse;
7
7
  var _orderCollectionIdentity = require("../../../modules/Order/utils/orderCollectionIdentity");
8
+ var _pickupTiming = require("../../../modules/Order/pickupTiming");
8
9
  /**
9
10
  * 把 `/order/sales/{id}` 响应(OrderData)解析为 ISalesDetail。
10
11
  *
@@ -64,6 +65,7 @@ function pickCustomer(raw) {
64
65
  */
65
66
  function isRootBooking(booking) {
66
67
  if (!booking || typeof booking !== 'object') return false;
68
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return false;
67
69
  const parentId = booking.parent_id;
68
70
  if (parentId === 0 || parentId === null || parentId === undefined) return true;
69
71
  return false;
@@ -71,6 +73,7 @@ function isRootBooking(booking) {
71
73
  function indexBookingsByProductUid(bookings) {
72
74
  const result = {};
73
75
  for (const b of bookings) {
76
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(b)) continue;
74
77
  const uid = b && (b.product_uid || b.metadata?.product_uid) || '';
75
78
  if (!uid) continue;
76
79
  const key = String(uid);
@@ -326,7 +326,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
326
326
  date: string;
327
327
  status: string;
328
328
  week: string;
329
- weekNum: 0 | 2 | 1 | 3 | 4 | 5 | 6;
329
+ weekNum: 0 | 2 | 1 | 5 | 4 | 3 | 6;
330
330
  }[]>;
331
331
  submitTimeSlot(timeSlots: TimeSliceItem): void;
332
332
  private getScheduleDataByIds;
@@ -52,10 +52,10 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
52
52
  * 在解决方案对外 ready 前准备 Invoice Document;失败只降级模板,不阻断销售。
53
53
  * 已由页面取得的 OpenData 直接交给模块接管,没有注入数据时再走 target-scoped 加载。
54
54
  */
55
- private prepareInvoiceTemplateAtSolutionReady;
55
+ protected prepareInvoiceTemplateAtSolutionReady(injectedOpenData?: OpenDataConfig): Promise<void>;
56
56
  protected getBookingTicketBusinessCode(): string | null;
57
57
  private getIdGeneratorPlugin;
58
- private loadOpenDataConfig;
58
+ protected loadOpenDataConfig(): Promise<OpenDataConfig | null>;
59
59
  getOpenData(): Promise<OpenDataConfig | null>;
60
60
  getInvoiceLayoutConfig(): Promise<InvoiceLayoutConfig>;
61
61
  getInvoiceTemplateSettings(): Promise<InvoiceTemplateSettings>;
@@ -8,12 +8,14 @@ exports.buildCustomerView = buildCustomerView;
8
8
  exports.indexBookingsByProductUid = indexBookingsByProductUid;
9
9
  exports.isWalkInCustomer = isWalkInCustomer;
10
10
  var _addTimeAvailability = require("./addTimeAvailability");
11
+ var _pickupTiming = require("../../../modules/Order/pickupTiming");
11
12
  /**
12
13
  * 仅按 booking.product_uid 建索引,避免发明 booking 与商品行之间的其它等价关系。
13
14
  */
14
15
  function indexBookingsByProductUid(bookings) {
15
16
  if (!Array.isArray(bookings) || bookings.length === 0) return {};
16
17
  return bookings.reduce((acc, booking) => {
18
+ if ((0, _pickupTiming.isFulfillmentTimingBooking)(booking)) return acc;
17
19
  const uid = booking?.product_uid;
18
20
  if (uid === undefined || uid === null || uid === '') return acc;
19
21
  const key = String(uid);
@@ -105,7 +107,7 @@ function isGiftProductLine(line) {
105
107
  function buildCartView(lines, bookings, options = {}) {
106
108
  const productLines = Array.isArray(lines) ? lines : [];
107
109
  const bookingList = Array.isArray(bookings) ? bookings : [];
108
- const childBookings = bookingList.filter(booking => booking.parent_id !== 0);
110
+ const childBookings = bookingList.filter(booking => booking.parent_id !== 0 && !(0, _pickupTiming.isFulfillmentTimingBooking)(booking));
109
111
  const bookingsByProductUid = indexBookingsByProductUid(childBookings);
110
112
  const linesByUid = indexProductsByUid(productLines);
111
113
  const addTimeLinesByBookingUid = indexAddTimeProductsByBookingUid(productLines);
@@ -59,6 +59,13 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
59
59
  protected defaultName: string;
60
60
  protected defaultVersion: string;
61
61
  protected store: UnifiedBookingSalesState;
62
+ protected pickupTimingEnabled: boolean;
63
+ private runtimeOpenDataScope;
64
+ private runtimeOpenDataSnapshot;
65
+ private runtimeOpenDataModule;
66
+ private runtimeOpenDataCore;
67
+ private runtimeOpenDataGeneration;
68
+ private runtimeOpenDataDestroyed;
62
69
  private unifiedProductCatalog;
63
70
  private availabilityRuntimeOffsetMinutes;
64
71
  private availabilityScheduleCatalog;
@@ -142,12 +149,14 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
142
149
  private resolveDiscountAction;
143
150
  private resetCachedCustomerDiscountState;
144
151
  private refreshLegacySessionCustomerDiscounts;
145
- /**
146
- * Loads OpenData without borrowing BookingTicket. Cache ownership is tied to the exact
147
- * business/channel target so a reused solution instance cannot leak another entry's menus.
148
- */
152
+ private getRuntimeOpenDataScope;
153
+ private resetRuntimeOpenData;
154
+ protected prepareInvoiceTemplateAtSolutionReady(injectedOpenData?: OpenDataConfig): Promise<void>;
155
+ protected loadOpenDataConfig(): Promise<OpenDataConfig | null>;
156
+ /** A successful configuration belongs to this business runtime until an explicit refresh. */
149
157
  loadOpenData(params: UnifiedBookingSalesLoadOpenDataParams): Promise<OpenDataConfig>;
150
158
  getOpenData(): Promise<OpenDataConfig | null>;
159
+ protected loadPickupTimingOpenData(_force: boolean): Promise<Record<string, unknown>>;
151
160
  private resetAllergyItemRuleState;
152
161
  private fetchAllergyItemRuleConfigs;
153
162
  private ensureAllergyItemRuleConfigsLoaded;
@@ -55,7 +55,6 @@ var _platform = require("../../utils/platform");
55
55
  var _itemRule = require("../../model/strategy/adapter/itemRule");
56
56
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
57
57
  const OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow', 'basic', 'checkout', 'availability', 'documents'];
58
- const OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
59
58
  function normalizePositiveInteger(value) {
60
59
  const normalized = Number(value);
61
60
  if (!Number.isInteger(normalized) || normalized <= 0) return undefined;
@@ -1199,6 +1198,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1199
1198
  defaultName = 'unifiedBookingSales';
1200
1199
  defaultVersion = '1.0.0';
1201
1200
  store = {};
1201
+ pickupTimingEnabled = true;
1202
+ runtimeOpenDataScope = null;
1203
+ runtimeOpenDataSnapshot = null;
1204
+ runtimeOpenDataModule = null;
1205
+ runtimeOpenDataCore;
1206
+ runtimeOpenDataGeneration = 0;
1207
+ runtimeOpenDataDestroyed = false;
1202
1208
  unifiedProductCatalog = [];
1203
1209
  availabilityRuntimeOffsetMinutes = null;
1204
1210
  availabilityScheduleCatalog = null;
@@ -1218,16 +1224,16 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1218
1224
  allergyItemRuleConfigs = [];
1219
1225
  allergyItemRuleConfigsPromise = null;
1220
1226
  async initialize(core, options = {}) {
1227
+ this.resetRuntimeOpenData();
1228
+ this.runtimeOpenDataDestroyed = false;
1229
+ const openDataGeneration = this.runtimeOpenDataGeneration;
1221
1230
  this.availabilityScheduleCatalog = null;
1222
- this.openDataTarget = null;
1223
1231
  this.availabilityRuntimeOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)();
1224
1232
  this.clearAvailabilityRemoteResourceState();
1225
1233
  this.resetAllergyItemRuleState();
1226
1234
  await super.initialize(core, options);
1227
- const openDataBusinessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1228
- const openDataChannel = String(this.otherParams?.channel ?? '').trim();
1229
- if (this.otherParams?.openData && openDataBusinessCode && openDataChannel) {
1230
- this.openDataTarget = `${openDataBusinessCode}+${openDataChannel}`;
1235
+ if (this.runtimeOpenDataDestroyed || openDataGeneration !== this.runtimeOpenDataGeneration) {
1236
+ throw new Error('Store settings changed. Please try again.');
1231
1237
  }
1232
1238
  this.captureAvailabilityScheduleCatalog();
1233
1239
  }
@@ -1305,7 +1311,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1305
1311
  const businessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1306
1312
  const channel = String(this.otherParams?.channel ?? '').trim();
1307
1313
  const currentTarget = businessCode && channel ? `${businessCode}+${channel}` : null;
1308
- let openData = !currentTarget || this.openDataTarget === currentTarget ? this.otherParams?.openData : null;
1314
+ let openData = (!currentTarget || this.openDataTarget === currentTarget) && (this.runtimeOpenDataScope === null || this.runtimeOpenDataScope === this.getRuntimeOpenDataScope()) ? this.otherParams?.openData : null;
1309
1315
  if (!openData && this.store.openData) {
1310
1316
  try {
1311
1317
  openData = await this.getOpenData();
@@ -1369,6 +1375,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1369
1375
  const previousScope = this.getAvailabilityScopeDescriptor(this.otherParams || {});
1370
1376
  const previousItemRuleConfig = this.otherParams?.itemRule;
1371
1377
  const explicitlySetsOpenData = Object.prototype.hasOwnProperty.call(params, 'openData');
1378
+ const nextParams = cover ? params : {
1379
+ ...this.otherParams,
1380
+ ...params
1381
+ };
1382
+ if (explicitlySetsOpenData || this.getRuntimeOpenDataScope() !== this.getRuntimeOpenDataScope(nextParams)) {
1383
+ this.clearOpenDataCache();
1384
+ }
1372
1385
  await super.setOtherParams(params, {
1373
1386
  cover
1374
1387
  });
@@ -1385,6 +1398,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1385
1398
  }
1386
1399
  }
1387
1400
  async destroy() {
1401
+ this.runtimeOpenDataDestroyed = true;
1402
+ this.clearOpenDataCache();
1388
1403
  this.availabilityScheduleCatalog = null;
1389
1404
  this.availabilityRuntimeOffsetMinutes = null;
1390
1405
  await this.resetAvailabilityProjectionState('destroy');
@@ -1685,52 +1700,117 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1685
1700
  action: this.resolveDiscountAction(this.resolveDiscountOrderIdentity(tempOrder))
1686
1701
  });
1687
1702
  }
1703
+ getRuntimeOpenDataScope(otherParams = this.otherParams) {
1704
+ return JSON.stringify({
1705
+ shop: this.getPickupTimingContextData('shop_id'),
1706
+ appShop: this.appPlugin ? this.getAppData('shop_id') : undefined,
1707
+ tenant: this.appPlugin ? this.getAppData('tenant_id') : undefined,
1708
+ businessCode: String(otherParams?.businessCode ?? otherParams?.business_code ?? '').trim(),
1709
+ channel: String(otherParams?.channel || super.getSubmitOrderSalesChannel()).trim(),
1710
+ platform: (0, _platform.normalizeRuntimePlatform)(otherParams?.platform),
1711
+ timezone: this.getPickupTimingContextData('timezone'),
1712
+ businessTimezone: otherParams?.businessTimezone,
1713
+ cacheId: otherParams === this.otherParams ? this.cacheId : otherParams?.cacheId
1714
+ });
1715
+ }
1716
+ resetRuntimeOpenData() {
1717
+ this.runtimeOpenDataGeneration += 1;
1718
+ this.runtimeOpenDataScope = null;
1719
+ this.runtimeOpenDataSnapshot = null;
1720
+ this.runtimeOpenDataModule = null;
1721
+ this.runtimeOpenDataCore = undefined;
1722
+ this.openDataTarget = null;
1723
+ this.loadOpenDataInFlight = null;
1724
+ this.loadOpenDataInFlightTarget = null;
1725
+ this.otherParams = {
1726
+ ...this.otherParams,
1727
+ openData: null
1728
+ };
1729
+ this.store.openData?.clearCache?.();
1730
+ }
1731
+ async prepareInvoiceTemplateAtSolutionReady(injectedOpenData) {
1732
+ const businessCode = this.getBookingTicketBusinessCode();
1733
+ if (!businessCode || !this.store.openData) return;
1734
+ if (injectedOpenData) {
1735
+ this.otherParams.openData = injectedOpenData;
1736
+ this.openDataTarget = `${businessCode}+${this.getSubmitOrderSalesChannel()}`;
1737
+ }
1738
+ try {
1739
+ await this.loadOpenDataConfig();
1740
+ } catch (error) {
1741
+ console.warn('[UnifiedBookingSales] Invoice 模板预加载失败,使用默认配置', error);
1742
+ }
1743
+ }
1744
+ async loadOpenDataConfig() {
1745
+ const businessCode = this.getBookingTicketBusinessCode();
1746
+ if (!businessCode) return null;
1747
+ return this.loadOpenData({
1748
+ businessCode,
1749
+ channel: this.getSubmitOrderSalesChannel()
1750
+ });
1751
+ }
1688
1752
 
1689
- /**
1690
- * Loads OpenData without borrowing BookingTicket. Cache ownership is tied to the exact
1691
- * business/channel target so a reused solution instance cannot leak another entry's menus.
1692
- */
1753
+ /** A successful configuration belongs to this business runtime until an explicit refresh. */
1693
1754
  async loadOpenData(params) {
1694
- if (!this.store.openData) {
1755
+ const module = this.store.openData;
1756
+ if (!module) {
1695
1757
  throw new Error('[UnifiedBookingSales] openData 模块未初始化');
1696
1758
  }
1759
+ if (this.runtimeOpenDataDestroyed) {
1760
+ throw new Error('Store settings changed. Please try again.');
1761
+ }
1697
1762
  const businessCode = String(params.businessCode || '').trim();
1698
1763
  const channel = String(params.channel || '').trim();
1699
1764
  if (!businessCode || !channel) {
1700
1765
  throw new Error('[UnifiedBookingSales] OpenData 需要 businessCode 与 channel');
1701
1766
  }
1702
1767
  const target = `${businessCode}+${channel}`;
1703
- const cachedData = this.store.openData.getOpenData();
1704
- const lastFetchedAt = this.store.openData.getLastFetchedAt();
1705
- if (!params.force && this.openDataTarget === target && cachedData && lastFetchedAt && Date.now() - lastFetchedAt < OPEN_DATA_CACHE_TTL) {
1706
- if (typeof this.store.openData.ensureInvoiceTemplateSettings === 'function') {
1707
- await this.store.openData.ensureInvoiceTemplateSettings(target);
1768
+ this.otherParams.businessCode = businessCode;
1769
+ this.otherParams.channel = channel;
1770
+ const scope = this.getRuntimeOpenDataScope();
1771
+ if (params.force) {
1772
+ this.clearOpenDataCache();
1773
+ }
1774
+ if (this.runtimeOpenDataScope !== scope || this.runtimeOpenDataModule !== module || this.runtimeOpenDataCore !== this.core) {
1775
+ // Only the first, explicitly injected target may seed a new runtime snapshot.
1776
+ const injectedOpenData = this.runtimeOpenDataScope === null && (!this.openDataTarget || this.openDataTarget === target) && isRecord(this.otherParams.openData) ? this.otherParams.openData : null;
1777
+ if (this.runtimeOpenDataScope !== null) {
1778
+ // The caller's Pickup Timing scope already changes with this runtime scope.
1779
+ this.resetRuntimeOpenData();
1708
1780
  }
1709
- return cachedData;
1781
+ this.runtimeOpenDataScope = scope;
1782
+ this.runtimeOpenDataModule = module;
1783
+ this.runtimeOpenDataCore = this.core;
1784
+ this.runtimeOpenDataSnapshot = injectedOpenData;
1710
1785
  }
1711
1786
  if (this.loadOpenDataInFlight) {
1712
- if (this.loadOpenDataInFlightTarget === target) {
1713
- return await this.loadOpenDataInFlight;
1787
+ return this.loadOpenDataInFlight;
1788
+ }
1789
+ const generation = this.runtimeOpenDataGeneration;
1790
+ const core = this.core;
1791
+ const request = (async () => {
1792
+ let openData = this.runtimeOpenDataSnapshot;
1793
+ if (openData) {
1794
+ await module.ensureInvoiceTemplateSettings?.(target, openData);
1795
+ } else {
1796
+ openData = await module.fetchOpenData({
1797
+ scope: 'board',
1798
+ target,
1799
+ section_code: [...OPEN_DATA_SECTION_CODES]
1800
+ });
1714
1801
  }
1715
- await this.loadOpenDataInFlight;
1716
- return this.loadOpenData(params);
1717
- }
1802
+ if (this.runtimeOpenDataDestroyed || generation !== this.runtimeOpenDataGeneration || scope !== this.getRuntimeOpenDataScope() || module !== this.store.openData || core !== this.core) {
1803
+ throw new Error('Store settings changed. Please try again.');
1804
+ }
1805
+ this.runtimeOpenDataSnapshot = openData;
1806
+ this.openDataTarget = target;
1807
+ this.otherParams.openData = openData;
1808
+ return openData;
1809
+ })();
1718
1810
  this.loadOpenDataInFlightTarget = target;
1719
- const request = this.store.openData.fetchOpenData({
1720
- scope: 'board',
1721
- target,
1722
- section_code: [...OPEN_DATA_SECTION_CODES]
1723
- });
1724
1811
  this.loadOpenDataInFlight = request;
1725
1812
  try {
1726
- const openData = await request;
1727
- if (this.loadOpenDataInFlight === request) {
1728
- this.openDataTarget = target;
1729
- this.otherParams.openData = openData;
1730
- this.otherParams.businessCode = businessCode;
1731
- this.otherParams.channel = channel;
1732
- }
1733
- return openData;
1813
+ return await request;
1734
1814
  } finally {
1735
1815
  if (this.loadOpenDataInFlight === request) {
1736
1816
  this.loadOpenDataInFlight = null;
@@ -1739,13 +1819,19 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1739
1819
  }
1740
1820
  }
1741
1821
  async getOpenData() {
1742
- const businessCode = String(this.otherParams?.businessCode || '').trim();
1743
- const channel = String(this.otherParams?.channel || '').trim();
1744
- if (!businessCode || !channel) return null;
1745
- return this.loadOpenData({
1822
+ return this.loadOpenDataConfig();
1823
+ }
1824
+ async loadPickupTimingOpenData(_force) {
1825
+ const scope = this.getPickupTimingScope();
1826
+ const businessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1827
+ const config = await this.loadOpenData({
1746
1828
  businessCode,
1747
- channel
1829
+ channel: this.getSubmitOrderSalesChannel()
1748
1830
  });
1831
+ if (scope !== this.getPickupTimingScope()) {
1832
+ throw new Error('Store settings changed. Please try again.');
1833
+ }
1834
+ return config;
1749
1835
  }
1750
1836
  resetAllergyItemRuleState() {
1751
1837
  this.allergyItemRuleConfigs = [];
@@ -1840,10 +1926,8 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1840
1926
  });
1841
1927
  }
1842
1928
  clearOpenDataCache() {
1843
- this.openDataTarget = null;
1844
- this.loadOpenDataInFlight = null;
1845
- this.loadOpenDataInFlightTarget = null;
1846
- super.clearOpenDataCache();
1929
+ this.resetRuntimeOpenData();
1930
+ this.resetPickupTimingCache();
1847
1931
  }
1848
1932
  async getInvoiceLayoutConfig() {
1849
1933
  const openData = await this.getOpenData();
@@ -1863,7 +1947,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1863
1947
  const businessCode = String(this.otherParams?.businessCode || '').trim();
1864
1948
  const channel = String(this.otherParams?.channel || '').trim();
1865
1949
  if (!businessCode || !channel) return false;
1866
- return this.openDataTarget === `${businessCode}+${channel}`;
1950
+ return this.openDataTarget === `${businessCode}+${channel}` && this.runtimeOpenDataScope === this.getRuntimeOpenDataScope();
1867
1951
  }
1868
1952
 
1869
1953
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "0.10.50",
4
+ "version": "0.10.52",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",