@pisell/pisellos 2.2.309 → 2.2.311

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 (122) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  2. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  3. package/dist/model/strategy/adapter/walletPass/evaluator.js +4 -2
  4. package/dist/modules/BookingContext/index.js +4 -4
  5. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +58 -19
  6. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +24 -12
  7. package/dist/modules/Customer/index.js +6 -3
  8. package/dist/modules/Customer/types.d.ts +2 -0
  9. package/dist/modules/Discount/availability.d.ts +7 -0
  10. package/dist/modules/Discount/availability.js +49 -0
  11. package/dist/modules/Discount/entitlementPass.d.ts +34 -0
  12. package/dist/modules/Discount/entitlementPass.js +147 -0
  13. package/dist/modules/Discount/index.d.ts +2 -2
  14. package/dist/modules/Discount/index.js +37 -151
  15. package/dist/modules/Discount/types.d.ts +36 -5
  16. package/dist/modules/OpenData/index.d.ts +1 -1
  17. package/dist/modules/OpenData/index.js +21 -12
  18. package/dist/modules/Order/index.d.ts +32 -4
  19. package/dist/modules/Order/index.js +733 -373
  20. package/dist/modules/Order/types.d.ts +17 -0
  21. package/dist/modules/Order/utils/orderCollectionIdentity.js +16 -12
  22. package/dist/modules/Order/utils.d.ts +19 -5
  23. package/dist/modules/Order/utils.js +306 -86
  24. package/dist/modules/Payment/index.d.ts +0 -5
  25. package/dist/modules/Payment/index.js +35 -85
  26. package/dist/modules/Payment/types.d.ts +21 -1
  27. package/dist/modules/Payment/types.js +2 -0
  28. package/dist/modules/Payment/walletpass.d.ts +1 -1
  29. package/dist/modules/Payment/walletpass.js +80 -47
  30. package/dist/modules/ProductList/index.js +18 -5
  31. package/dist/modules/Rules/index.d.ts +2 -2
  32. package/dist/modules/Rules/index.js +480 -186
  33. package/dist/modules/Rules/types.d.ts +4 -0
  34. package/dist/server/index.js +8 -5
  35. package/dist/server/modules/floor-plan/index.js +34 -3
  36. package/dist/server/modules/order/index.js +6 -0
  37. package/dist/server/modules/order/types.d.ts +12 -0
  38. package/dist/server/modules/payment/index.d.ts +25 -6
  39. package/dist/server/modules/payment/index.js +660 -133
  40. package/dist/server/utils/small-ticket.d.ts +8 -5
  41. package/dist/server/utils/small-ticket.js +1269 -199
  42. package/dist/solution/BaseSales/index.d.ts +11 -2
  43. package/dist/solution/BaseSales/index.js +1463 -1173
  44. package/dist/solution/BaseSales/types.d.ts +7 -3
  45. package/dist/solution/BaseSales/utils/cartPromotion.js +141 -126
  46. package/dist/solution/BookingTicket/index.d.ts +11 -2
  47. package/dist/solution/BookingTicket/index.js +663 -542
  48. package/dist/solution/BookingTicket/types.d.ts +5 -1
  49. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  50. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +338 -147
  51. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  52. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  53. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +19 -6
  54. package/dist/solution/ShopDiscount/index.d.ts +2 -0
  55. package/dist/solution/ShopDiscount/index.js +99 -42
  56. package/dist/solution/ShopDiscount/utils.d.ts +4 -2
  57. package/dist/solution/ShopDiscount/utils.js +43 -23
  58. package/dist/solution/UnifiedBookingSales/index.d.ts +2 -2
  59. package/dist/solution/UnifiedBookingSales/index.js +5 -5
  60. package/dist/utils/os-private-payload.d.ts +5 -0
  61. package/dist/utils/os-private-payload.js +17 -0
  62. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  63. package/lib/model/strategy/adapter/walletPass/evaluator.js +2 -0
  64. package/lib/modules/BookingContext/index.js +3 -3
  65. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +35 -6
  66. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +13 -6
  67. package/lib/modules/Customer/index.js +5 -1
  68. package/lib/modules/Customer/types.d.ts +2 -0
  69. package/lib/modules/Discount/availability.d.ts +7 -0
  70. package/lib/modules/Discount/availability.js +43 -0
  71. package/lib/modules/Discount/entitlementPass.d.ts +34 -0
  72. package/lib/modules/Discount/entitlementPass.js +141 -0
  73. package/lib/modules/Discount/index.d.ts +2 -2
  74. package/lib/modules/Discount/index.js +23 -129
  75. package/lib/modules/Discount/types.d.ts +36 -5
  76. package/lib/modules/OpenData/index.d.ts +1 -1
  77. package/lib/modules/OpenData/index.js +11 -2
  78. package/lib/modules/Order/index.d.ts +32 -4
  79. package/lib/modules/Order/index.js +341 -71
  80. package/lib/modules/Order/types.d.ts +17 -0
  81. package/lib/modules/Order/utils/orderCollectionIdentity.js +14 -11
  82. package/lib/modules/Order/utils.d.ts +19 -5
  83. package/lib/modules/Order/utils.js +215 -13
  84. package/lib/modules/Payment/index.d.ts +0 -5
  85. package/lib/modules/Payment/index.js +3 -28
  86. package/lib/modules/Payment/types.d.ts +21 -1
  87. package/lib/modules/Payment/types.js +1 -0
  88. package/lib/modules/Payment/walletpass.d.ts +1 -1
  89. package/lib/modules/Payment/walletpass.js +29 -9
  90. package/lib/modules/ProductList/index.js +14 -1
  91. package/lib/modules/Rules/index.d.ts +2 -2
  92. package/lib/modules/Rules/index.js +357 -110
  93. package/lib/modules/Rules/types.d.ts +4 -0
  94. package/lib/server/index.js +8 -5
  95. package/lib/server/modules/floor-plan/index.js +29 -1
  96. package/lib/server/modules/order/index.js +6 -0
  97. package/lib/server/modules/order/types.d.ts +12 -0
  98. package/lib/server/modules/payment/index.d.ts +25 -6
  99. package/lib/server/modules/payment/index.js +350 -78
  100. package/lib/server/utils/small-ticket.d.ts +8 -5
  101. package/lib/server/utils/small-ticket.js +1009 -121
  102. package/lib/solution/BaseSales/index.d.ts +11 -2
  103. package/lib/solution/BaseSales/index.js +164 -30
  104. package/lib/solution/BaseSales/types.d.ts +7 -3
  105. package/lib/solution/BaseSales/utils/cartPromotion.js +12 -4
  106. package/lib/solution/BookingTicket/index.d.ts +11 -2
  107. package/lib/solution/BookingTicket/index.js +103 -41
  108. package/lib/solution/BookingTicket/types.d.ts +5 -1
  109. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  110. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +174 -42
  111. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  112. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  113. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +11 -3
  114. package/lib/solution/ShopDiscount/index.d.ts +2 -0
  115. package/lib/solution/ShopDiscount/index.js +55 -9
  116. package/lib/solution/ShopDiscount/utils.d.ts +4 -2
  117. package/lib/solution/ShopDiscount/utils.js +42 -25
  118. package/lib/solution/UnifiedBookingSales/index.d.ts +2 -2
  119. package/lib/solution/UnifiedBookingSales/index.js +5 -5
  120. package/lib/utils/os-private-payload.d.ts +5 -0
  121. package/lib/utils/os-private-payload.js +22 -0
  122. package/package.json +1 -1
