@pisell/pisellos 0.10.16 → 0.10.18

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 (63) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/modules/OpenData/index.d.ts +5 -2
  3. package/dist/modules/OpenData/index.js +27 -12
  4. package/dist/modules/OpenData/types.d.ts +42 -0
  5. package/dist/modules/OpenData/types.js +7 -1
  6. package/dist/modules/OpenData/utils.d.ts +15 -1
  7. package/dist/modules/OpenData/utils.js +85 -0
  8. package/dist/modules/Order/index.d.ts +3 -1
  9. package/dist/modules/Order/index.js +68 -44
  10. package/dist/modules/Order/types.d.ts +3 -0
  11. package/dist/modules/Order/types.js +1 -0
  12. package/dist/modules/ProductList/index.js +96 -74
  13. package/dist/modules/Rules/index.js +11 -8
  14. package/dist/modules/SalesSummary/index.js +6 -4
  15. package/dist/server/index.d.ts +11 -0
  16. package/dist/server/index.js +845 -481
  17. package/dist/server/modules/order/index.d.ts +8 -0
  18. package/dist/server/modules/order/index.js +251 -215
  19. package/dist/server/utils/small-ticket.d.ts +9 -1
  20. package/dist/server/utils/small-ticket.js +117 -43
  21. package/dist/solution/BaseSales/index.d.ts +7 -0
  22. package/dist/solution/BaseSales/index.js +251 -144
  23. package/dist/solution/BookingByStep/index.d.ts +2 -2
  24. package/dist/solution/BookingTicket/index.d.ts +14 -19
  25. package/dist/solution/BookingTicket/index.js +545 -429
  26. package/dist/solution/Sales/index.d.ts +20 -4
  27. package/dist/solution/Sales/index.js +348 -75
  28. package/dist/solution/Sales/types.d.ts +10 -0
  29. package/dist/solution/UnifiedBookingSales/index.d.ts +5 -4
  30. package/dist/solution/UnifiedBookingSales/index.js +495 -422
  31. package/dist/solution/UnifiedBookingSales/types.d.ts +7 -1
  32. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  33. package/lib/modules/OpenData/index.d.ts +5 -2
  34. package/lib/modules/OpenData/index.js +50 -3
  35. package/lib/modules/OpenData/types.d.ts +42 -0
  36. package/lib/modules/OpenData/types.js +9 -1
  37. package/lib/modules/OpenData/utils.d.ts +15 -1
  38. package/lib/modules/OpenData/utils.js +85 -0
  39. package/lib/modules/Order/index.d.ts +3 -1
  40. package/lib/modules/Order/index.js +41 -21
  41. package/lib/modules/Order/types.d.ts +3 -0
  42. package/lib/modules/Order/types.js +1 -0
  43. package/lib/modules/ProductList/index.js +9 -6
  44. package/lib/modules/Rules/index.js +8 -6
  45. package/lib/modules/SalesSummary/index.js +3 -1
  46. package/lib/server/index.d.ts +11 -0
  47. package/lib/server/index.js +395 -96
  48. package/lib/server/modules/order/index.d.ts +8 -0
  49. package/lib/server/modules/order/index.js +23 -0
  50. package/lib/server/utils/small-ticket.d.ts +9 -1
  51. package/lib/server/utils/small-ticket.js +107 -35
  52. package/lib/solution/BaseSales/index.d.ts +7 -0
  53. package/lib/solution/BaseSales/index.js +125 -28
  54. package/lib/solution/BookingByStep/index.d.ts +2 -2
  55. package/lib/solution/BookingTicket/index.d.ts +14 -19
  56. package/lib/solution/BookingTicket/index.js +70 -17
  57. package/lib/solution/Sales/index.d.ts +20 -4
  58. package/lib/solution/Sales/index.js +183 -15
  59. package/lib/solution/Sales/types.d.ts +10 -0
  60. package/lib/solution/UnifiedBookingSales/index.d.ts +5 -4
  61. package/lib/solution/UnifiedBookingSales/index.js +41 -15
  62. package/lib/solution/UnifiedBookingSales/types.d.ts +7 -1
  63. package/package.json +1 -1
@@ -21,6 +21,7 @@ Object.keys(_types).forEach(function (key) {
21
21
  }
22
22
  });
23
23
  });
24
+ var _OpenData = require("../../modules/OpenData");
24
25
  var _BookingContext = require("../../modules/BookingContext");
25
26
  var _BaseSales = require("../BaseSales");
26
27
  var _scan = _interopRequireDefault(require("./utils/scan"));
@@ -39,7 +40,7 @@ var _bookingStatus = require("./utils/bookingStatus");
39
40
  var _resolveBestAddTimePlan = require("./utils/resolveBestAddTimePlan");
