@pisell/pisellos 2.3.78 → 2.3.80
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/modules/ProductList/index.d.ts +15 -2
- package/dist/modules/ProductList/index.js +168 -68
- package/dist/solution/BaseSales/index.d.ts +1 -1
- package/dist/solution/BookingByStep/index.d.ts +4 -4
- package/dist/solution/BookingTicket/index.d.ts +15 -3
- package/dist/solution/UnifiedBookingSales/index.js +231 -68
- package/dist/solution/UnifiedBookingSales/types.d.ts +4 -0
- package/lib/model/strategy/adapter/promotion/index.js +1 -46
- package/lib/modules/ProductList/index.d.ts +15 -2
- package/lib/modules/ProductList/index.js +73 -2
- package/lib/solution/BaseSales/index.d.ts +1 -1
- package/lib/solution/BookingByStep/index.d.ts +4 -4
- package/lib/solution/BookingTicket/index.d.ts +15 -3
- package/lib/solution/UnifiedBookingSales/index.js +166 -12
- package/lib/solution/UnifiedBookingSales/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -18,6 +18,19 @@ export declare class ProductList extends BaseModule implements Module {
|
|
|
18
18
|
* productList.updateOtherParams({ channel: 'pos' });
|
|
19
19
|
*/
|
|
20
20
|
updateOtherParams(params: Record<string, any>): void;
|
|
21
|
+
/**
|
|
22
|
+
* 判断当前商品查询是否会被本地 OS Server 接管。
|
|
23
|
+
*
|
|
24
|
+
* 不能只看请求参数中的 osServer=true:H5 RequestPlugin 会透传该参数,但仍然直接请求远端接口。
|
|
25
|
+
* 这里按 RequestPlugin 实际使用的 baseUrl 检查已注册路由,避免 OS Server 与客户端重复执行
|
|
26
|
+
* Data Variant 评估。
|
|
27
|
+
*/
|
|
28
|
+
private isProductQueryHandledByOsServer;
|
|
29
|
+
/**
|
|
30
|
+
* 未启用商品 OS 路由时,在客户端补做 Data Variant 解析。
|
|
31
|
+
* OS Server 已接管时直接信任服务端结果,确保同一批商品只评估一次。
|
|
32
|
+
*/
|
|
33
|
+
private resolveClientDataVariants;
|
|
21
34
|
storeChange(path?: string, value?: any): Promise<void>;
|
|
22
35
|
/**
|
|
23
36
|
* 获取加时商品列表。
|
|
@@ -25,11 +38,11 @@ export declare class ProductList extends BaseModule implements Module {
|
|
|
25
38
|
* @example
|
|
26
39
|
* const products = await productList.getAddTimeProducts({ schedule_date: '2026-06-16' });
|
|
27
40
|
*/
|
|
28
|
-
getAddTimeProducts(params?: ProductListLoadProductsParams): Promise<
|
|
41
|
+
getAddTimeProducts(params?: ProductListLoadProductsParams): Promise<ProductData[]>;
|
|
29
42
|
loadProducts({ category_ids, product_ids, collection, menu_list_ids, customer_id: paramsCustomerId, with_count, schedule_datetime, schedule_date, cacheId, with_schedule, extension_type, status, strategy_context, }?: ProductListLoadProductsParams, options?: {
|
|
30
43
|
callback?: (result: any) => void;
|
|
31
44
|
subscriberId?: string;
|
|
32
|
-
}): Promise<
|
|
45
|
+
}): Promise<ProductData[]>;
|
|
33
46
|
loadProductsPrice({ ids, customer_id, schedule_date, channel, }: {
|
|
34
47
|
ids?: number[];
|
|
35
48
|
customer_id?: number;
|
|
@@ -55,6 +55,58 @@ class ProductList extends _BaseModule.BaseModule {
|
|
|
55
55
|
updateOtherParams(params) {
|
|
56
56
|
this.otherParams = params || {};
|
|
57
57
|
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 判断当前商品查询是否会被本地 OS Server 接管。
|
|
61
|
+
*
|
|
62
|
+
* 不能只看请求参数中的 osServer=true:H5 RequestPlugin 会透传该参数,但仍然直接请求远端接口。
|
|
63
|
+
* 这里按 RequestPlugin 实际使用的 baseUrl 检查已注册路由,避免 OS Server 与客户端重复执行
|
|
64
|
+
* Data Variant 评估。
|
|
65
|
+
*/
|
|
66
|
+
isProductQueryHandledByOsServer() {
|
|
67
|
+
const server = this.core?.server;
|
|
68
|
+
if (!server || typeof server.hasRoute !== 'function') return false;
|
|
69
|
+
const requestBaseUrl = String(this.request?.baseUrl || '/shop').replace(/\/+$/, '');
|
|
70
|
+
const routePath = `${requestBaseUrl}/product/query`;
|
|
71
|
+
return server.hasRoute('post', routePath) === true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 未启用商品 OS 路由时,在客户端补做 Data Variant 解析。
|
|
76
|
+
* OS Server 已接管时直接信任服务端结果,确保同一批商品只评估一次。
|
|
77
|
+
*/
|
|
78
|
+
resolveClientDataVariants(products, queryPayload, handledByOsServer) {
|
|
79
|
+
if (handledByOsServer || products.length === 0) return products;
|
|
80
|
+
const evaluator = this.core?.context?.dataVariantEvaluator;
|
|
81
|
+
if (!evaluator || typeof evaluator.resolveProducts !== 'function') {
|
|
82
|
+
return products;
|
|
83
|
+
}
|
|
84
|
+
const strategyContext = queryPayload.strategy_context && typeof queryPayload.strategy_context === 'object' ? queryPayload.strategy_context : {};
|
|
85
|
+
const fatherModule = String(this.otherParams?.fatherModule || '').trim();
|
|
86
|
+
const scheduleModule = (fatherModule ? this.core.getModule(`${fatherModule}_schedule`) : null) || this.core.getModule('schedule');
|
|
87
|
+
const scheduleList = scheduleModule?.getScheduleList?.();
|
|
88
|
+
const contextScheduleList = this.core?.context?.scheduleList;
|
|
89
|
+
const contextMenuList = this.core?.context?.menuList;
|
|
90
|
+
const businessData = {
|
|
91
|
+
products,
|
|
92
|
+
scheduleDateTime: queryPayload.schedule_datetime || queryPayload.schedule_date,
|
|
93
|
+
scheduleList: Array.isArray(scheduleList) ? scheduleList : Array.isArray(contextScheduleList) ? contextScheduleList : [],
|
|
94
|
+
menuList: Array.isArray(this.otherParams?.menuList) ? this.otherParams.menuList : Array.isArray(contextMenuList) ? contextMenuList : [],
|
|
95
|
+
customerId: queryPayload.customer_id,
|
|
96
|
+
channel: strategyContext.channel,
|
|
97
|
+
orderType: strategyContext.orderType || strategyContext.order_type,
|
|
98
|
+
businessCode: strategyContext.business_code ?? strategyContext.businessCode,
|
|
99
|
+
availableWalletIds: strategyContext.available_wallet_ids,
|
|
100
|
+
custom: strategyContext
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
const result = evaluator.resolveProducts(businessData);
|
|
104
|
+
return Array.isArray(result?.products) ? result.products : products;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('[ProductList] Data Variant 客户端解析失败', error);
|
|
107
|
+
return products;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
58
110
|
async storeChange(path, value) {
|
|
59
111
|
// No longer needed - products are stored as plain objects instead of Product instances
|
|
60
112
|
}
|
|
@@ -149,13 +201,32 @@ class ProductList extends _BaseModule.BaseModule {
|
|
|
149
201
|
extension_type,
|
|
150
202
|
strategy_context
|
|
151
203
|
};
|
|
204
|
+
const handledByOsServer = this.isProductQueryHandledByOsServer();
|
|
205
|
+
const originalCallback = options?.callback;
|
|
206
|
+
const subscriptionCallback = typeof originalCallback === 'function' ? async result => {
|
|
207
|
+
const callbackList = result?.data?.list;
|
|
208
|
+
if (!Array.isArray(callbackList)) {
|
|
209
|
+
originalCallback(result);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const products = this.resolveClientDataVariants(callbackList, queryPayload, handledByOsServer);
|
|
213
|
+
await this.addProduct(products);
|
|
214
|
+
originalCallback(handledByOsServer ? result : {
|
|
215
|
+
...result,
|
|
216
|
+
data: {
|
|
217
|
+
...result.data,
|
|
218
|
+
list: products
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} : undefined;
|
|
152
222
|
const productsData = await this.request.post(`/product/query`, queryPayload, {
|
|
153
223
|
osServer: true,
|
|
154
|
-
callback:
|
|
224
|
+
callback: subscriptionCallback,
|
|
155
225
|
subscriberId: options?.subscriberId,
|
|
156
226
|
customToast: () => {}
|
|
157
227
|
});
|
|
158
|
-
const
|
|
228
|
+
const resolvedList = this.resolveClientDataVariants(productsData.data.list || [], queryPayload, handledByOsServer);
|
|
229
|
+
const sortedList = resolvedList.slice().sort((a, b) => Number(b.sort) - Number(a.sort));
|
|
159
230
|
// if (sortedList.length) {
|
|
160
231
|
// sortedList.forEach((n: any) => {
|
|
161
232
|
// if (n.is_eject !== 1 && n['schedule.ids'] && n['schedule.ids'].length) {
|
|
@@ -495,7 +495,7 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
|
|
|
495
495
|
getUnfulfilledPromotions(): OrderUnfulfilledPromotion[];
|
|
496
496
|
/** 最近一次促销计算输出的赠品操作 diff。 */
|
|
497
497
|
getLastGiftActions(): OrderLastGiftActions | null;
|
|
498
|
-
getProductList(): Promise<
|
|
498
|
+
getProductList(): Promise<import("../..").ProductData[] | undefined>;
|
|
499
499
|
getOtherParams(): Record<string, any>;
|
|
500
500
|
setOtherParams(params: Record<string, any>, { cover }?: {
|
|
501
501
|
cover?: boolean;
|
|
@@ -67,7 +67,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
67
67
|
product_ids?: number[];
|
|
68
68
|
collection?: number | string[];
|
|
69
69
|
schedule_date?: string;
|
|
70
|
-
}): Promise<
|
|
70
|
+
}): Promise<ProductData[]>;
|
|
71
71
|
/**
|
|
72
72
|
* 通过 schedule 来读取商品,适用于 session 类商品
|
|
73
73
|
*
|
|
@@ -87,7 +87,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
87
87
|
date: string;
|
|
88
88
|
product_ids?: number[];
|
|
89
89
|
category_ids?: number[];
|
|
90
|
-
}): Promise<
|
|
90
|
+
}): Promise<ProductData[]>;
|
|
91
91
|
/**
|
|
92
92
|
* 更新完商品数据、切换日期、或者在较后的流程里登录了,检测当前购物车里是否有商品,如果有,则需要更新购物车里的商品价格
|
|
93
93
|
*
|
|
@@ -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 | 1 |
|
|
329
|
+
weekNum: 0 | 1 | 5 | 2 | 3 | 4 | 6;
|
|
330
330
|
}[]>;
|
|
331
331
|
submitTimeSlot(timeSlots: TimeSliceItem): void;
|
|
332
332
|
private getScheduleDataByIds;
|
|
@@ -345,7 +345,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
|
|
|
345
345
|
count: number;
|
|
346
346
|
left: number;
|
|
347
347
|
summaryCount: number;
|
|
348
|
-
status: "
|
|
348
|
+
status: "lots_of_space" | "filling_up_fast" | "sold_out";
|
|
349
349
|
}[];
|
|
350
350
|
/**
|
|
351
351
|
* 找到多个资源的公共可用时间段
|
|
@@ -128,7 +128,7 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
|
|
|
128
128
|
loadProducts(params?: ILoadProductsParams, options?: {
|
|
129
129
|
callback?: (result: any) => void;
|
|
130
130
|
subscriberId?: string;
|
|
131
|
-
}): Promise<
|
|
131
|
+
}): Promise<ProductData[]>;
|
|
132
132
|
/**
|
|
133
133
|
* 获取加时商品列表。
|
|
134
134
|
*
|
|
@@ -334,7 +334,7 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
|
|
|
334
334
|
* 获取当前的客户搜索条件
|
|
335
335
|
* @returns 当前搜索条件
|
|
336
336
|
*/
|
|
337
|
-
getCurrentCustomerSearchParams(): Omit<import("../../modules").ShopGetCustomerListParams, "
|
|
337
|
+
getCurrentCustomerSearchParams(): Omit<import("../../modules").ShopGetCustomerListParams, "num" | "skip">;
|
|
338
338
|
/**
|
|
339
339
|
* 获取客户列表状态(包含滚动加载相关状态)
|
|
340
340
|
* @returns 客户状态
|
|
@@ -453,7 +453,19 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
|
|
|
453
453
|
* 加车两阶段主决策(规格弹窗 / 资源编辑 / 直接加车)。
|
|
454
454
|
* 与 ticketBooking `handleSelectProduct` + `handleBooking4Service` 等价。
|
|
455
455
|
*/
|
|
456
|
-
decideAddProduct(item: any, options?: Partial<AddProductDecideContext>):
|
|
456
|
+
decideAddProduct(item: any, options?: Partial<AddProductDecideContext>): {
|
|
457
|
+
action: "reloadCatalog";
|
|
458
|
+
} | {
|
|
459
|
+
action: "add";
|
|
460
|
+
cacheItem: any;
|
|
461
|
+
} | {
|
|
462
|
+
action: "requiresDetail";
|
|
463
|
+
payload: AddProductRequiresDetailPayload;
|
|
464
|
+
} | {
|
|
465
|
+
action: "requiresBookingEdit";
|
|
466
|
+
cacheItem: any;
|
|
467
|
+
payload: import("./utils/addProductDecision").AddProductRequiresBookingEditPayload;
|
|
468
|
+
};
|
|
457
469
|
/** 规格弹窗 callback 后的第二段决策。 */
|
|
458
470
|
decideAfterDetail(cacheItem: any, options?: Partial<AddProductDecideContext>): import("./utils/addProductDecision").AddProductDecision;
|
|
459
471
|
/**
|
|
@@ -980,6 +980,125 @@ function buildBookingFromAvailabilitySelection(params) {
|
|
|
980
980
|
}
|
|
981
981
|
};
|
|
982
982
|
}
|
|
983
|
+
const POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES = new Set(['past', 'booking_cutoff', 'resource_full', 'resource_unavailable', 'resource_capacity_exceeded',
|
|
984
|
+
// A required group whose selected resource is outside its window is surfaced
|
|
985
|
+
// as missing_resource at group level. Structural validation below still
|
|
986
|
+
// rejects a genuinely missing selection.
|
|
987
|
+
'missing_resource']);
|
|
988
|
+
function matchesForcedSelectionGroup(selection, group) {
|
|
989
|
+
const hasGroupId = typeof selection.requirementGroupId === 'string' && selection.requirementGroupId.trim().length > 0;
|
|
990
|
+
const hasFormId = selection.formId !== undefined && selection.formId !== null;
|
|
991
|
+
if (!hasGroupId && !hasFormId) return false;
|
|
992
|
+
if (hasGroupId && selection.requirementGroupId !== group.id) return false;
|
|
993
|
+
if (hasFormId && !sameAvailabilityId(selection.formId, group.formId)) return false;
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* POS may intentionally overbook a configured time/resource. Rebuild the
|
|
999
|
+
* assignments from the submitted, structurally valid resource identities so
|
|
1000
|
+
* a full/unavailable option is not silently dropped by normal availability
|
|
1001
|
+
* evaluation. This deliberately ignores capacity, occupancy and resource
|
|
1002
|
+
* windows, while retaining product/group/resource identity and selection
|
|
1003
|
+
* cardinality checks.
|
|
1004
|
+
*/
|
|
1005
|
+
function buildPosForcedAvailabilityAssignments(params) {
|
|
1006
|
+
const {
|
|
1007
|
+
product,
|
|
1008
|
+
resources,
|
|
1009
|
+
candidate,
|
|
1010
|
+
requirementSelections,
|
|
1011
|
+
partySize
|
|
1012
|
+
} = params;
|
|
1013
|
+
const groups = product.requirementGroups || [];
|
|
1014
|
+
const conflicts = [];
|
|
1015
|
+
const assignments = [];
|
|
1016
|
+
const selectionsByGroup = new Map();
|
|
1017
|
+
const addConflict = (code, message, details = {}) => conflicts.push({
|
|
1018
|
+
code,
|
|
1019
|
+
message,
|
|
1020
|
+
productId: candidate.productId,
|
|
1021
|
+
candidateKey: candidate.key,
|
|
1022
|
+
...details
|
|
1023
|
+
});
|
|
1024
|
+
requirementSelections.forEach(selection => {
|
|
1025
|
+
const matchedGroups = groups.filter(group => matchesForcedSelectionGroup(selection, group));
|
|
1026
|
+
if (matchedGroups.length !== 1) {
|
|
1027
|
+
addConflict('invalid_requirement_group', '选择的资源组不属于当前商品', {
|
|
1028
|
+
requirementGroupId: selection.requirementGroupId,
|
|
1029
|
+
formId: selection.formId
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const group = matchedGroups[0];
|
|
1034
|
+
if (selectionsByGroup.has(group.id)) {
|
|
1035
|
+
addConflict('invalid_selection_count', `资源组 ${group.id} 被重复提交`, {
|
|
1036
|
+
requirementGroupId: group.id,
|
|
1037
|
+
formId: group.formId
|
|
1038
|
+
});
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
selectionsByGroup.set(group.id, selection);
|
|
1042
|
+
if (!Array.isArray(selection.resourceIds)) {
|
|
1043
|
+
addConflict('invalid_selection_count', `资源组 ${group.id} 的资源选择必须是数组`, {
|
|
1044
|
+
requirementGroupId: group.id,
|
|
1045
|
+
formId: group.formId
|
|
1046
|
+
});
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
const selectedResourceIds = Array.from(new Map(selection.resourceIds.map(resourceId => [String(resourceId), resourceId])).values());
|
|
1050
|
+
const required = group.required !== false;
|
|
1051
|
+
const min = Math.max(required ? 1 : 0, Number(group.min ?? (required ? 1 : 0)) || 0);
|
|
1052
|
+
const max = Math.max(min, Number(group.max ?? Math.max(min, 1)) || Math.max(min, 1));
|
|
1053
|
+
if (selectedResourceIds.length < min) {
|
|
1054
|
+
addConflict('missing_resource', `资源组 ${group.id} 缺少已选资源`, {
|
|
1055
|
+
requirementGroupId: group.id,
|
|
1056
|
+
formId: group.formId
|
|
1057
|
+
});
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
if (selectedResourceIds.length > max) {
|
|
1061
|
+
addConflict('invalid_selection_count', `资源组 ${group.id} 已选资源数量超出上限`, {
|
|
1062
|
+
requirementGroupId: group.id,
|
|
1063
|
+
formId: group.formId
|
|
1064
|
+
});
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
selectedResourceIds.forEach(resourceId => {
|
|
1068
|
+
const resource = resources.find(item => sameAvailabilityId(item.id, resourceId));
|
|
1069
|
+
const belongsToGroup = Boolean(resource && !(group.formId !== undefined && resource.formId !== undefined && !sameAvailabilityId(group.formId, resource.formId)) && !(group.resourceIds?.length && !group.resourceIds.some(id => sameAvailabilityId(id, resourceId))));
|
|
1070
|
+
if (!resource || !belongsToGroup) {
|
|
1071
|
+
addConflict('resource_unavailable', `资源 ${String(resourceId)} 不属于当前商品资源组`, {
|
|
1072
|
+
requirementGroupId: group.id,
|
|
1073
|
+
formId: group.formId,
|
|
1074
|
+
resourceId
|
|
1075
|
+
});
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
assignments.push({
|
|
1079
|
+
productId: product.id,
|
|
1080
|
+
resourceId,
|
|
1081
|
+
formId: group.formId ?? resource.formId,
|
|
1082
|
+
startAt: candidate.bookingStartAt || candidate.startAt,
|
|
1083
|
+
endAt: candidate.bookingEndAt || candidate.endAt,
|
|
1084
|
+
capacityRequired: Math.max(1, partySize),
|
|
1085
|
+
scheduleId: candidate.primaryScheduleRef?.scheduleId,
|
|
1086
|
+
timeSlotId: candidate.primaryScheduleRef?.timeSlotId
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
});
|
|
1090
|
+
groups.filter(group => group.required !== false).forEach(group => {
|
|
1091
|
+
if (selectionsByGroup.has(group.id)) return;
|
|
1092
|
+
addConflict('missing_resource', `资源组 ${group.id} 缺少已选资源`, {
|
|
1093
|
+
requirementGroupId: group.id,
|
|
1094
|
+
formId: group.formId
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
return {
|
|
1098
|
+
assignments,
|
|
1099
|
+
conflicts
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
983
1102
|
class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
984
1103
|
defaultName = 'unifiedBookingSales';
|
|
985
1104
|
defaultVersion = '1.0.0';
|
|
@@ -1529,7 +1648,11 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
1529
1648
|
if (!normalizedPolicyId) {
|
|
1530
1649
|
throw new Error('[UnifiedBookingSales] getProtocol 需要 policyId');
|
|
1531
1650
|
}
|
|
1532
|
-
|
|
1651
|
+
let policyUrl = 'shop-policy';
|
|
1652
|
+
if (this.otherParams.platform === 'kiosk' || this.otherParams.platform === 'pos' || this.otherParams.platform === 'shop') {
|
|
1653
|
+
policyUrl = 'policy';
|
|
1654
|
+
}
|
|
1655
|
+
return this.request.get(`/${policyUrl}/${encodeURIComponent(normalizedPolicyId)}`);
|
|
1533
1656
|
}
|
|
1534
1657
|
|
|
1535
1658
|
/**
|
|
@@ -1721,6 +1844,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
1721
1844
|
}
|
|
1722
1845
|
buildUnifiedOrderProduct(sourceProduct, ownershipKey, product) {
|
|
1723
1846
|
const normalizedProductRaw = product?.raw || {};
|
|
1847
|
+
const quotationShelfId = sourceProduct._extend?.quotation_shelf_id ?? sourceProduct.quotation_shelf_id ?? sourceProduct.metadata?.quotation_shelf_id;
|
|
1724
1848
|
const orderProduct = this.transformBaseProductToOrderProduct({
|
|
1725
1849
|
payload: {
|
|
1726
1850
|
...sourceProduct,
|
|
@@ -1737,11 +1861,14 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
1737
1861
|
sourceProduct
|
|
1738
1862
|
});
|
|
1739
1863
|
if (orderProduct && typeof orderProduct === 'object') {
|
|
1740
|
-
// BaseSales may rebuild metadata from its price source. Stamp ownership
|
|
1741
|
-
//
|
|
1864
|
+
// BaseSales may rebuild metadata from its price source. Stamp ownership and the selected
|
|
1865
|
+
// quotation shelf after that transform so neither booking adds nor edits lose quote identity.
|
|
1742
1866
|
orderProduct.metadata = {
|
|
1743
1867
|
...(orderProduct.metadata || {}),
|
|
1744
|
-
[ownershipKey]: true
|
|
1868
|
+
[ownershipKey]: true,
|
|
1869
|
+
...(quotationShelfId !== undefined ? {
|
|
1870
|
+
quotation_shelf_id: quotationShelfId
|
|
1871
|
+
} : {})
|
|
1745
1872
|
};
|
|
1746
1873
|
}
|
|
1747
1874
|
return orderProduct;
|
|
@@ -2901,12 +3028,39 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2901
3028
|
refreshedSnapshotId: refreshedProjection.meta.snapshotId
|
|
2902
3029
|
});
|
|
2903
3030
|
}
|
|
2904
|
-
const
|
|
3031
|
+
const product = refreshedProjection.products.find(item => sameAvailabilityId(item.id, refreshedCandidateBase.productId));
|
|
3032
|
+
if (!product) {
|
|
3033
|
+
throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '缓存重校验后商品不在 projection 中', {
|
|
3034
|
+
productId: refreshedCandidateBase.productId
|
|
3035
|
+
});
|
|
3036
|
+
}
|
|
3037
|
+
let validation = (0, _ResourcePlanner.validateAvailabilitySelection)(refreshedProjection, {
|
|
2905
3038
|
candidate: refreshedCandidateBase,
|
|
2906
3039
|
requirementSelections: params.requirementSelections,
|
|
2907
3040
|
partySize,
|
|
2908
3041
|
bookingCount
|
|
2909
3042
|
});
|
|
3043
|
+
const allowPosAvailabilityOverride = String(this.otherParams?.platform ?? '').trim().toLowerCase() === 'pos';
|
|
3044
|
+
if (allowPosAvailabilityOverride && !validation.ok && validation.conflicts.every(conflict => POS_OVERRIDABLE_AVAILABILITY_CONFLICT_CODES.has(conflict.code))) {
|
|
3045
|
+
const forced = buildPosForcedAvailabilityAssignments({
|
|
3046
|
+
product,
|
|
3047
|
+
resources: refreshedProjection.resources,
|
|
3048
|
+
candidate: refreshedCandidateBase,
|
|
3049
|
+
requirementSelections: params.requirementSelections,
|
|
3050
|
+
partySize
|
|
3051
|
+
});
|
|
3052
|
+
validation = forced.conflicts.length > 0 ? {
|
|
3053
|
+
...validation,
|
|
3054
|
+
assignments: [],
|
|
3055
|
+
conflicts: forced.conflicts
|
|
3056
|
+
} : {
|
|
3057
|
+
...validation,
|
|
3058
|
+
ok: true,
|
|
3059
|
+
status: validation.availability.status,
|
|
3060
|
+
assignments: forced.assignments,
|
|
3061
|
+
conflicts: []
|
|
3062
|
+
};
|
|
3063
|
+
}
|
|
2910
3064
|
if (!validation.ok) {
|
|
2911
3065
|
const capacityConflict = validation.conflicts.find(conflict => conflict.code === 'resource_capacity_exceeded');
|
|
2912
3066
|
throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', capacityConflict?.message || '缓存重校验后资源已不可提交', {
|
|
@@ -2944,12 +3098,6 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
2944
3098
|
expectedOrderRevision: revalidationEntry.orderRevision
|
|
2945
3099
|
};
|
|
2946
3100
|
const refreshedCandidate = this.decorateAvailabilityCandidate(normalizedContextId, revalidatedContext.activeVersion, refreshedCandidateBase);
|
|
2947
|
-
const product = refreshedProjection.products.find(item => sameAvailabilityId(item.id, refreshedCandidate.productId));
|
|
2948
|
-
if (!product) {
|
|
2949
|
-
throw new _ResourcePlanner.AvailabilityError('REVALIDATION_FAILED', '缓存重校验后商品不在 projection 中', {
|
|
2950
|
-
productId: refreshedCandidate.productId
|
|
2951
|
-
});
|
|
2952
|
-
}
|
|
2953
3101
|
const existingBooking = editingLine ? this.findLinkedBooking(editingLine) : undefined;
|
|
2954
3102
|
const existingBookingUid = existingBooking ? getBookingUid(existingBooking) : '';
|
|
2955
3103
|
if (params.mode === 'edit' && !existingBookingUid) {
|
|
@@ -3004,7 +3152,13 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
|
|
|
3004
3152
|
product_variant_id: transformed.product_variant_id ?? editingLine.product_variant_id ?? 0,
|
|
3005
3153
|
unique_identification_number: params.productLineUid,
|
|
3006
3154
|
updates: transformed,
|
|
3007
|
-
booking
|
|
3155
|
+
booking,
|
|
3156
|
+
...(params.allowBookingReprice !== undefined ? {
|
|
3157
|
+
allow_booking_reprice: params.allowBookingReprice
|
|
3158
|
+
} : {}),
|
|
3159
|
+
...(params.allowBookingDiscountReapply !== undefined ? {
|
|
3160
|
+
allow_booking_discount_reapply: params.allowBookingDiscountReapply
|
|
3161
|
+
} : {})
|
|
3008
3162
|
});
|
|
3009
3163
|
productLineUid = params.productLineUid;
|
|
3010
3164
|
bookingUid = existingBookingUid;
|
|
@@ -151,6 +151,10 @@ export interface ContextualCommitAvailabilitySelectionParams {
|
|
|
151
151
|
requirementSelections: AvailabilityRequirementSelection[];
|
|
152
152
|
partySize: number;
|
|
153
153
|
bookingCount?: number;
|
|
154
|
+
/** 编辑预约时,允许本次显式报价覆盖已落库订单行的历史价格。 */
|
|
155
|
+
allowBookingReprice?: boolean;
|
|
156
|
+
/** 编辑预约重报价后,允许订单模块重新应用已选编辑态折扣。 */
|
|
157
|
+
allowBookingDiscountReapply?: boolean;
|
|
154
158
|
}
|
|
155
159
|
export type UnifiedBookingSalesCommitAvailabilitySelectionParams = ContextualCommitAvailabilitySelectionParams;
|
|
156
160
|
export interface UnifiedBookingSalesCommitAvailabilitySelectionResult {
|