@pisell/pisellos 0.10.43 → 0.10.45

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 (118) 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 +9 -0
  8. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  9. package/dist/model/strategy/adapter/walletPass/evaluator.js +4 -2
  10. package/dist/modules/Customer/index.js +2 -1
  11. package/dist/modules/Discount/availability.d.ts +7 -0
  12. package/dist/modules/Discount/availability.js +49 -0
  13. package/dist/modules/Discount/index.d.ts +1 -3
  14. package/dist/modules/Discount/index.js +12 -151
  15. package/dist/modules/Discount/types.d.ts +6 -0
  16. package/dist/modules/Order/index.d.ts +41 -9
  17. package/dist/modules/Order/index.js +1457 -872
  18. package/dist/modules/Order/salesNotes.d.ts +31 -0
  19. package/dist/modules/Order/salesNotes.js +492 -0
  20. package/dist/modules/Order/types.d.ts +105 -8
  21. package/dist/modules/Order/types.js +6 -1
  22. package/dist/modules/Order/utils.d.ts +4 -2
  23. package/dist/modules/Order/utils.js +224 -79
  24. package/dist/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  25. package/dist/modules/Payment/cashRecommendationAlgorithm.js +17 -367
  26. package/dist/modules/Product/index.d.ts +1 -1
  27. package/dist/modules/ProductList/index.d.ts +2 -1
  28. package/dist/modules/ProductList/index.js +100 -20
  29. package/dist/modules/ProductList/types.d.ts +10 -0
  30. package/dist/modules/Rules/index.js +50 -16
  31. package/dist/server/index.d.ts +7 -0
  32. package/dist/server/index.js +45 -3
  33. package/dist/server/modules/order/index.js +6 -0
  34. package/dist/server/modules/order/types.d.ts +9 -0
  35. package/dist/server/utils/small-ticket.d.ts +1 -0
  36. package/dist/server/utils/small-ticket.js +142 -14
  37. package/dist/solution/BaseSales/index.d.ts +15 -1
  38. package/dist/solution/BaseSales/index.js +1240 -877
  39. package/dist/solution/BaseSales/types.d.ts +2 -2
  40. package/dist/solution/BookingByStep/index.d.ts +1 -1
  41. package/dist/solution/BookingTicket/index.d.ts +1 -0
  42. package/dist/solution/BookingTicket/index.js +37 -16
  43. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  44. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  45. package/dist/solution/RegisterAndLogin/config.js +95 -40
  46. package/dist/solution/RegisterAndLogin/index.js +4 -1
  47. package/dist/solution/ScanOrder/index.d.ts +1 -0
  48. package/dist/solution/ScanOrder/index.js +31 -27
  49. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  50. package/dist/solution/ScanOrder/utils.js +2 -0
  51. package/dist/solution/ShopDiscount/index.d.ts +1 -0
  52. package/dist/solution/ShopDiscount/index.js +59 -22
  53. package/dist/solution/ShopDiscount/utils.d.ts +4 -2
  54. package/dist/solution/ShopDiscount/utils.js +40 -21
  55. package/dist/solution/UnifiedBookingSales/index.d.ts +15 -2
  56. package/dist/solution/UnifiedBookingSales/index.js +777 -510
  57. package/dist/solution/UnifiedBookingSales/types.d.ts +14 -1
  58. package/dist/solution/VenueBooking/index.d.ts +1 -0
  59. package/dist/solution/VenueBooking/index.js +8 -4
  60. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  61. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  62. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  63. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  64. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  65. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  66. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  67. package/lib/model/strategy/adapter/walletPass/evaluator.js +2 -0
  68. package/lib/modules/Customer/index.js +1 -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/index.d.ts +1 -3
  72. package/lib/modules/Discount/index.js +10 -131
  73. package/lib/modules/Discount/types.d.ts +6 -0
  74. package/lib/modules/Order/index.d.ts +41 -9
  75. package/lib/modules/Order/index.js +456 -69
  76. package/lib/modules/Order/salesNotes.d.ts +31 -0
  77. package/lib/modules/Order/salesNotes.js +382 -0
  78. package/lib/modules/Order/types.d.ts +105 -8
  79. package/lib/modules/Order/types.js +5 -1
  80. package/lib/modules/Order/utils.d.ts +4 -2
  81. package/lib/modules/Order/utils.js +134 -3
  82. package/lib/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  83. package/lib/modules/Payment/cashRecommendationAlgorithm.js +9 -354
  84. package/lib/modules/Product/index.d.ts +1 -1
  85. package/lib/modules/ProductList/index.d.ts +2 -1
  86. package/lib/modules/ProductList/index.js +47 -2
  87. package/lib/modules/ProductList/types.d.ts +10 -0
  88. package/lib/modules/Rules/index.js +42 -5
  89. package/lib/server/index.d.ts +7 -0
  90. package/lib/server/index.js +39 -3
  91. package/lib/server/modules/order/index.js +6 -0
  92. package/lib/server/modules/order/types.d.ts +9 -0
  93. package/lib/server/utils/small-ticket.d.ts +1 -0
  94. package/lib/server/utils/small-ticket.js +136 -12
  95. package/lib/solution/BaseSales/index.d.ts +15 -1
  96. package/lib/solution/BaseSales/index.js +192 -22
  97. package/lib/solution/BaseSales/types.d.ts +2 -2
  98. package/lib/solution/BookingByStep/index.d.ts +1 -1
  99. package/lib/solution/BookingTicket/index.d.ts +1 -0
  100. package/lib/solution/BookingTicket/index.js +23 -5
  101. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  102. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  103. package/lib/solution/RegisterAndLogin/config.js +49 -8
  104. package/lib/solution/RegisterAndLogin/index.js +4 -1
  105. package/lib/solution/ScanOrder/index.d.ts +1 -0
  106. package/lib/solution/ScanOrder/index.js +5 -1
  107. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  108. package/lib/solution/ScanOrder/utils.js +1 -0
  109. package/lib/solution/ShopDiscount/index.d.ts +1 -0
  110. package/lib/solution/ShopDiscount/index.js +29 -3
  111. package/lib/solution/ShopDiscount/utils.d.ts +4 -2
  112. package/lib/solution/ShopDiscount/utils.js +39 -23
  113. package/lib/solution/UnifiedBookingSales/index.d.ts +15 -2
  114. package/lib/solution/UnifiedBookingSales/index.js +125 -1
  115. package/lib/solution/UnifiedBookingSales/types.d.ts +14 -1
  116. package/lib/solution/VenueBooking/index.d.ts +1 -0
  117. package/lib/solution/VenueBooking/index.js +5 -1
  118. package/package.json +1 -1