@@ -160,7 +160,9 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
160
160
  addTimeProductsCatalog = [];
161
161
  orderCustomerDiscountRefreshInFlight = null;
162
162
  orderCustomerDiscountRefreshCustomerId = null;
163
+ orderCustomerDiscountRefreshTarget = null;
163
164
  orderCustomerPromotionRefreshInFlight = null;
165
+ customerResolveRequests = new Map();
164
166
  loadOpenDataInFlight = null;
165
167
  loadOpenDataInFlightTarget = null;
166
168
  openDataTarget = null;
@@ -193,10 +195,37 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
193
195
  throw new Error('bookingTicket解决方案需要 request 插件支持');
194
196
  }
195
197
  this.platform = this.otherParams?.platform;
198
+ await this.prepareInvoiceTemplateAtSolutionReady(this.getInjectedOpenDataConfig());
196
199
  await this.configureIdGeneratorFromOpenData();
197
200
  }
201
+ getInjectedOpenDataConfig() {
202
+ const openData = this.otherParams?.openData;
203
+ return openData && typeof openData === 'object' && !Array.isArray(openData) ? openData : undefined;
204
+ }
205
+
206
+ /**
207
+ * 在解决方案对外 ready 前准备 Invoice Document;失败只降级模板,不阻断销售。
208
+ * 已由页面取得的 OpenData 直接交给模块接管,没有注入数据时再走 target-scoped 加载。
209
+ */
210
+ async prepareInvoiceTemplateAtSolutionReady(injectedOpenData) {
211
+ const businessCode = this.getBookingTicketBusinessCode();
212
+ if (!businessCode || !this.store.openData) return;
213
+ const channel = this.getSubmitOrderSalesChannel();
214
+ const target = `${businessCode}+${channel}`;
215
+ try {
216
+ if (injectedOpenData && typeof this.store.openData.ensureInvoiceTemplateSettings === 'function') {
217
+ this.openDataTarget = target;
218
+ await this.store.openData.ensureInvoiceTemplateSettings(target, injectedOpenData);
219
+ this.otherParams.openData = injectedOpenData;
220
+ return;
221
+ }
222
+ await this.loadOpenDataConfig();
223
+ } catch (error) {
224
+ console.warn('[BookingTicket] Invoice 模板预加载失败,使用默认配置', error);
225
+ }
226
+ }
198
227
  getBookingTicketBusinessCode() {
199
- const candidates = [this.otherParams?.businessCode, this.otherParams?.business_code, this.getOrderHeader()?.business_code, this.store.order?.getTempOrder()?.business_code, this.getSalesOrder()?.raw?.business_code];
228
+ const candidates = [this.otherParams?.businessCode, this.otherParams?.business_code, this.getOrderHeader()?.business_code, typeof this.store.order?.getTempOrder === 'function' ? this.store.order.getTempOrder()?.business_code : undefined, this.getSalesOrder()?.raw?.business_code];
200
229
  for (const candidate of candidates) {
201
230
  if (candidate === undefined || candidate === null) continue;
202
231
  const normalized = String(candidate).trim();
@@ -415,7 +444,15 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
415
444
 
416
445
  // 缺 Holder 等场景 isAvailable=false,但仍需把 Pass 归属客户带入订单,供 Holder 弹窗使用。
417
446
  if (!hasOrderCustomer && extractedCustomerId && !isUserPlatform) {
418
- await this.hydrateOrderCustomerFromScanDiscounts(scannedList);
447
+ try {
448
+ await this.hydrateOrderCustomerFromScanDiscounts(scannedList);
449
+ } catch (error) {
450
+ // 客户映射失败不能阻断原有 Pass 的可用性、展示或优惠处理。
451
+ console.warn('[BookingTicket] scan discount customer resolve failed', {
452
+ customerId: extractedCustomerId,
453
+ error
454
+ });
455
+ }
419
456
  }
420
457
  if (raw.isAvailable) {
421
458
  await this.store.order.recalculateSummary({
@@ -466,43 +503,39 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
466
503
  return null;
467
504
  }
468
505
  async resolveCustomerById(customerId) {
469
- const localCustomers = this.store.customer.getCustomers();
470
- const localCustomer = this.findCustomerById(localCustomers, customerId);
471
- const normalizedLocalCustomer = localCustomer ? this.normalizeCustomer(localCustomer) : null;
472
- if (normalizedLocalCustomer && this.hasUsableCustomerDetails(normalizedLocalCustomer)) {
473
- return normalizedLocalCustomer;
474
- }
475
- const result = await this.store.customer.queryCustomerList({
476
- ids: [customerId],
477
- skip: 1,
478
- num: 1
479
- });
480
- const remoteCustomer = this.findCustomerById(result?.list || [], customerId) || result?.list?.[0] || null;
481
- if (remoteCustomer) {
482
- return this.normalizeCustomer(remoteCustomer);
483
- }
484
- try {
485
- const detailCustomer = await this.store.customer.queryCustomerDetail(customerId);
486
- return detailCustomer ? this.normalizeCustomer(detailCustomer) : null;
487
- } catch (error) {
488
- console.warn('[BookingTicket] resolveCustomerById detail request failed', {
489
- customerId,
490
- error
491
- });
506
+ const rawCustomerId = String(customerId ?? '').trim();
507
+ const numericCustomerId = Number(rawCustomerId);
508
+ if (!rawCustomerId || !Number.isInteger(numericCustomerId) || numericCustomerId <= 1) {
492
509
  return null;
493
510
  }
511
+ const normalizedCustomerId = String(numericCustomerId);
512
+ const existingRequest = this.customerResolveRequests.get(normalizedCustomerId);
513
+ if (existingRequest) return existingRequest;
514
+ const request = this.request.post(`/customer/resolve/${encodeURIComponent(normalizedCustomerId)}`, {
515
+ with_franchisor_data: 1,
516
+ with: ['latestWalletDetail.wallet', 'contactsInfo', 'formRecord']
517
+ }).then(result => {
518
+ const customer = result?.data;
519
+ const resolvedCustomerId = Number(customer && typeof customer === 'object' && !Array.isArray(customer) ? this.getRawCustomerId(customer) : NaN);
520
+ if (result?.status !== true || result?.code !== 200 || !customer || typeof customer !== 'object' || Array.isArray(customer) || !Number.isInteger(resolvedCustomerId) || resolvedCustomerId <= 1) {
521
+ const error = new Error(result?.message || 'Customer resolve failed');
522
+ error.code = result?.code;
523
+ throw error;
524
+ }
525
+ return this.normalizeCustomer(customer);
526
+ }).finally(() => {
527
+ if (this.customerResolveRequests.get(normalizedCustomerId) === request) {
528
+ this.customerResolveRequests.delete(normalizedCustomerId);
529
+ }
530
+ });
531
+ this.customerResolveRequests.set(normalizedCustomerId, request);
532
+ return request;
494
533
  }
495
534
 
496
535
  /** 兼容既有优惠码扫码逻辑。 */
497
536
  resolveCustomerByDiscountCustomerId(customerId) {
498
537
  return this.resolveCustomerById(customerId);
499
538
  }
500
- findCustomerById(customers = [], customerId) {
501
- return customers.find(customer => {
502
- const rawId = this.getRawCustomerId(customer);
503
- return rawId !== null && String(rawId) === String(customerId);
504
- }) || null;
505
- }
506
539
  normalizeCustomer(customer) {
507
540
  const raw = customer;
508
541
  const rawId = this.getRawCustomerId(customer);
@@ -520,9 +553,6 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
520
553
  const raw = customer;
521
554
  return customer.id ?? raw.customer_id ?? raw.customerId ?? null;
522
555
  }
523
- hasUsableCustomerDetails(customer) {
524
- return !!(customer.name || customer.phone || customer.email || customer.display_name || customer.customer_name);
525
- }
526
556
 
527
557
  /**
528
558
  * 更新当前预约的 appointment_status。
@@ -1094,7 +1124,13 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
1094
1124
  }
1095
1125
  refreshDiscountConfigAfterOrderCustomerChange(customerId) {
1096
1126
  if (!customerId || customerId === 1) return Promise.resolve();
1097
- if (this.orderCustomerDiscountRefreshInFlight && this.orderCustomerDiscountRefreshCustomerId === customerId) {
1127
+ const tempOrder = this.store.order.getTempOrder?.();
1128
+ const orderIdentity = this.getOrderIdentity?.();
1129
+ const orderId = tempOrder?.order_id || orderIdentity?.orderId;
1130
+ const action = !orderId || String(orderId).startsWith('local_order') ? 'create' : 'edit';
1131
+ const orderTarget = orderId ? `order:${String(orderId)}` : `external:${String(tempOrder?.external_sale_number || orderIdentity?.externalSaleNumber || orderIdentity?.localId || 'new')}`;
1132
+ const refreshTarget = `${customerId}:${action}:${orderTarget}`;
1133
+ if (this.orderCustomerDiscountRefreshInFlight && this.orderCustomerDiscountRefreshTarget === refreshTarget) {
1098
1134
  return this.orderCustomerDiscountRefreshInFlight;
1099
1135
  }
1100
1136
  const previousRefresh = this.orderCustomerDiscountRefreshInFlight;
@@ -1106,22 +1142,33 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
1106
1142
  if (!currentCustomer || Number(currentCustomer.customer_id) !== customerId) {
1107
1143
  return;
1108
1144
  }
1109
- const tempOrder = this.store.order.getTempOrder?.();
1110
- const orderId = tempOrder?.order_id || this.getOrderIdentity?.()?.orderId;
1111
- const action = !orderId || String(orderId).startsWith('local_order') ? 'create' : 'edit';
1145
+ const currentTempOrder = this.store.order.getTempOrder?.();
1146
+ const currentOrderIdentity = this.getOrderIdentity?.();
1147
+ const currentOrderId = currentTempOrder?.order_id || currentOrderIdentity?.orderId;
1148
+ const currentAction = !currentOrderId || String(currentOrderId).startsWith('local_order') ? 'create' : 'edit';
1149
+ const currentOrderTarget = currentOrderId ? `order:${String(currentOrderId)}` : `external:${String(currentTempOrder?.external_sale_number || currentOrderIdentity?.externalSaleNumber || currentOrderIdentity?.localId || 'new')}`;
1150
+ if (`${customerId}:${currentAction}:${currentOrderTarget}` !== refreshTarget) {
1151
+ return;
1152
+ }
1153
+
1154
+ // 上一个订单的迟到请求可能在客户切换后重新写入 BaseSales cache key;
1155
+ // 客户变更触发的目标订单刷新必须绕过该缓存。
1112
1156
  await this.loadDiscountConfig({
1113
1157
  customerId,
1114
- action
1158
+ action,
1159
+ force: true
1115
1160
  });
1116
1161
  })();
1117
1162
  this.orderCustomerDiscountRefreshInFlight = refreshTask;
1118
1163
  this.orderCustomerDiscountRefreshCustomerId = customerId;
1164
+ this.orderCustomerDiscountRefreshTarget = refreshTarget;
1119
1165
  refreshTask.catch(error => {
1120
1166
  console.error('Failed to load discount config after customer change:', error);
1121
1167
  }).finally(() => {
1122
1168
  if (this.orderCustomerDiscountRefreshInFlight !== refreshTask) return;
1123
1169
  this.orderCustomerDiscountRefreshInFlight = null;
1124
1170
  this.orderCustomerDiscountRefreshCustomerId = null;
1171
+ this.orderCustomerDiscountRefreshTarget = null;
1125
1172
  });
1126
1173
  return refreshTask;
1127
1174
  }
@@ -1642,6 +1689,7 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
1642
1689
  const result = await (0, _applyGlobalScan.applyGlobalScan)({
1643
1690
  setOrderCustomer: customer => this.setOrderCustomer(customer),
1644
1691
  getOrderCustomerSnapshot: () => this.getOrderCustomerSnapshot(),
1692
+ resolveCustomerById: customerId => this.resolveCustomerById(customerId),
1645
1693
  getBusinessCode: () => this.otherParams?.businessCode,
1646
1694
  buildAddProductCtx: extra => this.buildAddProductCtx(extra),
1647
1695
  decideAddProduct: (item, options) => this.decideAddProduct(item, options),
@@ -1662,6 +1710,18 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
1662
1710
  };
1663
1711
  }
1664
1712
  }
1713
+
1714
+ /** 查询码类型;复用原有识别和缓存,不写订单、不打开选择 UI。 */
1715
+ async getGlobalScanCodeType(code) {
1716
+ const trimmed = typeof code === 'string' ? code.trim() : '';
1717
+ if (!trimmed) return null;
1718
+ const scan = (0, _handleScan.handleGlobalScan)(this.request, value => this.store.products.findProductsByCodeOrBarcode(value), this.getSubmitOrderSalesChannel());
1719
+ const raw = await scan({
1720
+ type: 'nativeScanner',
1721
+ value: trimmed
1722
+ });
1723
+ return (0, _formatGlobalScan.formatGlobalScanResult)(raw, trimmed)?.searchType ?? null;
1724
+ }
1665
1725
  resolveBestAddTimePlan(addTimeProducts, targetMinutes) {
1666
1726
  return (0, _resolveBestAddTimePlan.resolveBestAddTimePlan)(addTimeProducts, targetMinutes);
1667
1727
  }
@@ -1784,10 +1844,12 @@ class BookingTicketImpl extends _BaseSales.BaseSalesImpl {
1784
1844
  await super.setOtherParams(params, {
1785
1845
  cover
1786
1846
  });
1847
+ const invoiceContextChanged = params?.businessCode !== undefined || params?.business_code !== undefined || params?.channel !== undefined || params?.platform !== undefined || Object.prototype.hasOwnProperty.call(params, 'openData');
1848
+ if (invoiceContextChanged) {
1849
+ await this.prepareInvoiceTemplateAtSolutionReady(Object.prototype.hasOwnProperty.call(params, 'openData') ? this.getInjectedOpenDataConfig() : undefined);
1850
+ }
1787
1851
  if (params?.businessCode !== undefined || params?.business_code !== undefined) {
1788
1852
  await this.configureIdGeneratorFromOpenData();
1789
- } else if (params?.channel !== undefined) {
1790
- await this.loadOpenDataConfig();
1791
1853
  }
1792
1854
  }
1793
1855
  getDeviceTaskPlugin() {
@@ -230,12 +230,16 @@ export interface GlobalScanHostBridge {
230
230
  /**
231
231
  * walk-in 扫 walletPass:直接核销优惠码(对齐 SaleDetail Voucher.handleApplyCode / cart.scanCode)。
232
232
  */
233
- applyPromotionCode?: (code: string, customerId?: number) => Promise<{
233
+ applyPromotionCode?: (code: string, customerId?: number, options?: {
234
+ suppressFailureNotification?: boolean;
235
+ }) => Promise<{
234
236
  isAvailable?: boolean;
235
237
  isAccepted?: boolean;
236
238
  type?: string;
237
239
  unavailableReason?: string;
238
240
  }>;
241
+ /** 当前订单已有其他客户时,由宿主复用客户组件完成确认及切换。 */
242
+ requestCustomerSwitch?: (customer: ICustomer) => Promise<boolean | null>;
239
243
  onNotify?: (type: 'success' | 'fail' | 'info', messageKey?: string) => void;
240
244
  refresh?: () => void;
241
245
  }
@@ -5,6 +5,7 @@ import { GlobalScanFormattedResult } from './formatGlobalScan';
5
5
  export interface GlobalScanApplyHost {
6
6
  setOrderCustomer(customer: ICustomer | null): void;
7
7
  getOrderCustomerSnapshot(): ICustomer | null;
8
+ resolveCustomerById?(customerId: string | number): Promise<ICustomer | null>;
8
9
  getBusinessCode(): string | undefined;
9
10
  buildAddProductCtx(extra?: Record<string, any>): Record<string, any>;
10
11
  decideAddProduct(item: any, options?: Record<string, any>): AddProductDecision;
@@ -26,6 +27,7 @@ export interface GlobalScanApplyHost {
26
27
  addProductToOrder(product: any, booking?: any): Promise<any>;
27
28
  refreshWalletAssets?(params?: {
28
29
  preserveSelection?: boolean;
30
+ retainSearchResultCodes?: string[];
29
31
  }): Promise<any>;
30
32
  scanWalletAsset?(code: string): Promise<{
31
33
  type: 'walletCode' | 'normalCode';
@@ -5,10 +5,21 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.applyGlobalScan = applyGlobalScan;
7
7
  var _addProductDecision = require("../addProductDecision");
8
- var _cartView = require("../cartView");
9
8
  var _scanProductValues = require("./scanProductValues");
10
9
  /** applyGlobalScan 所需的 BookingTicket 运行时能力 */
11
10
 
11
+ function getRealCustomerId(customer) {
12
+ const customerId = Number(customer?.id ?? customer?.customer_id);
13
+ return Number.isInteger(customerId) && customerId > 1 ? customerId : undefined;
14
+ }
15
+ function getWalletAssetCustomerId(asset) {
16
+ const customerId = Number(asset?.customer_id ?? asset?.holder?.customer_id);
17
+ return Number.isInteger(customerId) && customerId > 1 ? customerId : undefined;
18
+ }
19
+ function getErrorMessage(error) {
20
+ return error instanceof Error ? error.message : String(error || '');
21
+ }
22
+
12
23
  /**
13
24
  * 处理单条扫码商品的加车决策(含弹窗链)。
14
25
  */
@@ -93,17 +104,81 @@ async function applyGlobalScan(host, formatted, bridge) {
93
104
  const {
94
105
  searchType,
95
106
  data,
96
- scanCode
107
+ scanCode,
108
+ customerId: formattedCustomerId,
109
+ isCrossShopWallet,
110
+ isWalletPassUnavailableAtStore
97
111
  } = formatted;
98
112
  if (searchType === 'wallet') {
99
- if (!data) {
113
+ const currentCustomer = host.getOrderCustomerSnapshot();
114
+ const currentCustomerId = getRealCustomerId(currentCustomer);
115
+ let customer = data;
116
+ if (isCrossShopWallet) {
117
+ const sourceCustomerId = getRealCustomerId({
118
+ id: formattedCustomerId
119
+ });
120
+ if (sourceCustomerId === undefined || !host.resolveCustomerById) {
121
+ bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
122
+ return {
123
+ status: 'failed',
124
+ reason: 'invalid_data'
125
+ };
126
+ }
127
+ try {
128
+ customer = await host.resolveCustomerById(sourceCustomerId);
129
+ } catch (error) {
130
+ bridge?.onNotify?.('fail', getErrorMessage(error) || 'pisell2.text.scan-global-failed');
131
+ return {
132
+ status: 'failed',
133
+ reason: 'invalid_data'
134
+ };
135
+ }
136
+ }
137
+ const customerId = getRealCustomerId(customer) ?? (!isCrossShopWallet ? getRealCustomerId({
138
+ id: formattedCustomerId
139
+ }) : undefined);
140
+ if (!customer || customerId === undefined) {
100
141
  bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
101
142
  return {
102
143
  status: 'failed',
103
144
  reason: 'invalid_data'
104
145
  };
105
146
  }
106
- host.setOrderCustomer(data);
147
+ const orderCustomer = customer.id === undefined || customer.id === null ? {
148
+ ...customer,
149
+ id: customerId
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
+ }
181
+ host.setOrderCustomer(orderCustomer);
107
182
  bridge?.onNotify?.('success', 'pisell2.text.add-client-success');
108
183
  return {
109
184
  status: 'success',
@@ -111,8 +186,70 @@ async function applyGlobalScan(host, formatted, bridge) {
111
186
  };
112
187
  }
113
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
+ }
114
196
  const snapshot = host.getOrderCustomerSnapshot();
115
- const scannedCustomerIsWalkIn = (0, _cartView.isWalkInCustomer)(data);
197
+ let activeCustomer = snapshot;
198
+ const bindWalletPassCustomer = async asset => {
199
+ if (getRealCustomerId(activeCustomer) !== undefined) {
200
+ return {
201
+ resolveFailed: false,
202
+ customerBound: false
203
+ };
204
+ }
205
+ const assetCustomerId = getWalletAssetCustomerId(asset) ?? getRealCustomerId({
206
+ id: formattedCustomerId
207
+ });
208
+ let customer = null;
209
+ if (assetCustomerId !== undefined) {
210
+ if (!host.resolveCustomerById) {
211
+ bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
212
+ return {
213
+ resolveFailed: true,
214
+ customerBound: false
215
+ };
216
+ }
217
+ try {
218
+ customer = await host.resolveCustomerById(assetCustomerId);
219
+ } catch (error) {
220
+ console.warn('[GlobalScan] Wallet asset customer lookup failed', error);
221
+ bridge?.onNotify?.('fail', getErrorMessage(error) || 'pisell2.text.scan-global-failed');
222
+ return {
223
+ resolveFailed: true,
224
+ customerBound: false
225
+ };
226
+ }
227
+ }
228
+ const customerId = getRealCustomerId(customer);
229
+ if (!customer || customerId === undefined) {
230
+ if (assetCustomerId !== undefined) {
231
+ bridge?.onNotify?.('fail', 'pisell2.text.scan-global-failed');
232
+ }
233
+ return {
234
+ resolveFailed: assetCustomerId !== undefined,
235
+ customerBound: false
236
+ };
237
+ }
238
+ const orderCustomer = customer.id === undefined || customer.id === null ? {
239
+ ...customer,
240
+ id: customerId
241
+ } : customer;
242
+ host.setOrderCustomer(orderCustomer);
243
+ activeCustomer = orderCustomer;
244
+ await host.refreshWalletAssets?.({
245
+ preserveSelection: false,
246
+ retainSearchResultCodes: [scanCode]
247
+ });
248
+ return {
249
+ resolveFailed: false,
250
+ customerBound: true
251
+ };
252
+ };
116
253
  const trySelectWalletAsset = async () => {
117
254
  if (!host.scanWalletAsset) return null;
118
255
  const walletResult = await host.scanWalletAsset(scanCode).catch(error => {
@@ -123,61 +260,56 @@ async function applyGlobalScan(host, formatted, bridge) {
123
260
  if (walletResult.type !== 'normalCode' || !walletResult.asset) {
124
261
  return null;
125
262
  }
263
+ const bindResult = await bindWalletPassCustomer(walletResult.asset);
126
264
  if (walletResult.selected) {
127
- bridge?.onNotify?.('success');
265
+ if (!bindResult.resolveFailed) {
266
+ bridge?.onNotify?.('success');
267
+ }
128
268
  return {
129
269
  status: 'success',
130
270
  kind: 'promotion'
131
271
  };
132
272
  }
133
273
  // 已识别为 Wallet 支付资产但当前不可用:保留禁用展示,不回退优惠码。
134
- bridge?.onNotify?.('fail', 'pisell2.text.goodpass.code-invalid-cart');
274
+ if (!bindResult.resolveFailed) {
275
+ bridge?.onNotify?.('fail', 'pisell2.text.goodpass.code-invalid-cart');
276
+ }
135
277
  return {
136
278
  status: 'failed',
137
279
  reason: 'invalid_data'
138
280
  };
139
281
  };
140
- if ((0, _cartView.isWalkInCustomer)(snapshot) || scannedCustomerIsWalkIn) {
141
- // 当前购物车或 walletPass 绑定客户为 walk-in 时不绑客户,避免覆盖已有下单客户。
142
- const walletSelection = await trySelectWalletAsset();
143
- if (walletSelection) return walletSelection;
144
- if (!bridge?.applyPromotionCode) {
145
- return {
146
- status: 'failed',
147
- reason: 'no_bridge'
148
- };
149
- }
150
- const result = await bridge.applyPromotionCode(scanCode);
151
- if (result?.isAccepted ?? result?.isAvailable) {
152
- return {
153
- status: 'success',
154
- kind: 'promotion'
155
- };
156
- }
282
+
283
+ // batch-search 可能返回来源门店客户;先映射到当前门店,确保 Wallet
284
+ // 支付资产查询使用本店客户上下文。
285
+ const bindResult = await bindWalletPassCustomer();
286
+ const walletSelection = await trySelectWalletAsset();
287
+ if (walletSelection) return walletSelection;
288
+ if (!bridge?.applyPromotionCode) {
157
289
  return {
158
290
  status: 'failed',
159
- reason: 'invalid_data'
291
+ reason: 'no_bridge'
160
292
  };
161
293
  }
162
- host.setOrderCustomer(data);
163
- await host.refreshWalletAssets?.({
164
- preserveSelection: false
165
- });
166
- const walletSelection = await trySelectWalletAsset();
167
- if (walletSelection) return walletSelection;
168
- if (bridge?.applyPromotionCode) {
169
- const promotionResult = await bridge.applyPromotionCode(scanCode, Number(data?.id ?? data?.customer_id) || undefined);
170
- if (promotionResult?.isAccepted ?? promotionResult?.isAvailable) {
171
- return {
172
- status: 'success',
173
- kind: 'promotion'
174
- };
175
- }
294
+ const promotionCustomerId = getRealCustomerId(activeCustomer);
295
+ const promotionResult = bindResult.customerBound ? await bridge.applyPromotionCode(scanCode, promotionCustomerId, {
296
+ suppressFailureNotification: true
297
+ }) : await bridge.applyPromotionCode(scanCode, promotionCustomerId);
298
+ if (promotionResult?.isAccepted ?? promotionResult?.isAvailable) {
299
+ return {
300
+ status: 'success',
301
+ kind: 'promotion'
302
+ };
303
+ }
304
+ if (bindResult.customerBound) {
305
+ return {
306
+ status: 'success',
307
+ kind: 'customer'
308
+ };
176
309
  }
177
- bridge?.onNotify?.('success', 'pisell2.text.add-client-success');
178
310
  return {
179
- status: 'success',
180
- kind: 'customer'
311
+ status: 'failed',
312
+ reason: 'invalid_data'
181
313
  };
182
314
  }
183
315
  if (searchType === 'product' || searchType === 'local_product') {
@@ -14,7 +14,8 @@ const searchWalletPass = async (request, code) => {
14
14
  // 翻译识别码名称 0:不翻译 1:翻译
15
15
  relation_product: 1,
16
16
  // 组装关联商品: 0不处理 1处理
17
- with: ['customer'],
17
+ with_franchisor_data: 1,
18
+ with: ['customer', 'franchiseeWalletPass:shop_id,source_shop_id,product_id,use'],
18
19
  tags: ['point_card',
19
20
  // 积分卡
20
21
  'gift_card',
@@ -25,6 +26,8 @@ const searchWalletPass = async (request, code) => {
25
26
  // 折扣卡
26
27
  'good_pass',
27
28
  // 商品券
29
+ 'entitlement_pass',
30
+ // 月卡 / 年卡 / 多次使用卡
28
31
  'session_ticket',
29
32
  // 场次票
30
33
  'appointment_ticket' // 预约类门票
@@ -51,17 +54,18 @@ exports.searchWalletPass = searchWalletPass;
51
54
  const searchWallet = async (request, code) => {
52
55
  const params = {
53
56
  code,
54
- with_customer: 1
57
+ with_customer: 1,
58
+ with_franchisor_data: 1
55
59
  };
56
60
  try {
57
61
  const res = await request?.post('/wallet/detail/search', params);
58
- if (res?.code == 200 && !!res?.data) {
62
+ if (res?.status === true && res?.code === 200 && !!res?.data) {
59
63
  return {
60
64
  searchType: 'wallet',
61
65
  response: res
62
66
  };
63
67
  }
64
- throw new Error('没有找到对应的识别码');
68
+ throw new Error(res?.message || '没有找到对应的识别码');
65
69
  } catch (error) {
66
70
  throw error;
67
71
  }
@@ -6,6 +6,12 @@ export interface GlobalScanFormattedResult {
6
6
  searchType: GlobalScanSearchType;
7
7
  data: any;
8
8
  scanCode: string;
9
+ /** Wallet / Wallet Pass 来源客户 ID;跨店时必须先解析为当前店客户。 */
10
+ customerId?: string | number;
11
+ /** 仅 Wallet 返回;true 时禁止直接使用响应内的来源客户。 */
12
+ isCrossShopWallet?: boolean;
13
+ /** 总店 Wallet Pass 未共享给当前子店。 */
14
+ isWalletPassUnavailableAtStore?: boolean;
9
15
  }
10
16
  interface RawScanSearchResult {
11
17
  searchType?: GlobalScanSearchType;
@@ -36,14 +36,22 @@ function formatGlobalScanResult(raw, scanCode) {
36
36
  return {
37
37
  searchType,
38
38
  data: resultData?.customer,
39
- scanCode
39
+ scanCode,
40
+ customerId: resultData?.customer_id,
41
+ isCrossShopWallet: resultData?.is_cross_shop_wallet === true || Number(resultData?.is_cross_shop_wallet) === 1
40
42
  };
41
43
  }
42
44
  if (searchType === 'walletPass') {
45
+ const firstResult = resultData?.[0];
46
+ const isWalletPassUnavailableAtStore = firstResult?.is_franchisor_pass === 1 && firstResult?.franchisee_wallet_pass?.use !== 'enabled';
43
47
  return {
44
48
  searchType,
45
- data: resultData?.[0]?.customer,
46
- scanCode
49
+ data: firstResult?.customer,
50
+ scanCode,
51
+ customerId: firstResult?.customer_id ?? firstResult?.holder?.customer_id ?? firstResult?.customer?.id ?? firstResult?.customer?.customer_id,
52
+ ...(isWalletPassUnavailableAtStore ? {
53
+ isWalletPassUnavailableAtStore: true
54
+ } : {})
47
55
  };
48
56
  }
49
57
  if (searchType === 'product' || searchType === 'local_product') {
@@ -43,6 +43,7 @@ export declare class ShopDiscountImpl extends BaseModule implements Module {
43
43
  batchSearchByProductIds(productIds: number[]): Promise<Discount[]>;
44
44
  scanCode(code: string, customerId?: number): Promise<{
45
45
  isAvailable: boolean;
46
+ isAccepted?: boolean;
46
47
  productList: Record<string, any>[];
47
48
  discountList: Discount[];
48
49
  type: "server" | "clientCalc";
@@ -62,6 +63,7 @@ export declare class ShopDiscountImpl extends BaseModule implements Module {
62
63
  action?: 'create' | 'edit';
63
64
  with_good_pass?: 0 | 1;
64
65
  with_discount_card?: 0 | 1;
66
+ with_entitlement_pass?: 0 | 1;
65
67
  }): Promise<void>;
66
68
  }
67
69
  export default ShopDiscountImpl;