@pisell/pisellos 2.2.302 → 2.2.304

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 (157) hide show
  1. package/dist/model/strategy/adapter/dataVariant/evaluator.d.ts +18 -2
  2. package/dist/model/strategy/adapter/dataVariant/evaluator.js +287 -101
  3. package/dist/model/strategy/adapter/dataVariant/type.d.ts +25 -0
  4. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  5. package/dist/model/strategy/adapter/itemRule/adapter.js +135 -35
  6. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  7. package/dist/model/strategy/adapter/itemRule/evaluator.js +25 -1
  8. package/dist/model/strategy/adapter/itemRule/type.d.ts +44 -0
  9. package/dist/model/strategy/adapter/itemRule/type.js +19 -1
  10. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  11. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +10 -7
  12. package/dist/modules/Discount/index.d.ts +2 -0
  13. package/dist/modules/Discount/index.js +41 -5
  14. package/dist/modules/Discount/types.d.ts +21 -0
  15. package/dist/modules/OpenData/index.d.ts +15 -2
  16. package/dist/modules/OpenData/index.js +307 -19
  17. package/dist/modules/OpenData/types.d.ts +55 -0
  18. package/dist/modules/OpenData/types.js +7 -1
  19. package/dist/modules/OpenData/utils.d.ts +21 -1
  20. package/dist/modules/OpenData/utils.js +138 -0
  21. package/dist/modules/Order/index.d.ts +50 -7
  22. package/dist/modules/Order/index.js +1690 -815
  23. package/dist/modules/Order/salesNotes.d.ts +31 -0
  24. package/dist/modules/Order/salesNotes.js +492 -0
  25. package/dist/modules/Order/types.d.ts +117 -10
  26. package/dist/modules/Order/types.js +6 -1
  27. package/dist/modules/Order/utils/virtualSettlement.d.ts +63 -0
  28. package/dist/modules/Order/utils/virtualSettlement.js +284 -0
  29. package/dist/modules/Order/utils.d.ts +5 -1
  30. package/dist/modules/Order/utils.js +138 -33
  31. package/dist/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  32. package/dist/modules/Payment/cashRecommendationAlgorithm.js +17 -367
  33. package/dist/modules/Payment/walletpass.js +20 -21
  34. package/dist/modules/Product/types.d.ts +23 -0
  35. package/dist/modules/ProductList/index.d.ts +2 -1
  36. package/dist/modules/ProductList/index.js +100 -20
  37. package/dist/modules/ProductList/types.d.ts +10 -0
  38. package/dist/modules/Rules/index.d.ts +5 -0
  39. package/dist/modules/Rules/index.js +30 -14
  40. package/dist/modules/SalesSummary/index.d.ts +1 -0
  41. package/dist/modules/SalesSummary/index.js +4 -3
  42. package/dist/modules/SalesSummary/types.d.ts +1 -0
  43. package/dist/modules/SalesSummary/utils.d.ts +1 -0
  44. package/dist/modules/SalesSummary/utils.js +21 -1
  45. package/dist/plugins/request.d.ts +2 -1
  46. package/dist/plugins/request.js +4 -2
  47. package/dist/server/index.d.ts +21 -0
  48. package/dist/server/index.js +1774 -1285
  49. package/dist/server/modules/order/types.d.ts +50 -0
  50. package/dist/server/modules/order/types.js +2 -0
  51. package/dist/server/utils/small-ticket.d.ts +4 -1
  52. package/dist/server/utils/small-ticket.js +461 -96
  53. package/dist/solution/BaseSales/index.d.ts +24 -3
  54. package/dist/solution/BaseSales/index.js +1484 -873
  55. package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +2 -0
  56. package/dist/solution/BaseSales/utils/parseSalesResponse.js +1 -1
  57. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -0
  58. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +72 -9
  59. package/dist/solution/BookingByStep/index.d.ts +1 -1
  60. package/dist/solution/BookingTicket/index.d.ts +22 -5
  61. package/dist/solution/BookingTicket/index.js +558 -443
  62. package/dist/solution/BookingTicket/utils/addProductDecision.js +2 -1
  63. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  64. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  65. package/dist/solution/RegisterAndLogin/config.js +95 -40
  66. package/dist/solution/RegisterAndLogin/index.js +4 -1
  67. package/dist/solution/Sales/index.d.ts +16 -1
  68. package/dist/solution/Sales/index.js +338 -63
  69. package/dist/solution/Sales/types.d.ts +10 -0
  70. package/dist/solution/ScanOrder/index.d.ts +1 -0
  71. package/dist/solution/ScanOrder/index.js +31 -27
  72. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  73. package/dist/solution/ScanOrder/utils.js +2 -0
  74. package/dist/solution/UnifiedBookingSales/index.d.ts +20 -6
  75. package/dist/solution/UnifiedBookingSales/index.js +874 -527
  76. package/dist/solution/UnifiedBookingSales/types.d.ts +21 -2
  77. package/dist/solution/VenueBooking/index.d.ts +1 -0
  78. package/dist/solution/VenueBooking/index.js +8 -4
  79. package/lib/model/strategy/adapter/dataVariant/evaluator.d.ts +18 -2
  80. package/lib/model/strategy/adapter/dataVariant/evaluator.js +137 -6
  81. package/lib/model/strategy/adapter/dataVariant/type.d.ts +25 -0
  82. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  83. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  84. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  85. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  86. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  87. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  88. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  89. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
  90. package/lib/modules/Discount/index.d.ts +2 -0
  91. package/lib/modules/Discount/index.js +34 -2
  92. package/lib/modules/Discount/types.d.ts +21 -0
  93. package/lib/modules/OpenData/index.d.ts +15 -2
  94. package/lib/modules/OpenData/index.js +230 -3
  95. package/lib/modules/OpenData/types.d.ts +55 -0
  96. package/lib/modules/OpenData/types.js +9 -1
  97. package/lib/modules/OpenData/utils.d.ts +21 -1
  98. package/lib/modules/OpenData/utils.js +126 -0
  99. package/lib/modules/Order/index.d.ts +50 -7
  100. package/lib/modules/Order/index.js +704 -64
  101. package/lib/modules/Order/salesNotes.d.ts +31 -0
  102. package/lib/modules/Order/salesNotes.js +382 -0
  103. package/lib/modules/Order/types.d.ts +117 -10
  104. package/lib/modules/Order/types.js +5 -1
  105. package/lib/modules/Order/utils/virtualSettlement.d.ts +63 -0
  106. package/lib/modules/Order/utils/virtualSettlement.js +262 -0
  107. package/lib/modules/Order/utils.d.ts +5 -1
  108. package/lib/modules/Order/utils.js +127 -21
  109. package/lib/modules/Payment/cashRecommendationAlgorithm.d.ts +4 -5
  110. package/lib/modules/Payment/cashRecommendationAlgorithm.js +9 -354
  111. package/lib/modules/Payment/walletpass.js +14 -11
  112. package/lib/modules/Product/types.d.ts +23 -0
  113. package/lib/modules/ProductList/index.d.ts +2 -1
  114. package/lib/modules/ProductList/index.js +47 -2
  115. package/lib/modules/ProductList/types.d.ts +10 -0
  116. package/lib/modules/Rules/index.d.ts +5 -0
  117. package/lib/modules/Rules/index.js +22 -12
  118. package/lib/modules/SalesSummary/index.d.ts +1 -0
  119. package/lib/modules/SalesSummary/index.js +4 -2
  120. package/lib/modules/SalesSummary/types.d.ts +1 -0
  121. package/lib/modules/SalesSummary/utils.d.ts +1 -0
  122. package/lib/modules/SalesSummary/utils.js +17 -1
  123. package/lib/plugins/request.d.ts +2 -1
  124. package/lib/plugins/request.js +3 -2
  125. package/lib/server/index.d.ts +21 -0
  126. package/lib/server/index.js +492 -108
  127. package/lib/server/modules/order/types.d.ts +50 -0
  128. package/lib/server/modules/order/types.js +1 -0
  129. package/lib/server/utils/small-ticket.d.ts +4 -1
  130. package/lib/server/utils/small-ticket.js +427 -72
  131. package/lib/solution/BaseSales/index.d.ts +24 -3
  132. package/lib/solution/BaseSales/index.js +433 -30
  133. package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +2 -0
  134. package/lib/solution/BaseSales/utils/parseSalesResponse.js +1 -1
  135. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -0
  136. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +38 -0
  137. package/lib/solution/BookingByStep/index.d.ts +1 -1
  138. package/lib/solution/BookingTicket/index.d.ts +22 -5
  139. package/lib/solution/BookingTicket/index.js +60 -15
  140. package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -0
  141. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  142. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  143. package/lib/solution/RegisterAndLogin/config.js +49 -8
  144. package/lib/solution/RegisterAndLogin/index.js +4 -1
  145. package/lib/solution/Sales/index.d.ts +16 -1
  146. package/lib/solution/Sales/index.js +168 -1
  147. package/lib/solution/Sales/types.d.ts +10 -0
  148. package/lib/solution/ScanOrder/index.d.ts +1 -0
  149. package/lib/solution/ScanOrder/index.js +5 -1
  150. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  151. package/lib/solution/ScanOrder/utils.js +1 -0
  152. package/lib/solution/UnifiedBookingSales/index.d.ts +20 -6
  153. package/lib/solution/UnifiedBookingSales/index.js +169 -16
  154. package/lib/solution/UnifiedBookingSales/types.d.ts +21 -2
  155. package/lib/solution/VenueBooking/index.d.ts +1 -0
  156. package/lib/solution/VenueBooking/index.js +5 -1
  157. package/package.json +1 -1
@@ -9,6 +9,7 @@ var _types = require("./types");
9
9
  var _utils = require("./utils");
10
10
  var _utils2 = require("../Product/utils");
11
11
  var _dayjs = _interopRequireDefault(require("dayjs"));
12
+ var _salesNotes = require("./salesNotes");
12
13
  var _paymentUtils = require("./payment-utils");
13
14
  var _paymentNumber = require("../../utils/payment-number");
14
15
  var _cartPromotion = require("../../solution/BaseSales/utils/cartPromotion");
@@ -17,6 +18,7 @@ var _manualProductDiscount = require("./utils/manualProductDiscount");
17
18
  var _bundleDiscountHydration = require("./utils/bundleDiscountHydration");
18
19
  var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
19
20
  var _discountProductLineIdentity = require("./utils/discountProductLineIdentity");
21
+ var _virtualSettlement = require("./utils/virtualSettlement");
20
22
  var _Discount = require("../Discount");
21
23
  var _Rules = require("../Rules");
22
24
  var _types2 = require("../Rules/types");
