@pisell/pisellos 0.10.42 → 0.10.44

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 (101) hide show
  1. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  2. package/dist/model/strategy/adapter/itemRule/adapter.js +135 -35
  3. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  4. package/dist/model/strategy/adapter/itemRule/evaluator.js +25 -1
  5. package/dist/model/strategy/adapter/itemRule/type.d.ts +44 -0
  6. package/dist/model/strategy/adapter/itemRule/type.js +19 -1
  7. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  8. package/dist/modules/Order/index.d.ts +40 -8
  9. package/dist/modules/Order/index.js +1438 -878
  10. package/dist/modules/Order/salesNotes.d.ts +31 -0
  11. package/dist/modules/Order/salesNotes.js +492 -0
  12. package/dist/modules/Order/types.d.ts +105 -8
  13. package/dist/modules/Order/types.js +6 -1
  14. package/dist/modules/Order/utils.d.ts +3 -1
  15. package/dist/modules/Order/utils.js +191 -71
  16. package/dist/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  17. package/dist/modules/Payment/cashRecommendationAlgorithm.js +17 -367
  18. package/dist/modules/ProductList/index.d.ts +2 -1
  19. package/dist/modules/ProductList/index.js +100 -20
  20. package/dist/modules/ProductList/types.d.ts +10 -0
  21. package/dist/modules/Rules/index.js +2 -1
  22. package/dist/server/index.d.ts +7 -0
  23. package/dist/server/index.js +45 -3
  24. package/dist/server/modules/order/index.js +6 -0
  25. package/dist/server/modules/order/types.d.ts +9 -0
  26. package/dist/server/utils/small-ticket.d.ts +1 -0
  27. package/dist/server/utils/small-ticket.js +142 -14
  28. package/dist/solution/BaseSales/index.d.ts +15 -1
  29. package/dist/solution/BaseSales/index.js +1240 -877
  30. package/dist/solution/BookingByStep/index.d.ts +1 -1
  31. package/dist/solution/BookingTicket/index.d.ts +1 -0
  32. package/dist/solution/BookingTicket/index.js +37 -16
  33. package/dist/solution/BookingTicket/types.d.ts +5 -1
  34. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  35. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +171 -96
  36. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +1 -1
  37. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +2 -0
  38. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +12 -3
  39. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  40. package/dist/solution/RegisterAndLogin/config.js +95 -40
  41. package/dist/solution/RegisterAndLogin/index.js +4 -1
  42. package/dist/solution/ScanOrder/index.d.ts +1 -0
  43. package/dist/solution/ScanOrder/index.js +31 -27
  44. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  45. package/dist/solution/ScanOrder/utils.js +2 -0
  46. package/dist/solution/UnifiedBookingSales/index.d.ts +15 -2
  47. package/dist/solution/UnifiedBookingSales/index.js +777 -510
  48. package/dist/solution/UnifiedBookingSales/types.d.ts +14 -1
  49. package/dist/solution/VenueBooking/index.d.ts +1 -0
  50. package/dist/solution/VenueBooking/index.js +8 -4
  51. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  52. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  53. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  54. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  55. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  56. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  57. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  58. package/lib/modules/Order/index.d.ts +40 -8
  59. package/lib/modules/Order/index.js +434 -61
  60. package/lib/modules/Order/salesNotes.d.ts +31 -0
  61. package/lib/modules/Order/salesNotes.js +382 -0
  62. package/lib/modules/Order/types.d.ts +105 -8
  63. package/lib/modules/Order/types.js +5 -1
  64. package/lib/modules/Order/utils.d.ts +3 -1
  65. package/lib/modules/Order/utils.js +114 -1
  66. package/lib/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  67. package/lib/modules/Payment/cashRecommendationAlgorithm.js +9 -354
  68. package/lib/modules/ProductList/index.d.ts +2 -1
  69. package/lib/modules/ProductList/index.js +47 -2
  70. package/lib/modules/ProductList/types.d.ts +10 -0
  71. package/lib/modules/Rules/index.js +2 -1
  72. package/lib/server/index.d.ts +7 -0
  73. package/lib/server/index.js +39 -3
  74. package/lib/server/modules/order/index.js +6 -0
  75. package/lib/server/modules/order/types.d.ts +9 -0
  76. package/lib/server/utils/small-ticket.d.ts +1 -0
  77. package/lib/server/utils/small-ticket.js +136 -12
  78. package/lib/solution/BaseSales/index.d.ts +15 -1
  79. package/lib/solution/BaseSales/index.js +192 -22
  80. package/lib/solution/BookingByStep/index.d.ts +1 -1
  81. package/lib/solution/BookingTicket/index.d.ts +1 -0
  82. package/lib/solution/BookingTicket/index.js +23 -5
  83. package/lib/solution/BookingTicket/types.d.ts +5 -1
  84. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  85. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +65 -18
  86. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +1 -1
  87. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +2 -0
  88. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +5 -1
  89. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  90. package/lib/solution/RegisterAndLogin/config.js +49 -8
  91. package/lib/solution/RegisterAndLogin/index.js +4 -1
  92. package/lib/solution/ScanOrder/index.d.ts +1 -0
  93. package/lib/solution/ScanOrder/index.js +5 -1
  94. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  95. package/lib/solution/ScanOrder/utils.js +1 -0
  96. package/lib/solution/UnifiedBookingSales/index.d.ts +15 -2
  97. package/lib/solution/UnifiedBookingSales/index.js +125 -1
  98. package/lib/solution/UnifiedBookingSales/types.d.ts +14 -1
  99. package/lib/solution/VenueBooking/index.d.ts +1 -0
  100. package/lib/solution/VenueBooking/index.js +5 -1
  101. package/package.json +1 -1
