@pisell/pisellos 2.3.101 → 2.3.103

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 (56) 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/promotion/index.js +9 -0
  5. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +10 -7
  6. package/dist/modules/Discount/index.d.ts +2 -0
  7. package/dist/modules/Discount/index.js +38 -5
  8. package/dist/modules/Discount/types.d.ts +11 -0
  9. package/dist/modules/Order/index.d.ts +29 -3
  10. package/dist/modules/Order/index.js +1105 -703
  11. package/dist/modules/Order/types.d.ts +14 -6
  12. package/dist/modules/Order/types.js +1 -1
  13. package/dist/modules/Order/utils/virtualSettlement.d.ts +63 -0
  14. package/dist/modules/Order/utils/virtualSettlement.js +284 -0
  15. package/dist/modules/Order/utils.d.ts +1 -0
  16. package/dist/modules/Order/utils.js +63 -24
  17. package/dist/modules/Product/types.d.ts +23 -0
  18. package/dist/modules/SalesSummary/index.d.ts +1 -0
  19. package/dist/modules/SalesSummary/index.js +4 -3
  20. package/dist/modules/SalesSummary/types.d.ts +1 -0
  21. package/dist/modules/SalesSummary/utils.d.ts +1 -0
  22. package/dist/modules/SalesSummary/utils.js +21 -1
  23. package/dist/solution/BaseSales/index.d.ts +10 -1
  24. package/dist/solution/BaseSales/index.js +1074 -746
  25. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -0
  26. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +72 -9
  27. package/dist/solution/BookingByStep/index.d.ts +2 -2
  28. package/dist/solution/BookingTicket/utils/addProductDecision.js +2 -1
  29. package/lib/model/strategy/adapter/dataVariant/evaluator.d.ts +18 -2
  30. package/lib/model/strategy/adapter/dataVariant/evaluator.js +137 -6
  31. package/lib/model/strategy/adapter/dataVariant/type.d.ts +25 -0
  32. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
  33. package/lib/modules/Discount/index.d.ts +2 -0
  34. package/lib/modules/Discount/index.js +31 -2
  35. package/lib/modules/Discount/types.d.ts +11 -0
  36. package/lib/modules/Order/index.d.ts +29 -3
  37. package/lib/modules/Order/index.js +326 -51
  38. package/lib/modules/Order/types.d.ts +14 -6
  39. package/lib/modules/Order/types.js +1 -1
  40. package/lib/modules/Order/utils/virtualSettlement.d.ts +63 -0
  41. package/lib/modules/Order/utils/virtualSettlement.js +262 -0
  42. package/lib/modules/Order/utils.d.ts +1 -0
  43. package/lib/modules/Order/utils.js +57 -20
  44. package/lib/modules/Product/types.d.ts +23 -0
  45. package/lib/modules/SalesSummary/index.d.ts +1 -0
  46. package/lib/modules/SalesSummary/index.js +4 -2
  47. package/lib/modules/SalesSummary/types.d.ts +1 -0
  48. package/lib/modules/SalesSummary/utils.d.ts +1 -0
  49. package/lib/modules/SalesSummary/utils.js +17 -1
  50. package/lib/solution/BaseSales/index.d.ts +10 -1
  51. package/lib/solution/BaseSales/index.js +228 -20
  52. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -0
  53. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +38 -0
  54. package/lib/solution/BookingByStep/index.d.ts +2 -2
  55. package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -0
  56. package/package.json +1 -1
@@ -18,6 +18,7 @@ var _manualProductDiscount = require("./utils/manualProductDiscount");
18
18
  var _bundleDiscountHydration = require("./utils/bundleDiscountHydration");
19
19
  var _orderCollectionIdentity = require("./utils/orderCollectionIdentity");
20
20
  var _discountProductLineIdentity = require("./utils/discountProductLineIdentity");
21
+ var _virtualSettlement = require("./utils/virtualSettlement");
21
22
  var _Discount = require("../Discount");
22
23
  var _Rules = require("../Rules");
23
24
  var _types2 = require("../Rules/types");