@@ -70,6 +72,13 @@ function getVoucherPaymentLifecycleKey(payment) {
70
72
  if (!isVoucherPaymentRecord(payment)) return null;
71
73
  return `${String(payment.voucher_id)}::${String(payment.status || '')}`;
72
74
  }
75
+ function hasPersistedOrderId(orderId) {
76
+ if (orderId === undefined || orderId === null || orderId === '') return false;
77
+ if (typeof orderId === 'string' && orderId.startsWith('LOCAL_')) return false;
78
+ const normalizedOrderId = Number(orderId);
79
+ if (Number.isFinite(normalizedOrderId)) return normalizedOrderId > 0;
80
+ return String(orderId).trim() !== '';
81
+ }
73
82
  class OrderModule extends _BaseModule.BaseModule {
74
83
  defaultName = 'order';
75
84
  defaultVersion = '1.0.0';
@@ -93,8 +102,10 @@ class OrderModule extends _BaseModule.BaseModule {
93
102
  promotionEvaluator = null;
94
103
  /** 赠品选择 resolver;由 SDK host bridge 注入 */
95
104
  giftSelectResolver = null;
96
- /** applyPromotion 递归保护 flag(写路径同步调 applyPromotion,禁止重入) */
105
+ /** 促销评估递归保护 flag(写路径同步评估促销,禁止重入) */
97
106
  isApplyingPromotion = false;
107
+ /** 保持旧事件语义:同步事件监听器内触发的订单写入不得递归评估促销。 */
108
+ isEmittingPromotionApplied = false;
98
109
  /** 商品曾应用过客户券卡,客户变更剥离后需强制跑一次 calcDiscount 复位价格 */
99
110
  hasActiveCustomerBoundDiscount = false;
100
111
  /** 最近一次 applyPromotion 的未满足促销提示 */
@@ -260,12 +271,12 @@ class OrderModule extends _BaseModule.BaseModule {
260
271
  product.product_bundle = product.product_bundle.map(bundle => {
261
272
  const hasBundleDiscount = Array.isArray(bundle?.discount_list) && bundle.discount_list.length > 0;
262
273
  if (hasBundleDiscount) return bundle;
263
- const restoredPrice = bundle?.original_price ?? bundle?.product_price ?? bundle?.price;
274
+ const restoredPrice = bundle?.price ?? bundle?.original_price ?? bundle?.product_price;
264
275
  if (restoredPrice === undefined || restoredPrice === null || restoredPrice === '') return bundle;
265
276
  return {
266
277
  ...bundle,
267
278
  price: restoredPrice,
268
- bundle_selling_price: restoredPrice
279
+ bundle_selling_price: bundle?.bundle_selling_price ?? restoredPrice
269
280
  };
270
281
  });
271
282
  }
@@ -297,6 +308,7 @@ class OrderModule extends _BaseModule.BaseModule {
297
308
  this.store.tempOrder = null;
298
309
  this.store.summary = (0, _utils.createEmptySummary)();
299
310
  this.store.availableWalletIds = [];
311
+ this.store.virtualCurrencyList = [];
300
312
  this.store.lastOrderInfo = undefined;
301
313
  this.store.syncState = undefined;
302
314
  this.store.discountList = [];
@@ -309,6 +321,9 @@ class OrderModule extends _BaseModule.BaseModule {
309
321
  if (!Array.isArray(this.store.availableWalletIds)) {
310
322
  this.store.availableWalletIds = [];
311
323
  }
324
+ if (!Array.isArray(this.store.virtualCurrencyList)) {
325
+ this.store.virtualCurrencyList = [];
326
+ }
312
327
  this.request = this.core.getPlugin('request');
313
328
  const appPlugin = this.core.getPlugin('app');
314
329
  if (!appPlugin) {
@@ -363,6 +378,12 @@ class OrderModule extends _BaseModule.BaseModule {
363
378
  this.otherParams = otherParams;
364
379
  }
365
380
 
381
+ /** PC/H5 属于在线店铺;其它或缺失平台统一沿用 POS 行为。 */
382
+ isOnlineStorePlatform() {
383
+ const platform = String(this.otherParams?.platform || '').trim().toLowerCase();
384
+ return platform === 'pc' || platform === 'h5';
385
+ }
386
+
366
387
  /**
367
388
  * 记录信息日志
368
389
  */
@@ -450,6 +471,7 @@ class OrderModule extends _BaseModule.BaseModule {
450
471
  with_wallet_pass_holder: 1,
451
472
  with_available_wallet_flag: 1,
452
473
  with_available_wallet_pass_flag: 1,
474
+ with_virtual_currency: 1,
453
475
  request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
454
476
  ...(orderId ? {
455
477
  order_id: orderId
@@ -457,6 +479,14 @@ class OrderModule extends _BaseModule.BaseModule {
457
479
  });
458
480
  const discountList = prepareConfigResult?.discountList;
459
481
  this.store.availableWalletIds = prepareConfigResult?.availableWalletIds || [];
482
+ this.store.virtualCurrencyList = prepareConfigResult?.virtualCurrencyList || [];
483
+ if (prepareConfigResult) {
484
+ this.writePointCardSnapshotFromPrepareConfig({
485
+ orderId,
486
+ customerId: params.customerId,
487
+ availableWalletPassList: prepareConfigResult.availableWalletPassList || []
488
+ });
489
+ }
460
490
  if (discountList) {
461
491
  const currentDiscountList = this.store.discount?.getDiscountList() || [];
462
492
  const currentEditDiscounts = currentDiscountList.filter(discount => discount?.isEditMode);
@@ -484,6 +514,9 @@ class OrderModule extends _BaseModule.BaseModule {
484
514
  getAvailableWalletIds() {
485
515
  return Array.isArray(this.store.availableWalletIds) ? [...this.store.availableWalletIds] : [];
486
516
  }
517
+ getVirtualCurrencyList() {
518
+ return (0, _lodashEs.cloneDeep)(this.store.virtualCurrencyList || []);
519
+ }
487
520
  async syncScannedDiscountsToOriginalDiscountList(discountList) {
488
521
  const scanDiscounts = (discountList || []).filter(discount => discount?.isScan);
489
522
  if (!scanDiscounts.length) return;
@@ -507,6 +540,16 @@ class OrderModule extends _BaseModule.BaseModule {
507
540
  });
508
541
  }
509
542
  async scanCode(code, customerId) {
543
+ if (this.isActiveVirtualSettlementOrder(this.store.tempOrder)) {
544
+ return {
545
+ type: 'clientCalc',
546
+ isAvailable: false,
547
+ isAccepted: false,
548
+ discountList: [],
549
+ scannedDiscountList: [],
550
+ unavailableReason: _types2.UnavailableReason.ProductNotApplicable
551
+ };
552
+ }
510
553
  const resultDiscountWithReason = (await this.store.discount?.batchSearch(code, {
511
554
  customerId
512
555
  })) || {
@@ -602,6 +645,14 @@ class OrderModule extends _BaseModule.BaseModule {
602
645
  // 也需要让 Rules.calcDiscount 把 DiscountModule 的 isSelected / isAvailable /
603
646
  // appliedProductDetails / savedAmount 复位。
604
647
  if (!tempOrder) return;
648
+ if (this.isActiveVirtualSettlementOrder(tempOrder)) {
649
+ tempOrder.products = (0, _virtualSettlement.normalizeVirtualSettlementProducts)(tempOrder.products);
650
+ tempOrder.discount_list = [];
651
+ tempOrder.shop_discount = '0.00';
652
+ void this.store.discount?.setDiscountList([]);
653
+ void this.store.discount?.setOriginalDiscountList([]);
654
+ return;
655
+ }
605
656
  this.syncLinkedBookingHoldersToProducts(tempOrder);
606
657
  delete tempOrder.discount_list;
607
658
  const discountList = this.store.discount?.getDiscountList() || [];
@@ -678,7 +729,7 @@ class OrderModule extends _BaseModule.BaseModule {
678
729
  }
679
730
 
680
731
  /**
681
- * 应用购物车促销:计算 → 差异化赠品 → 写回 tempOrder.products → emit onPromotionApplied。
732
+ * 只执行购物车促销评估与赠品写回,不发布稳定态事件。
682
733
  *
683
734
  * 调用时机:写路径(add/update/remove/clear/setCustomer)末尾自动触发;外部一般无需手动调。
684
735
  *
@@ -686,22 +737,27 @@ class OrderModule extends _BaseModule.BaseModule {
686
737
  * - 不调用任何公开 CRUD API(如 removeProductFromOrder),全部内部 mutate tempOrder.products,避免事件风暴;
687
738
  * - 用 `isApplyingPromotion` 防递归;
688
739
  * - 多选项赠品依赖 giftSelectResolver,未注入则跳过且 console.warn;
689
- * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进。
740
+ * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进;
741
+ * - 返回的 payload 只能在 applyDiscount(商品写路径还需 summary/persist)完成后发布。
690
742
  */
691
- async applyPromotion() {
692
- if (this.isApplyingPromotion) {
693
- return;
743
+ async evaluatePromotion() {
744
+ if (this.isApplyingPromotion || this.isEmittingPromotionApplied) {
745
+ return null;
694
746
  }
695
747
  if (!this.promotionEvaluator) {
696
- return;
748
+ return null;
697
749
  }
698
750
  const tempOrder = this.store.tempOrder;
699
751
  if (!tempOrder) {
700
- return;
752
+ return null;
753
+ }
754
+ if (this.isActiveVirtualSettlementOrder(tempOrder)) {
755
+ tempOrder.products = (0, _virtualSettlement.normalizeVirtualSettlementProducts)(tempOrder.products);
756
+ return null;
701
757
  }
702
758
  const strategyConfigs = this.promotionEvaluator.getStrategyConfigs?.();
703
759
  if (Array.isArray(strategyConfigs) && strategyConfigs.length === 0) {
704
- return;
760
+ return null;
705
761
  }
706
762
  this.isApplyingPromotion = true;
707
763
  try {
@@ -793,14 +849,46 @@ class OrderModule extends _BaseModule.BaseModule {
793
849
  })),
794
850
  products: tempOrder.products
795
851
  };
796
- this.effectsEmit('onPromotionApplied', payload);
852
+ return payload;
797
853
  } catch (error) {
798
854
  console.error('[OrderModule] applyPromotion failed', error);
855
+ return null;
799
856
  } finally {
800
857
  this.isApplyingPromotion = false;
801
858
  }
802
859
  }
803
860
 
861
+ /** 发布促销稳定态;products 必须读取折扣计算后的当前数组。 */
862
+ emitPromotionApplied(payload) {
863
+ if (!payload) return;
864
+ const products = this.store.tempOrder?.products || payload.products;
865
+ this.isEmittingPromotionApplied = true;
866
+ try {
867
+ void this.effectsEmit('onPromotionApplied', {
868
+ ...payload,
869
+ products
870
+ });
871
+ } finally {
872
+ // effectsEmit 同步调用监听器后返回 Promise;保持既有的非阻塞事件行为。
873
+ this.isEmittingPromotionApplied = false;
874
+ }
875
+ }
876
+
877
+ /** 公开入口:促销、折扣、summary 与持久化完成后发布稳定态事件。 */
878
+ async applyPromotion() {
879
+ const payload = await this.evaluatePromotion();
880
+ if (!payload) return;
881
+ const tempOrder = this.store.tempOrder;
882
+ if (!tempOrder) return;
883
+ this.applyDiscount();
884
+ this.sanitizeTempOrderProducts(tempOrder);
885
+ await this.recalculateSummary({
886
+ createIfMissing: true
887
+ });
888
+ this.persistTempOrder();
889
+ this.emitPromotionApplied(payload);
890
+ }
891
+
804
892
  /** 最近一次 applyPromotion 输出的未满足促销列表 */
805
893
  getUnfulfilledPromotions() {
806
894
  return this.lastUnfulfilledPromotions;
@@ -904,6 +992,7 @@ class OrderModule extends _BaseModule.BaseModule {
904
992
  this.store.tempOrder = {
905
993
  ...this.store.tempOrder,
906
994
  products: [...this.store.tempOrder.products],
995
+ notes: [...(this.store.tempOrder.notes || [])],
907
996
  bookings: this.store.tempOrder.bookings ? [...this.store.tempOrder.bookings] : []
908
997
  };
909
998
  this.persistTempOrder();
@@ -916,6 +1005,32 @@ class OrderModule extends _BaseModule.BaseModule {
916
1005
  now: (0, _utils.formatDateTime)(new Date())
917
1006
  });
918
1007
  }
1008
+ clearTempOrderPointCardSnapshot(tempOrder) {
1009
+ const orderMetaList = (0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list);
1010
+ if (!orderMetaList) return;
1011
+ const nextOrderMetaList = {
1012
+ ...orderMetaList
1013
+ };
1014
+ delete nextOrderMetaList.point_cards_snapshot;
1015
+ if (Object.keys(nextOrderMetaList).length === 0) {
1016
+ delete tempOrder.order_meta_list;
1017
+ return;
1018
+ }
1019
+ tempOrder.order_meta_list = nextOrderMetaList;
1020
+ }
1021
+ writePointCardSnapshotFromPrepareConfig(params) {
1022
+ const tempOrder = this.store.tempOrder;
1023
+ if (!tempOrder) return;
1024
+ if (String(tempOrder.customer_id ?? '') !== String(params.customerId)) return;
1025
+ const effectiveOrderId = params.orderId ?? tempOrder.order_id;
1026
+ if (hasPersistedOrderId(effectiveOrderId)) return;
1027
+ tempOrder.order_meta_list = {
1028
+ ...((0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list) || {}),
1029
+ point_cards_snapshot: (0, _utils.buildPointCardSnapshotFromWalletPassList)(params.availableWalletPassList)
1030
+ };
1031
+ this.persistTempOrder();
1032
+ this.saveDraftInBackground();
1033
+ }
919
1034
  createExternalSaleNumber() {
920
1035
  const timestamp = (0, _dayjs.default)().format('YYYYMMDDHHmmssSSS');
921
1036
  const random6 = String(Math.floor(Math.random() * 1000000)).padStart(6, '0');
@@ -1828,7 +1943,8 @@ class OrderModule extends _BaseModule.BaseModule {
1828
1943
  return product;
1829
1944
  }
1830
1945
  sanitizeTempOrderProducts(tempOrder) {
1831
- tempOrder.products = (tempOrder.products || []).map(product => this.sanitizeOrderProductForTempOrder(product));
1946
+ tempOrder.products = (tempOrder.products || []).map(product => this.sanitizeOrderProductForTempOrder(this.normalizeVirtualSettlementProductIfEnabled(product)));
1947
+ this.syncTempOrderSettlementState(tempOrder);
1832
1948
  (0, _discountProductLineIdentity.ensureUniqueProductLineUids)(tempOrder);
1833
1949
  }
1834
1950
  ensureExtend(tempOrder) {
@@ -2149,6 +2265,7 @@ class OrderModule extends _BaseModule.BaseModule {
2149
2265
  products: summaryProducts,
2150
2266
  isPriceIncludeTax: tempOrder.is_price_include_tax,
2151
2267
  shopDiscount: tempOrder.shop_discount,
2268
+ virtualSettlementMode: this.shouldUseVirtualSettlementSummary(tempOrder),
2152
2269
  ...(this.otherParams?.channel ? {
2153
2270
  channel: this.otherParams.channel
2154
2271
  } : {})
@@ -2236,6 +2353,179 @@ class OrderModule extends _BaseModule.BaseModule {
2236
2353
  getTempOrderNote() {
2237
2354
  return this.store.tempOrder?.note || '';
2238
2355
  }
2356
+ ensureSalesNotesRuntimeState(tempOrder) {
2357
+ const hadNotesSnapshot = Array.isArray(tempOrder.notes);
2358
+ if (!Array.isArray(tempOrder.notes)) tempOrder.notes = [];
2359
+ if (!tempOrder._extend || typeof tempOrder._extend !== 'object') {
2360
+ tempOrder._extend = {
2361
+ productsByUid: {}
2362
+ };
2363
+ }
2364
+ if (!tempOrder._extend.salesNotesState) {
2365
+ tempOrder._extend.salesNotesState = {
2366
+ loaded: hadNotesSnapshot,
2367
+ dirty: false
2368
+ };
2369
+ }
2370
+ return tempOrder._extend.salesNotesState;
2371
+ }
2372
+ ensureSalesNotesLoadedForMutation(tempOrder) {
2373
+ const state = this.ensureSalesNotesRuntimeState(tempOrder);
2374
+ if (state.loaded) return;
2375
+ if (tempOrder.order_id === null || tempOrder.order_id === undefined) {
2376
+ state.loaded = true;
2377
+ tempOrder.notes = [];
2378
+ return;
2379
+ }
2380
+ throw new Error('[Order Notes] 当前订单 Notes 尚未完整加载,禁止提交部分快照');
2381
+ }
2382
+ normalizeSalesNoteUuid(value, field) {
2383
+ const uuid = String(value ?? '').trim();
2384
+ if (!uuid) throw new Error(`[Order Notes] ${field} 不能为空`);
2385
+ if (uuid.length > 255) {
2386
+ throw new Error(`[Order Notes] ${field} 不能超过 255 个字符`);
2387
+ }
2388
+ return uuid;
2389
+ }
2390
+ getSalesNotePrimaryRelation(note) {
2391
+ return note.note_relations.find(relation => relation.is_primary === 1) || null;
2392
+ }
2393
+ removeSalesNotesForMissingProductLines(tempOrder) {
2394
+ const state = this.ensureSalesNotesRuntimeState(tempOrder);
2395
+ if (!state.loaded || !Array.isArray(tempOrder.notes) || tempOrder.notes.length === 0) {
2396
+ return;
2397
+ }
2398
+ const remainingProductUids = new Set((tempOrder.products || []).map(product => product.metadata?.unique_identification_number || product.unique_identification_number).filter(uid => typeof uid === 'string' && uid.length > 0));
2399
+ const nextNotes = tempOrder.notes.filter(note => {
2400
+ const primary = this.getSalesNotePrimaryRelation(note);
2401
+ if (!primary || primary.target_type !== 'order_detail') return true;
2402
+ return remainingProductUids.has(primary.target_uuid);
2403
+ });
2404
+ if (nextNotes.length === tempOrder.notes.length) return;
2405
+ tempOrder.notes = nextNotes;
2406
+ state.dirty = true;
2407
+ }
2408
+ setSalesNoteOnTempOrder(tempOrder, input) {
2409
+ this.ensureSalesNotesLoadedForMutation(tempOrder);
2410
+ const note_type = String(input.note_type || '').trim();
2411
+ const policy = (0, _salesNotes.getSalesNotePolicy)(note_type);
2412
+ if (!policy) {
2413
+ throw new Error(`[Order Notes] 当前未注册备注类型: ${note_type || '(empty)'}`);
2414
+ }
2415
+ if (input.target?.target_type !== 'order_detail') {
2416
+ throw new Error('[Order Notes] 本期只支持 order_detail 作为主体');
2417
+ }
2418
+ const target_uuid = this.normalizeSalesNoteUuid(input.target.target_uuid, 'target_uuid');
2419
+ const targetProductExists = (tempOrder.products || []).some(product => {
2420
+ const productLineUid = product.metadata?.unique_identification_number || product.unique_identification_number;
2421
+ return String(productLineUid ?? '') === target_uuid;
2422
+ });
2423
+ if (!targetProductExists) {
2424
+ throw new Error('[Order Notes] 目标商品行不存在');
2425
+ }
2426
+ const text = (0, _salesNotes.buildSalesNoteText)(input.text);
2427
+ const notes = tempOrder.notes || [];
2428
+ // 当前 allergy policy 固定同一商品行只保留一条;业务调用方不参与去重规则。
2429
+ const existingIndex = policy.uniquePerPrimaryTarget ? notes.findIndex(note => {
2430
+ const primary = this.getSalesNotePrimaryRelation(note);
2431
+ return note.note_type === note_type && primary?.target_type === 'order_detail' && primary.target_uuid === target_uuid;
2432
+ }) : -1;
2433
+ const existing = existingIndex >= 0 ? notes[existingIndex] : null;
2434
+ const existingPrimary = existing?.note_relations.find(relation => relation.is_primary === 1 && relation.target_type === 'order_detail');
2435
+ const existingOrderRelation = existing?.note_relations.find(relation => relation.is_primary === 0 && relation.target_type === 'order');
2436
+ const note = {
2437
+ uuid: existing?.uuid || (0, _utils.createUuidV4)(),
2438
+ note_type: policy.note_type,
2439
+ content: {
2440
+ text
2441
+ },
2442
+ importance: policy.importance,
2443
+ note_relations: [{
2444
+ uuid: existingPrimary?.uuid || (0, _utils.createUuidV4)(),
2445
+ relation_type: policy.primary_relation_type,
2446
+ target_type: 'order_detail',
2447
+ target_uuid,
2448
+ is_primary: 1
2449
+ }, {
2450
+ uuid: existingOrderRelation?.uuid || (0, _utils.createUuidV4)(),
2451
+ relation_type: policy.secondary_order_relation_type,
2452
+ target_type: 'order',
2453
+ target_uuid: String(tempOrder.shop_order_number ?? ''),
2454
+ is_primary: 0
2455
+ }]
2456
+ };
2457
+ const duplicateIndex = notes.findIndex((item, index) => item.uuid === note.uuid && index !== existingIndex);
2458
+ if (duplicateIndex >= 0) {
2459
+ throw new Error('[Order Notes] uuid 已被其他 Note 使用');
2460
+ }
2461
+ tempOrder.notes = existingIndex >= 0 ? notes.map((item, index) => index === existingIndex ? note : item) : [...notes, note];
2462
+ const state = this.ensureSalesNotesRuntimeState(tempOrder);
2463
+ state.loaded = true;
2464
+ state.dirty = true;
2465
+ return note;
2466
+ }
2467
+ getSalesNotes() {
2468
+ const tempOrder = this.ensureTempOrder();
2469
+ this.ensureSalesNotesRuntimeState(tempOrder);
2470
+ return (0, _lodashEs.cloneDeep)(tempOrder.notes || []);
2471
+ }
2472
+ createSalesNotesMutationSnapshot() {
2473
+ const tempOrder = this.ensureTempOrder();
2474
+ const state = this.ensureSalesNotesRuntimeState(tempOrder);
2475
+ return {
2476
+ orderIdentity: {
2477
+ orderId: tempOrder.order_id ?? null,
2478
+ externalSaleNumber: String(tempOrder.external_sale_number || '')
2479
+ },
2480
+ notes: (0, _lodashEs.cloneDeep)(tempOrder.notes || []),
2481
+ state: (0, _lodashEs.cloneDeep)(state)
2482
+ };
2483
+ }
2484
+ restoreSalesNotesMutationSnapshot(snapshot) {
2485
+ if (!snapshot || !Array.isArray(snapshot.notes) || !snapshot.orderIdentity || typeof snapshot.orderIdentity.externalSaleNumber !== 'string' || typeof snapshot.state?.loaded !== 'boolean' || typeof snapshot.state?.dirty !== 'boolean') {
2486
+ throw new Error('[Order Notes] 无效的 mutation snapshot');
2487
+ }
2488
+ const tempOrder = this.store.tempOrder;
2489
+ if (!tempOrder) throw new Error('[Order Notes] 当前订单不存在');
2490
+ const snapshotOrderId = snapshot.orderIdentity.orderId;
2491
+ const currentOrderId = tempOrder.order_id;
2492
+ const hasSnapshotOrderId = snapshotOrderId !== null && snapshotOrderId !== undefined && String(snapshotOrderId).trim().length > 0;
2493
+ const hasCurrentOrderId = currentOrderId !== null && currentOrderId !== undefined && String(currentOrderId).trim().length > 0;
2494
+ const isSameOrder = hasSnapshotOrderId && hasCurrentOrderId ? String(snapshotOrderId) === String(currentOrderId) : Boolean(snapshot.orderIdentity.externalSaleNumber) && snapshot.orderIdentity.externalSaleNumber === String(tempOrder.external_sale_number || '');
2495
+ if (!isSameOrder) {
2496
+ throw new Error('[Order Notes] 当前订单已切换,拒绝恢复旧订单快照');
2497
+ }
2498
+ this.ensureSalesNotesRuntimeState(tempOrder);
2499
+ tempOrder.notes = (0, _lodashEs.cloneDeep)(snapshot.notes);
2500
+ tempOrder._extend.salesNotesState = (0, _lodashEs.cloneDeep)(snapshot.state);
2501
+ this.notifyTempOrderChanged();
2502
+ }
2503
+ async setSalesNote(input) {
2504
+ const tempOrder = this.ensureTempOrder();
2505
+ const note = this.setSalesNoteOnTempOrder(tempOrder, input);
2506
+ this.notifyTempOrderChanged();
2507
+ return (0, _lodashEs.cloneDeep)(note);
2508
+ }
2509
+ async removeSalesNote(uuid) {
2510
+ const tempOrder = this.ensureTempOrder();
2511
+ this.ensureSalesNotesLoadedForMutation(tempOrder);
2512
+ const normalizedUuid = this.normalizeSalesNoteUuid(uuid, 'uuid');
2513
+ const notes = tempOrder.notes || [];
2514
+ const nextNotes = notes.filter(note => note.uuid !== normalizedUuid);
2515
+ if (nextNotes.length === notes.length) return;
2516
+ tempOrder.notes = nextNotes;
2517
+ const state = this.ensureSalesNotesRuntimeState(tempOrder);
2518
+ state.dirty = true;
2519
+ this.notifyTempOrderChanged();
2520
+ }
2521
+ async clearSalesNotes() {
2522
+ const tempOrder = this.ensureTempOrder();
2523
+ tempOrder.notes = [];
2524
+ const state = this.ensureSalesNotesRuntimeState(tempOrder);
2525
+ state.loaded = true;
2526
+ state.dirty = true;
2527
+ this.notifyTempOrderChanged();
2528
+ }
2239
2529
  updateTempOrderShopDiscount(amount) {
2240
2530
  const tempOrder = this.ensureTempOrder();
2241
2531
  const discount = _decimal.default.max(new _decimal.default(Number(amount) || 0), 0).toDecimalPlaces(2).toFixed(2);
@@ -2275,6 +2565,31 @@ class OrderModule extends _BaseModule.BaseModule {
2275
2565
  this.persistTempOrder();
2276
2566
  return tempOrder.buzzer;
2277
2567
  }
2568
+ updateTempOrderShopServiceData(patch) {
2569
+ const tempOrder = this.ensureTempOrder();
2570
+ const metadata = tempOrder.metadata && typeof tempOrder.metadata === 'object' && !Array.isArray(tempOrder.metadata) ? tempOrder.metadata : {};
2571
+ const currentShopServiceData = metadata.shop_service_data && typeof metadata.shop_service_data === 'object' && !Array.isArray(metadata.shop_service_data) ? metadata.shop_service_data : {};
2572
+ const shopServiceData = {
2573
+ ...currentShopServiceData,
2574
+ ...patch
2575
+ };
2576
+ tempOrder.metadata = {
2577
+ ...metadata,
2578
+ shop_service_data: shopServiceData
2579
+ };
2580
+ this.persistTempOrder();
2581
+ return shopServiceData;
2582
+ }
2583
+ updateTempOrderPickupReferenceMode(mode) {
2584
+ return this.updateTempOrderShopServiceData({
2585
+ pickup_reference_mode: mode
2586
+ });
2587
+ }
2588
+ updateTempOrderServiceType(serviceType) {
2589
+ return this.updateTempOrderShopServiceData({
2590
+ service_type: serviceType
2591
+ });
2592
+ }
2278
2593
  updateTempOrderContactsInfo(contactsInfo) {
2279
2594
  const tempOrder = this.ensureTempOrder();
2280
2595
  tempOrder.contacts_info = contactsInfo && typeof contactsInfo === 'object' && !Array.isArray(contactsInfo) ? contactsInfo : null;
@@ -2384,6 +2699,10 @@ class OrderModule extends _BaseModule.BaseModule {
2384
2699
  email: tempOrder.email
2385
2700
  });
2386
2701
  const nextCustomerId = tempOrder.customer_id ?? null;
2702
+ if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '') && !hasPersistedOrderId(tempOrder.order_id)) {
2703
+ this.store.availableWalletIds = [];
2704
+ this.clearTempOrderPointCardSnapshot(tempOrder);
2705
+ }
2387
2706
  this.persistTempOrder();
2388
2707
  return {
2389
2708
  customerId: tempOrder.customer_id,
@@ -2419,6 +2738,10 @@ class OrderModule extends _BaseModule.BaseModule {
2419
2738
  const nextCustomerId = tempOrder.customer_id ?? null;
2420
2739
  if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '')) {
2421
2740
  this.store.availableWalletIds = [];
2741
+ this.store.virtualCurrencyList = [];
2742
+ if (!hasPersistedOrderId(tempOrder.order_id)) {
2743
+ this.clearTempOrderPointCardSnapshot(tempOrder);
2744
+ }
2422
2745
  (0, _utils.clearTempOrderHolderAssignments)(tempOrder);
2423
2746
  this.clearCustomerBoundDiscounts(tempOrder);
2424
2747
  this.applyDiscount();
@@ -2457,6 +2780,10 @@ class OrderModule extends _BaseModule.BaseModule {
2457
2780
  tempOrder.customer = null;
2458
2781
  if (tempOrder._extend) delete tempOrder._extend.customerSnapshot;
2459
2782
  this.store.availableWalletIds = [];
2783
+ this.store.virtualCurrencyList = [];
2784
+ if (!hasPersistedOrderId(tempOrder.order_id)) {
2785
+ this.clearTempOrderPointCardSnapshot(tempOrder);
2786
+ }
2460
2787
  this.clearCustomerBoundDiscounts(tempOrder);
2461
2788
  this.applyDiscount();
2462
2789
  this.persistTempOrder();
@@ -2776,6 +3103,78 @@ class OrderModule extends _BaseModule.BaseModule {
2776
3103
  const bookingUid = product?.booking_uid ?? product?.metadata?.booking_uid;
2777
3104
  return bookingUid !== undefined && bookingUid !== null && String(bookingUid) !== '';
2778
3105
  }
3106
+ isMultiCurrencySettlementEnabled() {
3107
+ return this.otherParams?.enableMultiCurrencySettlement === true;
3108
+ }
3109
+ isActiveVirtualSettlementOrder(tempOrder) {
3110
+ return this.isMultiCurrencySettlementEnabled() && (0, _virtualSettlement.isVirtualSettlementOrder)(tempOrder);
3111
+ }
3112
+ shouldUseVirtualSettlementSummary(tempOrder) {
3113
+ return this.isActiveVirtualSettlementOrder(tempOrder) || tempOrder?.metadata?.currency_settlement_config?.settlement_type === 'virtual_currency';
3114
+ }
3115
+ normalizeVirtualSettlementProductIfEnabled(product) {
3116
+ return this.isMultiCurrencySettlementEnabled() ? (0, _virtualSettlement.normalizeVirtualSettlementOrderProduct)(product) : product;
3117
+ }
3118
+ assertAndLockProductSettlement(tempOrder, product) {
3119
+ if (!this.isMultiCurrencySettlementEnabled()) return;
3120
+ const currentKey = (0, _virtualSettlement.getTempOrderSettlementKey)(tempOrder);
3121
+ const incomingKey = (0, _virtualSettlement.getSettlementKey)((0, _virtualSettlement.getSelectedSettlementSnapshot)(product)) || 'legal_currency';
3122
+ if (currentKey && currentKey !== incomingKey) {
3123
+ throw new _virtualSettlement.OrderSettlementConflictError(currentKey, incomingKey);
3124
+ }
3125
+ const snapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(product);
3126
+ if (snapshot?.settlement_type === 'virtual_currency' && tempOrder.customer_id && Number(tempOrder.customer_id) !== 1) {
3127
+ const currencyProductId = Number(snapshot.currency_product_id);
3128
+ const asset = (this.store.virtualCurrencyList || []).find(item => Number(item?.product_id) === currencyProductId);
3129
+ if (!asset) {
3130
+ throw new _virtualSettlement.VirtualCurrencyAssetNotFoundError(currencyProductId);
3131
+ }
3132
+ const existedAmount = tempOrder.products.reduce((sum, item) => sum.plus(new _decimal.default(item.selling_price || 0).times(item.num || 1)), new _decimal.default(0));
3133
+ const requiredAmount = existedAmount.plus(new _decimal.default(snapshot.price || 0).times(product.num || 1));
3134
+ const balance = new _decimal.default(asset.balance || 0);
3135
+ if (balance.lt(requiredAmount)) {
3136
+ throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
3137
+ }
3138
+ }
3139
+ (0, _virtualSettlement.applyTempOrderSettlementState)(tempOrder, snapshot || {
3140
+ settlement_type: 'legal_currency',
3141
+ price: product.selling_price ?? product.original_price ?? product.payment_price ?? 0,
3142
+ unit_label: tempOrder.currency_code,
3143
+ symbol: tempOrder.currency_symbol,
3144
+ amount_format: tempOrder.currency_format
3145
+ });
3146
+ }
3147
+ syncTempOrderSettlementState(tempOrder) {
3148
+ if (!this.isMultiCurrencySettlementEnabled()) return;
3149
+ tempOrder.metadata = tempOrder.metadata || {};
3150
+ if (!tempOrder.products.length) {
3151
+ (0, _virtualSettlement.releaseTempOrderSettlementState)(tempOrder);
3152
+ return;
3153
+ }
3154
+ const snapshot = (0, _virtualSettlement.getTempOrderSettlementSnapshot)(tempOrder);
3155
+ if (snapshot) {
3156
+ (0, _virtualSettlement.applyTempOrderSettlementState)(tempOrder, snapshot);
3157
+ return;
3158
+ }
3159
+ tempOrder.metadata.cart_settlement_key = 'legal_currency';
3160
+ }
3161
+ prepareVirtualSettlementTempOrder(tempOrder) {
3162
+ if (!this.isMultiCurrencySettlementEnabled()) return;
3163
+ this.syncTempOrderSettlementState(tempOrder);
3164
+ if (!this.isActiveVirtualSettlementOrder(tempOrder)) return;
3165
+ tempOrder.products = (0, _virtualSettlement.normalizeVirtualSettlementProducts)(tempOrder.products);
3166
+ tempOrder.discount_list = [];
3167
+ tempOrder.shop_discount = '0.00';
3168
+ tempOrder.surcharge_fee = '0.00';
3169
+ tempOrder.surcharges = [];
3170
+ const virtualOrder = tempOrder;
3171
+ virtualOrder.discount_amount = '0.00';
3172
+ virtualOrder.product_tax_fee = '0.00';
3173
+ virtualOrder.tax_fee = '0.00';
3174
+ virtualOrder.shipping_fee = '0.00';
3175
+ virtualOrder.shipping_tax_fee = '0.00';
3176
+ virtualOrder.tip_fee = '0.00';
3177
+ }
2779
3178
 
2780
3179
  // ─── TempOrder: 商品 CRUD ───
2781
3180
 
@@ -2787,13 +3186,13 @@ class OrderModule extends _BaseModule.BaseModule {
2787
3186
  */
2788
3187
  async finalizeProductOrderMutation(tempOrder, options) {
2789
3188
  this.syncTempOrderHolderFromBookings(tempOrder);
2790
- if (!options?.skipEditDiscountConfigRefresh) {
3189
+ this.prepareVirtualSettlementTempOrder(tempOrder);
3190
+ const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
3191
+ if (!options?.skipEditDiscountConfigRefresh && !isVirtualSettlement) {
2791
3192
  await this.ensureEditDiscountConfigForProductChange(tempOrder);
2792
3193
  }
2793
- if (!options?.skipDiscountRecalculation) {
2794
- await this.applyPromotion();
2795
- }
2796
- if (!options?.skipDiscountRecalculation) {
3194
+ const promotionPayload = !options?.skipDiscountRecalculation && !isVirtualSettlement ? await this.evaluatePromotion() : null;
3195
+ if (!options?.skipDiscountRecalculation && !isVirtualSettlement) {
2797
3196
  this.applyDiscount();
2798
3197
  }
2799
3198
  this.sanitizeTempOrderProducts(tempOrder);
@@ -2801,6 +3200,7 @@ class OrderModule extends _BaseModule.BaseModule {
2801
3200
  createIfMissing: true
2802
3201
  });
2803
3202
  this.persistTempOrder();
3203
+ this.emitPromotionApplied(promotionPayload);
2804
3204
  }
2805
3205
 
2806
3206
  /**
@@ -2821,9 +3221,10 @@ class OrderModule extends _BaseModule.BaseModule {
2821
3221
  booking
2822
3222
  }) : null;
2823
3223
  const productToAdd = linked?.product || product;
3224
+ this.assertAndLockProductSettlement(tempOrder, productToAdd);
2824
3225
  const disableMerge = options?.disableMerge === true;
2825
3226
  const incomingFingerprint = disableMerge ? '' : (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(productToAdd), productToAdd.product_bundle);
2826
- const normalizedIncoming = (0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd));
3227
+ const normalizedIncoming = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd)));
2827
3228
  if (disableMerge) {
2828
3229
  normalizedIncoming.metadata = {
2829
3230
  ...(normalizedIncoming.metadata || {}),
@@ -2864,6 +3265,7 @@ class OrderModule extends _BaseModule.BaseModule {
2864
3265
  const matchedProduct = matchedIndex === -1 ? null : tempOrder.products[matchedIndex];
2865
3266
  const existedFingerprint = matchedProduct ? (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(matchedProduct), matchedProduct.product_bundle) : '';
2866
3267
  const commitProductLine = shouldMerge => {
3268
+ let committedProduct;
2867
3269
  if (matchedIndex === -1 || !matchedProduct || !shouldMerge) {
2868
3270
  this.captureProductRuntime(tempOrder, productToAdd, normalizedIncoming);
2869
3271
  if (linked) {
@@ -2879,16 +3281,17 @@ class OrderModule extends _BaseModule.BaseModule {
2879
3281
  } else {
2880
3282
  tempOrder.products = [...tempOrder.products, normalizedIncoming];
2881
3283
  }
3284
+ committedProduct = normalizedIncoming;
2882
3285
  } else {
2883
3286
  const targetProduct = matchedProduct;
2884
3287
  const targetUid = targetProduct.metadata?.unique_identification_number;
2885
- const normalizedProduct = (0, _utils.mergeOrderProductDisplayFields)(targetProduct, (0, _utils3.normalizeOrderProduct)({
3288
+ const normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(targetProduct, (0, _utils3.normalizeOrderProduct)({
2886
3289
  ...productToAdd,
2887
3290
  metadata: {
2888
3291
  ...(productToAdd.metadata || {}),
2889
3292
  unique_identification_number: targetUid
2890
3293
  }
2891
- }));
3294
+ })));
2892
3295
  normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
2893
3296
  const nextNum = (0, _utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num);
2894
3297
  tempOrder.products[matchedIndex] = {
@@ -2914,6 +3317,7 @@ class OrderModule extends _BaseModule.BaseModule {
2914
3317
  num: nextNum
2915
3318
  };
2916
3319
  this.captureProductRuntime(tempOrder, productToAdd, tempOrder.products[matchedIndex], targetUid);
3320
+ committedProduct = tempOrder.products[matchedIndex];
2917
3321
  if (matchedIndex !== tempOrder.products.length - 1) {
2918
3322
  const mergedProduct = tempOrder.products[matchedIndex];
2919
3323
  tempOrder.products = [...tempOrder.products.slice(0, matchedIndex), ...tempOrder.products.slice(matchedIndex + 1), mergedProduct];
@@ -2922,10 +3326,10 @@ class OrderModule extends _BaseModule.BaseModule {
2922
3326
  if (linked) {
2923
3327
  tempOrder.bookings = [...(tempOrder.bookings || []), linked.booking];
2924
3328
  }
3329
+ return committedProduct;
2925
3330
  };
2926
3331
  if (!matchedProduct) {
2927
- commitProductLine(false);
2928
- return;
3332
+ return commitProductLine(false);
2929
3333
  }
2930
3334
  const shouldMerge = this.shouldMergeProductToOrder({
2931
3335
  incomingProduct: productToAdd,
@@ -2940,7 +3344,68 @@ class OrderModule extends _BaseModule.BaseModule {
2940
3344
  if (shouldMerge && typeof shouldMerge === 'object' && typeof shouldMerge.then === 'function') {
2941
3345
  return Promise.resolve(shouldMerge).then(commitProductLine);
2942
3346
  }
2943
- commitProductLine(shouldMerge);
3347
+ return commitProductLine(shouldMerge);
3348
+ }
3349
+
3350
+ /**
3351
+ * booking 数量拆行时,若 Holder 数量与拆行数严格一致,则按下标分配。
3352
+ * 数量不一致时保持旧数据,避免猜测 Holder 与商品份数的对应关系。
3353
+ */
3354
+ createSplitBookingLine(product, booking, index, splitCount) {
3355
+ const singleLine = (0, _lodashEs.cloneDeep)(product);
3356
+ singleLine.num = 1;
3357
+ delete singleLine.product_quantity;
3358
+ const singleBooking = (0, _lodashEs.cloneDeep)(booking);
3359
+ const bookingHolder = booking?.holder && typeof booking.holder === 'object' ? booking.holder : null;
3360
+ const metadataHolder = booking?.metadata?.holder && typeof booking.metadata.holder === 'object' ? booking.metadata.holder : null;
3361
+ const rawHolderIds = bookingHolder?.form_record ?? booking?.metadata?.holder_id ?? metadataHolder?.form_record ?? product?.metadata?.holder_id;
3362
+ const holderIds = (Array.isArray(rawHolderIds) ? rawHolderIds : [rawHolderIds]).map(holderId => holderId && typeof holderId === 'object' ? holderId.form_record_id : holderId).filter(holderId => holderId !== undefined && holderId !== null && holderId !== '');
3363
+ if (holderIds.length !== splitCount) {
3364
+ return {
3365
+ product: singleLine,
3366
+ booking: singleBooking
3367
+ };
3368
+ }
3369
+ const holderId = holderIds[index];
3370
+ const rawHolderName = bookingHolder?.name ?? metadataHolder?.name;
3371
+ const holderNames = Array.isArray(rawHolderName) ? rawHolderName.map(name => String(name || '').trim()) : typeof rawHolderName === 'string' ? rawHolderName.split(',').map(name => name.trim()) : [];
3372
+ const holderName = holderNames.length === splitCount ? holderNames[index] : undefined;
3373
+ const splitHolder = holder => ({
3374
+ ...holder,
3375
+ form_record: [holderId],
3376
+ ...(holderName ? {
3377
+ name: holderName
3378
+ } : Object.prototype.hasOwnProperty.call(holder, 'name') ? {
3379
+ name: undefined
3380
+ } : {})
3381
+ });
3382
+ singleLine.metadata = {
3383
+ ...(singleLine.metadata || {}),
3384
+ holder_id: [holderId]
3385
+ };
3386
+ if (singleLine._extend && typeof singleLine._extend === 'object') {
3387
+ singleLine._extend = {
3388
+ ...singleLine._extend,
3389
+ holder_id: [holderId]
3390
+ };
3391
+ }
3392
+ if (singleBooking.holder && typeof singleBooking.holder === 'object') {
3393
+ singleBooking.holder = splitHolder(singleBooking.holder);
3394
+ }
3395
+ singleBooking.metadata = {
3396
+ ...(singleBooking.metadata || {}),
3397
+ holder_id: [holderId],
3398
+ ...(singleBooking.metadata?.holder && typeof singleBooking.metadata.holder === 'object' ? {
3399
+ holder: splitHolder(singleBooking.metadata.holder)
3400
+ } : {})
3401
+ };
3402
+ if (Object.prototype.hasOwnProperty.call(singleBooking, 'holder_id')) {
3403
+ singleBooking.holder_id = [holderId];
3404
+ }
3405
+ return {
3406
+ product: singleLine,
3407
+ booking: singleBooking
3408
+ };
2944
3409
  }
2945
3410
 
2946
3411
  /**
@@ -2952,36 +3417,99 @@ class OrderModule extends _BaseModule.BaseModule {
2952
3417
  */
2953
3418
  async addProductToOrder(product, booking, options) {
2954
3419
  const tempOrder = this.ensureTempOrder();
3420
+ const salesNoteDrafts = (options?.salesNotes || []).map(draft => ({
3421
+ note_type: String(draft.note_type || '').trim(),
3422
+ text: {
3423
+ kind: 'resolved',
3424
+ text: (0, _salesNotes.buildSalesNoteText)(draft.text)
3425
+ }
3426
+ }));
3427
+ const mutationSnapshot = salesNoteDrafts.length > 0 ? {
3428
+ products: (0, _lodashEs.cloneDeep)(tempOrder.products || []),
3429
+ bookings: (0, _lodashEs.cloneDeep)(tempOrder.bookings || []),
3430
+ notes: (0, _lodashEs.cloneDeep)(tempOrder.notes || []),
3431
+ extend: (0, _lodashEs.cloneDeep)(tempOrder._extend || {})
3432
+ } : null;
3433
+ const attachSalesNoteDrafts = orderProduct => {
3434
+ if (!salesNoteDrafts.length) return;
3435
+ const productLineUid = orderProduct.metadata?.unique_identification_number || orderProduct.unique_identification_number;
3436
+ if (!productLineUid) {
3437
+ throw new Error('[Order Notes] 商品行缺少 unique_identification_number');
3438
+ }
3439
+ salesNoteDrafts.forEach(draft => {
3440
+ this.setSalesNoteOnTempOrder(tempOrder, {
3441
+ note_type: draft.note_type,
3442
+ target: {
3443
+ target_type: 'order_detail',
3444
+ target_uuid: String(productLineUid)
3445
+ },
3446
+ text: draft.text
3447
+ });
3448
+ });
3449
+ };
3450
+ const rollbackProductAndNotesMutation = () => {
3451
+ if (!mutationSnapshot) return;
3452
+ tempOrder.products = mutationSnapshot.products;
3453
+ tempOrder.bookings = mutationSnapshot.bookings;
3454
+ tempOrder.notes = mutationSnapshot.notes;
3455
+ tempOrder._extend = mutationSnapshot.extend;
3456
+ };
2955
3457
  const insertAfterProductUid = options?.insertAfterProductUid;
2956
3458
  const anchorIndex = booking && insertAfterProductUid ? tempOrder.products.findIndex(line => {
2957
3459
  const uid = line.metadata?.unique_identification_number || line.unique_identification_number;
2958
3460
  return uid !== undefined && uid !== null && String(uid) === String(insertAfterProductUid);
2959
3461
  }) : -1;
2960
3462
  let insertAtIndex = anchorIndex >= 0 ? anchorIndex + 1 : undefined;
2961
- if (booking) {
2962
- const productRecord = product;
2963
- const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
2964
- if (splitCount > 1) {
2965
- for (let i = 0; i < splitCount; i += 1) {
2966
- const singleLine = (0, _lodashEs.cloneDeep)(productRecord);
2967
- singleLine.num = 1;
2968
- delete singleLine.product_quantity;
2969
- const appendResult = this.appendProductLineToTempOrder(tempOrder, singleLine, (0, _lodashEs.cloneDeep)(booking), {
3463
+ try {
3464
+ if (booking) {
3465
+ const productRecord = product;
3466
+ const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
3467
+ if (splitCount > 1) {
3468
+ for (let i = 0; i < splitCount; i += 1) {
3469
+ const splitLine = this.createSplitBookingLine(productRecord, booking, i, splitCount);
3470
+ if (i > 0) {
3471
+ delete splitLine.product.unique_identification_number;
3472
+ delete splitLine.product.identity_key;
3473
+ if (splitLine.product.metadata) {
3474
+ splitLine.product.metadata = {
3475
+ ...splitLine.product.metadata
3476
+ };
3477
+ delete splitLine.product.metadata.unique_identification_number;
3478
+ delete splitLine.product.metadata.identity_key;
3479
+ }
3480
+ delete splitLine.booking.booking_uid;
3481
+ if (splitLine.booking.metadata) {
3482
+ splitLine.booking.metadata = {
3483
+ ...splitLine.booking.metadata
3484
+ };
3485
+ delete splitLine.booking.metadata.unique_identification_number;
3486
+ }
3487
+ }
3488
+ const committedProduct = await this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking, {
3489
+ insertAtIndex,
3490
+ disableMerge: options?.disableMerge
3491
+ });
3492
+ attachSalesNoteDrafts(committedProduct);
3493
+ if (insertAtIndex !== undefined) insertAtIndex += 1;
3494
+ }
3495
+ } else {
3496
+ const committedProduct = await this.appendProductLineToTempOrder(tempOrder, product, (0, _lodashEs.cloneDeep)(booking), {
2970
3497
  insertAtIndex,
2971
3498
  disableMerge: options?.disableMerge
2972
3499
  });
2973
- if (appendResult) await appendResult;
2974
- if (insertAtIndex !== undefined) insertAtIndex += 1;
3500
+ attachSalesNoteDrafts(committedProduct);
2975
3501
  }
2976
- await this.finalizeProductOrderMutation(tempOrder);
2977
- return tempOrder.products;
3502
+ } else {
3503
+ const committedProduct = await this.appendProductLineToTempOrder(tempOrder, product, undefined, {
3504
+ insertAtIndex,
3505
+ disableMerge: options?.disableMerge
3506
+ });
3507
+ attachSalesNoteDrafts(committedProduct);
2978
3508
  }
3509
+ } catch (error) {
3510
+ rollbackProductAndNotesMutation();
3511
+ throw error;
2979
3512
  }
2980
- const appendResult = this.appendProductLineToTempOrder(tempOrder, product, booking, {
2981
- insertAtIndex,
2982
- disableMerge: options?.disableMerge
2983
- });
2984
- if (appendResult) await appendResult;
2985
3513
  await this.finalizeProductOrderMutation(tempOrder);
2986
3514
  this.logInfo('addProductToOrder success', {
2987
3515
  product_id: product.product_id,
@@ -3013,10 +3541,8 @@ class OrderModule extends _BaseModule.BaseModule {
3013
3541
  const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
3014
3542
  if (splitCount > 1) {
3015
3543
  for (let i = 0; i < splitCount; i += 1) {
3016
- const singleLine = (0, _lodashEs.cloneDeep)(productRecord);
3017
- singleLine.num = 1;
3018
- delete singleLine.product_quantity;
3019
- const appendResult = this.appendProductLineToTempOrder(tempOrder, singleLine, (0, _lodashEs.cloneDeep)(booking));
3544
+ const splitLine = this.createSplitBookingLine(productRecord, booking, i, splitCount);
3545
+ const appendResult = this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking);
3020
3546
  if (appendResult) await appendResult;
3021
3547
  }
3022
3548
  continue;
@@ -3229,7 +3755,7 @@ class OrderModule extends _BaseModule.BaseModule {
3229
3755
  }
3230
3756
  // normalizeOrderProduct 幂等:v2 metadata 存在 → 保留 main_product_* 折扣重算 composite;
3231
3757
  // 否则 → 把顶层 selling_price 视作 source,派生 main_product_* 并写 price_schema_version: 2。
3232
- let normalizedProduct = (0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct));
3758
+ let normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct)));
3233
3759
  normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3234
3760
  const preservedBookingUid = nextProduct.booking_uid || nextProduct.metadata?.booking_uid || targetProduct.booking_uid || targetProduct.metadata?.booking_uid;
3235
3761
  if (!booking && preservedBookingUid) {
@@ -3272,7 +3798,7 @@ class OrderModule extends _BaseModule.BaseModule {
3272
3798
  async updateOrderProduct(params) {
3273
3799
  const tempOrder = this.ensureTempOrder();
3274
3800
  const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
3275
- await this.applyPromotion();
3801
+ const promotionPayload = await this.evaluatePromotion();
3276
3802
  this.applyDiscount({
3277
3803
  reapplyBookingDiscountProductUids: params.allow_booking_discount_reapply && updatedProductUid ? [String(updatedProductUid)] : undefined
3278
3804
  });
@@ -3281,6 +3807,7 @@ class OrderModule extends _BaseModule.BaseModule {
3281
3807
  createIfMissing: true
3282
3808
  });
3283
3809
  this.persistTempOrder();
3810
+ this.emitPromotionApplied(promotionPayload);
3284
3811
  this.logInfo('updateOrderProduct success', {
3285
3812
  params
3286
3813
  });
@@ -3346,23 +3873,45 @@ class OrderModule extends _BaseModule.BaseModule {
3346
3873
  throw new Error('[Order] 目标商品不存在,无法更新数量');
3347
3874
  }
3348
3875
  const targetProduct = tempOrder.products[productIndex];
3349
- const normalizedProduct = (0, _utils3.normalizeOrderProduct)({
3876
+ const normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils3.normalizeOrderProduct)({
3350
3877
  ...targetProduct,
3351
3878
  num: (0, _utils3.getSafeProductNum)(num),
3352
3879
  metadata: {
3353
3880
  ...(targetProduct.metadata || {}),
3354
3881
  unique_identification_number: targetProduct.metadata?.unique_identification_number || unique_identification_number
3355
3882
  }
3356
- });
3883
+ }));
3357
3884
  normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3885
+ const settlementSnapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(normalizedProduct);
3886
+ if (this.isMultiCurrencySettlementEnabled() && settlementSnapshot?.settlement_type === 'virtual_currency' && tempOrder.customer_id && Number(tempOrder.customer_id) !== 1) {
3887
+ const currencyProductId = Number(settlementSnapshot.currency_product_id);
3888
+ const asset = (this.store.virtualCurrencyList || []).find(item => Number(item?.product_id) === currencyProductId);
3889
+ if (!asset) {
3890
+ throw new _virtualSettlement.VirtualCurrencyAssetNotFoundError(currencyProductId);
3891
+ }
3892
+ const requiredAmount = tempOrder.products.reduce((sum, item, index) => {
3893
+ const current = index === productIndex ? normalizedProduct : item;
3894
+ const currentSnapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(current);
3895
+ return sum.plus(new _decimal.default(currentSnapshot?.price || current.selling_price || 0).times(current.num || 1));
3896
+ }, new _decimal.default(0));
3897
+ const balance = new _decimal.default(asset.balance || 0);
3898
+ if (balance.lt(requiredAmount)) {
3899
+ throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
3900
+ }
3901
+ }
3358
3902
  tempOrder.products[productIndex] = normalizedProduct;
3359
- await this.applyPromotion();
3360
- this.applyDiscount();
3903
+ this.prepareVirtualSettlementTempOrder(tempOrder);
3904
+ const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
3905
+ const promotionPayload = !isVirtualSettlement ? await this.evaluatePromotion() : null;
3906
+ if (!isVirtualSettlement) {
3907
+ this.applyDiscount();
3908
+ }
3361
3909
  this.sanitizeTempOrderProducts(tempOrder);
3362
3910
  await this.recalculateSummary({
3363
3911
  createIfMissing: true
3364
3912
  });
3365
3913
  this.persistTempOrder();
3914
+ this.emitPromotionApplied(promotionPayload);
3366
3915
  this.logInfo('updateOrderProductQuantity success', {
3367
3916
  params
3368
3917
  });
@@ -3407,7 +3956,7 @@ class OrderModule extends _BaseModule.BaseModule {
3407
3956
  */
3408
3957
  async finalizeAfterProductsMutation(tempOrder, options) {
3409
3958
  this.syncTempOrderHolderFromBookings(tempOrder);
3410
- await this.applyPromotion();
3959
+ const promotionPayload = await this.evaluatePromotion();
3411
3960
  this.applyDiscount({
3412
3961
  reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
3413
3962
  });
@@ -3416,6 +3965,7 @@ class OrderModule extends _BaseModule.BaseModule {
3416
3965
  createIfMissing: true
3417
3966
  });
3418
3967
  this.persistTempOrder();
3968
+ this.emitPromotionApplied(promotionPayload);
3419
3969
  }
3420
3970
 
3421
3971
  /**
@@ -3426,10 +3976,12 @@ class OrderModule extends _BaseModule.BaseModule {
3426
3976
  */
3427
3977
  async removeProductsFromOrder(identities, options) {
3428
3978
  const tempOrder = this.ensureTempOrder();
3429
- this.repairDuplicateProductLineIdentities(tempOrder);
3430
3979
  if (!identities.length) {
3431
3980
  return tempOrder.products;
3432
3981
  }
3982
+ // 商品删除会同步删除以商品行为主关系的 Notes;快照不完整时禁止产生孤儿 Relation。
3983
+ this.ensureSalesNotesLoadedForMutation(tempOrder);
3984
+ this.repairDuplicateProductLineIdentities(tempOrder);
3433
3985
  const productsBeforeRemove = tempOrder.products || [];
3434
3986
  const bookingsBeforeRemove = tempOrder.bookings || [];
3435
3987
  const preserveDisplayPosition = options?.preserveDisplayPosition;
@@ -3489,6 +4041,7 @@ class OrderModule extends _BaseModule.BaseModule {
3489
4041
  tempOrder.products = nextProductIndex >= 0 ? [...productsWithoutRemaining.slice(0, nextProductIndex), remainingProduct, ...productsWithoutRemaining.slice(nextProductIndex)] : [...productsWithoutRemaining, remainingProduct];
3490
4042
  }
3491
4043
  }
4044
+ this.removeSalesNotesForMissingProductLines(tempOrder);
3492
4045
  await this.finalizeAfterProductsMutation(tempOrder);
3493
4046
  this.logInfo('removeProductsFromOrder success', {
3494
4047
  identities
@@ -3501,12 +4054,15 @@ class OrderModule extends _BaseModule.BaseModule {
3501
4054
 
3502
4055
  /**
3503
4056
  * 仅清空购物车行(products / bookings),不重置整单。
3504
- * 用于「仅清空商品」;客户、备注、支付等协议字段保持不变。
4057
+ * 普通整单 note 保持不变;以商品行为主体的 Sales Notes 同步删除。
3505
4058
  */
3506
4059
  async clearOrderCartLines() {
3507
4060
  const tempOrder = this.ensureTempOrder();
4061
+ this.ensureSalesNotesLoadedForMutation(tempOrder);
3508
4062
  tempOrder.products = [];
3509
4063
  tempOrder.bookings = [];
4064
+ (0, _virtualSettlement.releaseTempOrderSettlementState)(tempOrder);
4065
+ this.removeSalesNotesForMissingProductLines(tempOrder);
3510
4066
  this.syncTempOrderHolderFromBookings(tempOrder);
3511
4067
  if (tempOrder._extend && typeof tempOrder._extend === 'object') {
3512
4068
  tempOrder._extend = {
@@ -3514,17 +4070,53 @@ class OrderModule extends _BaseModule.BaseModule {
3514
4070
  productsByUid: {}
3515
4071
  };
3516
4072
  }
3517
- await this.applyPromotion();
4073
+ const promotionPayload = await this.evaluatePromotion();
3518
4074
  this.applyDiscount();
3519
4075
  this.sanitizeTempOrderProducts(tempOrder);
3520
4076
  await this.recalculateSummary({
3521
4077
  createIfMissing: true
3522
4078
  });
3523
4079
  this.persistTempOrder();
4080
+ this.emitPromotionApplied(promotionPayload);
3524
4081
  this.logInfo('clearOrderCartLines success', {});
3525
4082
  return tempOrder;
3526
4083
  }
3527
4084
 
4085
+ /**
4086
+ * 切换购物车结算单位时清理所有会影响新币种的订单状态,但保留当前客户。
4087
+ */
4088
+ async clearOrderForSettlementSwitch() {
4089
+ const tempOrder = this.ensureTempOrder();
4090
+ this.ensureSalesNotesLoadedForMutation(tempOrder);
4091
+ tempOrder.products = [];
4092
+ tempOrder.bookings = [];
4093
+ this.removeSalesNotesForMissingProductLines(tempOrder);
4094
+ tempOrder.discount_list = [];
4095
+ tempOrder.payments = [];
4096
+ tempOrder.refunds = [];
4097
+ tempOrder.shop_discount = '0.00';
4098
+ tempOrder.surcharges = [];
4099
+ (0, _virtualSettlement.releaseTempOrderSettlementState)(tempOrder);
4100
+ this.syncTempOrderHolderFromBookings(tempOrder);
4101
+ if (tempOrder._extend && typeof tempOrder._extend === 'object') {
4102
+ tempOrder._extend = {
4103
+ ...tempOrder._extend,
4104
+ productsByUid: {}
4105
+ };
4106
+ }
4107
+ await this.store.discount?.setDiscountList([]);
4108
+ await this.store.discount?.setOriginalDiscountList([]);
4109
+ this.hasActiveCustomerBoundDiscount = false;
4110
+ this.lastUnfulfilledPromotions = [];
4111
+ this.lastGiftActions = null;
4112
+ await this.recalculateSummary({
4113
+ createIfMissing: true
4114
+ });
4115
+ this.persistTempOrder();
4116
+ this.logInfo('clearOrderForSettlementSwitch success', {});
4117
+ return tempOrder;
4118
+ }
4119
+
3528
4120
  // ─── TempOrder: 提交 ───
3529
4121
 
3530
4122
  buildCurrentSubmitPayloadForLocalPrint(params) {
@@ -3614,9 +4206,14 @@ class OrderModule extends _BaseModule.BaseModule {
3614
4206
  }
3615
4207
  async runSubmitTempOrder(params) {
3616
4208
  const tempOrder = this.ensureTempOrder();
4209
+ this.prepareVirtualSettlementTempOrder(tempOrder);
4210
+ const isOnlineStorePlatform = this.isOnlineStorePlatform();
3617
4211
  const shouldConfirmPendingVoucherPayments = params?.confirmPendingVoucherPayments !== false;
3618
4212
  const hasPaymentOverride = params?.payments !== undefined;
3619
- const preparedPaymentOverride = hasPaymentOverride ? this.prepareVoucherPaymentsForSubmit(params?.payments || [], shouldConfirmPendingVoucherPayments) : undefined;
4213
+ let preparedPaymentOverride;
4214
+ if (hasPaymentOverride) {
4215
+ preparedPaymentOverride = isOnlineStorePlatform ? params?.payments || [] : this.prepareVoucherPaymentsForSubmit(params?.payments || [], shouldConfirmPendingVoucherPayments);
4216
+ }
3620
4217
  if (!hasPaymentOverride) {
3621
4218
  if (shouldConfirmPendingVoucherPayments) {
3622
4219
  this.confirmPendingVoucherPayments({
@@ -3636,6 +4233,7 @@ class OrderModule extends _BaseModule.BaseModule {
3636
4233
  })) || this.store.summary || (0, _utils.createEmptySummary)();
3637
4234
  const effectiveCacheId = params?.cacheId ?? this.cacheId;
3638
4235
  const hasPaymentStatusOverride = params?.paymentStatus !== undefined;
4236
+ const shouldOverridePaymentStatus = hasPaymentStatusOverride && !isOnlineStorePlatform && !shouldConfirmPendingVoucherPayments;
3639
4237
  const hasSmallTicketDataFlagOverride = params?.smallTicketDataFlag !== undefined;
3640
4238
  const enhancePayload = hasPaymentOverride || hasPaymentStatusOverride || hasSmallTicketDataFlagOverride || params?.enhancePayload ? (payload, ctx) => {
3641
4239
  const nextPayload = {
@@ -3643,7 +4241,7 @@ class OrderModule extends _BaseModule.BaseModule {
3643
4241
  ...(hasPaymentOverride ? {
3644
4242
  payments: preparedPaymentOverride || []
3645
4243
  } : {}),
3646
- ...(hasPaymentStatusOverride && !shouldConfirmPendingVoucherPayments ? {
4244
+ ...(shouldOverridePaymentStatus ? {
3647
4245
  payment_status: params?.paymentStatus
3648
4246
  } : {}),
3649
4247
  ...(hasSmallTicketDataFlagOverride ? {
@@ -3663,6 +4261,7 @@ class OrderModule extends _BaseModule.BaseModule {
3663
4261
  type: submitSnapshot.type ?? params?.type,
3664
4262
  summary: latestSummary,
3665
4263
  request_unique_idempotency_token: params?.request_unique_idempotency_token,
4264
+ includePaymentStatus: !isOnlineStorePlatform,
3666
4265
  enhance: enhancePayload
3667
4266
  });
3668
4267
  if (params?.syncMode) {
@@ -3709,6 +4308,12 @@ class OrderModule extends _BaseModule.BaseModule {
3709
4308
  this.logSubmitBackendRejected(result, payload);
3710
4309
  return result;
3711
4310
  }
4311
+ const isLocalPendingSubmit = this.isLocalPendingSubmitResult(result);
4312
+ // need_sync=1 仅表示已进入本地同步队列,不能视为远端已确认 Notes。
4313
+ if (Object.prototype.hasOwnProperty.call(payload, 'notes') && !isLocalPendingSubmit) {
4314
+ const salesNotesState = this.ensureSalesNotesRuntimeState(tempOrder);
4315
+ salesNotesState.dirty = false;
4316
+ }
3712
4317
  const submittedOrderId = this.extractOrderIdFromSubmitResult(result);
3713
4318
  this.logInfo('runSubmitTempOrder: 提交成功', {
3714
4319
  submittedOrderId,
@@ -3722,9 +4327,10 @@ class OrderModule extends _BaseModule.BaseModule {
3722
4327
  tempOrder.order_id = submittedOrderId;
3723
4328
  const resultRecord = result;
3724
4329
  await this.markLocalOrderSynced(submittedOrderId, resultRecord?.data || resultRecord || {});
3725
- } else if (this.isLocalPendingSubmitResult(result)) {
4330
+ }
4331
+ if (isLocalPendingSubmit) {
3726
4332
  this.store.syncState = 'local';
3727
- } else {
4333
+ } else if (submittedOrderId === null || submittedOrderId === undefined) {
3728
4334
  this.store.syncState = 'submitted';
3729
4335
  }
3730
4336
  return result;
@@ -4110,7 +4716,7 @@ class OrderModule extends _BaseModule.BaseModule {
4110
4716
  params.forceRemote = false;
4111
4717
  }
4112
4718
  const res = await this.request.get(`/order/sales/${encodeURIComponent(lookup)}`, {
4113
- with: ['products', 'bookings', 'payments', 'customer', 'summary', 'contacts_info', 'refunds'],
4719
+ with: ['products', 'bookings', 'payments', 'customer', 'summary', 'contacts_info', 'order_meta_list', 'refunds', 'notes'],
4114
4720
  ...(params.forceRemote ? {
4115
4721
  forceRemote: true
4116
4722
  } : {})
@@ -4163,7 +4769,8 @@ class OrderModule extends _BaseModule.BaseModule {
4163
4769
  const isMergedSalesDetailHydrate = options?.source === 'mergedSalesDetail';
4164
4770
  const nextTempOrder = this.normalizeTempOrderForRuntime(raw, {
4165
4771
  // merge 前已只给远端商品补标记;这里不能把本地未提交商品一并变成 saved item。
4166
- makeEditMark: !isSessionStorageHydrate && !isMergedSalesDetailHydrate
4772
+ makeEditMark: !isSessionStorageHydrate && !isMergedSalesDetailHydrate,
4773
+ allowUnresolvedSalesNotes: isSessionStorageHydrate
4167
4774
  });
4168
4775
  this.store.tempOrder = nextTempOrder;
4169
4776
  if (isSessionStorageHydrate) {
@@ -4385,6 +4992,17 @@ class OrderModule extends _BaseModule.BaseModule {
4385
4992
  nextTempOrder.payments = Array.isArray(raw.payments) ? raw.payments.map(p => ({
4386
4993
  ...(p || {})
4387
4994
  })) : [];
4995
+ const rawHasSalesNotes = Object.prototype.hasOwnProperty.call(raw, 'notes');
4996
+ const restoredSalesNotesState = raw._extend?.salesNotesState;
4997
+ const allowUnresolvedSalesNotes = options?.allowUnresolvedSalesNotes === true || restoredSalesNotesState?.dirty === true || Number(raw.is_draft_order || 0) === 1;
4998
+ const decodedSalesNotes = rawHasSalesNotes ? (0, _salesNotes.decodeSalesNotesSnapshot)(raw.notes, {
4999
+ allowUnresolvedOrderTarget: allowUnresolvedSalesNotes
5000
+ }) : {
5001
+ notes: [],
5002
+ complete: false
5003
+ };
5004
+ const hasUnresolvedSalesNoteOrderTarget = decodedSalesNotes.notes.some(note => note.note_relations.some(relation => relation.is_primary === 0 && relation.relation_type === 'related_to' && relation.target_type === 'order' && relation.target_uuid === ''));
5005
+ nextTempOrder.notes = decodedSalesNotes.notes;
4388
5006
  nextTempOrder.surcharges = Array.isArray(raw.surcharges) ? raw.surcharges.map(s => ({
4389
5007
  ...(s || {})
4390
5008
  })) : [];
@@ -4397,6 +5015,7 @@ class OrderModule extends _BaseModule.BaseModule {
4397
5015
  nextTempOrder.contacts_info = raw.contacts_info && typeof raw.contacts_info === 'object' && !Array.isArray(raw.contacts_info) ? {
4398
5016
  ...raw.contacts_info
4399
5017
  } : null;
5018
+ nextTempOrder.order_meta_list = (0, _utils.normalizeOrderMetaList)(raw.order_meta_list);
4400
5019
  nextTempOrder._extend = raw._extend && typeof raw._extend === 'object' ? {
4401
5020
  ...raw._extend,
4402
5021
  productsByUid: raw._extend.productsByUid || {}
@@ -4404,6 +5023,10 @@ class OrderModule extends _BaseModule.BaseModule {
4404
5023
  productsByUid: {}
4405
5024
  };
4406
5025
  const tempOrderExtend = nextTempOrder._extend;
5026
+ tempOrderExtend.salesNotesState = {
5027
+ loaded: rawHasSalesNotes ? decodedSalesNotes.complete : restoredSalesNotesState?.loaded === true,
5028
+ dirty: decodedSalesNotes.complete && (restoredSalesNotesState?.dirty === true || allowUnresolvedSalesNotes && hasUnresolvedSalesNoteOrderTarget)
5029
+ };
4407
5030
  const productsByUid = tempOrderExtend.productsByUid || {};
4408
5031
  for (const [index, product] of nextTempOrder.products.entries()) {
4409
5032
  const uid = product.metadata?.unique_identification_number;
@@ -4550,6 +5173,23 @@ class OrderModule extends _BaseModule.BaseModule {
4550
5173
  row.metadata.booking_uid = row.booking_uid;
4551
5174
  }
4552
5175
  row = (0, _bundleDiscountHydration.restoreHydratedBundleDiscounts)(row, _utils.normalizeOrderProductDiscountList);
5176
+ const rawOriginalPrice = Number(row.original_price);
5177
+ const rawSellingPrice = Number(row.selling_price);
5178
+ const hasProductDiscounts = Array.isArray(row.discount_list) && row.discount_list.length > 0;
5179
+ const hasBundleDiscounts = row.product_bundle.some(bundle => Array.isArray(bundle?.discount_list) && bundle.discount_list.length > 0);
5180
+ const isUndiscountedV2Line = String(row.metadata.price_schema_version) === '2' && Number.isFinite(rawOriginalPrice) && Number.isFinite(rawSellingPrice) && new _decimal.default(rawOriginalPrice).eq(rawSellingPrice) && !hasProductDiscounts && !hasBundleDiscounts;
5181
+ if (isUndiscountedV2Line) {
5182
+ const hasPriceValue = value => value !== undefined && value !== null && value !== '';
5183
+ row.product_bundle = row.product_bundle.map(bundle => {
5184
+ if (!bundle || (0, _utils.isMarkdownBundle)(bundle) || hasPriceValue(bundle.original_price) || hasPriceValue(bundle.product_price) || !hasPriceValue(bundle.bundle_selling_price)) {
5185
+ return bundle;
5186
+ }
5187
+ return {
5188
+ ...bundle,
5189
+ original_price: bundle.bundle_selling_price
5190
+ };
5191
+ });
5192
+ }
4553
5193
  const existingIdentity = row.metadata.unique_identification_number || row.unique_identification_number;
4554
5194
  if (!existingIdentity) {
4555
5195
  row.metadata.unique_identification_number = (0, _utils.createUuidV4)();
@@ -4571,14 +5211,14 @@ class OrderModule extends _BaseModule.BaseModule {
4571
5211
  };
4572
5212
  if (result.metadata) delete result.metadata.price_schema_version;
4573
5213
  const restored = (0, _bundleDiscountHydration.restoreHydratedBundleDiscounts)(result, _utils.normalizeOrderProductDiscountList);
4574
- return this.sanitizeOrderProductForTempOrder(restored);
5214
+ return this.sanitizeOrderProductForTempOrder(this.normalizeVirtualSettlementProductIfEnabled(restored));
4575
5215
  }
4576
5216
  getLastOrderInfo() {
4577
5217
  return this.store?.lastOrderInfo;
4578
5218
  }
4579
5219
  async getOrderInfo(order_id) {
4580
5220
  const res = await this.request.get(`/order/sales/${order_id}`, {
4581
- with: ['products', 'bookings']
5221
+ with: ['products', 'bookings', 'notes']
4582
5222
  }, {
4583
5223
  osServer: true
4584
5224
  });