@@ -25,6 +25,15 @@ Object.keys(_types).forEach(function (key) {
25
25
  });
26
26
  });
27
27
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
28
+ function normalizeQuerySet(values) {
29
+ if (!Array.isArray(values) || values.length === 0) return null;
30
+ const normalized = values.map(value => String(value ?? '').trim()).filter(Boolean);
31
+ return normalized.length ? new Set(normalized) : null;
32
+ }
33
+ function matchesAny(values, querySet) {
34
+ if (!querySet) return true;
35
+ return values.some(value => querySet.has(String(value ?? '').trim()));
36
+ }
28
37
  class ProductList extends _BaseModule.BaseModule {
29
38
  defaultName = 'productList';
30
39
  defaultVersion = '1.0.0';
@@ -281,10 +290,46 @@ class ProductList extends _BaseModule.BaseModule {
281
290
  const product = this.store.list.find(product => product.id === id);
282
291
  return product ? (0, _lodashEs.cloneDeep)(product) : undefined;
283
292
  }
293
+ async queryProducts(params = {}) {
294
+ const includeIds = normalizeQuerySet(params.includeIds);
295
+ const excludeIds = normalizeQuerySet(params.excludeIds);
296
+ const categoryIds = normalizeQuerySet(params.categoryIds);
297
+ const collectionIds = normalizeQuerySet(params.collectionIds);
298
+ const extensionTypes = normalizeQuerySet(params.extensionTypes);
299
+ const statuses = normalizeQuerySet(params.statuses);
300
+ const channelApplications = normalizeQuerySet(params.channelApplications);
301
+ const codeOrBarcode = typeof params.codeOrBarcode === 'string' ? params.codeOrBarcode.trim() : '';
302
+ const products = (this.store.list || []).filter(product => {
303
+ const productId = String(product.id ?? '').trim();
304
+ if (includeIds && !includeIds.has(productId)) return false;
305
+ if (excludeIds && excludeIds.has(productId)) return false;
306
+ if (categoryIds) {
307
+ const productCategoryIds = (product.categories || []).flatMap(category => [category?.id, category?.pivot?.product_category_id]);
308
+ if (!matchesAny(productCategoryIds, categoryIds)) return false;
309
+ }
310
+ if (collectionIds) {
311
+ const productCollectionIds = (product.collection || []).map(collection => collection?.id);
312
+ if (!matchesAny(productCollectionIds, collectionIds)) return false;
313
+ }
314
+ if (extensionTypes && !extensionTypes.has(String(product.extension_type ?? '').trim())) return false;
315
+ if (statuses && !statuses.has(String(product.status ?? '').trim())) return false;
316
+ if (channelApplications) {
317
+ if (!matchesAny(product.channel_application || [], channelApplications)) return false;
318
+ }
319
+ if (codeOrBarcode) {
320
+ const code = typeof product.code === 'string' ? product.code.trim() : '';
321
+ const barcode = typeof product.barcode === 'string' ? product.barcode.trim() : '';
322
+ if (code !== codeOrBarcode && barcode !== codeOrBarcode) return false;
323
+ }
324
+ return true;
325
+ });
326
+ return (0, _lodashEs.cloneDeep)(products);
327
+ }
284
328
  async getProductByIds(ids) {
285
329
  await this.core.effects.emit(`${this.name}:onGetProductByIds`, this.store.list);
286
- const products = this.store.list.filter(product => ids.includes(product.id));
287
- return products;
330
+ return this.queryProducts({
331
+ includeIds: ids
332
+ });
288
333
  }
289
334
  async addProduct(products) {
290
335
  // list 需要根据 id 去重
@@ -15,6 +15,16 @@ export interface ProductListLoadProductsParams {
15
15
  /** 智能定价条件上下文,仅影响策略命中,不参与商品集合筛选 */
16
16
  strategy_context?: Record<string, any>;
17
17
  }
18
+ export interface ProductListStoreQueryParams {
19
+ includeIds?: Array<number | string>;
20
+ excludeIds?: Array<number | string>;
21
+ categoryIds?: Array<number | string>;
22
+ collectionIds?: Array<number | string>;
23
+ extensionTypes?: string[];
24
+ statuses?: ProductData['status'][];
25
+ channelApplications?: string[];
26
+ codeOrBarcode?: string;
27
+ }
18
28
  export interface ProductListData {
19
29
  list: ProductData[];
20
30
  selectProducts: ProductData[];
@@ -12,6 +12,7 @@ var _decimal = _interopRequireDefault(require("decimal.js"));
12
12
  var _lodashEs = require("lodash-es");
13
13
  var _dayjs = _interopRequireDefault(require("dayjs"));
14
14
  var _entitlementPass = require("../Discount/entitlementPass");
15
+ var _availability = require("../Discount/availability");
15
16
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
17
  class RulesModule extends _BaseModule.BaseModule {
17
18
  defaultName = 'rules';
@@ -240,7 +241,13 @@ class RulesModule extends _BaseModule.BaseModule {
240
241
  if (discount.isEditMode) {
241
242
  editModeDiscount.push(discount);
242
243
  } else {
243
- addModeDiscount.push(discount);
244
+ // 试算不能把 config / 不可用原因写回原有选券;每次重新计算以清除旧原因。
245
+ addModeDiscount.push({
246
+ ...discount,
247
+ usageLimitFailKey: (0, _availability.getDiscountUsageLimitFailKey)(discount),
248
+ reason: undefined,
249
+ reasonCode: (0, _availability.getDiscountUsageBlockReason)(discount)
250
+ });
244
251
  }
245
252
  });
246
253
 
@@ -481,8 +488,15 @@ class RulesModule extends _BaseModule.BaseModule {
481
488
 
482
489
  // 🔥 为每个折扣卡/商品券执行策略检查并附加config
483
490
  const evaluator = this.getWalletPassEvaluator();
491
+ addModeDiscount.forEach(discount => {
492
+ if (discount.reasonCode) discount.config = {
493
+ isAvailable: false
494
+ };
495
+ });
484
496
  if (evaluator) {
485
497
  addModeDiscount.forEach(discount => {
498
+ // 即使没有匹配的动态策略,服务端状态和次数限制也不能被覆盖为可用。
499
+ if (discount.reasonCode) return;
486
500
  const discountType = discount.tag || discount.type || '';
487
501
  // 对两类全额抵扣卡和折扣卡执行附加策略检查。
488
502
  if (['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType)) {
@@ -501,7 +515,7 @@ class RulesModule extends _BaseModule.BaseModule {
501
515
  balance: entitlementQuota?.type === 'unlimited' ? Number.MAX_SAFE_INTEGER : entitlementQuota?.type === 'limited' ? entitlementQuota.balance : Number(discount.balance || 0),
502
516
  type: discountType,
503
517
  product_id: discount.product_id,
504
- unified_available_status: 1,
518
+ unified_available_status: discount.unified_available_status ?? 1,
505
519
  available_product_type: discount.limited_relation_product_data?.type,
506
520
  available_product_ids: discount.limited_relation_product_data?.product_ids
507
521
  };
@@ -532,6 +546,8 @@ class RulesModule extends _BaseModule.BaseModule {
532
546
  discount.config = {
533
547
  isAvailable: false
534
548
  };
549
+ discount.reason = result.reason;
550
+ discount.reasonCode = result.reasonCode || 'not_meet_the_required_conditions';
535
551
  }
536
552
  }
537
553
  });
@@ -672,6 +688,8 @@ class RulesModule extends _BaseModule.BaseModule {
672
688
 
673
689
  // 记录每个优惠券适用的商品详细信息
674
690
  const discountApplicableProducts = new Map();
691
+ const discountTimeReasons = new Map();
692
+ const discountHolderMismatch = new Set();
675
693
 
676
694
  // 初始化每个优惠券的适用性和可抵扣商品
677
695
  addModeDiscount.forEach(discount => {
@@ -691,6 +709,7 @@ class RulesModule extends _BaseModule.BaseModule {
691
709
 
692
710
  // 辅助函数:检查商品是否对某个折扣卡可用
693
711
  const checkItemApplicableForDiscount = (flatItem, discount) => {
712
+ if (discount.config?.isAvailable === false) return false;
694
713
  let product;
695
714
  if (flatItem.type === 'main') {
696
715
  product = flatItem.product;
@@ -896,16 +915,23 @@ class RulesModule extends _BaseModule.BaseModule {
896
915
  isNeedHolder: resolveIsFormSubject(_tempVar) && !_tempVar?.isNormalProduct,
897
916
  holder_id: _tempVar?.holder_id || product.holder_id
898
917
  }, holders);
899
- let timeLimit = true;
900
- timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss')).length;
918
+ const timeReason = (0, _utils.getDiscountTimeUnavailableReason)(discount, (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss'));
919
+ const timeLimit = !timeReason;
901
920
  // 是符合折扣的商品
902
- const isLimitedProduct = (limitedData.type === 'product_all' && limitedData.filter !== 1 || limitedData.type === 'product_all' && limitedData.filter === 1 && !limitedData.exclude_product_ids.includes(product.id) || limitedData.product_ids && limitedData.product_ids.includes(product.id)) && isHolderMatch;
921
+ const isProductInScope = limitedData.type === 'product_all' && limitedData.filter !== 1 || limitedData.type === 'product_all' && limitedData.filter === 1 && !limitedData.exclude_product_ids.includes(product.id) || limitedData.product_ids && limitedData.product_ids.includes(product.id);
922
+ const isLimitedProduct = isProductInScope && isHolderMatch;
903
923
 
904
924
  // 编辑的商品 使用了优惠券不可用
905
925
  const isAvailableProduct = flatItem.type === 'main' ? !(product?.booking_id && product?.discount_list?.length && product?.discount_list?.every(discount => discount.id && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discount.tag || discount.type))) : !(flatItem?.booking_id && !!flatItem?.bundleItem?.discount_list?.length && flatItem?.bundleItem?.discount_list?.every(discount => discount.id));
906
926
 
907
927
  // 套餐是否可用判断
908
928
  const isBundleAvailable = this.checkPackageSubItemUsageRules(discount, flatItem);
929
+ if (isProductInScope && isBundleAvailable && !isItemRewardProductDiscount) {
930
+ const timeReasons = discountTimeReasons.get(discount.id) || [];
931
+ timeReasons.push(timeReason);
932
+ discountTimeReasons.set(discount.id, timeReasons);
933
+ if (!isHolderMatch && timeLimit) discountHolderMismatch.add(discount.id);
934
+ }
909
935
 
910
936
  // 判断优惠券是否适用于该商品
911
937
  if (isAvailableProduct && !isItemRewardProductDiscount && isLimitedProduct && timeLimit && isBundleAvailable && discount.config?.isAvailable && (!(0, _entitlementPass.isEntitlementPass)(discount) || (0, _entitlementPass.getRemainingEntitlementUses)(discount, 0, 1) > 0)) {
@@ -2028,11 +2054,19 @@ class RulesModule extends _BaseModule.BaseModule {
2028
2054
 
2029
2055
  // 根据是否有适用商品决定isAvailable
2030
2056
  const isAvailable = applicableProducts.length > 0;
2057
+ const timeReasons = discountTimeReasons.get(discount.id) || [];
2058
+ const timeReason = timeReasons.length ? timeReasons.every(Boolean) ? timeReasons[0] : undefined : (0, _utils.getDiscountTimeUnavailableReason)(discount, (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss'));
2059
+ const reasonCode = isAvailable ? undefined : (0, _availability.getDiscountUsageBlockReason)(discount) || timeReason || discount.reasonCode || (discountHolderMismatch.has(discount.id) ? 'holder_not_matched' : 'product_not_applicable');
2060
+ const availabilityReason = {
2061
+ reasonCode,
2062
+ reason: reasonCode && reasonCode === discount.reasonCode ? discount.reason : undefined
2063
+ };
2031
2064
 
2032
2065
  // 如果此优惠券被明确设置为不选中,保持其状态,但正确设置isAvailable
2033
2066
  if (typeof discount.isSelected === 'boolean' && discount.isSelected === false && discount.isManualSelect) {
2034
2067
  return {
2035
2068
  ...discount,
2069
+ ...availabilityReason,
2036
2070
  isSelected: false,
2037
2071
  isAvailable: isAvailable,
2038
2072
  // 正确设置可用性
@@ -2046,6 +2080,8 @@ class RulesModule extends _BaseModule.BaseModule {
2046
2080
  if (isDiscountUsed(discount)) {
2047
2081
  return {
2048
2082
  ...discount,
2083
+ reason: undefined,
2084
+ reasonCode: undefined,
2049
2085
  isSelected: true,
2050
2086
  isManualSelect: options?.scan ? false : discount.isManualSelect,
2051
2087
  // 标记为可用,因为它已被应用
@@ -2068,6 +2104,7 @@ class RulesModule extends _BaseModule.BaseModule {
2068
2104
  }
2069
2105
  return {
2070
2106
  ...discount,
2107
+ ...availabilityReason,
2071
2108
  // 如果有适用的商品,标记为可用
2072
2109
  isAvailable: isAvailable,
2073
2110
  isSelected,
@@ -421,6 +421,7 @@ declare class Server {
421
421
  private shouldUseLocalOrderQuery;
422
422
  /**
423
423
  * 预约查询是否走本地:
424
+ * - 日历周/月/资源视图通过 compact_fields 显式标识,始终走远端;
424
425
  * - 当 sales_time_between 的开始和结束都不是今天时,走远端;
425
426
  * - 其余情况走本地。
426
427
  */
@@ -524,6 +525,12 @@ declare class Server {
524
525
  private stringifyCheckoutResourceIdentifier;
525
526
  private normalizeCheckoutSubmitData;
526
527
  private ensureCheckoutOrderNumbers;
528
+ /**
529
+ * Notes 允许在本地草稿阶段暂存空订单目标;checkout 的统一出口负责用最终
530
+ * shop_order_number 完成绑定并校验,保证后端请求、pending、设备任务和打印
531
+ * 都使用同一份完整快照。
532
+ */
533
+ private finalizeCheckoutSalesNotes;
527
534
  private dispatchCheckoutSyncTask;
528
535
  private dispatchPrintOtherReceiptTask;
529
536
  private buildPrintOtherReceiptIdempotencyKey;
@@ -27,6 +27,7 @@ var _filterBookings = require("./modules/order/utils/filterBookings");
27
27
  var _smallTicket = require("./utils/small-ticket");
28
28
  var _BaseSales = require("../solution/BaseSales");
29
29
  var _paymentUtils = require("../modules/Order/payment-utils");
30
+ var _salesNotes = require("../modules/Order/salesNotes");
30
31
  var _orderCollectionIdentity = require("../modules/Order/utils/orderCollectionIdentity");
31
32
  var _osPrivatePayload = require("../utils/os-private-payload");
32
33
  var _OpenData = require("../modules/OpenData");
@@ -2172,10 +2173,12 @@ class Server {
2172
2173
 
2173
2174
  /**
2174
2175
  * 预约查询是否走本地:
2176
+ * - 日历周/月/资源视图通过 compact_fields 显式标识,始终走远端;
2175
2177
  * - 当 sales_time_between 的开始和结束都不是今天时,走远端;
2176
2178
  * - 其余情况走本地。
2177
2179
  */
2178
2180
  shouldUseLocalBookingQuery(data) {
2181
+ if (data?.compact_fields === true) return false;
2179
2182
  const range = data?.sales_time_between;
2180
2183
  if (!Array.isArray(range) || range.length < 2) return true;
2181
2184
  return !this.isRangeBothNotToday(range[0], range[1]);
@@ -3682,14 +3685,16 @@ class Server {
3682
3685
  };
3683
3686
  const needsShopOrderNumber = next.shop_order_number === undefined || next.shop_order_number === null || next.shop_order_number === '';
3684
3687
  const needsShopFullOrderNumber = next.shop_full_order_number === undefined || next.shop_full_order_number === null || next.shop_full_order_number === '';
3685
- if (!needsShopOrderNumber && !needsShopFullOrderNumber) return next;
3688
+ if (!needsShopOrderNumber && !needsShopFullOrderNumber) {
3689
+ return this.finalizeCheckoutSalesNotes(next, title);
3690
+ }
3686
3691
  const idGenerator = this.getIdGeneratorPlugin();
3687
3692
  if (!idGenerator) {
3688
3693
  this.logWarning(`${title}: idGenerator 插件不可用,无法生成订单号`, {
3689
3694
  needsShopOrderNumber,
3690
3695
  needsShopFullOrderNumber
3691
3696
  });
3692
- return next;
3697
+ return this.finalizeCheckoutSalesNotes(next, title);
3693
3698
  }
3694
3699
  try {
3695
3700
  if (needsShopOrderNumber && idGenerator.generateShopOrderNumber) {
@@ -3711,7 +3716,38 @@ class Server {
3711
3716
  error: error instanceof Error ? error.message : String(error)
3712
3717
  });
3713
3718
  }
3714
- return next;
3719
+ return this.finalizeCheckoutSalesNotes(next, title);
3720
+ }
3721
+
3722
+ /**
3723
+ * Notes 允许在本地草稿阶段暂存空订单目标;checkout 的统一出口负责用最终
3724
+ * shop_order_number 完成绑定并校验,保证后端请求、pending、设备任务和打印
3725
+ * 都使用同一份完整快照。
3726
+ */
3727
+ finalizeCheckoutSalesNotes(data, title) {
3728
+ if (!data || typeof data !== 'object') return data;
3729
+ if (!Object.prototype.hasOwnProperty.call(data, 'notes')) return data;
3730
+ if (data.notes === undefined || data.notes === null) return data;
3731
+ if (!Array.isArray(data.notes)) {
3732
+ throw new Error('[Order Notes] checkout notes 必须为数组');
3733
+ }
3734
+ if (data.notes.length === 0) return data;
3735
+ const productTargetUuids = (Array.isArray(data.products) ? data.products : []).map(product => product?.metadata?.unique_identification_number ?? product?.unique_identification_number).filter(targetUuid => targetUuid !== undefined && targetUuid !== null && String(targetUuid).trim().length > 0).map(targetUuid => String(targetUuid));
3736
+ const shopOrderNumber = String(data.shop_order_number ?? '').trim();
3737
+ const notes = (0, _salesNotes.finalizeSalesNotesForCheckout)({
3738
+ notes: data.notes,
3739
+ shopOrderNumber,
3740
+ productTargetUuids
3741
+ });
3742
+ this.logInfo(`${title}: checkout Notes 已绑定订单号`, {
3743
+ shop_order_number: shopOrderNumber,
3744
+ note_count: notes.length
3745
+ });
3746
+ return {
3747
+ ...data,
3748
+ shop_order_number: shopOrderNumber,
3749
+ notes
3750
+ };
3715
3751
  }
3716
3752
  async dispatchCheckoutSyncTask(params) {
3717
3753
  this.registerDeviceTaskActions();
@@ -2017,6 +2017,12 @@ class OrderModule extends _BaseModule.BaseModule {
2017
2017
  mergedRecord[field] = (0, _lodashEs.cloneDeep)(incomingRecord[field]);
2018
2018
  }
2019
2019
 
2020
+ // order_meta_list 是一次性生成的完整业务快照。incoming 明确携带时
2021
+ // 必须整体覆盖(包括 null / 空对象 / 空数组);省略时才保留 existing。
2022
+ if (Object.prototype.hasOwnProperty.call(incomingRecord, 'order_meta_list')) {
2023
+ mergedRecord.order_meta_list = (0, _lodashEs.cloneDeep)(incomingRecord.order_meta_list);
2024
+ }
2025
+
2020
2026
  // 子项数组遵循完整快照语义:incoming 自有且为数组(包括 [])时权威替换;
2021
2027
  // 字段缺席或不是数组时视为稀疏 patch,继续保留 existing。
2022
2028
  for (const field of ORDER_AUTHORITATIVE_COLLECTION_FIELDS) {
@@ -456,6 +456,15 @@ export interface OrderData {
456
456
  par_value: string;
457
457
  used_par_value: string;
458
458
  }>;
459
+ virtual_currency_snapshot?: Array<{
460
+ id: number | string;
461
+ tag: 'virtual_currency';
462
+ product_id?: number | string;
463
+ product_name: string;
464
+ par_value: string;
465
+ used_par_value: string;
466
+ name: Record<string, string>;
467
+ }>;
459
468
  };
460
469
  /**
461
470
  * 金额汇总(仅输出)。
@@ -45,6 +45,7 @@ export interface SmallTicketProduct {
45
45
  product_uid?: string | number;
46
46
  booking_uid?: string | number;
47
47
  product_note?: string;
48
+ allergy?: string;
48
49
  options?: Array<{
49
50
  name: string;
50
51
  num?: number;
@@ -311,6 +311,7 @@ function buildBaseData(params) {
311
311
  }
312
312
  function buildProducts(order, locale, productMap) {
313
313
  const products = Array.isArray(order.products) ? order.products : [];
314
+ const allergyByProductUid = buildProductAllergyMap(order, locale);
314
315
  return products.map(rawProduct => {
315
316
  const product = restoreReceiptDiscountHierarchy(rawProduct);
316
317
  const quantity = toNumber(product.product_quantity ?? product.quantity ?? product.num ?? 1, 1);
@@ -351,6 +352,7 @@ function buildProducts(order, locale, productMap) {
351
352
  originalPrice
352
353
  });
353
354
  const productNote = resolveProductNote(product, locale);
355
+ const allergy = isPresentId(productUid) ? allergyByProductUid.get(String(productUid).trim()) || '' : '';
354
356
  const basePrice = resolveProductBasePrice(product);
355
357
  const resolvedCombinations = Array.isArray(product.combinations) && product.combinations.length ? normalizeExistingCombinations(product.combinations, locale, currencySymbol) : combinations;
356
358
  const shouldIncludeBasePrice = options.length > 0 || resolvedCombinations.length > 0 || hasProductDiscount(product, extensionList);
@@ -380,6 +382,9 @@ function buildProducts(order, locale, productMap) {
380
382
  ...(productNote ? {
381
383
  product_note: productNote
382
384
  } : {}),
385
+ ...(allergy ? {
386
+ allergy
387
+ } : {}),
383
388
  ...(options.length ? {
384
389
  options
385
390
  } : {}),
@@ -782,6 +787,21 @@ function resolveProductNote(product, locale) {
782
787
  if (productNote) return productNote;
783
788
  return getLocalizedValue(product.note, locale).trim();
784
789
  }
790
+ function buildProductAllergyMap(order, locale) {
791
+ const allergyByProductUid = new Map();
792
+ for (const note of normalizeArray(order.notes)) {
793
+ if (!note || typeof note !== 'object' || Array.isArray(note) || String(note.note_type || '').trim() !== 'allergy') {
794
+ continue;
795
+ }
796
+ const content = getLocalizedValue(note.content?.text, locale).trim();
797
+ if (!content) continue;
798
+ const relation = normalizeArray(note.note_relations).find(item => item && typeof item === 'object' && !Array.isArray(item) && item.relation_type === 'about' && item.target_type === 'order_detail' && Number(item.is_primary) === 1);
799
+ const productUid = String(relation?.target_uuid ?? '').trim();
800
+ if (!productUid || allergyByProductUid.has(productUid)) continue;
801
+ allergyByProductUid.set(productUid, content);
802
+ }
803
+ return allergyByProductUid;
804
+ }
785
805
  function resolveBundleProductTitle(params) {
786
806
  const {
787
807
  bundle,
@@ -1358,20 +1378,30 @@ function resolveRefundPayment(refund, payments) {
1358
1378
  return matchedPayment || refund;
1359
1379
  }
1360
1380
  function buildWallets(params) {
1381
+ const virtualCurrencySnapshots = collectVirtualCurrencySnapshots(params.order);
1361
1382
  const displayedWalletPayments = normalizeArray(params.order.payments).filter(payment => isPrintablePayment(payment) && !!getFundRecord(payment));
1362
- const paymentWallets = displayedWalletPayments.map(payment => buildPaymentItems({
1363
- payment,
1364
- locale: params.locale,
1365
- currencySymbol: params.currencySymbol,
1366
- includeFundRecordBalance: true,
1367
- includeFundRecordUsage: false,
1368
- includePaymentAmount: false
1369
- }));
1383
+ const paymentWallets = displayedWalletPayments.map(payment => {
1384
+ const virtualCurrencySnapshot = findVirtualCurrencySnapshotForPayment(payment, virtualCurrencySnapshots.byId);
1385
+ return buildPaymentItems({
1386
+ payment,
1387
+ locale: params.locale,
1388
+ currencySymbol: params.currencySymbol,
1389
+ includeFundRecordBalance: true,
1390
+ includeFundRecordUsage: false,
1391
+ includePaymentAmount: false,
1392
+ virtualCurrencyUnit: virtualCurrencySnapshot ? getVirtualCurrencySnapshotUnit(virtualCurrencySnapshot, params.locale) : undefined
1393
+ });
1394
+ });
1370
1395
  const displayedWalletPassIds = collectDisplayedWalletPassIds(displayedWalletPayments);
1371
1396
  return [...paymentWallets, ...buildVoucherAssets(params), ...buildPointCardSnapshotAssets({
1372
1397
  order: params.order,
1373
1398
  locale: params.locale,
1374
1399
  displayedWalletPassIds
1400
+ }), ...buildVirtualCurrencySnapshotAssets({
1401
+ snapshots: virtualCurrencySnapshots.items,
1402
+ order: params.order,
1403
+ locale: params.locale,
1404
+ displayedWalletPassIds
1375
1405
  })];
1376
1406
  }
1377
1407
  function collectDisplayedWalletPassIds(payments) {
@@ -1418,6 +1448,88 @@ function buildPointCardSnapshotAssets(params) {
1418
1448
  return [rows];
1419
1449
  });
1420
1450
  }
1451
+ function collectVirtualCurrencySnapshots(order) {
1452
+ const items = [];
1453
+ const byId = new Map();
1454
+ const snapshots = order.order_meta_list?.virtual_currency_snapshot;
1455
+ if (!Array.isArray(snapshots)) return {
1456
+ items,
1457
+ byId
1458
+ };
1459
+ snapshots.forEach(snapshot => {
1460
+ if (!snapshot || typeof snapshot !== 'object' || Array.isArray(snapshot)) return;
1461
+ const virtualCurrency = snapshot;
1462
+ if (stringify(virtualCurrency.tag).trim().toLowerCase() !== 'virtual_currency') return;
1463
+ const id = normalizeWalletPassId(virtualCurrency.id);
1464
+ if (!id || byId.has(id)) return;
1465
+ byId.set(id, virtualCurrency);
1466
+ items.push(virtualCurrency);
1467
+ });
1468
+ return {
1469
+ items,
1470
+ byId
1471
+ };
1472
+ }
1473
+ function findVirtualCurrencySnapshotForPayment(payment, snapshotsById) {
1474
+ const fundRecord = getFundRecord(payment);
1475
+ for (const value of [fundRecord?.voucher_id, payment.voucher_id]) {
1476
+ const id = normalizeWalletPassId(value);
1477
+ if (!id) continue;
1478
+ const snapshot = snapshotsById.get(id);
1479
+ if (snapshot) return snapshot;
1480
+ }
1481
+ return undefined;
1482
+ }
1483
+ function buildVirtualCurrencySnapshotAssets(params) {
1484
+ const snapshotTime = firstPresentValue(params.order.updated_at, params.order.created_at);
1485
+ return params.snapshots.flatMap(virtualCurrency => {
1486
+ const id = normalizeWalletPassId(virtualCurrency.id);
1487
+ if (!id || params.displayedWalletPassIds.has(id)) return [];
1488
+ const rows = [{
1489
+ item: stringify(virtualCurrency.product_name).trim(),
1490
+ value: ''
1491
+ }];
1492
+ const balance = getVirtualCurrencySnapshotBalance(virtualCurrency);
1493
+ if (balance) {
1494
+ rows.push({
1495
+ item: label(params.locale, 'balance'),
1496
+ value: formatVirtualCurrencyBalance(balance, getVirtualCurrencySnapshotUnit(virtualCurrency, params.locale))
1497
+ });
1498
+ }
1499
+ if (isPresentId(snapshotTime)) {
1500
+ rows.push({
1501
+ item: formatDateTime(snapshotTime, params.locale),
1502
+ value: ''
1503
+ });
1504
+ }
1505
+ return [rows];
1506
+ });
1507
+ }
1508
+ function getVirtualCurrencySnapshotBalance(snapshot) {
1509
+ if (!isNumericAmount(snapshot.par_value) || !isNumericAmount(snapshot.used_par_value)) {
1510
+ return null;
1511
+ }
1512
+ return _decimal.default.max(new _decimal.default(snapshot.par_value).minus(new _decimal.default(snapshot.used_par_value)), 0);
1513
+ }
1514
+ function getVirtualCurrencySnapshotUnit(snapshot, locale) {
1515
+ return getNonEmptyLocalizedValue(snapshot.name, locale);
1516
+ }
1517
+ function getNonEmptyLocalizedValue(value, locale) {
1518
+ if (value === undefined || value === null) return '';
1519
+ if (typeof value !== 'object' || Array.isArray(value)) return stringify(value).trim();
1520
+ const dashedLocale = locale.replace('_', '-');
1521
+ const underscoredLocale = locale.replace('-', '_');
1522
+ for (const candidate of [value[locale], value[dashedLocale], value[underscoredLocale], value.original, value.auto, value.default, value.en]) {
1523
+ const text = stringify(candidate).trim();
1524
+ if (text) return text;
1525
+ }
1526
+ return '';
1527
+ }
1528
+ function formatVirtualCurrencyBalance(value, unit) {
1529
+ const amount = trimDecimal(value);
1530
+ const normalizedUnit = unit.trim();
1531
+ return normalizedUnit ? `${amount} ${normalizedUnit}` : amount;
1532
+ }
1421
1533
  function normalizeWalletPassId(value) {
1422
1534
  if (typeof value === 'number') return Number.isFinite(value) ? String(value) : null;
1423
1535
  if (typeof value !== 'string') return null;
@@ -1470,8 +1582,9 @@ function buildVoucherAssets(params) {
1470
1582
  }, ...(timeRow ? [timeRow] : [])]];
1471
1583
  });
1472
1584
  }
1585
+ const TICKET_VOUCHER_TAGS = new Set(['ticket', 'product_package_ticket', 'appointment_ticket', 'session_ticket']);
1473
1586
  function isTicketVoucher(voucher) {
1474
- return stringify(voucher.tag).trim().toLowerCase() === 'ticket';
1587
+ return TICKET_VOUCHER_TAGS.has(stringify(voucher.tag).trim().toLowerCase());
1475
1588
  }
1476
1589
  function getVoucherValidity(voucher, locale) {
1477
1590
  const availableTime = firstPresentValue(voucher.available_time, voucher.avaliable_time, voucher.metadata?.available_time, voucher.metadata?.avaliable_time);
@@ -1583,7 +1696,8 @@ function buildPaymentItems(params) {
1583
1696
  currencySymbol,
1584
1697
  includeFundRecordBalance,
1585
1698
  includeFundRecordUsage,
1586
- includePaymentAmount
1699
+ includePaymentAmount,
1700
+ virtualCurrencyUnit
1587
1701
  } = params;
1588
1702
  const serviceFee = getPaymentServiceFee(payment);
1589
1703
  const items = [{
@@ -1609,7 +1723,8 @@ function buildPaymentItems(params) {
1609
1723
  locale,
1610
1724
  currencySymbol,
1611
1725
  includeBalance: includeFundRecordBalance,
1612
- includeUsage: includeFundRecordUsage
1726
+ includeUsage: includeFundRecordUsage,
1727
+ virtualCurrencyUnit
1613
1728
  }));
1614
1729
  } else {
1615
1730
  const remainingAmount = payment.remaining_amount ?? payment.balance ?? payment.metadata?.remaining_amount;
@@ -1674,7 +1789,8 @@ function buildFundRecordPaymentItems(params) {
1674
1789
  locale,
1675
1790
  currencySymbol,
1676
1791
  includeBalance,
1677
- includeUsage
1792
+ includeUsage,
1793
+ virtualCurrencyUnit
1678
1794
  } = params;
1679
1795
  const items = [];
1680
1796
  const usingBeforeValue = fundRecord.using_before_value;
@@ -1696,6 +1812,14 @@ function buildFundRecordPaymentItems(params) {
1696
1812
  return items;
1697
1813
  }
1698
1814
  if (includeBalance && usingAfterValue !== undefined && usingAfterValue !== null) {
1815
+ if (virtualCurrencyUnit !== undefined) {
1816
+ if (!isNumericAmount(usingAfterValue)) return items;
1817
+ items.push({
1818
+ item: label(locale, 'balance'),
1819
+ value: formatVirtualCurrencyBalance(new _decimal.default(usingAfterValue), virtualCurrencyUnit)
1820
+ });
1821
+ return items;
1822
+ }
1699
1823
  items.push({
1700
1824
  item: label(locale, 'balance'),
1701
1825
  value: formatMoney(usingAfterValue, currencySymbol)
@@ -2,7 +2,7 @@ import { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import { BaseModule } from '../../modules/BaseModule';
3
3
  import { BaseSalesOrderProduct, BaseSalesOrderProductIdentity, BaseSalesPaymentStatus, BaseSalesUpdateOrderProductQuantityParams, BaseSalesCalculateProductBookingPriceParams, BaseSalesPriceRecalculationContextChange, BaseSalesProductBookingPriceResult, BaseSalesQuotationCustomerScopeInfo, BaseSalesScanCodeResult, BaseSalesCashPaymentConfig, BaseSalesPayCashParams, BaseSalesPayCashResult, BaseSalesCommitPaymentMethodParams, BaseSalesCommitPaymentMethodResult, BaseSalesConfirmWalletPaymentResult } from './types';
4
4
  import { OrderModule } from '../../modules/Order';
5
- import type { AddProductBookingInput, AddProductToOrderOptions, LoadSalesDetailParams, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, OrderPaymentSource, SendCustomerPayLinkParams, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, UpdateTempOrderCustomerInput, UpdateOrderBookingParams, UpdateOrderProductParams, OrderTempOrder, OrderSummary, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, ReplaceTempOrderOptions } from '../../modules/Order/types';
5
+ import type { AddProductBookingInput, AddProductToOrderOptions, LoadSalesDetailParams, OrderPaymentData, OrderPaymentUpdateOptions, OrderPaymentUpdateResult, OrderPaymentSource, SendCustomerPayLinkParams, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, UpdateTempOrderCustomerInput, UpdateOrderBookingParams, UpdateOrderProductParams, OrderTempOrder, OrderSummary, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions, RemoveProductsFromOrderOptions, ReplaceTempOrderOptions, SalesNote, SalesNotesMutationSnapshot, SetSalesNoteInput, PickupReferenceMode, ShopServiceData, ShopServiceType } from '../../modules/Order/types';
6
6
  import type { SubmitPayloadEnhancer } from '../../modules/Order/utils';
7
7
  import type { Discount, VirtualCurrencyAsset } from '../../modules/Discount/types';
8
8
  import { RequestPlugin, WindowPlugin } from '../../plugins';
@@ -232,6 +232,12 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
232
232
  getOrderSyncState(): import("../../modules/Order/types").OrderSyncState;
233
233
  getOrderAmountSnapshot(): import("../../modules/Order/types").OrderAmountSnapshot | null;
234
234
  getTempOrderNote(): string;
235
+ getSalesNotes(): SalesNote[];
236
+ createSalesNotesMutationSnapshot(): SalesNotesMutationSnapshot;
237
+ restoreSalesNotesMutationSnapshot(snapshot: SalesNotesMutationSnapshot): void;
238
+ setSalesNote(input: SetSalesNoteInput): Promise<SalesNote>;
239
+ removeSalesNote(uuid: string): Promise<void>;
240
+ clearSalesNotes(): Promise<void>;
235
241
  updateTempOrderNote(note: string): string;
236
242
  updateTempOrderNote(note: string, sync: true): Promise<string>;
237
243
  updateTempOrderNote(note: string, sync: boolean): string | Promise<string>;
@@ -240,6 +246,8 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
240
246
  setPickupRef(buzzer: string): string;
241
247
  updateTempOrderShopDiscount(amount: string | number): string;
242
248
  updateTempOrderDepositAmount(amount: string | number): OrderSummary;
249
+ setPickupReferenceMode(mode: PickupReferenceMode): ShopServiceData;
250
+ setServiceType(serviceType: ShopServiceType): ShopServiceData;
243
251
  updateTempOrderContactsInfo(contactsInfo: Record<string, any> | null): Record<string, any> | null;
244
252
  /** @deprecated OrderModule 不再负责 tempOrder localStorage 持久化。 */
245
253
  setEnableTempOrderPersist(enabled: boolean): void;
@@ -263,6 +271,12 @@ export declare class BaseSalesImpl extends BaseModule implements Module {
263
271
  private mergeHistoricalDiscountList;
264
272
  private shouldSkipAutoApplyFetchedDiscountsInEditMode;
265
273
  private mergeCurrentDiscountSelectionState;
274
+ private resolveDiscountConfigCustomerId;
275
+ /**
276
+ * Stable across notifyTempOrderChanged's immutable object replacement, while
277
+ * still distinguishing two local orders for the same customer.
278
+ */
279
+ private getDiscountConfigOrderIdentityKey;
266
280
  loadDiscountConfig(params?: {
267
281
  customerId?: number;
268
282
  action?: 'create' | 'edit';