@@ -106,16 +106,12 @@ async function applyGlobalScan(host, formatted, bridge) {
106
106
  data,
107
107
  scanCode,
108
108
  customerId: formattedCustomerId,
109
- isCrossShopWallet
109
+ isCrossShopWallet,
110
+ isWalletPassUnavailableAtStore
110
111
  } = formatted;
111
112
  if (searchType === 'wallet') {
112
113
  const currentCustomer = host.getOrderCustomerSnapshot();
113
- if (getRealCustomerId(currentCustomer) !== undefined) {
114
- return {
115
- status: 'success',
116
- kind: 'customer'
117
- };
118
- }
114
+ const currentCustomerId = getRealCustomerId(currentCustomer);
119
115
  let customer = data;
120
116
  if (isCrossShopWallet) {
121
117
  const sourceCustomerId = getRealCustomerId({
@@ -152,6 +148,36 @@ async function applyGlobalScan(host, formatted, bridge) {
152
148
  ...customer,
153
149
  id: customerId
154
150
  } : customer;
151
+ if (currentCustomerId === customerId) {
152
+ return {
153
+ status: 'success',
154
+ kind: 'customer'
155
+ };
156
+ }
157
+ if (currentCustomerId !== undefined) {
158
+ // 未接入新桥接的宿主保持原行为:已有客户时不直接覆盖。
159
+ if (!bridge?.requestCustomerSwitch) {
160
+ return {
161
+ status: 'success',
162
+ kind: 'customer'
163
+ };
164
+ }
165
+ try {
166
+ const switched = await bridge.requestCustomerSwitch(orderCustomer);
167
+ return switched ? {
168
+ status: 'success',
169
+ kind: 'customer'
170
+ } : {
171
+ status: 'cancelled'
172
+ };
173
+ } catch (error) {
174
+ bridge.onNotify?.('fail', getErrorMessage(error) || 'pisell2.text.scan-global-failed');
175
+ return {
176
+ status: 'failed',
177
+ reason: 'invalid_data'
178
+ };
179
+ }
180
+ }
155
181
  host.setOrderCustomer(orderCustomer);
156
182
  bridge?.onNotify?.('success', 'pisell2.text.add-client-success');
157
183
  return {
@@ -160,13 +186,20 @@ async function applyGlobalScan(host, formatted, bridge) {
160
186
  };
161
187
  }
162
188
  if (searchType === 'walletPass') {
189
+ if (isWalletPassUnavailableAtStore) {
190
+ bridge?.onNotify?.('fail', 'pisell2.text.wallet-pass-not-available-at-store');
191
+ return {
192
+ status: 'failed',
193
+ reason: 'invalid_data'
194
+ };
195
+ }
163
196
  const snapshot = host.getOrderCustomerSnapshot();
164
- const currentCustomerIsWalkIn = getRealCustomerId(snapshot) === undefined;
165
197
  let activeCustomer = snapshot;
166
198
  const bindWalletPassCustomer = async asset => {
167
- if (!currentCustomerIsWalkIn) {
199
+ if (getRealCustomerId(activeCustomer) !== undefined) {
168
200
  return {
169
- resolveFailed: false
201
+ resolveFailed: false,
202
+ customerBound: false
170
203
  };
171
204
  }
172
205
  const assetCustomerId = getWalletAssetCustomerId(asset) ?? getRealCustomerId({
@@ -177,7 +210,8 @@ async function applyGlobalScan(host, formatted, bridge) {
177
210
  if (!host.resolveCustomerById) {
178
211
  bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
179
212
  return {
180
- resolveFailed: true
213
+ resolveFailed: true,
214
+ customerBound: false
181
215
  };
182
216
  }
183
217
  try {
@@ -186,7 +220,8 @@ async function applyGlobalScan(host, formatted, bridge) {
186
220
  console.warn('[GlobalScan] Wallet asset customer lookup failed', error);
187
221
  bridge?.onNotify?.('fail', getErrorMessage(error) || 'pisell2.text.scan-global-failed');
188
222
  return {
189
- resolveFailed: true
223
+ resolveFailed: true,
224
+ customerBound: false
190
225
  };
191
226
  }
192
227
  }
@@ -196,7 +231,8 @@ async function applyGlobalScan(host, formatted, bridge) {
196
231
  bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
197
232
  }
198
233
  return {
199
- resolveFailed: assetCustomerId !== undefined
234
+ resolveFailed: assetCustomerId !== undefined,
235
+ customerBound: false
200
236
  };
201
237
  }
202
238
  const orderCustomer = customer.id === undefined || customer.id === null ? {
@@ -210,7 +246,8 @@ async function applyGlobalScan(host, formatted, bridge) {
210
246
  retainSearchResultCodes: [scanCode]
211
247
  });
212
248
  return {
213
- resolveFailed: false
249
+ resolveFailed: false,
250
+ customerBound: true
214
251
  };
215
252
  };
216
253
  const trySelectWalletAsset = async () => {
@@ -242,24 +279,34 @@ async function applyGlobalScan(host, formatted, bridge) {
242
279
  reason: 'invalid_data'
243
280
  };
244
281
  };
282
+
283
+ // batch-search 可能返回来源门店客户;先映射到当前门店,确保 Wallet
284
+ // 支付资产查询使用本店客户上下文。
285
+ const bindResult = await bindWalletPassCustomer();
245
286
  const walletSelection = await trySelectWalletAsset();
246
287
  if (walletSelection) return walletSelection;
247
-
248
- // 非支付类 Wallet Pass 仍可能由 batch-search 返回所属客户。
249
- await bindWalletPassCustomer();
250
288
  if (!bridge?.applyPromotionCode) {
251
289
  return {
252
290
  status: 'failed',
253
291
  reason: 'no_bridge'
254
292
  };
255
293
  }
256
- const promotionResult = await bridge.applyPromotionCode(scanCode, getRealCustomerId(activeCustomer));
294
+ const promotionCustomerId = getRealCustomerId(activeCustomer);
295
+ const promotionResult = bindResult.customerBound ? await bridge.applyPromotionCode(scanCode, promotionCustomerId, {
296
+ suppressFailureNotification: true
297
+ }) : await bridge.applyPromotionCode(scanCode, promotionCustomerId);
257
298
  if (promotionResult?.isAccepted ?? promotionResult?.isAvailable) {
258
299
  return {
259
300
  status: 'success',
260
301
  kind: 'promotion'
261
302
  };
262
303
  }
304
+ if (bindResult.customerBound) {
305
+ return {
306
+ status: 'success',
307
+ kind: 'customer'
308
+ };
309
+ }
263
310
  return {
264
311
  status: 'failed',
265
312
  reason: 'invalid_data'
@@ -15,7 +15,7 @@ const searchWalletPass = async (request, code) => {
15
15
  relation_product: 1,
16
16
  // 组装关联商品: 0不处理 1处理
17
17
  with_franchisor_data: 1,
18
- with: ['customer'],
18
+ with: ['customer', 'franchiseeWalletPass:shop_id,source_shop_id,product_id,use'],
19
19
  tags: ['point_card',
20
20
  // 积分卡
21
21
  'gift_card',
@@ -10,6 +10,8 @@ export interface GlobalScanFormattedResult {
10
10
  customerId?: string | number;
11
11
  /** 仅 Wallet 返回;true 时禁止直接使用响应内的来源客户。 */
12
12
  isCrossShopWallet?: boolean;
13
+ /** 总店 Wallet Pass 未共享给当前子店。 */
14
+ isWalletPassUnavailableAtStore?: boolean;
13
15
  }
14
16
  interface RawScanSearchResult {
15
17
  searchType?: GlobalScanSearchType;
@@ -43,11 +43,15 @@ function formatGlobalScanResult(raw, scanCode) {
43
43
  }
44
44
  if (searchType === 'walletPass') {
45
45
  const firstResult = resultData?.[0];
46
+ const isWalletPassUnavailableAtStore = firstResult?.is_franchisor_pass === 1 && firstResult?.franchisee_wallet_pass?.use !== 'enabled';
46
47
  return {
47
48
  searchType,
48
49
  data: firstResult?.customer,
49
50
  scanCode,
50
- customerId: firstResult?.customer_id ?? firstResult?.holder?.customer_id ?? firstResult?.customer?.id ?? firstResult?.customer?.customer_id
51
+ customerId: firstResult?.customer_id ?? firstResult?.holder?.customer_id ?? firstResult?.customer?.id ?? firstResult?.customer?.customer_id,
52
+ ...(isWalletPassUnavailableAtStore ? {
53
+ isWalletPassUnavailableAtStore: true
54
+ } : {})
51
55
  };
52
56
  }
53
57
  if (searchType === 'product' || searchType === 'local_product') {
@@ -10,7 +10,11 @@ export interface ApiConfig {
10
10
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
11
11
  transformParams?: (params: any) => any;
12
12
  transformResponse?: (response: any) => any;
13
- options?: Record<string, any>;
13
+ options?: Record<string, any> | ((params: any) => Record<string, any>);
14
+ }
15
+ export interface ApiCallerOptions {
16
+ baseUrl?: string;
17
+ shopDomain?: string;
14
18
  }
15
19
  /**
16
20
  * 渠道配置接口
@@ -65,12 +69,14 @@ export declare function getChannelConfig(channel: string): ChannelConfig;
65
69
  export declare class ApiCaller {
66
70
  private request;
67
71
  private config;
72
+ private options;
68
73
  constructor(request: {
69
74
  get: <T = any>(url: string, data?: any, options?: any) => Promise<T>;
70
75
  post: <T = any>(url: string, data?: any, options?: any) => Promise<T>;
71
76
  put: <T = any>(url: string, data?: any, options?: any) => Promise<T>;
72
77
  delete: <T = any>(url: string, options?: any) => Promise<T>;
73
- }, config: ChannelConfig);
78
+ }, config: ChannelConfig, options?: ApiCallerOptions);
79
+ private resolveUrl;
74
80
  /**
75
81
  * 执行 API 调用
76
82
  */
@@ -84,4 +90,4 @@ export declare function createApiCaller(request: {
84
90
  post: <T = any>(url: string, data?: any, options?: any) => Promise<T>;
85
91
  put: <T = any>(url: string, data?: any, options?: any) => Promise<T>;
86
92
  delete: <T = any>(url: string, options?: any) => Promise<T>;
87
- }, channel: string): ApiCaller;
93
+ }, channel: string, options?: ApiCallerOptions): ApiCaller;
@@ -15,6 +15,8 @@ exports.getChannelConfig = getChannelConfig;
15
15
  * API 配置接口
16
16
  */
17
17
 
18
+ const h5CustomerAuthApiNames = new Set(['sendEmailVerificationCode', 'sendEmailLoginCode', 'sendSmsRegisterCode', 'sendEmailRegisterLink', 'verifyEmailRegistrationLink', 'emailPasswordLogin', 'emailCodeRegister', 'phoneCodeRegister', 'resendEmailRegisterLink', 'checkEmailLinkCode', 'sendSmsLoginCode', 'phoneCodeLogin', 'guestLogin', 'checkEmailExists', 'checkEmailCode', 'checkMobileCode', 'phonePasswordLogin', 'sendPasswordResetEmail', 'sendPasswordResetSms', 'sendResetPasswordLink', 'checkResetPasswordCode', 'resetPasswordByCode', 'resetPasswordByEmail', 'resetPasswordByPhone', 'verifyCode', 'register', 'login', 'oauthLogin', 'facebookLogin', 'appleLogin']);
19
+
18
20
  /**
19
21
  * 渠道配置接口
20
22
  */
@@ -278,6 +280,31 @@ const onlineStoreConfig = {
278
280
  })
279
281
  }
280
282
  };
283
+ function withH5AuthPrefix(config) {
284
+ const nextConfig = {};
285
+ Object.entries(config).forEach(([key, apiConfig]) => {
286
+ if (!apiConfig.url.startsWith('/auth/')) {
287
+ nextConfig[key] = apiConfig;
288
+ return;
289
+ }
290
+ nextConfig[key] = {
291
+ ...apiConfig,
292
+ url: apiConfig.url.startsWith('/h5/') ? apiConfig.url : `/h5${apiConfig.url}`,
293
+ options: params => {
294
+ const {
295
+ withCredentials,
296
+ ...baseOptions
297
+ } = (typeof apiConfig.options === 'function' ? apiConfig.options(params) : apiConfig.options) || {};
298
+ return {
299
+ ...baseOptions,
300
+ prefix: false
301
+ };
302
+ }
303
+ };
304
+ });
305
+ return nextConfig;
306
+ }
307
+ const h5OnlineStoreConfig = withH5AuthPrefix(onlineStoreConfig);
281
308
 
282
309
  /**
283
310
  * 默认配置(通用配置)
@@ -1066,6 +1093,7 @@ const shopWebposConfig = {
1066
1093
  */
1067
1094
  const channelConfigMap = exports.channelConfigMap = {
1068
1095
  'online-store': onlineStoreConfig,
1096
+ 'online-store-h5': h5OnlineStoreConfig,
1069
1097
  'default': defaultConfig,
1070
1098
  'webpos': webposConfig,
1071
1099
  'shop.webpos': shopWebposConfig
@@ -1083,9 +1111,16 @@ function getChannelConfig(channel) {
1083
1111
  * 负责根据配置执行 API 调用
1084
1112
  */
1085
1113
  class ApiCaller {
1086
- constructor(request, config) {
1114
+ constructor(request, config, options = {}) {
1087
1115
  this.request = request;
1088
1116
  this.config = config;
1117
+ this.options = options;
1118
+ }
1119
+ resolveUrl(url) {
1120
+ if (!this.options.baseUrl || !url.startsWith('/h5/auth/')) {
1121
+ return url;
1122
+ }
1123
+ return `${this.options.baseUrl.replace(/\/$/, '')}${url}`;
1089
1124
  }
1090
1125
 
1091
1126
  /**
@@ -1111,6 +1146,12 @@ class ApiCaller {
1111
1146
  ...options,
1112
1147
  ...additionalOptions
1113
1148
  };
1149
+ if (apiConfig.url.startsWith('/h5/auth/') && h5CustomerAuthApiNames.has(apiName)) {
1150
+ requestOptions.h5CustomerAuth = true;
1151
+ if (this.options.shopDomain) {
1152
+ requestOptions.h5AuthShopDomain = this.options.shopDomain;
1153
+ }
1154
+ }
1114
1155
 
1115
1156
  // 处理动态 headers
1116
1157
  if (requestOptions.headers && typeof requestOptions.headers === 'function') {
@@ -1123,19 +1164,19 @@ class ApiCaller {
1123
1164
  switch (apiConfig.method) {
1124
1165
  case 'GET':
1125
1166
  if (transformedParams && Object.keys(transformedParams).length > 0) {
1126
- response = await this.request.get(apiConfig.url, transformedParams, requestOptions);
1167
+ response = await this.request.get(this.resolveUrl(apiConfig.url), transformedParams, requestOptions);
1127
1168
  } else {
1128
- response = await this.request.get(apiConfig.url, undefined, requestOptions);
1169
+ response = await this.request.get(this.resolveUrl(apiConfig.url), undefined, requestOptions);
1129
1170
  }
1130
1171
  break;
1131
1172
  case 'POST':
1132
- response = await this.request.post(apiConfig.url, transformedParams, requestOptions);
1173
+ response = await this.request.post(this.resolveUrl(apiConfig.url), transformedParams, requestOptions);
1133
1174
  break;
1134
1175
  case 'PUT':
1135
- response = await this.request.put(apiConfig.url, transformedParams, requestOptions);
1176
+ response = await this.request.put(this.resolveUrl(apiConfig.url), transformedParams, requestOptions);
1136
1177
  break;
1137
1178
  case 'DELETE':
1138
- response = await this.request.delete(apiConfig.url, requestOptions);
1179
+ response = await this.request.delete(this.resolveUrl(apiConfig.url), requestOptions);
1139
1180
  break;
1140
1181
  default:
1141
1182
  throw new Error(`不支持的 HTTP 方法: ${apiConfig.method}`);
@@ -1150,7 +1191,7 @@ class ApiCaller {
1150
1191
  * 创建 API 调用器
1151
1192
  */
1152
1193
  exports.ApiCaller = ApiCaller;
1153
- function createApiCaller(request, channel) {
1194
+ function createApiCaller(request, channel, options) {
1154
1195
  const config = getChannelConfig(channel);
1155
- return new ApiCaller(request, config);
1196
+ return new ApiCaller(request, config, options);
1156
1197
  }
@@ -227,7 +227,10 @@ class RegisterAndLoginImpl extends _BaseModule.BaseModule {
227
227
  }
228
228
 
229
229
  // 创建 API 调用器
230
- this.apiCaller = (0, _config.createApiCaller)(this.request, this.channel);
230
+ this.apiCaller = (0, _config.createApiCaller)(this.request, this.channel, {
231
+ baseUrl: this.otherParams.h5AuthBaseUrl,
232
+ shopDomain: this.otherParams.h5AuthShopDomain
233
+ });
231
234
 
232
235
  // 尝试从本地存储恢复登录状态
233
236
  await this.restoreLoginState();
@@ -13,6 +13,7 @@ interface ScanOrderItemRuleRuntimeConfig {
13
13
  pax?: Partial<PaxInfo>;
14
14
  historicalItems?: CartItemSummary[];
15
15
  serviceType?: string;
16
+ channel?: string;
16
17
  submissionIndex?: number;
17
18
  custom?: Record<string, any>;
18
19
  }
@@ -1200,7 +1200,11 @@ class ScanOrderImpl extends _BaseSales.BaseSalesImpl {
1200
1200
  return sourceMap;
1201
1201
  }
1202
1202
  getItemRuleRuntimeConfig() {
1203
- return this.itemRuleRuntimeConfig || {};
1203
+ const runtimeConfig = this.itemRuleRuntimeConfig || {};
1204
+ return {
1205
+ ...runtimeConfig,
1206
+ channel: runtimeConfig.channel ?? runtimeConfig.custom?.channel ?? this.otherParams?.channel
1207
+ };
1204
1208
  }
1205
1209
  async ensureItemRuleConfigsLoaded() {
1206
1210
  if (this.itemRuleConfigs.length > 0) return this.itemRuleConfigs;
@@ -78,6 +78,7 @@ export declare function buildItemRuleBusinessData(params: {
78
78
  pax?: Partial<PaxInfo>;
79
79
  historicalItems?: CartItemSummary[];
80
80
  serviceType?: string;
81
+ channel?: string;
81
82
  submissionIndex?: number;
82
83
  custom?: Record<string, any>;
83
84
  };
@@ -333,6 +333,7 @@ function buildItemRuleBusinessData(params) {
333
333
  cartItems,
334
334
  historicalItems,
335
335
  serviceType: runtimeConfig.serviceType || 'dine-in',
336
+ channel: runtimeConfig.channel ?? runtimeConfig.custom?.channel,
336
337
  submissionIndex: typeof runtimeConfig.submissionIndex === 'number' ? toNonNegativeInt(runtimeConfig.submissionIndex) : tempOrder.order_id ? 1 : 0,
337
338
  custom: runtimeConfig.custom || {},
338
339
  strategyConfigs: itemRuleConfigs
@@ -2,10 +2,11 @@ import type { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import type { ProductData } from '../../modules';
3
3
  import { type InvoiceLayoutConfig, type InvoiceTemplateSettings, type OpenDataConfig } from '../../modules/OpenData';
4
4
  import type { LoadScheduleAvailableDateParams, ScheduleItem } from '../../modules/Schedule/types';
5
- import type { ProductListLoadProductsParams } from '../../modules/ProductList';
5
+ import type { ProductListLoadProductsParams, ProductListStoreQueryParams } from '../../modules/ProductList';
6
6
  import type { AvailabilityDateRange, AvailabilityOccupancy, AvailabilityProduct, AvailabilityResource, GetProductTimeRangesRequest } from '../../modules/ResourcePlanner';
7
- import type { AvailabilityContextRef, ContextualAvailabilityQuery, ContextualAvailabilityQueryResult, ContextualCommitAvailabilitySelectionParams, ContextualProductTimeRangeGroup, UnifiedBookingSalesAddRetailProductParams, UnifiedBookingSalesApplyPlannerDiscountsParams, UnifiedBookingSalesCommitAvailabilitySelectionResult, UnifiedBookingSalesDiscountResult, UnifiedBookingSalesLoadProductsByScheduleDateParams, UnifiedBookingSalesLoadOpenDataParams, UnifiedBookingSalesPrepareAvailabilityProjectionParams, UnifiedBookingSalesPolicyResponse, UnifiedBookingSalesStageRetailProductsParams, UnifiedBookingSalesState } from './types';
7
+ import type { AvailabilityContextRef, ContextualAvailabilityQuery, ContextualAvailabilityQueryResult, ContextualCommitAvailabilitySelectionParams, ContextualProductTimeRangeGroup, UnifiedBookingSalesAddRetailProductParams, UnifiedBookingSalesApplyPlannerDiscountsParams, UnifiedBookingSalesCommitAvailabilitySelectionResult, UnifiedBookingSalesDiscountResult, UnifiedBookingSalesLoadProductsByScheduleDateParams, UnifiedBookingSalesLoadOpenDataParams, UnifiedBookingSalesPrepareAvailabilityProjectionParams, UnifiedBookingSalesPolicyResponse, UnifiedBookingSalesResolveProductAllergyRequirementParams, UnifiedBookingSalesStageRetailProductsParams, UnifiedBookingSalesState } from './types';
8
8
  import { BookingTicket } from '../BookingTicket';
9
+ import type { AllergyRequirement } from '../../model/strategy/adapter/itemRule';
9
10
  export * from './types';
10
11
  export { AvailabilityError } from '../../modules/ResourcePlanner';
11
12
  /**
@@ -73,6 +74,9 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
73
74
  private availabilityRemoteResourceQuerySequence;
74
75
  private availabilityRemoteResourceEpoch;
75
76
  private availabilityCommitLocks;
77
+ private allergyItemRuleEvaluator;
78
+ private allergyItemRuleConfigs;
79
+ private allergyItemRuleConfigsPromise;
76
80
  initialize(core: PisellCore, options?: ModuleOptions): Promise<void>;
77
81
  protected getSubmitOrderSalesChannel(): string;
78
82
  private createAvailabilityFacadeError;
@@ -144,6 +148,14 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
144
148
  */
145
149
  loadOpenData(params: UnifiedBookingSalesLoadOpenDataParams): Promise<OpenDataConfig>;
146
150
  getOpenData(): Promise<OpenDataConfig | null>;
151
+ private resetAllergyItemRuleState;
152
+ private fetchAllergyItemRuleConfigs;
153
+ private ensureAllergyItemRuleConfigsLoaded;
154
+ /**
155
+ * 评估当前渠道与商品是否命中 ALLERGY_CHECK。
156
+ * 只返回优先级最高 Action 中当前商品 Target 的 dataSource,不跨商品或 Action 合并。
157
+ */
158
+ resolveProductAllergyRequirement(params: UnifiedBookingSalesResolveProductAllergyRequirementParams): Promise<AllergyRequirement | null>;
147
159
  clearOpenDataCache(): void;
148
160
  getInvoiceLayoutConfig(): Promise<InvoiceLayoutConfig>;
149
161
  getInvoiceTemplateSettings(): Promise<InvoiceTemplateSettings>;
@@ -160,6 +172,7 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
160
172
  callback?: (result: any) => void;
161
173
  subscriberId?: string;
162
174
  }): Promise<ProductData[]>;
175
+ queryProducts(params?: ProductListStoreQueryParams): Promise<ProductData[]>;
163
176
  /** Date-first entry point. It delegates protocol details and cache ownership to ScheduleModule. */
164
177
  loadScheduleAvailableDate(params: LoadScheduleAvailableDateParams): Promise<import("../../modules/Date/types").ITime[]>;
165
178
  /**
@@ -52,6 +52,7 @@ var _remoteOccupancyCache = require("./remoteOccupancyCache");
52
52
  var _BookingTicket = require("../BookingTicket");
53
53
  var _localClock = require("../../modules/ResourcePlanner/localClock");
54
54
  var _platform = require("../../utils/platform");
55
+ var _itemRule = require("../../model/strategy/adapter/itemRule");
55
56
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
56
57
  const OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow', 'basic', 'checkout', 'availability', 'documents'];
57
58
  const OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
@@ -84,6 +85,22 @@ function getCandidateStartTime(candidate) {
84
85
  return undefined;
85
86
  }
86
87
  }
88
+ function normalizePositiveIntegerList(value) {
89
+ if (!Array.isArray(value)) return [];
90
+ return Array.from(new Set(value.map(item => normalizePositiveInteger(item)).filter(item => item !== undefined)));
91
+ }
92
+ function toConfiguredBoolean(value) {
93
+ if (typeof value === 'boolean') return value;
94
+ if (typeof value === 'number') return value !== 0;
95
+ if (typeof value === 'string') {
96
+ return ['1', 'true', 'yes', 'y', 'on'].includes(value.trim().toLowerCase());
97
+ }
98
+ return false;
99
+ }
100
+ function normalizeItemRuleStrategyConfigs(value) {
101
+ if (!Array.isArray(value)) return [];
102
+ return value.filter(config => Boolean(config && typeof config === 'object' && config.metadata?.type === 'item_rule'));
103
+ }
87
104
  function isRecord(value) {
88
105
  return !!value && typeof value === 'object' && !Array.isArray(value);
89
106
  }
@@ -1197,11 +1214,15 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1197
1214
  availabilityRemoteResourceQuerySequence = 0;
1198
1215
  availabilityRemoteResourceEpoch = 0;
1199
1216
  availabilityCommitLocks = new Set();
1217
+ allergyItemRuleEvaluator = new _itemRule.ItemRuleEvaluator();
1218
+ allergyItemRuleConfigs = [];
1219
+ allergyItemRuleConfigsPromise = null;
1200
1220
  async initialize(core, options = {}) {
1201
1221
  this.availabilityScheduleCatalog = null;
1202
1222
  this.openDataTarget = null;
1203
1223
  this.availabilityRuntimeOffsetMinutes = (0, _localClock.captureRuntimeOffsetMinutes)();
1204
1224
  this.clearAvailabilityRemoteResourceState();
1225
+ this.resetAllergyItemRuleState();
1205
1226
  await super.initialize(core, options);
1206
1227
  const openDataBusinessCode = String(this.otherParams?.businessCode ?? this.otherParams?.business_code ?? '').trim();
1207
1228
  const openDataChannel = String(this.otherParams?.channel ?? '').trim();
@@ -1346,6 +1367,7 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1346
1367
  cover = false
1347
1368
  } = {}) {
1348
1369
  const previousScope = this.getAvailabilityScopeDescriptor(this.otherParams || {});
1370
+ const previousItemRuleConfig = this.otherParams?.itemRule;
1349
1371
  const explicitlySetsOpenData = Object.prototype.hasOwnProperty.call(params, 'openData');
1350
1372
  await super.setOtherParams(params, {
1351
1373
  cover
@@ -1358,11 +1380,15 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1358
1380
  if (shouldResetAvailabilityState) {
1359
1381
  await this.resetAvailabilityProjectionState('set_other_params_reset');
1360
1382
  }
1383
+ if (shouldResetAvailabilityState || explicitlySetsOpenData || previousItemRuleConfig !== this.otherParams?.itemRule) {
1384
+ this.resetAllergyItemRuleState();
1385
+ }
1361
1386
  }
1362
1387
  async destroy() {
1363
1388
  this.availabilityScheduleCatalog = null;
1364
1389
  this.availabilityRuntimeOffsetMinutes = null;
1365
1390
  await this.resetAvailabilityProjectionState('destroy');
1391
+ this.resetAllergyItemRuleState();
1366
1392
  await super.destroy();
1367
1393
  }
1368
1394
  createAvailabilityContextId() {
@@ -1721,6 +1747,98 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1721
1747
  channel
1722
1748
  });
1723
1749
  }
1750
+ resetAllergyItemRuleState() {
1751
+ this.allergyItemRuleConfigs = [];
1752
+ this.allergyItemRuleConfigsPromise = null;
1753
+ this.allergyItemRuleEvaluator.setStrategyConfigs([]);
1754
+ }
1755
+ async fetchAllergyItemRuleConfigs(strategyModelIds) {
1756
+ if (!strategyModelIds.length) return [];
1757
+ try {
1758
+ const response = await this.request.get('/promotion', {
1759
+ skip: 1,
1760
+ num: 99,
1761
+ status: 'active',
1762
+ ids: strategyModelIds
1763
+ });
1764
+ if (response?.code !== 200 || !Array.isArray(response?.data?.list)) {
1765
+ throw new Error('[UnifiedBookingSales] 商品过敏策略加载失败');
1766
+ }
1767
+ const list = response.data.list;
1768
+ return normalizeItemRuleStrategyConfigs(list.map(item => item?.metadata));
1769
+ } catch (error) {
1770
+ // this.logMethodError('fetchAllergyItemRuleConfigs', error, {
1771
+ // strategyModelIds,
1772
+ // });
1773
+ throw error;
1774
+ }
1775
+ }
1776
+ async ensureAllergyItemRuleConfigsLoaded() {
1777
+ if (this.allergyItemRuleConfigs.length > 0) {
1778
+ return this.allergyItemRuleConfigs;
1779
+ }
1780
+ if (this.allergyItemRuleConfigsPromise) {
1781
+ return this.allergyItemRuleConfigsPromise;
1782
+ }
1783
+ this.allergyItemRuleConfigsPromise = (async () => {
1784
+ const staticConfigs = normalizeItemRuleStrategyConfigs(this.otherParams?.itemRule?.strategyConfigs);
1785
+ if (staticConfigs.length > 0) {
1786
+ this.allergyItemRuleConfigs = staticConfigs;
1787
+ this.allergyItemRuleEvaluator.setStrategyConfigs(staticConfigs);
1788
+ return staticConfigs;
1789
+ }
1790
+ const openData = await this.getOpenData();
1791
+ const enabled = toConfiguredBoolean(openData?.['sale.enable_item_rules']);
1792
+ const strategyModelIds = normalizePositiveIntegerList(openData?.['sale.enabled_item_rules']);
1793
+ if (!enabled || !strategyModelIds.length) {
1794
+ this.allergyItemRuleEvaluator.setStrategyConfigs([]);
1795
+ return [];
1796
+ }
1797
+ const loaded = await this.fetchAllergyItemRuleConfigs(strategyModelIds);
1798
+ this.allergyItemRuleConfigs = loaded;
1799
+ this.allergyItemRuleEvaluator.setStrategyConfigs(loaded);
1800
+ return loaded;
1801
+ })();
1802
+ return this.allergyItemRuleConfigsPromise;
1803
+ }
1804
+
1805
+ /**
1806
+ * 评估当前渠道与商品是否命中 ALLERGY_CHECK。
1807
+ * 只返回优先级最高 Action 中当前商品 Target 的 dataSource,不跨商品或 Action 合并。
1808
+ */
1809
+ async resolveProductAllergyRequirement(params) {
1810
+ const productId = normalizePositiveInteger(params.productId);
1811
+ if (!productId) return null;
1812
+ const strategyConfigs = await this.ensureAllergyItemRuleConfigsLoaded();
1813
+ if (!strategyConfigs.length) return null;
1814
+ const productVariantId = normalizePositiveInteger(params.productVariantId);
1815
+ const runtimeConfig = this.otherParams?.itemRule || {};
1816
+ const runtimePax = runtimeConfig.pax || {};
1817
+ const cartItems = (this.getOrderProducts?.() || []).map(item => ({
1818
+ product_id: Number(item?.product_id ?? item?.id),
1819
+ product_variant_id: Number(item?.product_variant_id ?? 0) || undefined,
1820
+ quantity: Number(item?.num ?? item?.quantity ?? 0)
1821
+ })).filter(item => Number.isFinite(item.product_id) && item.product_id > 0 && Number.isFinite(item.quantity) && item.quantity > 0);
1822
+ return this.allergyItemRuleEvaluator.getAllergyRequirement({
1823
+ pax: {
1824
+ total: Number(runtimePax.total ?? 0) || 0,
1825
+ adult: Number(runtimePax.adult ?? 0) || 0,
1826
+ child: Number(runtimePax.child ?? 0) || 0
1827
+ },
1828
+ cartItems,
1829
+ serviceType: String(runtimeConfig.serviceType ?? this.otherParams?.serviceType ?? ''),
1830
+ channel: String(params.channel ?? this.otherParams?.channel ?? this.otherParams?.platform ?? ''),
1831
+ currentProduct: {
1832
+ product_id: productId,
1833
+ ...(productVariantId ? {
1834
+ product_variant_id: productVariantId
1835
+ } : {})
1836
+ },
1837
+ submissionIndex: Number(runtimeConfig.submissionIndex ?? 0) || 0,
1838
+ strategyConfigs,
1839
+ custom: runtimeConfig.custom
1840
+ });
1841
+ }
1724
1842
  clearOpenDataCache() {
1725
1843
  this.openDataTarget = null;
1726
1844
  this.loadOpenDataInFlight = null;
@@ -1783,6 +1901,10 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
1783
1901
  await this.core.effects.emit(`${this.name}:onProductsLoaded`, result);
1784
1902
  return result;
1785
1903
  }
1904
+ async queryProducts(params = {}) {
1905
+ if (!this.store.products) throw new Error('[UnifiedBookingSales] products 模块未初始化');
1906
+ return this.store.products.queryProducts(params);
1907
+ }
1786
1908
 
1787
1909
  /** Date-first entry point. It delegates protocol details and cache ownership to ScheduleModule. */
1788
1910
  async loadScheduleAvailableDate(params) {
@@ -3333,7 +3455,9 @@ class UnifiedBookingSalesImpl extends _BookingTicket.BookingTicket {
3333
3455
  } else {
3334
3456
  const beforeProductLineUids = new Set((this.getTempOrder()?.products || []).map(line => getProductLineUid(line)).filter(Boolean));
3335
3457
  const beforeBookingUids = new Set((this.getTempOrder()?.bookings || []).map(item => getBookingUid(item)).filter(Boolean));
3336
- const products = await this.addProductToOrder(transformed, booking);
3458
+ const products = await this.addProductToOrder(transformed, booking, params.salesNotes?.length ? {
3459
+ salesNotes: params.salesNotes
3460
+ } : undefined);
3337
3461
  const addedLines = products.filter(line => sameAvailabilityId(line.product_id ?? line.id, refreshedCandidate.productId) && line.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeProductLineUids.has(getProductLineUid(line)));
3338
3462
  productLineUids = addedLines.map(line => getProductLineUid(line)).filter(Boolean);
3339
3463
  const addedBookings = (this.getTempOrder()?.bookings || []).filter(item => item.metadata?.availability_candidate_key === refreshedCandidate.key && !beforeBookingUids.has(getBookingUid(item)));