@@ -94,8 +95,10 @@ class OrderModule extends _BaseModule.BaseModule {
94
95
  promotionEvaluator = null;
95
96
  /** 赠品选择 resolver;由 SDK host bridge 注入 */
96
97
  giftSelectResolver = null;
97
- /** applyPromotion 递归保护 flag(写路径同步调 applyPromotion,禁止重入) */
98
+ /** 促销评估递归保护 flag(写路径同步评估促销,禁止重入) */
98
99
  isApplyingPromotion = false;
100
+ /** 保持旧事件语义:同步事件监听器内触发的订单写入不得递归评估促销。 */
101
+ isEmittingPromotionApplied = false;
99
102
  /** 商品曾应用过客户券卡,客户变更剥离后需强制跑一次 calcDiscount 复位价格 */
100
103
  hasActiveCustomerBoundDiscount = false;
101
104
  /** 最近一次 applyPromotion 的未满足促销提示 */
@@ -298,6 +301,7 @@ class OrderModule extends _BaseModule.BaseModule {
298
301
  this.store.tempOrder = null;
299
302
  this.store.summary = (0, _utils.createEmptySummary)();
300
303
  this.store.availableWalletIds = [];
304
+ this.store.virtualCurrencyList = [];
301
305
  this.store.lastOrderInfo = undefined;
302
306
  this.store.syncState = undefined;
303
307
  this.store.discountList = [];
@@ -310,6 +314,9 @@ class OrderModule extends _BaseModule.BaseModule {
310
314
  if (!Array.isArray(this.store.availableWalletIds)) {
311
315
  this.store.availableWalletIds = [];
312
316
  }
317
+ if (!Array.isArray(this.store.virtualCurrencyList)) {
318
+ this.store.virtualCurrencyList = [];
319
+ }
313
320
  this.request = this.core.getPlugin('request');
314
321
  const appPlugin = this.core.getPlugin('app');
315
322
  if (!appPlugin) {
@@ -364,6 +371,12 @@ class OrderModule extends _BaseModule.BaseModule {
364
371
  this.otherParams = otherParams;
365
372
  }
366
373
 
374
+ /** PC/H5 属于在线店铺;其它或缺失平台统一沿用 POS 行为。 */
375
+ isOnlineStorePlatform() {
376
+ const platform = String(this.otherParams?.platform || '').trim().toLowerCase();
377
+ return platform === 'pc' || platform === 'h5';
378
+ }
379
+
367
380
  /**
368
381
  * 记录信息日志
369
382
  */
@@ -451,6 +464,7 @@ class OrderModule extends _BaseModule.BaseModule {
451
464
  with_wallet_pass_holder: 1,
452
465
  with_available_wallet_flag: 1,
453
466
  with_available_wallet_pass_flag: 1,
467
+ with_virtual_currency: 1,
454
468
  request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
455
469
  ...(orderId ? {
456
470
  order_id: orderId
@@ -458,6 +472,7 @@ class OrderModule extends _BaseModule.BaseModule {
458
472
  });
459
473
  const discountList = prepareConfigResult?.discountList;
460
474
  this.store.availableWalletIds = prepareConfigResult?.availableWalletIds || [];
475
+ this.store.virtualCurrencyList = prepareConfigResult?.virtualCurrencyList || [];
461
476
  if (discountList) {
462
477
  const currentDiscountList = this.store.discount?.getDiscountList() || [];
463
478
  const currentEditDiscounts = currentDiscountList.filter(discount => discount?.isEditMode);
@@ -485,6 +500,9 @@ class OrderModule extends _BaseModule.BaseModule {
485
500
  getAvailableWalletIds() {
486
501
  return Array.isArray(this.store.availableWalletIds) ? [...this.store.availableWalletIds] : [];
487
502
  }
503
+ getVirtualCurrencyList() {
504
+ return (0, _lodashEs.cloneDeep)(this.store.virtualCurrencyList || []);
505
+ }
488
506
  async syncScannedDiscountsToOriginalDiscountList(discountList) {
489
507
  const scanDiscounts = (discountList || []).filter(discount => discount?.isScan);
490
508
  if (!scanDiscounts.length) return;
@@ -508,6 +526,16 @@ class OrderModule extends _BaseModule.BaseModule {
508
526
  });
509
527
  }
510
528
  async scanCode(code, customerId) {
529
+ if (this.isActiveVirtualSettlementOrder(this.store.tempOrder)) {
530
+ return {
531
+ type: 'clientCalc',
532
+ isAvailable: false,
533
+ isAccepted: false,
534
+ discountList: [],
535
+ scannedDiscountList: [],
536
+ unavailableReason: _types2.UnavailableReason.ProductNotApplicable
537
+ };
538
+ }
511
539
  const resultDiscountWithReason = (await this.store.discount?.batchSearch(code, {
512
540
  customerId
513
541
  })) || {
@@ -603,6 +631,14 @@ class OrderModule extends _BaseModule.BaseModule {
603
631
  // 也需要让 Rules.calcDiscount 把 DiscountModule 的 isSelected / isAvailable /
604
632
  // appliedProductDetails / savedAmount 复位。
605
633
  if (!tempOrder) return;
634
+ if (this.isActiveVirtualSettlementOrder(tempOrder)) {
635
+ tempOrder.products = (0, _virtualSettlement.normalizeVirtualSettlementProducts)(tempOrder.products);
636
+ tempOrder.discount_list = [];
637
+ tempOrder.shop_discount = '0.00';
638
+ void this.store.discount?.setDiscountList([]);
639
+ void this.store.discount?.setOriginalDiscountList([]);
640
+ return;
641
+ }
606
642
  this.syncLinkedBookingHoldersToProducts(tempOrder);
607
643
  delete tempOrder.discount_list;
608
644
  const discountList = this.store.discount?.getDiscountList() || [];
@@ -679,7 +715,7 @@ class OrderModule extends _BaseModule.BaseModule {
679
715
  }
680
716
 
681
717
  /**
682
- * 应用购物车促销:计算 → 差异化赠品 → 写回 tempOrder.products → emit onPromotionApplied。
718
+ * 只执行购物车促销评估与赠品写回,不发布稳定态事件。
683
719
  *
684
720
  * 调用时机:写路径(add/update/remove/clear/setCustomer)末尾自动触发;外部一般无需手动调。
685
721
  *
@@ -687,22 +723,27 @@ class OrderModule extends _BaseModule.BaseModule {
687
723
  * - 不调用任何公开 CRUD API(如 removeProductFromOrder),全部内部 mutate tempOrder.products,避免事件风暴;
688
724
  * - 用 `isApplyingPromotion` 防递归;
689
725
  * - 多选项赠品依赖 giftSelectResolver,未注入则跳过且 console.warn;
690
- * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进。
726
+ * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进;
727
+ * - 返回的 payload 只能在 applyDiscount(商品写路径还需 summary/persist)完成后发布。
691
728
  */
692
- async applyPromotion() {
693
- if (this.isApplyingPromotion) {
694
- return;
729
+ async evaluatePromotion() {
730
+ if (this.isApplyingPromotion || this.isEmittingPromotionApplied) {
731
+ return null;
695
732
  }
696
733
  if (!this.promotionEvaluator) {
697
- return;
734
+ return null;
698
735
  }
699
736
  const tempOrder = this.store.tempOrder;
700
737
  if (!tempOrder) {
701
- return;
738
+ return null;
739
+ }
740
+ if (this.isActiveVirtualSettlementOrder(tempOrder)) {
741
+ tempOrder.products = (0, _virtualSettlement.normalizeVirtualSettlementProducts)(tempOrder.products);
742
+ return null;
702
743
  }
703
744
  const strategyConfigs = this.promotionEvaluator.getStrategyConfigs?.();
704
745
  if (Array.isArray(strategyConfigs) && strategyConfigs.length === 0) {
705
- return;
746
+ return null;
706
747
  }
707
748
  this.isApplyingPromotion = true;
708
749
  try {
@@ -794,14 +835,46 @@ class OrderModule extends _BaseModule.BaseModule {
794
835
  })),
795
836
  products: tempOrder.products
796
837
  };
797
- this.effectsEmit('onPromotionApplied', payload);
838
+ return payload;
798
839
  } catch (error) {
799
840
  console.error('[OrderModule] applyPromotion failed', error);
841
+ return null;
800
842
  } finally {
801
843
  this.isApplyingPromotion = false;
802
844
  }
803
845
  }
804
846
 
847
+ /** 发布促销稳定态;products 必须读取折扣计算后的当前数组。 */
848
+ emitPromotionApplied(payload) {
849
+ if (!payload) return;
850
+ const products = this.store.tempOrder?.products || payload.products;
851
+ this.isEmittingPromotionApplied = true;
852
+ try {
853
+ void this.effectsEmit('onPromotionApplied', {
854
+ ...payload,
855
+ products
856
+ });
857
+ } finally {
858
+ // effectsEmit 同步调用监听器后返回 Promise;保持既有的非阻塞事件行为。
859
+ this.isEmittingPromotionApplied = false;
860
+ }
861
+ }
862
+
863
+ /** 公开入口:促销、折扣、summary 与持久化完成后发布稳定态事件。 */
864
+ async applyPromotion() {
865
+ const payload = await this.evaluatePromotion();
866
+ if (!payload) return;
867
+ const tempOrder = this.store.tempOrder;
868
+ if (!tempOrder) return;
869
+ this.applyDiscount();
870
+ this.sanitizeTempOrderProducts(tempOrder);
871
+ await this.recalculateSummary({
872
+ createIfMissing: true
873
+ });
874
+ this.persistTempOrder();
875
+ this.emitPromotionApplied(payload);
876
+ }
877
+
805
878
  /** 最近一次 applyPromotion 输出的未满足促销列表 */
806
879
  getUnfulfilledPromotions() {
807
880
  return this.lastUnfulfilledPromotions;
@@ -1830,7 +1903,8 @@ class OrderModule extends _BaseModule.BaseModule {
1830
1903
  return product;
1831
1904
  }
1832
1905
  sanitizeTempOrderProducts(tempOrder) {
1833
- tempOrder.products = (tempOrder.products || []).map(product => this.sanitizeOrderProductForTempOrder(product));
1906
+ tempOrder.products = (tempOrder.products || []).map(product => this.sanitizeOrderProductForTempOrder(this.normalizeVirtualSettlementProductIfEnabled(product)));
1907
+ this.syncTempOrderSettlementState(tempOrder);
1834
1908
  (0, _discountProductLineIdentity.ensureUniqueProductLineUids)(tempOrder);
1835
1909
  }
1836
1910
  ensureExtend(tempOrder) {
@@ -2151,6 +2225,7 @@ class OrderModule extends _BaseModule.BaseModule {
2151
2225
  products: summaryProducts,
2152
2226
  isPriceIncludeTax: tempOrder.is_price_include_tax,
2153
2227
  shopDiscount: tempOrder.shop_discount,
2228
+ virtualSettlementMode: this.shouldUseVirtualSettlementSummary(tempOrder),
2154
2229
  ...(this.otherParams?.channel ? {
2155
2230
  channel: this.otherParams.channel
2156
2231
  } : {})
@@ -2619,6 +2694,7 @@ class OrderModule extends _BaseModule.BaseModule {
2619
2694
  const nextCustomerId = tempOrder.customer_id ?? null;
2620
2695
  if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '')) {
2621
2696
  this.store.availableWalletIds = [];
2697
+ this.store.virtualCurrencyList = [];
2622
2698
  (0, _utils.clearTempOrderHolderAssignments)(tempOrder);
2623
2699
  this.clearCustomerBoundDiscounts(tempOrder);
2624
2700
  this.applyDiscount();
@@ -2657,6 +2733,7 @@ class OrderModule extends _BaseModule.BaseModule {
2657
2733
  tempOrder.customer = null;
2658
2734
  if (tempOrder._extend) delete tempOrder._extend.customerSnapshot;
2659
2735
  this.store.availableWalletIds = [];
2736
+ this.store.virtualCurrencyList = [];
2660
2737
  this.clearCustomerBoundDiscounts(tempOrder);
2661
2738
  this.applyDiscount();
2662
2739
  this.persistTempOrder();
@@ -2976,6 +3053,78 @@ class OrderModule extends _BaseModule.BaseModule {
2976
3053
  const bookingUid = product?.booking_uid ?? product?.metadata?.booking_uid;
2977
3054
  return bookingUid !== undefined && bookingUid !== null && String(bookingUid) !== '';
2978
3055
  }
3056
+ isMultiCurrencySettlementEnabled() {
3057
+ return this.otherParams?.enableMultiCurrencySettlement === true;
3058
+ }
3059
+ isActiveVirtualSettlementOrder(tempOrder) {
3060
+ return this.isMultiCurrencySettlementEnabled() && (0, _virtualSettlement.isVirtualSettlementOrder)(tempOrder);
3061
+ }
3062
+ shouldUseVirtualSettlementSummary(tempOrder) {
3063
+ return this.isActiveVirtualSettlementOrder(tempOrder) || tempOrder?.metadata?.currency_settlement_config?.settlement_type === 'virtual_currency';
3064
+ }
3065
+ normalizeVirtualSettlementProductIfEnabled(product) {
3066
+ return this.isMultiCurrencySettlementEnabled() ? (0, _virtualSettlement.normalizeVirtualSettlementOrderProduct)(product) : product;
3067
+ }
3068
+ assertAndLockProductSettlement(tempOrder, product) {
3069
+ if (!this.isMultiCurrencySettlementEnabled()) return;
3070
+ const currentKey = (0, _virtualSettlement.getTempOrderSettlementKey)(tempOrder);
3071
+ const incomingKey = (0, _virtualSettlement.getSettlementKey)((0, _virtualSettlement.getSelectedSettlementSnapshot)(product)) || 'legal_currency';
3072
+ if (currentKey && currentKey !== incomingKey) {
3073
+ throw new _virtualSettlement.OrderSettlementConflictError(currentKey, incomingKey);
3074
+ }
3075
+ const snapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(product);
3076
+ if (snapshot?.settlement_type === 'virtual_currency' && tempOrder.customer_id && Number(tempOrder.customer_id) !== 1) {
3077
+ const currencyProductId = Number(snapshot.currency_product_id);
3078
+ const asset = (this.store.virtualCurrencyList || []).find(item => Number(item?.product_id) === currencyProductId);
3079
+ if (!asset) {
3080
+ throw new _virtualSettlement.VirtualCurrencyAssetNotFoundError(currencyProductId);
3081
+ }
3082
+ const existedAmount = tempOrder.products.reduce((sum, item) => sum.plus(new _decimal.default(item.selling_price || 0).times(item.num || 1)), new _decimal.default(0));
3083
+ const requiredAmount = existedAmount.plus(new _decimal.default(snapshot.price || 0).times(product.num || 1));
3084
+ const balance = new _decimal.default(asset.balance || 0);
3085
+ if (balance.lt(requiredAmount)) {
3086
+ throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
3087
+ }
3088
+ }
3089
+ (0, _virtualSettlement.applyTempOrderSettlementState)(tempOrder, snapshot || {
3090
+ settlement_type: 'legal_currency',
3091
+ price: product.selling_price ?? product.original_price ?? product.payment_price ?? 0,
3092
+ unit_label: tempOrder.currency_code,
3093
+ symbol: tempOrder.currency_symbol,
3094
+ amount_format: tempOrder.currency_format
3095
+ });
3096
+ }
3097
+ syncTempOrderSettlementState(tempOrder) {
3098
+ if (!this.isMultiCurrencySettlementEnabled()) return;
3099
+ tempOrder.metadata = tempOrder.metadata || {};
3100
+ if (!tempOrder.products.length) {
3101
+ (0, _virtualSettlement.releaseTempOrderSettlementState)(tempOrder);
3102
+ return;
3103
+ }
3104
+ const snapshot = (0, _virtualSettlement.getTempOrderSettlementSnapshot)(tempOrder);
3105
+ if (snapshot) {
3106
+ (0, _virtualSettlement.applyTempOrderSettlementState)(tempOrder, snapshot);
3107
+ return;
3108
+ }
3109
+ tempOrder.metadata.cart_settlement_key = 'legal_currency';
3110
+ }
3111
+ prepareVirtualSettlementTempOrder(tempOrder) {
3112
+ if (!this.isMultiCurrencySettlementEnabled()) return;
3113
+ this.syncTempOrderSettlementState(tempOrder);
3114
+ if (!this.isActiveVirtualSettlementOrder(tempOrder)) return;
3115
+ tempOrder.products = (0, _virtualSettlement.normalizeVirtualSettlementProducts)(tempOrder.products);
3116
+ tempOrder.discount_list = [];
3117
+ tempOrder.shop_discount = '0.00';
3118
+ tempOrder.surcharge_fee = '0.00';
3119
+ tempOrder.surcharges = [];
3120
+ const virtualOrder = tempOrder;
3121
+ virtualOrder.discount_amount = '0.00';
3122
+ virtualOrder.product_tax_fee = '0.00';
3123
+ virtualOrder.tax_fee = '0.00';
3124
+ virtualOrder.shipping_fee = '0.00';
3125
+ virtualOrder.shipping_tax_fee = '0.00';
3126
+ virtualOrder.tip_fee = '0.00';
3127
+ }
2979
3128
 
2980
3129
  // ─── TempOrder: 商品 CRUD ───
2981
3130
 
@@ -2987,13 +3136,13 @@ class OrderModule extends _BaseModule.BaseModule {
2987
3136
  */
2988
3137
  async finalizeProductOrderMutation(tempOrder, options) {
2989
3138
  this.syncTempOrderHolderFromBookings(tempOrder);
2990
- if (!options?.skipEditDiscountConfigRefresh) {
3139
+ this.prepareVirtualSettlementTempOrder(tempOrder);
3140
+ const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
3141
+ if (!options?.skipEditDiscountConfigRefresh && !isVirtualSettlement) {
2991
3142
  await this.ensureEditDiscountConfigForProductChange(tempOrder);
2992
3143
  }
2993
- if (!options?.skipDiscountRecalculation) {
2994
- await this.applyPromotion();
2995
- }
2996
- if (!options?.skipDiscountRecalculation) {
3144
+ const promotionPayload = !options?.skipDiscountRecalculation && !isVirtualSettlement ? await this.evaluatePromotion() : null;
3145
+ if (!options?.skipDiscountRecalculation && !isVirtualSettlement) {
2997
3146
  this.applyDiscount();
2998
3147
  }
2999
3148
  this.sanitizeTempOrderProducts(tempOrder);
@@ -3001,6 +3150,7 @@ class OrderModule extends _BaseModule.BaseModule {
3001
3150
  createIfMissing: true
3002
3151
  });
3003
3152
  this.persistTempOrder();
3153
+ this.emitPromotionApplied(promotionPayload);
3004
3154
  }
3005
3155
 
3006
3156
  /**
@@ -3021,9 +3171,10 @@ class OrderModule extends _BaseModule.BaseModule {
3021
3171
  booking
3022
3172
  }) : null;
3023
3173
  const productToAdd = linked?.product || product;
3174
+ this.assertAndLockProductSettlement(tempOrder, productToAdd);
3024
3175
  const disableMerge = options?.disableMerge === true;
3025
3176
  const incomingFingerprint = disableMerge ? '' : (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(productToAdd), productToAdd.product_bundle);
3026
- const normalizedIncoming = (0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd));
3177
+ const normalizedIncoming = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(productToAdd, (0, _utils3.normalizeOrderProduct)(productToAdd)));
3027
3178
  if (disableMerge) {
3028
3179
  normalizedIncoming.metadata = {
3029
3180
  ...(normalizedIncoming.metadata || {}),
@@ -3084,13 +3235,13 @@ class OrderModule extends _BaseModule.BaseModule {
3084
3235
  } else {
3085
3236
  const targetProduct = matchedProduct;
3086
3237
  const targetUid = targetProduct.metadata?.unique_identification_number;
3087
- const normalizedProduct = (0, _utils.mergeOrderProductDisplayFields)(targetProduct, (0, _utils3.normalizeOrderProduct)({
3238
+ const normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(targetProduct, (0, _utils3.normalizeOrderProduct)({
3088
3239
  ...productToAdd,
3089
3240
  metadata: {
3090
3241
  ...(productToAdd.metadata || {}),
3091
3242
  unique_identification_number: targetUid
3092
3243
  }
3093
- }));
3244
+ })));
3094
3245
  normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3095
3246
  const nextNum = (0, _utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num);
3096
3247
  tempOrder.products[matchedIndex] = {
@@ -3146,6 +3297,67 @@ class OrderModule extends _BaseModule.BaseModule {
3146
3297
  return commitProductLine(shouldMerge);
3147
3298
  }
3148
3299
 
3300
+ /**
3301
+ * booking 数量拆行时,若 Holder 数量与拆行数严格一致,则按下标分配。
3302
+ * 数量不一致时保持旧数据,避免猜测 Holder 与商品份数的对应关系。
3303
+ */
3304
+ createSplitBookingLine(product, booking, index, splitCount) {
3305
+ const singleLine = (0, _lodashEs.cloneDeep)(product);
3306
+ singleLine.num = 1;
3307
+ delete singleLine.product_quantity;
3308
+ const singleBooking = (0, _lodashEs.cloneDeep)(booking);
3309
+ const bookingHolder = booking?.holder && typeof booking.holder === 'object' ? booking.holder : null;
3310
+ const metadataHolder = booking?.metadata?.holder && typeof booking.metadata.holder === 'object' ? booking.metadata.holder : null;
3311
+ const rawHolderIds = bookingHolder?.form_record ?? booking?.metadata?.holder_id ?? metadataHolder?.form_record ?? product?.metadata?.holder_id;
3312
+ 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 !== '');
3313
+ if (holderIds.length !== splitCount) {
3314
+ return {
3315
+ product: singleLine,
3316
+ booking: singleBooking
3317
+ };
3318
+ }
3319
+ const holderId = holderIds[index];
3320
+ const rawHolderName = bookingHolder?.name ?? metadataHolder?.name;
3321
+ const holderNames = Array.isArray(rawHolderName) ? rawHolderName.map(name => String(name || '').trim()) : typeof rawHolderName === 'string' ? rawHolderName.split(',').map(name => name.trim()) : [];
3322
+ const holderName = holderNames.length === splitCount ? holderNames[index] : undefined;
3323
+ const splitHolder = holder => ({
3324
+ ...holder,
3325
+ form_record: [holderId],
3326
+ ...(holderName ? {
3327
+ name: holderName
3328
+ } : Object.prototype.hasOwnProperty.call(holder, 'name') ? {
3329
+ name: undefined
3330
+ } : {})
3331
+ });
3332
+ singleLine.metadata = {
3333
+ ...(singleLine.metadata || {}),
3334
+ holder_id: [holderId]
3335
+ };
3336
+ if (singleLine._extend && typeof singleLine._extend === 'object') {
3337
+ singleLine._extend = {
3338
+ ...singleLine._extend,
3339
+ holder_id: [holderId]
3340
+ };
3341
+ }
3342
+ if (singleBooking.holder && typeof singleBooking.holder === 'object') {
3343
+ singleBooking.holder = splitHolder(singleBooking.holder);
3344
+ }
3345
+ singleBooking.metadata = {
3346
+ ...(singleBooking.metadata || {}),
3347
+ holder_id: [holderId],
3348
+ ...(singleBooking.metadata?.holder && typeof singleBooking.metadata.holder === 'object' ? {
3349
+ holder: splitHolder(singleBooking.metadata.holder)
3350
+ } : {})
3351
+ };
3352
+ if (Object.prototype.hasOwnProperty.call(singleBooking, 'holder_id')) {
3353
+ singleBooking.holder_id = [holderId];
3354
+ }
3355
+ return {
3356
+ product: singleLine,
3357
+ booking: singleBooking
3358
+ };
3359
+ }
3360
+
3149
3361
  /**
3150
3362
  * 添加商品行。带 booking 时强制独立行;num>1 时拆成多条 num=1 的 product+booking(对齐 BookingByStep / ticketBooking addService)。
3151
3363
  *
@@ -3204,29 +3416,26 @@ class OrderModule extends _BaseModule.BaseModule {
3204
3416
  const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
3205
3417
  if (splitCount > 1) {
3206
3418
  for (let i = 0; i < splitCount; i += 1) {
3207
- const singleLine = (0, _lodashEs.cloneDeep)(productRecord);
3208
- const singleBooking = (0, _lodashEs.cloneDeep)(booking);
3209
- singleLine.num = 1;
3210
- delete singleLine.product_quantity;
3419
+ const splitLine = this.createSplitBookingLine(productRecord, booking, i, splitCount);
3211
3420
  if (i > 0) {
3212
- delete singleLine.unique_identification_number;
3213
- delete singleLine.identity_key;
3214
- if (singleLine.metadata) {
3215
- singleLine.metadata = {
3216
- ...singleLine.metadata
3421
+ delete splitLine.product.unique_identification_number;
3422
+ delete splitLine.product.identity_key;
3423
+ if (splitLine.product.metadata) {
3424
+ splitLine.product.metadata = {
3425
+ ...splitLine.product.metadata
3217
3426
  };
3218
- delete singleLine.metadata.unique_identification_number;
3219
- delete singleLine.metadata.identity_key;
3427
+ delete splitLine.product.metadata.unique_identification_number;
3428
+ delete splitLine.product.metadata.identity_key;
3220
3429
  }
3221
- delete singleBooking.booking_uid;
3222
- if (singleBooking.metadata) {
3223
- singleBooking.metadata = {
3224
- ...singleBooking.metadata
3430
+ delete splitLine.booking.booking_uid;
3431
+ if (splitLine.booking.metadata) {
3432
+ splitLine.booking.metadata = {
3433
+ ...splitLine.booking.metadata
3225
3434
  };
3226
- delete singleBooking.metadata.unique_identification_number;
3435
+ delete splitLine.booking.metadata.unique_identification_number;
3227
3436
  }
3228
3437
  }
3229
- const committedProduct = await this.appendProductLineToTempOrder(tempOrder, singleLine, singleBooking, {
3438
+ const committedProduct = await this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking, {
3230
3439
  insertAtIndex,
3231
3440
  disableMerge: options?.disableMerge
3232
3441
  });
@@ -3282,10 +3491,8 @@ class OrderModule extends _BaseModule.BaseModule {
3282
3491
  const splitCount = (0, _utils3.getSafeProductNum)(productRecord.num ?? productRecord.product_quantity ?? 1);
3283
3492
  if (splitCount > 1) {
3284
3493
  for (let i = 0; i < splitCount; i += 1) {
3285
- const singleLine = (0, _lodashEs.cloneDeep)(productRecord);
3286
- singleLine.num = 1;
3287
- delete singleLine.product_quantity;
3288
- const appendResult = this.appendProductLineToTempOrder(tempOrder, singleLine, (0, _lodashEs.cloneDeep)(booking));
3494
+ const splitLine = this.createSplitBookingLine(productRecord, booking, i, splitCount);
3495
+ const appendResult = this.appendProductLineToTempOrder(tempOrder, splitLine.product, splitLine.booking);
3289
3496
  if (appendResult) await appendResult;
3290
3497
  }
3291
3498
  continue;
@@ -3498,7 +3705,7 @@ class OrderModule extends _BaseModule.BaseModule {
3498
3705
  }
3499
3706
  // normalizeOrderProduct 幂等:v2 metadata 存在 → 保留 main_product_* 折扣重算 composite;
3500
3707
  // 否则 → 把顶层 selling_price 视作 source,派生 main_product_* 并写 price_schema_version: 2。
3501
- let normalizedProduct = (0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct));
3708
+ let normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct)));
3502
3709
  normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3503
3710
  const preservedBookingUid = nextProduct.booking_uid || nextProduct.metadata?.booking_uid || targetProduct.booking_uid || targetProduct.metadata?.booking_uid;
3504
3711
  if (!booking && preservedBookingUid) {
@@ -3541,7 +3748,7 @@ class OrderModule extends _BaseModule.BaseModule {
3541
3748
  async updateOrderProduct(params) {
3542
3749
  const tempOrder = this.ensureTempOrder();
3543
3750
  const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
3544
- await this.applyPromotion();
3751
+ const promotionPayload = await this.evaluatePromotion();
3545
3752
  this.applyDiscount({
3546
3753
  reapplyBookingDiscountProductUids: params.allow_booking_discount_reapply && updatedProductUid ? [String(updatedProductUid)] : undefined
3547
3754
  });
@@ -3550,6 +3757,7 @@ class OrderModule extends _BaseModule.BaseModule {
3550
3757
  createIfMissing: true
3551
3758
  });
3552
3759
  this.persistTempOrder();
3760
+ this.emitPromotionApplied(promotionPayload);
3553
3761
  this.logInfo('updateOrderProduct success', {
3554
3762
  params
3555
3763
  });
@@ -3615,23 +3823,45 @@ class OrderModule extends _BaseModule.BaseModule {
3615
3823
  throw new Error('[Order] 目标商品不存在,无法更新数量');
3616
3824
  }
3617
3825
  const targetProduct = tempOrder.products[productIndex];
3618
- const normalizedProduct = (0, _utils3.normalizeOrderProduct)({
3826
+ const normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils3.normalizeOrderProduct)({
3619
3827
  ...targetProduct,
3620
3828
  num: (0, _utils3.getSafeProductNum)(num),
3621
3829
  metadata: {
3622
3830
  ...(targetProduct.metadata || {}),
3623
3831
  unique_identification_number: targetProduct.metadata?.unique_identification_number || unique_identification_number
3624
3832
  }
3625
- });
3833
+ }));
3626
3834
  normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
3835
+ const settlementSnapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(normalizedProduct);
3836
+ if (this.isMultiCurrencySettlementEnabled() && settlementSnapshot?.settlement_type === 'virtual_currency' && tempOrder.customer_id && Number(tempOrder.customer_id) !== 1) {
3837
+ const currencyProductId = Number(settlementSnapshot.currency_product_id);
3838
+ const asset = (this.store.virtualCurrencyList || []).find(item => Number(item?.product_id) === currencyProductId);
3839
+ if (!asset) {
3840
+ throw new _virtualSettlement.VirtualCurrencyAssetNotFoundError(currencyProductId);
3841
+ }
3842
+ const requiredAmount = tempOrder.products.reduce((sum, item, index) => {
3843
+ const current = index === productIndex ? normalizedProduct : item;
3844
+ const currentSnapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(current);
3845
+ return sum.plus(new _decimal.default(currentSnapshot?.price || current.selling_price || 0).times(current.num || 1));
3846
+ }, new _decimal.default(0));
3847
+ const balance = new _decimal.default(asset.balance || 0);
3848
+ if (balance.lt(requiredAmount)) {
3849
+ throw new _virtualSettlement.VirtualCurrencyBalanceError(balance.toFixed(2), requiredAmount.toFixed(2));
3850
+ }
3851
+ }
3627
3852
  tempOrder.products[productIndex] = normalizedProduct;
3628
- await this.applyPromotion();
3629
- this.applyDiscount();
3853
+ this.prepareVirtualSettlementTempOrder(tempOrder);
3854
+ const isVirtualSettlement = this.isActiveVirtualSettlementOrder(tempOrder);
3855
+ const promotionPayload = !isVirtualSettlement ? await this.evaluatePromotion() : null;
3856
+ if (!isVirtualSettlement) {
3857
+ this.applyDiscount();
3858
+ }
3630
3859
  this.sanitizeTempOrderProducts(tempOrder);
3631
3860
  await this.recalculateSummary({
3632
3861
  createIfMissing: true
3633
3862
  });
3634
3863
  this.persistTempOrder();
3864
+ this.emitPromotionApplied(promotionPayload);
3635
3865
  this.logInfo('updateOrderProductQuantity success', {
3636
3866
  params
3637
3867
  });
@@ -3676,7 +3906,7 @@ class OrderModule extends _BaseModule.BaseModule {
3676
3906
  */
3677
3907
  async finalizeAfterProductsMutation(tempOrder, options) {
3678
3908
  this.syncTempOrderHolderFromBookings(tempOrder);
3679
- await this.applyPromotion();
3909
+ const promotionPayload = await this.evaluatePromotion();
3680
3910
  this.applyDiscount({
3681
3911
  reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
3682
3912
  });
@@ -3685,6 +3915,7 @@ class OrderModule extends _BaseModule.BaseModule {
3685
3915
  createIfMissing: true
3686
3916
  });
3687
3917
  this.persistTempOrder();
3918
+ this.emitPromotionApplied(promotionPayload);
3688
3919
  }
3689
3920
 
3690
3921
  /**
@@ -3780,6 +4011,7 @@ class OrderModule extends _BaseModule.BaseModule {
3780
4011
  this.ensureSalesNotesLoadedForMutation(tempOrder);
3781
4012
  tempOrder.products = [];
3782
4013
  tempOrder.bookings = [];
4014
+ (0, _virtualSettlement.releaseTempOrderSettlementState)(tempOrder);
3783
4015
  this.removeSalesNotesForMissingProductLines(tempOrder);
3784
4016
  this.syncTempOrderHolderFromBookings(tempOrder);
3785
4017
  if (tempOrder._extend && typeof tempOrder._extend === 'object') {
@@ -3788,17 +4020,53 @@ class OrderModule extends _BaseModule.BaseModule {
3788
4020
  productsByUid: {}
3789
4021
  };
3790
4022
  }
3791
- await this.applyPromotion();
4023
+ const promotionPayload = await this.evaluatePromotion();
3792
4024
  this.applyDiscount();
3793
4025
  this.sanitizeTempOrderProducts(tempOrder);
3794
4026
  await this.recalculateSummary({
3795
4027
  createIfMissing: true
3796
4028
  });
3797
4029
  this.persistTempOrder();
4030
+ this.emitPromotionApplied(promotionPayload);
3798
4031
  this.logInfo('clearOrderCartLines success', {});
3799
4032
  return tempOrder;
3800
4033
  }
3801
4034
 
4035
+ /**
4036
+ * 切换购物车结算单位时清理所有会影响新币种的订单状态,但保留当前客户。
4037
+ */
4038
+ async clearOrderForSettlementSwitch() {
4039
+ const tempOrder = this.ensureTempOrder();
4040
+ this.ensureSalesNotesLoadedForMutation(tempOrder);
4041
+ tempOrder.products = [];
4042
+ tempOrder.bookings = [];
4043
+ this.removeSalesNotesForMissingProductLines(tempOrder);
4044
+ tempOrder.discount_list = [];
4045
+ tempOrder.payments = [];
4046
+ tempOrder.refunds = [];
4047
+ tempOrder.shop_discount = '0.00';
4048
+ tempOrder.surcharges = [];
4049
+ (0, _virtualSettlement.releaseTempOrderSettlementState)(tempOrder);
4050
+ this.syncTempOrderHolderFromBookings(tempOrder);
4051
+ if (tempOrder._extend && typeof tempOrder._extend === 'object') {
4052
+ tempOrder._extend = {
4053
+ ...tempOrder._extend,
4054
+ productsByUid: {}
4055
+ };
4056
+ }
4057
+ await this.store.discount?.setDiscountList([]);
4058
+ await this.store.discount?.setOriginalDiscountList([]);
4059
+ this.hasActiveCustomerBoundDiscount = false;
4060
+ this.lastUnfulfilledPromotions = [];
4061
+ this.lastGiftActions = null;
4062
+ await this.recalculateSummary({
4063
+ createIfMissing: true
4064
+ });
4065
+ this.persistTempOrder();
4066
+ this.logInfo('clearOrderForSettlementSwitch success', {});
4067
+ return tempOrder;
4068
+ }
4069
+
3802
4070
  // ─── TempOrder: 提交 ───
3803
4071
 
3804
4072
  buildCurrentSubmitPayloadForLocalPrint(params) {
@@ -3888,9 +4156,14 @@ class OrderModule extends _BaseModule.BaseModule {
3888
4156
  }
3889
4157
  async runSubmitTempOrder(params) {
3890
4158
  const tempOrder = this.ensureTempOrder();
4159
+ this.prepareVirtualSettlementTempOrder(tempOrder);
4160
+ const isOnlineStorePlatform = this.isOnlineStorePlatform();
3891
4161
  const shouldConfirmPendingVoucherPayments = params?.confirmPendingVoucherPayments !== false;
3892
4162
  const hasPaymentOverride = params?.payments !== undefined;
3893
- const preparedPaymentOverride = hasPaymentOverride ? this.prepareVoucherPaymentsForSubmit(params?.payments || [], shouldConfirmPendingVoucherPayments) : undefined;
4163
+ let preparedPaymentOverride;
4164
+ if (hasPaymentOverride) {
4165
+ preparedPaymentOverride = isOnlineStorePlatform ? params?.payments || [] : this.prepareVoucherPaymentsForSubmit(params?.payments || [], shouldConfirmPendingVoucherPayments);
4166
+ }
3894
4167
  if (!hasPaymentOverride) {
3895
4168
  if (shouldConfirmPendingVoucherPayments) {
3896
4169
  this.confirmPendingVoucherPayments({
@@ -3910,6 +4183,7 @@ class OrderModule extends _BaseModule.BaseModule {
3910
4183
  })) || this.store.summary || (0, _utils.createEmptySummary)();
3911
4184
  const effectiveCacheId = params?.cacheId ?? this.cacheId;
3912
4185
  const hasPaymentStatusOverride = params?.paymentStatus !== undefined;
4186
+ const shouldOverridePaymentStatus = hasPaymentStatusOverride && !isOnlineStorePlatform && !shouldConfirmPendingVoucherPayments;
3913
4187
  const hasSmallTicketDataFlagOverride = params?.smallTicketDataFlag !== undefined;
3914
4188
  const enhancePayload = hasPaymentOverride || hasPaymentStatusOverride || hasSmallTicketDataFlagOverride || params?.enhancePayload ? (payload, ctx) => {
3915
4189
  const nextPayload = {
@@ -3917,7 +4191,7 @@ class OrderModule extends _BaseModule.BaseModule {
3917
4191
  ...(hasPaymentOverride ? {
3918
4192
  payments: preparedPaymentOverride || []
3919
4193
  } : {}),
3920
- ...(hasPaymentStatusOverride && !shouldConfirmPendingVoucherPayments ? {
4194
+ ...(shouldOverridePaymentStatus ? {
3921
4195
  payment_status: params?.paymentStatus
3922
4196
  } : {}),
3923
4197
  ...(hasSmallTicketDataFlagOverride ? {
@@ -3937,6 +4211,7 @@ class OrderModule extends _BaseModule.BaseModule {
3937
4211
  type: submitSnapshot.type ?? params?.type,
3938
4212
  summary: latestSummary,
3939
4213
  request_unique_idempotency_token: params?.request_unique_idempotency_token,
4214
+ includePaymentStatus: !isOnlineStorePlatform,
3940
4215
  enhance: enhancePayload
3941
4216
  });
3942
4217
  if (params?.syncMode) {
@@ -4868,7 +5143,7 @@ class OrderModule extends _BaseModule.BaseModule {
4868
5143
  };
4869
5144
  if (result.metadata) delete result.metadata.price_schema_version;
4870
5145
  const restored = (0, _bundleDiscountHydration.restoreHydratedBundleDiscounts)(result, _utils.normalizeOrderProductDiscountList);
4871
- return this.sanitizeOrderProductForTempOrder(restored);
5146
+ return this.sanitizeOrderProductForTempOrder(this.normalizeVirtualSettlementProductIfEnabled(restored));
4872
5147
  }
4873
5148
  getLastOrderInfo() {
4874
5149
  return this.store?.lastOrderInfo;