40
41
  var _utils = require("../../modules/Order/utils");
41
42
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
42
- const OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow'];
43
+ const OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow', 'documents'];
43
44
  const OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
44
45
  const USER_PLATFORM = ['pc', 'h5'];
45
46
 
@@ -160,7 +161,9 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
160
161
  orderCustomerDiscountRefreshInFlight = null;
161
162
  orderCustomerDiscountRefreshCustomerId = null;
162
163
  orderCustomerPromotionRefreshInFlight = null;
163
- loadOpenDataConfigInFlight = null;
164
+ loadOpenDataInFlight = null;
165
+ loadOpenDataInFlightTarget = null;
166
+ openDataTarget = null;
164
167
 
165
168
  /**
166
169
  * 在 BaseSales 默认模块清单(products/order/salesSummary/schedule)基础上追加 customer + bookingContext。
@@ -210,35 +213,69 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
210
213
  console.warn('[BookingTicket] businessCode 缺失,跳过 OpenData 配置加载,当前 module name: ', this.name);
211
214
  return null;
212
215
  }
216
+ const channel = this.getSubmitOrderSalesChannel();
217
+ const target = `${businessCode}+${channel}`;
213
218
  const lastFetchedAt = this.store.openData.getLastFetchedAt();
214
219
  const cachedData = this.store.openData.getOpenData();
215
- if (cachedData && lastFetchedAt && Date.now() - lastFetchedAt < OPEN_DATA_CACHE_TTL) {
220
+ if (cachedData && this.openDataTarget === target && lastFetchedAt && Date.now() - lastFetchedAt < OPEN_DATA_CACHE_TTL) {
216
221
  this.otherParams.openData = cachedData;
217
222
  return cachedData;
218
223
  }
219
- if (this.loadOpenDataConfigInFlight) return await this.loadOpenDataConfigInFlight;
220
- const channel = this.getSubmitOrderSalesChannel();
221
- this.loadOpenDataConfigInFlight = this.store.openData.fetchOpenData({
224
+ if (this.loadOpenDataInFlight) {
225
+ if (this.loadOpenDataInFlightTarget === target) {
226
+ return await this.loadOpenDataInFlight;
227
+ }
228
+ await this.loadOpenDataInFlight;
229
+ return this.loadOpenDataConfig();
230
+ }
231
+ this.loadOpenDataInFlightTarget = target;
232
+ const request = this.store.openData.fetchOpenData({
222
233
  scope: 'board',
223
- target: `${businessCode}+${channel}`,
234
+ target,
224
235
  // target: `dine_in+pos`,
225
236
  section_code: [...OPEN_DATA_SECTION_CODES]
226
237
  });
238
+ this.loadOpenDataInFlight = request;
227
239
  try {
228
- const openDataConfig = await this.loadOpenDataConfigInFlight;
229
- this.otherParams.openData = openDataConfig;
240
+ const openDataConfig = await request;
241
+ // clearOpenDataCache() 或新请求已接管时,旧响应只能返回给原调用方,不能复活缓存归属。
242
+ if (this.loadOpenDataInFlight === request) {
243
+ this.openDataTarget = target;
244
+ this.otherParams.openData = openDataConfig;
245
+ }
230
246
  return openDataConfig;
231
247
  } finally {
232
- this.loadOpenDataConfigInFlight = null;
248
+ if (this.loadOpenDataInFlight === request) {
249
+ this.loadOpenDataInFlight = null;
250
+ this.loadOpenDataInFlightTarget = null;
251
+ }
233
252
  }
234
253
  }
235
254
  async getOpenData() {
236
255
  return this.loadOpenDataConfig();
237
256
  }
257
+ async getInvoiceLayoutConfig() {
258
+ const openData = await this.loadOpenDataConfig();
259
+ if (!openData) return (0, _OpenData.getDefaultInvoiceLayoutConfig)();
260
+ return this.store.openData.getInvoiceLayoutConfig();
261
+ }
262
+ async getInvoiceTemplateSettings() {
263
+ const openData = await this.loadOpenDataConfig();
264
+ if (!openData) return (0, _OpenData.getDefaultInvoiceTemplateSettings)();
265
+ return this.store.openData.getInvoiceTemplateSettings();
266
+ }
267
+ isInvoiceOpenDataCacheCurrent() {
268
+ const businessCode = this.getBookingTicketBusinessCode();
269
+ const channel = this.getSubmitOrderSalesChannel();
270
+ if (!businessCode || !channel) return false;
271
+ return this.openDataTarget === `${businessCode}+${channel}`;
272
+ }
238
273
 
239
274
  /** 清除店铺级 OpenData;下一次 getOpenData 时会重新拉取。 */
