@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.
- package/dist/model/strategy/adapter/promotion/index.js +9 -0
- package/dist/modules/OpenData/index.d.ts +5 -2
- package/dist/modules/OpenData/index.js +27 -12
- package/dist/modules/OpenData/types.d.ts +42 -0
- package/dist/modules/OpenData/types.js +7 -1
- package/dist/modules/OpenData/utils.d.ts +15 -1
- package/dist/modules/OpenData/utils.js +85 -0
- package/dist/modules/Order/index.d.ts +3 -1
- package/dist/modules/Order/index.js +68 -44
- package/dist/modules/Order/types.d.ts +3 -0
- package/dist/modules/Order/types.js +1 -0
- package/dist/modules/ProductList/index.js +96 -74
- package/dist/modules/Rules/index.js +11 -8
- package/dist/modules/SalesSummary/index.js +6 -4
- package/dist/server/index.d.ts +11 -0
- package/dist/server/index.js +845 -481
- package/dist/server/modules/order/index.d.ts +8 -0
- package/dist/server/modules/order/index.js +251 -215
- package/dist/server/utils/small-ticket.d.ts +9 -1
- package/dist/server/utils/small-ticket.js +117 -43
- package/dist/solution/BaseSales/index.d.ts +7 -0
- package/dist/solution/BaseSales/index.js +251 -144
- package/dist/solution/BookingByStep/index.d.ts +2 -2
- package/dist/solution/BookingTicket/index.d.ts +14 -19
- package/dist/solution/BookingTicket/index.js +545 -429
- package/dist/solution/Sales/index.d.ts +20 -4
- package/dist/solution/Sales/index.js +348 -75
- package/dist/solution/Sales/types.d.ts +10 -0
- package/dist/solution/UnifiedBookingSales/index.d.ts +5 -4
- package/dist/solution/UnifiedBookingSales/index.js +495 -422
- package/dist/solution/UnifiedBookingSales/types.d.ts +7 -1
- package/lib/model/strategy/adapter/promotion/index.js +1 -46
- package/lib/modules/OpenData/index.d.ts +5 -2
- package/lib/modules/OpenData/index.js +50 -3
- package/lib/modules/OpenData/types.d.ts +42 -0
- package/lib/modules/OpenData/types.js +9 -1
- package/lib/modules/OpenData/utils.d.ts +15 -1
- package/lib/modules/OpenData/utils.js +85 -0
- package/lib/modules/Order/index.d.ts +3 -1
- package/lib/modules/Order/index.js +41 -21
- package/lib/modules/Order/types.d.ts +3 -0
- package/lib/modules/Order/types.js +1 -0
- package/lib/modules/ProductList/index.js +9 -6
- package/lib/modules/Rules/index.js +8 -6
- package/lib/modules/SalesSummary/index.js +3 -1
- package/lib/server/index.d.ts +11 -0
- package/lib/server/index.js +395 -96
- package/lib/server/modules/order/index.d.ts +8 -0
- package/lib/server/modules/order/index.js +23 -0
- package/lib/server/utils/small-ticket.d.ts +9 -1
- package/lib/server/utils/small-ticket.js +107 -35
- package/lib/solution/BaseSales/index.d.ts +7 -0
- package/lib/solution/BaseSales/index.js +125 -28
- package/lib/solution/BookingByStep/index.d.ts +2 -2
- package/lib/solution/BookingTicket/index.d.ts +14 -19
- package/lib/solution/BookingTicket/index.js +70 -17
- package/lib/solution/Sales/index.d.ts +20 -4
- package/lib/solution/Sales/index.js +183 -15
- package/lib/solution/Sales/types.d.ts +10 -0
- package/lib/solution/UnifiedBookingSales/index.d.ts +5 -4
- package/lib/solution/UnifiedBookingSales/index.js +41 -15
- package/lib/solution/UnifiedBookingSales/types.d.ts +7 -1
- 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
|
-
|
|
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.
|
|
220
|
-
|
|
221
|
-
|
|
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
|
|
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
|
|
229
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
328
|
+
* 使用「最近一个 boundary.time」到「下一个 boundary.time」
|
|
166
329
|
* - 否则回退自然日(00:00 到次日 00:00)
|
|
167
330
|
*/
|
|
168
|
-
|
|
169
|
-
const
|
|
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
|
|
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
|
|
183
|
-
if (!
|
|
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
|
|
351
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
*/
|