240
275
  clearOpenDataCache() {
241
- this.loadOpenDataConfigInFlight = null;
276
+ this.loadOpenDataInFlight = null;
277
+ this.loadOpenDataInFlightTarget = null;
278
+ this.openDataTarget = null;
242
279
  this.otherParams.openData = null;
243
280
  this.store.openData?.clearCache();
244
281
  }
@@ -275,7 +312,10 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
275
312
  const prefix = this.normalizeIdPrefix(openDataConfig?.['sale.short_number_prefix'], '');
276
313
  const shopOrderPrefix = this.normalizeIdPrefix(openDataConfig?.['sale.sale_number_prefix'], '');
277
314
  const resetReceiptSequenceDaily = typeof openDataConfig?.['sale.short_number_daily_reset'] === 'boolean' ? openDataConfig?.['sale.short_number_daily_reset'] : false;
278
- const operatingDayBoundary = this.getAppData('operating_day_boundary');
315
+ const operatingDayBoundary = this.getAppData('operating_day_boundary') || {
316
+ "type": "start_time",
317
+ "time": "00:00"
318
+ };
279
319
  const deviceId = await this.getShortNumberOrDeviceId();
280
320
  this.setPaymentNumberDevicePrefix(deviceId);
281
321
  const businessCode = this.getBookingTicketBusinessCode();
@@ -293,7 +333,7 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
293
333
  padReceiptSequence: false,
294
334
  receiptSequenceLength,
295
335
  resetReceiptSequenceDaily,
296
- receiptSequenceResetTime: operatingDayBoundary.time,
336
+ receiptSequenceResetTime: operatingDayBoundary?.time,
297
337
  receiptSequenceStart,
298
338
  shopOrderPrefix
299
339
  }
@@ -349,7 +389,12 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
349
389
  country_calling_code: selectedCustomer?.country_calling_code
350
390
  };
351
391
  customerModule.setSelectedCustomer(customer);
352
- this.setOrderCustomer(customer);
392
+ // 详情 hydrate 后仍需广播一次,让 CustomerContext 等 UI 订阅方拿到刚补齐的
393
+ // customerSnapshot;但它不是用户主动切换客户。通过 source 明确标记初始化事件,
394
+ // 商品报价/折扣订阅方即可跳过 prepare/config,其他旧订阅方仍可照常刷新 UI。
395
+ this.setOrderCustomer(customer, {
396
+ source: 'detailHydrate'
397
+ });
353
398
  // await this.store.order.saveDraft();
354
399
  }
355
400
  return sales;
@@ -926,7 +971,7 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
926
971
  * 同时透传 OrderModule 内部 emit 的 `order:onOrderCustomerChange` 之外,
927
972
  * 在 BookingTicket 命名空间也 emit 一次,方便上层统一从 Solution 订阅。
928
973
  */
929
- setOrderCustomer(customer) {
974
+ setOrderCustomer(customer, options) {
930
975
  if (!customer) {
931
976
  this.clearOrderCustomer();
932
977
  return;
@@ -947,12 +992,18 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
947
992
  const snapshotReplenished = !snapshotBeforeSet && Boolean(snapshotAfterOrderSet);
948
993
  if (snapshotReplenished) {
949
994
  const next = this.buildOrderCustomerPayload();
950
- this.core.effects.emit(`${this.name}:onOrderCustomerChange`, next);
995
+ this.core.effects.emit(`${this.name}:onOrderCustomerChange`, next && options?.source ? {
996
+ ...next,
997
+ source: options.source
998
+ } : next);
951
999
  }
952
1000
  return;
953
1001
  }
954
1002
  const next = this.buildOrderCustomerPayload();
955
- this.core.effects.emit(`${this.name}:onOrderCustomerChange`, next);
1003
+ this.core.effects.emit(`${this.name}:onOrderCustomerChange`, next && options?.source ? {
1004
+ ...next,
1005
+ source: options.source
1006
+ } : next);
956
1007
  this.refreshDiscountConfigAfterOrderCustomerChange(patch.customer_id);
957
1008
  // 客户切换可能改变可用促销/赠品;触发一次重算(recursion-safe)。
958
1009
  const promotionRefreshTask = this.store.order.applyPromotion().then(() => undefined);
@@ -1729,6 +1780,8 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
1729
1780
  });
1730
1781
  if (params?.businessCode !== undefined || params?.business_code !== undefined) {
1731
1782
  await this.configureIdGeneratorFromOpenData();
1783
+ } else if (params?.channel !== undefined) {
1784
+ await this.loadOpenDataConfig();
1732
1785
  }
1733
1786
  }
1734
1787
  getDeviceTaskPlugin() {
@@ -2,7 +2,7 @@ import dayjs from 'dayjs';
2
2
  import { Module, ModuleOptions, PisellCore } from '../../types';
3
3
  import { BaseModule } from '../../modules/BaseModule';
4
4
  import type { RequestPlugin } from '../../plugins/request';
5
- import { SalesState, SalesModuleAPI, Reservation, SalesResourceBookingItem, SalesResourceBookingItemWithGroups, SalesTimelineHighlights, ChangeBookingStatusParams, GetResourceBookingItemParams } from './types';
5
+ import { SalesState, SalesModuleAPI, Reservation, SalesResourceBookingItem, SalesResourceBookingItemWithGroups, SalesTimelineHighlights, ChangeBookingStatusParams, GetResourceBookingItemParams, SalesPrintLocalOrderReceiptOptions } from './types';
6
6
  import type { BookingData } from '../../server/modules/order/types';
7
7
  export * from './types';
8
8
  /**
@@ -26,6 +26,10 @@ export declare class SalesImpl extends BaseModule implements Module, SalesModule
26
26
  protected otherParams: Record<string, any>;
27
27
  protected request: RequestPlugin;
28
28
  private logger;
29
+ private invoiceOpenDataModule;
30
+ private invoiceOpenDataTarget;
31
+ private invoiceOpenDataInFlight;
32
+ private invoiceOpenDataInFlightTarget;
29
33
  constructor(name?: string, version?: string);
30
34
  initialize(core: PisellCore, options?: ModuleOptions): Promise<void>;
31
35
  /** 获取当前 store 快照 */
@@ -42,6 +46,17 @@ export declare class SalesImpl extends BaseModule implements Module, SalesModule
42
46
  resetReservationList(): Reservation[];
43
47
  cancelOrder(orderIds: Array<number | string>): Promise<any>;
44
48
  changeBookingStatus(params: ChangeBookingStatusParams): Promise<any>;
49
+ /**
50
+ * 将显式 channel 或当前运行上下文归一为 Invoice OpenData 使用的销售入口。
51
+ * Legacy Sales 的系统上下文没有可打印的 system target,因此统一回退 pos。
52
+ */
53
+ private getInvoiceSalesChannel;
54
+ private loadInvoiceOpenData;
55
+ private logInvoiceOpenDataFallback;
56
+ private resolveInvoiceConfigs;
57
+ printLocalOrderReceipt<T = any>(lookup: string | number, options: SalesPrintLocalOrderReceiptOptions): Promise<T>;
58
+ /** 清除独立 Documents 缓存;下一次打印会按当前 target 重新获取。 */
59
+ clearOpenDataCache(): void;
45
60
  destroy(): Promise<void>;
46
61
  /**
47
62
  * 时间轴粒度(分钟)
@@ -50,11 +65,12 @@ export declare class SalesImpl extends BaseModule implements Module, SalesModule
50
65
  */
51
66
  private getTimelineBucketMinutes;
52
67
  /**
53
- * 解析营业日统计窗口:
68
+ * 解析 current 所属营业日窗口:
54
69
  * - 当 operating_day_boundary.type === 'start_time'
55
- * 使用「今天 + boundary.time」到「明天 + boundary.time」
70
+ * 使用「最近一个 boundary.time」到「下一个 boundary.time」
56
71
  * - 否则回退自然日(00:00 到次日 00:00)
57
72
  */
73
+ private getOperatingDayRange;
58
74
  private getTimelineRangeByOperatingBoundary;
59
75
  /**
60
76
  * 统计时间轴每个时间片的预约数量。
@@ -95,7 +111,7 @@ export declare class SalesImpl extends BaseModule implements Module, SalesModule
95
111
  * 2) 若无 active,返回已超时且仍占用中的预约(允许并发多条)
96
112
  * 3) 若仍无,返回最近一组未来预约(同 start_time 的并发预约全部返回)
97
113
  * 4) 最后兜底返回一条(避免资源有历史预约时完全无反馈)
98
- * - includeAllFuture 时,保留选中预约并返回当天全部后续预约;仅详情接口启用。
114
+ * - includeAllFuture 时,保留选中预约并返回当前营业日全部后续预约;仅详情接口启用。
99
115
  */
100
116
  private pickBookingsForCurrentPoint;
101
117
  /**
@@ -9,6 +9,7 @@ var _exportNames = {
9
9
  };
10
10
  exports.default = exports.SalesImpl = exports.Sales = void 0;
11
11
  var _dayjs = _interopRequireDefault(require("dayjs"));
12
+ var _lodashEs = require("lodash-es");
12
13
  var _BaseModule = require("../../modules/BaseModule");
13
14
  var _types = require("./types");
14
15
  Object.keys(_types).forEach(function (key) {
@@ -22,8 +23,11 @@ Object.keys(_types).forEach(function (key) {
22
23
  }
23
24
  });
24
25
  });
26
+ var _OpenData = require("../../modules/OpenData");
25
27
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
26
28
  const UNPAID_PAYMENT_STATUSES = new Set(['unpaid', 'partially_paid', 'payment_processing']);
29
+ const INVOICE_OPEN_DATA_SECTION_CODES = ['documents'];
30
+ const INVOICE_OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
27
31
 
28
32
  /**
29
33
  * Sales 解决方案基础骨架
@@ -46,11 +50,22 @@ class SalesImpl extends _BaseModule.BaseModule {
46
50
  otherParams = {};
47
51
  request;
48
52
  logger; // LoggerManager 实例
49
-
53
+ invoiceOpenDataModule = null;
54
+ invoiceOpenDataTarget = null;
55
+ invoiceOpenDataInFlight = null;
56
+ invoiceOpenDataInFlightTarget = null;
50
57
  constructor(name, version) {
51
58
  super(name, version);
52
59
  }
53
60
  async initialize(core, options = {}) {
61
+ if (this.invoiceOpenDataModule) {
62
+ try {
63
+ this.clearOpenDataCache();
64
+ this.invoiceOpenDataModule.destroy();
65
+ } catch (error) {
66
+ console.warn('[Sales] 旧 Invoice OpenData 子模块销毁失败', error);
67
+ }
68
+ }
54
69
  this.core = core;
55
70
  this.options = options;
56
71
  this.otherParams = options.otherParams || {};
@@ -71,6 +86,17 @@ class SalesImpl extends _BaseModule.BaseModule {
71
86
  // 在这里补充从 Server/Order 读取 currentDay 原始数据并转换为预约列表。
72
87
 
73
88
  this.logger = appPlugin?.getApp().logger;
89
+ this.invoiceOpenDataTarget = null;
90
+ this.invoiceOpenDataInFlight = null;
91
+ this.invoiceOpenDataInFlightTarget = null;
92
+ this.invoiceOpenDataModule = new _OpenData.OpenDataModule(`${this.name}_invoiceOpenData`);
93
+ this.core.registerModule(this.invoiceOpenDataModule, {
94
+ initialState: {},
95
+ otherParams: {
96
+ fatherModule: this.name,
97
+ openCache: false
98
+ }
99
+ });
74
100
  await this.core.effects.emit(`${this.name}:onInited`, {});
75
101
  }
76
102
 
@@ -139,11 +165,148 @@ class SalesImpl extends _BaseModule.BaseModule {
139
165
  return orderModule.changeBookingStatus(params);
140
166
  }
141
167
 
168
+ /**
169
+ * 将显式 channel 或当前运行上下文归一为 Invoice OpenData 使用的销售入口。
170
+ * Legacy Sales 的系统上下文没有可打印的 system target,因此统一回退 pos。
171
+ */
172
+ getInvoiceSalesChannel(channel) {
173
+ const contextChannel = this.core?.context?.getChannel?.();
174
+ const normalized = String(channel ?? contextChannel ?? '').trim();
175
+ return !normalized || normalized === 'system' ? 'pos' : normalized;
176
+ }
177
+ async loadInvoiceOpenData(businessCode, channel) {
178
+ if (!this.invoiceOpenDataModule) {
179
+ throw new Error('Sales Invoice OpenData 模块未初始化');
180
+ }
181
+ const target = `${businessCode}+${channel}`;
182
+ const cachedData = this.invoiceOpenDataModule.getOpenData();
183
+ const lastFetchedAt = this.invoiceOpenDataModule.getLastFetchedAt();
184
+ if (cachedData && this.invoiceOpenDataTarget === target && typeof lastFetchedAt === 'number' && Date.now() - lastFetchedAt < INVOICE_OPEN_DATA_CACHE_TTL) {
185
+ return cachedData;
186
+ }
187
+ if (this.invoiceOpenDataInFlight) {
188
+ if (this.invoiceOpenDataInFlightTarget === target) {
189
+ return this.invoiceOpenDataInFlight;
190
+ }
191
+ try {
192
+ await this.invoiceOpenDataInFlight;
193
+ } catch {
194
+ // 另一 target 的失败不应阻断当前 target 获取自己的 Documents 配置。
195
+ }
196
+ return this.loadInvoiceOpenData(businessCode, channel);
197
+ }
198
+ const request = this.invoiceOpenDataModule.fetchOpenData({
199
+ scope: 'board',
200
+ target,
201
+ section_code: [...INVOICE_OPEN_DATA_SECTION_CODES]
202
+ });
203
+ this.invoiceOpenDataInFlight = request;
204
+ this.invoiceOpenDataInFlightTarget = target;
205
+ try {
206
+ const openData = await request;
207
+ // clearOpenDataCache() 或后续请求接管后,旧响应不得复活 target 归属。
208
+ if (this.invoiceOpenDataInFlight === request) {
209
+ this.invoiceOpenDataTarget = target;
210
+ }
211
+ return openData;
212
+ } finally {
213
+ if (this.invoiceOpenDataInFlight === request) {
214
+ this.invoiceOpenDataInFlight = null;
215
+ this.invoiceOpenDataInFlightTarget = null;
216
+ }
217
+ }
218
+ }
219
+ logInvoiceOpenDataFallback(error) {
220
+ const message = error instanceof Error ? error.message : String(error);
221
+ if (this.logger?.addLog) {
222
+ this.logger.addLog({
223
+ type: 'warning',
224
+ title: 'Sales Invoice OpenData 加载失败,使用默认配置',
225
+ metadata: {
226
+ message
227
+ }
228
+ });
229
+ return;
230
+ }
231
+ console.warn('[Sales] Invoice OpenData 加载失败,使用默认配置', error);
232
+ }
233
+ async resolveInvoiceConfigs(businessCode, channel) {
234
+ try {
235
+ await this.loadInvoiceOpenData(businessCode, channel);
236
+ if (this.invoiceOpenDataTarget !== `${businessCode}+${channel}`) {
237
+ return {
238
+ invoice: (0, _OpenData.getDefaultInvoiceLayoutConfig)(),
239
+ templateSettings: (0, _OpenData.getDefaultInvoiceTemplateSettings)()
240
+ };
241
+ }
242
+ return {
243
+ invoice: this.invoiceOpenDataModule?.getInvoiceLayoutConfig() ?? (0, _OpenData.getDefaultInvoiceLayoutConfig)(),
244
+ templateSettings: this.invoiceOpenDataModule?.getInvoiceTemplateSettings() ?? (0, _OpenData.getDefaultInvoiceTemplateSettings)()
245
+ };
246
+ } catch (error) {
247
+ this.logInvoiceOpenDataFallback(error);
248
+ return {
249
+ invoice: (0, _OpenData.getDefaultInvoiceLayoutConfig)(),
250
+ templateSettings: (0, _OpenData.getDefaultInvoiceTemplateSettings)()
251
+ };
252
+ }
253
+ }
254
+ async printLocalOrderReceipt(lookup, options) {
255
+ const normalizedLookup = typeof lookup === 'string' ? lookup.trim() : lookup;
256
+ if (normalizedLookup === '' || normalizedLookup === null || normalizedLookup === undefined) {
257
+ throw new Error('Sales 本地小票打印需要订单标识');
258
+ }
259
+ const businessCode = String(options?.businessCode ?? '').trim();
260
+ if (!businessCode) {
261
+ throw new Error('Sales 本地小票打印需要 businessCode');
262
+ }
263
+ const channel = this.getInvoiceSalesChannel(options?.channel);
264
+ const {
265
+ invoice,
266
+ templateSettings
267
+ } = await this.resolveInvoiceConfigs(businessCode, channel);
268
+ const lookupPayload = typeof normalizedLookup === 'number' ? {
269
+ order_id: normalizedLookup
270
+ } : {
271
+ external_sale_number: normalizedLookup
272
+ };
273
+ return this.request.post('/local/print-order', {
274
+ ...lookupPayload,
275
+ lookup_order_from_db: 1,
276
+ small_ticket_data_flag: 1,
277
+ business_code: businessCode,
278
+ order_sales_channel: channel,
279
+ invoice: (0, _lodashEs.cloneDeep)(invoice),
280
+ template_settings: (0, _lodashEs.cloneDeep)(templateSettings)
281
+ }, {
282
+ osServer: true,
283
+ customToast: () => {}
284
+ });
285
+ }
286
+
287
+ /** 清除独立 Documents 缓存;下一次打印会按当前 target 重新获取。 */
288
+ clearOpenDataCache() {
289
+ this.invoiceOpenDataInFlight = null;
290
+ this.invoiceOpenDataInFlightTarget = null;
291
+ this.invoiceOpenDataTarget = null;
292
+ this.invoiceOpenDataModule?.clearCache();
293
+ }
294
+
142
295
  // -------------------------------------------------------------------------
143
296
  // 生命周期
144
297
  // -------------------------------------------------------------------------
145
298
 
146
299
  async destroy() {
300
+ this.clearOpenDataCache();
301
+ const invoiceOpenDataModule = this.invoiceOpenDataModule;
302
+ this.invoiceOpenDataModule = null;
303
+ if (invoiceOpenDataModule) {
304
+ try {
305
+ await invoiceOpenDataModule.destroy();
306
+ } catch (error) {
307
+ console.warn('[Sales] Invoice OpenData 子模块销毁失败', error);
308
+ }
309
+ }
147
310
  await this.core.effects.emit(`${this.name}:onDestroy`, {});
148
311
  super.destroy();
149
312
  }
@@ -160,16 +323,15 @@ class SalesImpl extends _BaseModule.BaseModule {
160
323
  }
161
324
 
162
325
  /**
163
- * 解析营业日统计窗口:
326
+ * 解析 current 所属营业日窗口:
164
327
  * - 当 operating_day_boundary.type === 'start_time'
165
- * 使用「今天 + boundary.time」到「明天 + boundary.time」
328
+ * 使用「最近一个 boundary.time」到「下一个 boundary.time」
166
329
  * - 否则回退自然日(00:00 到次日 00:00)
167
330
  */
168
- getTimelineRangeByOperatingBoundary() {
169
- const today = (0, _dayjs.default)();
170
- const defaultStart = today.startOf('day');
331
+ getOperatingDayRange(referenceTime = (0, _dayjs.default)()) {
332
+ const defaultStart = referenceTime.startOf('day');
171
333
  const defaultEndExclusive = defaultStart.add(1, 'day');
172
- const appPlugin = this.core.getPlugin('app');
334
+ const appPlugin = this.core?.getPlugin('app');
173
335
  const coreData = appPlugin?.getApp()?.data?.store?.getStore?.().getDataByModel?.('core');
174
336
  const operatingDayBoundary = coreData?.core?.operating_day_boundary;
175
337
  if (operatingDayBoundary?.type !== 'start_time') {
@@ -179,18 +341,22 @@ class SalesImpl extends _BaseModule.BaseModule {
179
341
  };
180
342
  }
181
343
  const boundaryTime = String(operatingDayBoundary.time || '').trim();
182
- const startAt = (0, _dayjs.default)(`${today.format('YYYY-MM-DD')} ${boundaryTime}`);
183
- if (!startAt.isValid()) {
344
+ const boundaryStart = (0, _dayjs.default)(`${referenceTime.format('YYYY-MM-DD')} ${boundaryTime}`);
345
+ if (!boundaryStart.isValid()) {
184
346
  return {
185
347
  startAt: defaultStart,
186
348
  endExclusiveAt: defaultEndExclusive
187
349
  };
188
350
  }
351
+ const startAt = referenceTime.isBefore(boundaryStart) ? boundaryStart.subtract(1, 'day') : boundaryStart;
189
352
  return {
190
353
  startAt,
191
354
  endExclusiveAt: startAt.add(1, 'day')
192
355
  };
193
356
  }
357
+ getTimelineRangeByOperatingBoundary() {
358
+ return this.getOperatingDayRange();
359
+ }
194
360
 
195
361
  /**
196
362
  * 统计时间轴每个时间片的预约数量。
@@ -343,19 +509,21 @@ class SalesImpl extends _BaseModule.BaseModule {
343
509
  * 2) 若无 active,返回已超时且仍占用中的预约(允许并发多条)
344
510
  * 3) 若仍无,返回最近一组未来预约(同 start_time 的并发预约全部返回)
345
511
  * 4) 最后兜底返回一条(避免资源有历史预约时完全无反馈)
346
- * - includeAllFuture 时,保留选中预约并返回当天全部后续预约;仅详情接口启用。
512
+ * - includeAllFuture 时,保留选中预约并返回当前营业日全部后续预约;仅详情接口启用。
347
513
  */
348
514
  pickBookingsForCurrentPoint(current, bookings, options = {}) {
349
515
  if (bookings.length === 0) return [];
350
- const currentDayStart = current.startOf('day');
351
- const currentDayEnd = current.endOf('day');
516
+ const {
517
+ startAt: operatingDayStart,
518
+ endExclusiveAt: operatingDayEndExclusive
519
+ } = this.getOperatingDayRange(current);
352
520
 
353
- // 仅保留与 current 当天有时间交集的预约,避免跨天误命中
521
+ // 仅保留与 current 所属营业日窗口有时间交集的预约,避免跨营业日误命中
354
522
  const dayScopedBookings = bookings.filter(booking => {
355
523
  const startAt = this.toBookingDateTime(booking.start_date, booking.start_time);
356
524
  const endAt = this.toBookingDateTime(booking.end_date, booking.end_time);
357
525
  if (!startAt.isValid() || !endAt.isValid()) return false;
358
- return this.isSameOrBefore(startAt, currentDayEnd) && this.isSameOrAfter(endAt, currentDayStart);
526
+ return startAt.isBefore(operatingDayEndExclusive) && this.isSameOrAfter(endAt, operatingDayStart);
359
527
  });
360
528
  if (dayScopedBookings.length === 0) return [];
361
529
  const appendNextStartGroupIfNeeded = selectedBookings => {
@@ -404,7 +572,7 @@ class SalesImpl extends _BaseModule.BaseModule {
404
572
  // upcoming: 从未来预约里只取最早那一组(同 start_time)
405
573
  const upcoming = dayScopedBookings.filter(booking => {
406
574
  const startAt = this.toBookingDateTime(booking.start_date, booking.start_time);
407
- return startAt.isValid() && startAt.isAfter(current) && startAt.isSame(current, 'day');
575
+ return startAt.isValid() && startAt.isAfter(current);
408
576
  });
409
577
  if (upcoming.length > 0) {
410
578
  if (options.includeAllFuture) return upcoming;
@@ -50,6 +50,12 @@ export interface GetResourceBookingItemParams {
50
50
  bookingList: BookingData[];
51
51
  deviceTime?: string;
52
52
  }
53
+ export interface SalesPrintLocalOrderReceiptOptions {
54
+ /** 当前销售业务类型,用于隔离 Documents Invoice 配置。 */
55
+ businessCode: string;
56
+ /** 当前销售入口;缺失或为 system 时按 pos 处理。 */
57
+ channel?: string;
58
+ }
53
59
  /**
54
60
  * Sales 解决方案状态
55
61
  *
@@ -82,6 +88,10 @@ export interface SalesModuleAPI {
82
88
  cancelOrder: (orderIds: Array<number | string>) => Promise<any>;
83
89
  /** 变更预约状态 */
84
90
  changeBookingStatus: (params: ChangeBookingStatusParams) => Promise<any>;
91
+ /** 按本地订单标识打印小票;本地未命中时由 OS Server 负责远端兜底。 */
92
+ printLocalOrderReceipt: <T = any>(lookup: string | number, options: SalesPrintLocalOrderReceiptOptions) => Promise<T>;
93
+ /** 清除 Legacy Sales 独立维护的 Documents OpenData 缓存。 */
94
+ clearOpenDataCache: () => void;
85
95
  /** 获取资源维度的预约占用列表 */
86
96
  getResourceBookingList: (currentTime: string, bookingList: BookingData[], deviceTime?: string) => Promise<SalesResourceBookingItem[]>;
87
97
  /** 获取单个资源维度的预约占用、已完成、未支付列表 */
@@ -1,6 +1,6 @@
1
1
  import type { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import type { ProductData } from '../../modules';
3
- import type { OpenDataConfig } from '../../modules/OpenData';
3
+ import { type InvoiceLayoutConfig, type InvoiceTemplateSettings, type OpenDataConfig } from '../../modules/OpenData';
4
4
  import type { LoadScheduleAvailableDateParams, ScheduleItem } from '../../modules/Schedule/types';
5
5
  import type { ProductListLoadProductsParams } from '../../modules/ProductList';
6
6
  import type { AvailabilityDateRange, AvailabilityOccupancy, AvailabilityProduct, AvailabilityResource, GetProductTimeRangesRequest } from '../../modules/ResourcePlanner';
@@ -59,9 +59,6 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
59
59
  protected defaultVersion: string;
60
60
  protected store: UnifiedBookingSalesState;
61
61
  private unifiedProductCatalog;
62
- private openDataTarget;
63
- private loadOpenDataInFlight;
64
- private loadOpenDataInFlightTarget;
65
62
  private availabilityRuntimeOffsetMinutes;
66
63
  private availabilityScheduleCatalog;
67
64
  private availabilityProjectionSequence;
@@ -147,6 +144,10 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
147
144
  */
148
145
  loadOpenData(params: UnifiedBookingSalesLoadOpenDataParams): Promise<OpenDataConfig>;
149
146
  getOpenData(): Promise<OpenDataConfig | null>;
147
+ clearOpenDataCache(): void;
148
+ getInvoiceLayoutConfig(): Promise<InvoiceLayoutConfig>;
149
+ getInvoiceTemplateSettings(): Promise<InvoiceTemplateSettings>;
150
+ protected isInvoiceOpenDataCacheCurrent(): boolean;
150
151
  /**
151
152
  * Load a resource policy without depending on the legacy appointmentBooking solution.
152
153
  */