@pisell/pisellos 2.2.308 → 2.2.310

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 (130) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  3. package/dist/model/strategy/adapter/walletPass/evaluator.js +4 -2
  4. package/dist/modules/BookingContext/index.js +4 -4
  5. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +58 -19
  6. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +24 -12
  7. package/dist/modules/Customer/index.js +6 -3
  8. package/dist/modules/Customer/types.d.ts +2 -0
  9. package/dist/modules/Discount/availability.d.ts +7 -0
  10. package/dist/modules/Discount/availability.js +49 -0
  11. package/dist/modules/Discount/entitlementPass.d.ts +34 -0
  12. package/dist/modules/Discount/entitlementPass.js +147 -0
  13. package/dist/modules/Discount/index.d.ts +2 -2
  14. package/dist/modules/Discount/index.js +37 -151
  15. package/dist/modules/Discount/types.d.ts +36 -5
  16. package/dist/modules/OpenData/index.d.ts +1 -1
  17. package/dist/modules/OpenData/index.js +21 -12
  18. package/dist/modules/Order/index.d.ts +33 -4
  19. package/dist/modules/Order/index.js +830 -411
  20. package/dist/modules/Order/types.d.ts +17 -0
  21. package/dist/modules/Order/utils/orderCollectionIdentity.js +16 -12
  22. package/dist/modules/Order/utils/weighedProduct.d.ts +12 -0
  23. package/dist/modules/Order/utils/weighedProduct.js +94 -0
  24. package/dist/modules/Order/utils.d.ts +14 -2
  25. package/dist/modules/Order/utils.js +280 -79
  26. package/dist/modules/Payment/index.d.ts +0 -5
  27. package/dist/modules/Payment/index.js +35 -85
  28. package/dist/modules/Payment/types.d.ts +21 -1
  29. package/dist/modules/Payment/types.js +2 -0
  30. package/dist/modules/Payment/walletpass.d.ts +1 -1
  31. package/dist/modules/Payment/walletpass.js +80 -47
  32. package/dist/modules/ProductList/index.js +18 -5
  33. package/dist/modules/Rules/index.d.ts +2 -2
  34. package/dist/modules/Rules/index.js +484 -188
  35. package/dist/modules/Rules/types.d.ts +6 -0
  36. package/dist/server/index.js +8 -5
  37. package/dist/server/modules/order/index.js +6 -0
  38. package/dist/server/modules/order/types.d.ts +12 -0
  39. package/dist/server/modules/payment/index.d.ts +25 -6
  40. package/dist/server/modules/payment/index.js +660 -133
  41. package/dist/server/utils/small-ticket.d.ts +8 -5
  42. package/dist/server/utils/small-ticket.js +1269 -199
  43. package/dist/solution/BaseSales/index.d.ts +11 -2
  44. package/dist/solution/BaseSales/index.js +1463 -1173
  45. package/dist/solution/BaseSales/types.d.ts +7 -3
  46. package/dist/solution/BaseSales/utils/cartPromotion.js +141 -126
  47. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +40 -25
  48. package/dist/solution/BaseSales/utils.js +3 -2
  49. package/dist/solution/BookingTicket/index.d.ts +11 -2
  50. package/dist/solution/BookingTicket/index.js +663 -542
  51. package/dist/solution/BookingTicket/types.d.ts +5 -1
  52. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  53. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +338 -147
  54. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  55. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  56. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +19 -6
  57. package/dist/solution/ScanOrder/utils.js +3 -2
  58. package/dist/solution/ShopDiscount/index.d.ts +2 -0
  59. package/dist/solution/ShopDiscount/index.js +99 -42
  60. package/dist/solution/ShopDiscount/utils.d.ts +4 -2
  61. package/dist/solution/ShopDiscount/utils.js +43 -23
  62. package/dist/solution/UnifiedBookingSales/index.d.ts +2 -2
  63. package/dist/solution/UnifiedBookingSales/index.js +5 -5
  64. package/dist/utils/os-private-payload.d.ts +5 -0
  65. package/dist/utils/os-private-payload.js +17 -0
  66. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  67. package/lib/model/strategy/adapter/walletPass/evaluator.js +2 -0
  68. package/lib/modules/BookingContext/index.js +3 -3
  69. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +35 -6
  70. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +13 -6
  71. package/lib/modules/Customer/index.js +5 -1
  72. package/lib/modules/Customer/types.d.ts +2 -0
  73. package/lib/modules/Discount/availability.d.ts +7 -0
  74. package/lib/modules/Discount/availability.js +43 -0
  75. package/lib/modules/Discount/entitlementPass.d.ts +34 -0
  76. package/lib/modules/Discount/entitlementPass.js +141 -0
  77. package/lib/modules/Discount/index.d.ts +2 -2
  78. package/lib/modules/Discount/index.js +23 -129
  79. package/lib/modules/Discount/types.d.ts +36 -5
  80. package/lib/modules/OpenData/index.d.ts +1 -1
  81. package/lib/modules/OpenData/index.js +11 -2
  82. package/lib/modules/Order/index.d.ts +33 -4
  83. package/lib/modules/Order/index.js +411 -81
  84. package/lib/modules/Order/types.d.ts +17 -0
  85. package/lib/modules/Order/utils/orderCollectionIdentity.js +14 -11
  86. package/lib/modules/Order/utils/weighedProduct.d.ts +12 -0
  87. package/lib/modules/Order/utils/weighedProduct.js +89 -0
  88. package/lib/modules/Order/utils.d.ts +14 -2
  89. package/lib/modules/Order/utils.js +189 -5
  90. package/lib/modules/Payment/index.d.ts +0 -5
  91. package/lib/modules/Payment/index.js +3 -28
  92. package/lib/modules/Payment/types.d.ts +21 -1
  93. package/lib/modules/Payment/types.js +1 -0
  94. package/lib/modules/Payment/walletpass.d.ts +1 -1
  95. package/lib/modules/Payment/walletpass.js +29 -9
  96. package/lib/modules/ProductList/index.js +14 -1
  97. package/lib/modules/Rules/index.d.ts +2 -2
  98. package/lib/modules/Rules/index.js +361 -112
  99. package/lib/modules/Rules/types.d.ts +6 -0
  100. package/lib/server/index.js +8 -5
  101. package/lib/server/modules/order/index.js +6 -0
  102. package/lib/server/modules/order/types.d.ts +12 -0
  103. package/lib/server/modules/payment/index.d.ts +25 -6
  104. package/lib/server/modules/payment/index.js +350 -78
  105. package/lib/server/utils/small-ticket.d.ts +8 -5
  106. package/lib/server/utils/small-ticket.js +1009 -121
  107. package/lib/solution/BaseSales/index.d.ts +11 -2
  108. package/lib/solution/BaseSales/index.js +164 -30
  109. package/lib/solution/BaseSales/types.d.ts +7 -3
  110. package/lib/solution/BaseSales/utils/cartPromotion.js +12 -4
  111. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +24 -9
  112. package/lib/solution/BaseSales/utils.js +2 -1
  113. package/lib/solution/BookingTicket/index.d.ts +11 -2
  114. package/lib/solution/BookingTicket/index.js +103 -41
  115. package/lib/solution/BookingTicket/types.d.ts +5 -1
  116. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  117. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +174 -42
  118. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  119. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  120. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +11 -3
  121. package/lib/solution/ScanOrder/utils.js +2 -1
  122. package/lib/solution/ShopDiscount/index.d.ts +2 -0
  123. package/lib/solution/ShopDiscount/index.js +55 -9
  124. package/lib/solution/ShopDiscount/utils.d.ts +4 -2
  125. package/lib/solution/ShopDiscount/utils.js +42 -25
  126. package/lib/solution/UnifiedBookingSales/index.d.ts +2 -2
  127. package/lib/solution/UnifiedBookingSales/index.js +5 -5
  128. package/lib/utils/os-private-payload.d.ts +5 -0
  129. package/lib/utils/os-private-payload.js +22 -0
  130. package/package.json +1 -1
@@ -8,7 +8,10 @@ var _BaseModule = require("../BaseModule");
8
8
  var _types = require("./types");
9
9
  var _utils = require("./utils");
10
10
  var _utils2 = require("../Product/utils");
11
+ var _weighedProduct = require("./utils/weighedProduct");
11
12
  var _dayjs = _interopRequireDefault(require("dayjs"));
13
+ var _availability = require("../Discount/availability");
14
+ var _osPrivatePayload = require("../../utils/os-private-payload");
12
15
  var _salesNotes = require("./salesNotes");
13
16
  var _paymentUtils = require("./payment-utils");
14
17
  var _paymentNumber = require("../../utils/payment-number");
@@ -21,6 +24,7 @@ var _discountProductLineIdentity = require("./utils/discountProductLineIdentity"
21
24
  var _virtualSettlement = require("./utils/virtualSettlement");
22
25
  var _Discount = require("../Discount");
23
26
  var _Rules = require("../Rules");
27
+ var _entitlementPass = require("../Discount/entitlementPass");
24
28
  var _types2 = require("../Rules/types");
25
29
  var _decimal = _interopRequireDefault(require("decimal.js"));
26
30
  var _lodashEs = require("lodash-es");
@@ -40,9 +44,27 @@ function isSameDiscountList(left, right) {
40
44
  function isManualProductDiscountProduct(product) {
41
45
  return (0, _manualProductDiscount.hasManualProductDiscountFlag)(product?.metadata) || (product?.discount_list || []).some(item => item?.type === 'product');
42
46
  }
47
+ function syncReducedEntitlementPassUsageCount(discountList, previousQuantity, nextQuantity) {
48
+ if (!Array.isArray(discountList)) return discountList;
49
+ const previousCount = (0, _utils3.getSafeProductNum)(previousQuantity);
50
+ const nextCount = (0, _utils3.getSafeProductNum)(nextQuantity);
51
+ if (nextCount >= previousCount) return discountList;
52
+ return discountList.map(discount => {
53
+ if (!(0, _entitlementPass.isEntitlementPass)(discount)) return discount;
54
+ const rawUsageCount = Number(discount?.metadata?.num ?? previousCount);
55
+ const usageCount = Number.isFinite(rawUsageCount) ? Math.max(Math.floor(rawUsageCount), 0) : previousCount;
56
+ return {
57
+ ...discount,
58
+ metadata: {
59
+ ...(discount?.metadata || {}),
60
+ num: Math.min(usageCount, nextCount)
61
+ }
62
+ };
63
+ });
64
+ }
43
65
  const ORDER_PAYMENT_STATUS_PAID = 'paid';
44
66
  const ORDER_PAYMENT_STATUS_PENDING = 'payment_pending';
45
- const CUSTOMER_PROMOTION_DISCOUNT_TAGS = new Set(['good_pass', 'product_discount_card', 'discount_card']);
67
+ const CUSTOMER_PROMOTION_DISCOUNT_TAGS = new Set(['entitlement_pass', 'good_pass', 'product_discount_card', 'discount_card']);
46
68
  function isCustomerPromotionDiscount(discount) {
47
69
  return CUSTOMER_PROMOTION_DISCOUNT_TAGS.has(String(discount?.tag || discount?.type || '').toLowerCase());
48
70
  }
@@ -72,12 +94,27 @@ function getVoucherPaymentLifecycleKey(payment) {
72
94
  if (!isVoucherPaymentRecord(payment)) return null;
73
95
  return `${String(payment.voucher_id)}::${String(payment.status || '')}`;
74
96
  }
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() !== '';
97
+ function getDiscountIdentityKey(discount) {
98
+ const id = discount?.id;
99
+ if (id === undefined || id === null || id === '') return null;
100
+ return String(id);
101
+ }
102
+ function mergeCommittedEntitlementWithRuntime(committed, runtime) {
103
+ const committedAppliedProductDetails = committed.committedAppliedProductDetails?.length ? committed.committedAppliedProductDetails : committed.appliedProductDetails || [];
104
+ return {
105
+ ...committed,
106
+ ...runtime,
107
+ id: runtime.id ?? committed.id,
108
+ tag: runtime.tag || committed.tag,
109
+ type: runtime.type || committed.type,
110
+ isEditMode: false,
111
+ isDisabled: runtime.isDisabled ?? false,
112
+ isSelected: runtime.isSelected ?? committed.isSelected,
113
+ isUsed: committed.isUsed || runtime.isUsed,
114
+ committedAppliedProductDetails: [...committedAppliedProductDetails],
115
+ applicableProductDetails: [...(committed.applicableProductDetails || []), ...(runtime.applicableProductDetails || [])],
116
+ appliedProductDetails: [...committedAppliedProductDetails, ...(runtime.appliedProductDetails || [])]
117
+ };
81
118
  }
82
119
  class OrderModule extends _BaseModule.BaseModule {
83
120
  defaultName = 'order';
@@ -92,6 +129,10 @@ class OrderModule extends _BaseModule.BaseModule {
92
129
  openCache = false;
93
130
  sessionStoragePersistEnabled = false;
94
131
  sessionStorageRestoreRecord = null;
132
+ /** 只允许当前订单最新的 prepare/config 请求回写客户资产态。 */
133
+ customerWalletPrepareConfigEpoch = 0;
134
+ /** Payment 可能早于 prepare/config 返回;先按实例 ID 暂存卡号等待 Snapshot。 */
135
+ pointCardSnapshotCodeCandidates = new Map();
95
136
  salesSummaryModuleName;
96
137
  rulesHooksOverride;
97
138
  moduleHooksOverride;
@@ -130,6 +171,7 @@ class OrderModule extends _BaseModule.BaseModule {
130
171
  const key = Number.isFinite(id) ? id : String(resourceId);
131
172
  const title = item?.discount?.title ?? {};
132
173
  const amount = Number(item?.amount || 0);
174
+ const encoded = item?.metadata?.encoded || item?.encoded || item?.discount?.encoded || '';
133
175
  const discountCardType = item?.discount?.discount_card_type;
134
176
  const parValue = isDiscountCard ? discountCardType === 'fixed_amount' ? item?.discount?.fixed_amount ?? item?.amount : item?.discount?.percent : '0';
135
177
  const detail = {
@@ -140,11 +182,21 @@ class OrderModule extends _BaseModule.BaseModule {
140
182
  original_amount: String(item?.discount?.original_amount ?? item?.amount ?? '0'),
141
183
  num: Number(item?.metadata?.num ?? product?.num ?? product?.product_quantity ?? 1) || 1,
142
184
  metadata: item?.metadata,
143
- discount: item?.discount,
185
+ discount: {
186
+ ...(item?.discount || {}),
187
+ product_id: item?.discount?.product_id ?? product?.product_id
188
+ },
144
189
  _num: Number(item?.metadata?.num ?? product?.num ?? product?.product_quantity ?? 1) || 1
145
190
  };
146
191
  const existed = discountMap.get(key);
147
192
  if (existed) {
193
+ if (!existed.encoded && encoded) {
194
+ existed.encoded = encoded;
195
+ existed.metadata = {
196
+ ...existed.metadata,
197
+ encoded
198
+ };
199
+ }
148
200
  existed.amount = Number(existed.amount || 0) + amount;
149
201
  existed.used_par_value = String(Number(existed.used_par_value || 0) + amount);
150
202
  if (!isDiscountCard) {
@@ -164,8 +216,11 @@ class OrderModule extends _BaseModule.BaseModule {
164
216
  discountMap.set(key, {
165
217
  id: key,
166
218
  product_name: '',
167
- encoded: '',
168
- code: '',
219
+ encoded,
220
+ code: item?.code || item?.discount?.code || '',
221
+ metadata: {
222
+ encoded
223
+ },
169
224
  tag: type,
170
225
  product_id: item?.discount?.discount_product_id ?? product?.product_id ?? 0,
171
226
  relation_type: '',
@@ -456,13 +511,24 @@ class OrderModule extends _BaseModule.BaseModule {
456
511
  this.store.rules = rules;
457
512
  }
458
513
  createDefaultRulesHooks() {
459
- return (0, _utils.createDefaultOrderRulesHooks)();
514
+ return (0, _utils.createDefaultOrderRulesHooks)(() => this.store.tempOrder?.bookings || []);
515
+ }
516
+
517
+ /**
518
+ * Stable across notifyTempOrderChanged's immutable copy, but changes when a
519
+ * different local/server order becomes current.
520
+ */
521
+ getDiscountConfigOrderIdentityKey(tempOrder) {
522
+ const identity = this.getOrderIdentity();
523
+ return JSON.stringify([identity?.orderId ?? tempOrder?.order_id ?? '', tempOrder?.request_unique_idempotency_token ?? '', identity?.externalSaleNumber ?? tempOrder?.external_sale_number ?? '', identity?.localId ?? tempOrder?._extend?.localId ?? this.cacheId ?? '']);
460
524
  }
461
525
 
462
526
  // ─── Discount: 公共 API ───
463
527
 
464
528
  async loadDiscountConfig(params) {
465
529
  const orderId = params.orderId || this.store.tempOrder?.order_id || undefined;
530
+ const requestEpoch = ++this.customerWalletPrepareConfigEpoch;
531
+ const requestOrderIdentityKey = this.getDiscountConfigOrderIdentityKey(this.store.tempOrder);
466
532
  const prepareConfigResult = await this.store.discount?.loadPrepareConfig({
467
533
  customer_id: params.customerId,
468
534
  action: params.action || (orderId ? 'edit' : 'create'),
@@ -471,38 +537,90 @@ class OrderModule extends _BaseModule.BaseModule {
471
537
  with_wallet_pass_holder: 1,
472
538
  with_available_wallet_flag: 1,
473
539
  with_available_wallet_pass_flag: 1,
540
+ with_entitlement_pass: 1,
541
+ filter_order_wallet_pass_usage: 0,
474
542
  with_virtual_currency: 1,
475
543
  request_timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
476
544
  ...(orderId ? {
477
545
  order_id: orderId
478
546
  } : {})
479
547
  });
548
+ const isCurrentRequest = () => requestEpoch === this.customerWalletPrepareConfigEpoch && this.getDiscountConfigOrderIdentityKey(this.store.tempOrder) === requestOrderIdentityKey && String(this.store.tempOrder?.customer_id ?? '') === String(params.customerId);
549
+ if (!isCurrentRequest()) {
550
+ return this.getDiscountList();
551
+ }
480
552
  const discountList = prepareConfigResult?.discountList;
481
- this.store.availableWalletIds = prepareConfigResult?.availableWalletIds || [];
482
- this.store.virtualCurrencyList = prepareConfigResult?.virtualCurrencyList || [];
483
553
  if (prepareConfigResult) {
484
- this.writePointCardSnapshotFromPrepareConfig({
485
- orderId,
554
+ this.writeCustomerWalletSnapshotsFromPrepareConfig({
486
555
  customerId: params.customerId,
487
- availableWalletPassList: prepareConfigResult.availableWalletPassList || []
556
+ availableWalletIds: prepareConfigResult.availableWalletIds || [],
557
+ availableWalletPassList: prepareConfigResult.availableWalletPassList || [],
558
+ virtualCurrencyList: prepareConfigResult.virtualCurrencyList || []
488
559
  });
489
560
  }
490
561
  if (discountList) {
491
562
  const currentDiscountList = this.store.discount?.getDiscountList() || [];
492
- const currentEditDiscounts = currentDiscountList.filter(discount => discount?.isEditMode);
563
+ const currentCommittedDiscounts = currentDiscountList.filter(discount => discount?.isEditMode || discount?.committedAppliedProductDetails?.length);
493
564
  const currentScanDiscounts = currentDiscountList.filter(discount => discount?.isScan);
494
- const currentScanDiscountIds = new Set(currentScanDiscounts.map(discount => discount?.id));
565
+ const currentScanDiscountById = new Map(currentScanDiscounts.flatMap(discount => {
566
+ const id = getDiscountIdentityKey(discount);
567
+ return id === null ? [] : [[id, discount]];
568
+ }));
569
+ const committedDiscountById = new Map(currentCommittedDiscounts.flatMap(discount => {
570
+ const id = getDiscountIdentityKey(discount);
571
+ return id === null ? [] : [[id, discount]];
572
+ }));
573
+ const mergedCommittedDiscountIds = new Set();
574
+ const refreshedScanDiscountIds = new Set();
495
575
  const runtimeDiscountIds = new Set();
496
- const runtimeDiscounts = discountList.filter(discount => {
497
- if (currentScanDiscountIds.has(discount?.id)) return false;
498
- if (runtimeDiscountIds.has(discount?.id)) return false;
499
- runtimeDiscountIds.add(discount?.id);
500
- return true;
576
+ const runtimeDiscounts = discountList.flatMap(discount => {
577
+ const id = getDiscountIdentityKey(discount);
578
+ if (id === null) return [discount];
579
+ if (runtimeDiscountIds.has(id)) return [];
580
+ runtimeDiscountIds.add(id);
581
+ const currentScanDiscount = currentScanDiscountById.get(id);
582
+ if (currentScanDiscount && (!(0, _entitlementPass.isEntitlementPass)(currentScanDiscount) || !(0, _entitlementPass.isEntitlementPass)(discount))) {
583
+ return [];
584
+ }
585
+ const committedDiscount = committedDiscountById.get(id);
586
+ let nextDiscount = discount;
587
+ if (committedDiscount && (0, _entitlementPass.isEntitlementPass)(committedDiscount) && (0, _entitlementPass.isEntitlementPass)(discount)) {
588
+ mergedCommittedDiscountIds.add(id);
589
+ nextDiscount = mergeCommittedEntitlementWithRuntime(committedDiscount, discount);
590
+ }
591
+ if (currentScanDiscount) {
592
+ refreshedScanDiscountIds.add(id);
593
+ nextDiscount = {
594
+ ...nextDiscount,
595
+ isScan: true,
596
+ isSelected: currentScanDiscount.isSelected ?? nextDiscount.isSelected,
597
+ isManualSelect: currentScanDiscount.isManualSelect ?? nextDiscount.isManualSelect
598
+ };
599
+ }
600
+ return [nextDiscount];
601
+ });
602
+ const retainedCommittedDiscounts = currentCommittedDiscounts.filter(discount => {
603
+ const id = getDiscountIdentityKey(discount);
604
+ return id === null || !mergedCommittedDiscountIds.has(id);
605
+ }).map(discount => discount.committedAppliedProductDetails?.length ? {
606
+ ...discount,
607
+ isEditMode: true,
608
+ isDisabled: true,
609
+ isSelected: true,
610
+ isUsed: true,
611
+ appliedProductDetails: [...discount.committedAppliedProductDetails]
612
+ } : discount);
613
+ const retainedScanDiscounts = currentScanDiscounts.filter(discount => {
614
+ const id = getDiscountIdentityKey(discount);
615
+ return id === null || !refreshedScanDiscountIds.has(id);
501
616
  });
502
- const mergedDiscountList = [...currentEditDiscounts, ...currentScanDiscounts, ...runtimeDiscounts];
617
+ const mergedDiscountList = [...retainedCommittedDiscounts, ...retainedScanDiscounts, ...runtimeDiscounts];
618
+ if (!isCurrentRequest()) return this.getDiscountList();
503
619
  await this.store.discount?.setOriginalDiscountList(mergedDiscountList);
620
+ if (!isCurrentRequest()) return this.getDiscountList();
504
621
  await this.store.discount?.setDiscountList(mergedDiscountList);
505
622
  }
623
+ if (!isCurrentRequest()) return this.getDiscountList();
506
624
  if (params.apply !== false && this.store.tempOrder?.products?.length) {
507
625
  this.applyDiscount();
508
626
  }
@@ -517,6 +635,9 @@ class OrderModule extends _BaseModule.BaseModule {
517
635
  getVirtualCurrencyList() {
518
636
  return (0, _lodashEs.cloneDeep)(this.store.virtualCurrencyList || []);
519
637
  }
638
+ getCustomerWalletPrepareConfigEpoch() {
639
+ return this.customerWalletPrepareConfigEpoch;
640
+ }
520
641
  async syncScannedDiscountsToOriginalDiscountList(discountList) {
521
642
  const scanDiscounts = (discountList || []).filter(discount => discount?.isScan);
522
643
  if (!scanDiscounts.length) return;
@@ -614,19 +735,34 @@ class OrderModule extends _BaseModule.BaseModule {
614
735
  isAvailable: false,
615
736
  discountList: this.getDiscountList()
616
737
  };
617
- const shouldRetainForLater = !isAvailable && unavailableReason === _types2.UnavailableReason.ProductNotApplicable && withScanList.some(isCustomerPromotionDiscount);
738
+ const scannedIds = new Set(withScanList.map(discount => String(discount.id)));
739
+ const evaluatedScannedDiscounts = (evaluatedDiscountList || withScanList).filter(discount => scannedIds.has(String(discount.id)) && isCustomerPromotionDiscount(discount) && !(0, _availability.getDiscountLifecycleReason)(discount));
740
+ const shouldRetainForLater = !isAvailable && evaluatedScannedDiscounts.length > 0;
618
741
  const normalizedNewDiscountList = newDiscountList ? markScannedDiscounts(newDiscountList, withScanList) : undefined;
619
- const retainedDiscountList = evaluatedDiscountList ? markScannedDiscounts(evaluatedDiscountList, withScanList) : normalizedNewDiscountList;
620
742
  let resolvedDiscountList = normalizedNewDiscountList || this.getDiscountList();
621
743
  if (isAvailable && normalizedNewDiscountList) {
622
744
  await this.store.discount?.setDiscountList(normalizedNewDiscountList);
623
- this.applyDiscount();
745
+ // 实际写回商品时沿用扫码试算的优先级,避免旧商品行的选卡覆盖本次扫码。
746
+ this.applyDiscount({
747
+ preferredEntitlementPassIds: withScanList.filter(discount => (0, _entitlementPass.isEntitlementPass)(discount)).map(discount => discount.id)
748
+ });
624
749
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
625
750
  resolvedDiscountList = this.getDiscountList();
626
- } else if (shouldRetainForLater && retainedDiscountList) {
627
- // 卡券有效但当前购物车没有适用商品:先保留在 Promotion 中,
628
- // 不执行 applyDiscount,避免修改商品价格或订单金额。
629
- await this.store.discount?.setDiscountList(retainedDiscountList);
751
+ } else if (shouldRetainForLater) {
752
+ // 只合并本次识别的卡券;不把试算产生的其他选券状态写回订单。
753
+ const retainedDiscounts = new Map(this.getDiscountList().map(discount => [String(discount.id), discount]));
754
+ evaluatedScannedDiscounts.forEach(discount => {
755
+ retainedDiscounts.set(String(discount.id), {
756
+ ...discount,
757
+ isScan: true,
758
+ isAvailable: false,
759
+ isSelected: false,
760
+ reasonCode: discount.reasonCode || 'product_not_applicable',
761
+ appliedProductDetails: [],
762
+ savedAmount: 0
763
+ });
764
+ });
765
+ await this.store.discount?.setDiscountList([...retainedDiscounts.values()]);
630
766
  await this.syncScannedDiscountsToOriginalDiscountList(this.getDiscountList());
631
767
  resolvedDiscountList = this.getDiscountList();
632
768
  }
@@ -635,7 +771,7 @@ class OrderModule extends _BaseModule.BaseModule {
635
771
  isAvailable: isAvailable || false,
636
772
  isAccepted: Boolean(isAvailable || shouldRetainForLater),
637
773
  discountList: resolvedDiscountList,
638
- scannedDiscountList: resultDiscountList,
774
+ scannedDiscountList: evaluatedScannedDiscounts,
639
775
  unavailableReason
640
776
  };
641
777
  }
@@ -667,7 +803,9 @@ class OrderModule extends _BaseModule.BaseModule {
667
803
  isFormSubject: product => (0, _utils.resolveIsFormSubject)(tempOrder, product),
668
804
  orderTotalAmount: Number(this.store.summary?.total_amount || 0)
669
805
  }, {
670
- reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
806
+ reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids,
807
+ reapplyWeighedDiscountProductUids: options?.reapplyWeighedDiscountProductUids,
808
+ preferredEntitlementPassIds: options?.preferredEntitlementPassIds
671
809
  });
672
810
  if (result?.productList) {
673
811
  const previousProductsByUid = (0, _utils.indexOrderProductsByUid)(tempOrder.products);
@@ -1005,31 +1143,106 @@ class OrderModule extends _BaseModule.BaseModule {
1005
1143
  now: (0, _utils.formatDateTime)(new Date())
1006
1144
  });
1007
1145
  }
1008
- clearTempOrderPointCardSnapshot(tempOrder) {
1146
+ clearTempOrderCustomerWalletSnapshots(tempOrder) {
1009
1147
  const orderMetaList = (0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list);
1010
1148
  if (!orderMetaList) return;
1011
1149
  const nextOrderMetaList = {
1012
1150
  ...orderMetaList
1013
1151
  };
1014
1152
  delete nextOrderMetaList.point_cards_snapshot;
1153
+ delete nextOrderMetaList.virtual_currency_snapshot;
1015
1154
  if (Object.keys(nextOrderMetaList).length === 0) {
1016
1155
  delete tempOrder.order_meta_list;
1017
1156
  return;
1018
1157
  }
1019
1158
  tempOrder.order_meta_list = nextOrderMetaList;
1020
1159
  }
1021
- writePointCardSnapshotFromPrepareConfig(params) {
1160
+ clearCustomerWalletState(tempOrder) {
1161
+ this.clearCustomerWalletRuntimeState();
1162
+ this.clearTempOrderCustomerWalletSnapshots(tempOrder);
1163
+ }
1164
+ clearCustomerWalletRuntimeState() {
1165
+ this.customerWalletPrepareConfigEpoch += 1;
1166
+ this.pointCardSnapshotCodeCandidates.clear();
1167
+ this.store.availableWalletIds = [];
1168
+ this.store.virtualCurrencyList = [];
1169
+ }
1170
+ rememberPointCardSnapshotCodeCandidates(value) {
1171
+ (0, _utils.collectPointCardSnapshotCodeCandidates)(value).forEach(({
1172
+ id,
1173
+ code
1174
+ }) => {
1175
+ if (!this.pointCardSnapshotCodeCandidates.has(id)) {
1176
+ this.pointCardSnapshotCodeCandidates.set(id, code);
1177
+ }
1178
+ });
1179
+ }
1180
+ getRememberedPointCardSnapshotCodeCandidates() {
1181
+ return [...this.pointCardSnapshotCodeCandidates].map(([id, code]) => ({
1182
+ id,
1183
+ code
1184
+ }));
1185
+ }
1186
+ getPointCardSnapshotOrderIdentityKey(identity) {
1187
+ if (identity?.externalSaleNumber) {
1188
+ return `external:${identity.externalSaleNumber}`;
1189
+ }
1190
+ if (identity?.orderId !== undefined && identity.orderId !== null) {
1191
+ return `order:${identity.orderId}`;
1192
+ }
1193
+ if (identity?.localId) return `local:${identity.localId}`;
1194
+ if (identity?.orderNumber) return `number:${identity.orderNumber}`;
1195
+ return '';
1196
+ }
1197
+ isPointCardSnapshotMergeContextCurrent(context) {
1198
+ if (!context) return true;
1199
+ if (context.customerId !== undefined && String(context.customerId ?? '') !== String(this.store.tempOrder?.customer_id ?? '')) {
1200
+ return false;
1201
+ }
1202
+ const expectedOrderKey = this.getPointCardSnapshotOrderIdentityKey(context.orderIdentity);
1203
+ return !expectedOrderKey || expectedOrderKey === this.getPointCardSnapshotOrderIdentityKey(this.getOrderIdentity());
1204
+ }
1205
+ writeCustomerWalletSnapshotsFromPrepareConfig(params) {
1022
1206
  const tempOrder = this.store.tempOrder;
1023
1207
  if (!tempOrder) return;
1024
1208
  if (String(tempOrder.customer_id ?? '') !== String(params.customerId)) return;
1025
- const effectiveOrderId = params.orderId ?? tempOrder.order_id;
1026
- if (hasPersistedOrderId(effectiveOrderId)) return;
1209
+ const currentOrderMetaList = (0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list);
1210
+ const virtualCurrencyList = Array.isArray(params.virtualCurrencyList) ? params.virtualCurrencyList : [];
1211
+ const snapshotsWithExistingCodes = (0, _utils.mergePointCardSnapshotCodes)(params.availableWalletPassList, currentOrderMetaList?.point_cards_snapshot);
1212
+ tempOrder.order_meta_list = {
1213
+ ...(currentOrderMetaList || {}),
1214
+ point_cards_snapshot: (0, _utils.mergePointCardSnapshotCodes)(snapshotsWithExistingCodes, this.getRememberedPointCardSnapshotCodeCandidates()),
1215
+ virtual_currency_snapshot: (0, _utils.buildVirtualCurrencySnapshotFromList)(virtualCurrencyList)
1216
+ };
1217
+ this.store.availableWalletIds = [...params.availableWalletIds];
1218
+ this.store.virtualCurrencyList = (0, _lodashEs.cloneDeep)(virtualCurrencyList);
1219
+ this.persistTempOrder();
1220
+ this.saveDraftInBackground();
1221
+ }
1222
+
1223
+ /**
1224
+ * 将 Payment WalletPass 返回的实例 code 回填到当前订单 Snapshot。
1225
+ * 这里只按资产 ID 补空值,不新增卡、不覆盖 prepare/config 冻结的余额。
1226
+ */
1227
+ mergePointCardSnapshotCodes(walletPassList, context) {
1228
+ const tempOrder = this.store.tempOrder;
1229
+ const currentOrderMetaList = (0, _utils.normalizeOrderMetaList)(tempOrder?.order_meta_list);
1230
+ const currentSnapshots = currentOrderMetaList?.point_cards_snapshot || [];
1231
+ if (!tempOrder || !this.isPointCardSnapshotMergeContextCurrent(context)) {
1232
+ return currentSnapshots;
1233
+ }
1234
+ this.rememberPointCardSnapshotCodeCandidates(walletPassList);
1235
+ if (!currentSnapshots.length) return currentSnapshots;
1236
+ const nextSnapshots = (0, _utils.mergePointCardSnapshotCodes)(currentSnapshots, this.getRememberedPointCardSnapshotCodeCandidates());
1237
+ const hasChanged = nextSnapshots.some((snapshot, index) => snapshot.code !== currentSnapshots[index]?.code);
1238
+ if (!hasChanged) return currentSnapshots;
1027
1239
  tempOrder.order_meta_list = {
1028
- ...((0, _utils.normalizeOrderMetaList)(tempOrder.order_meta_list) || {}),
1029
- point_cards_snapshot: (0, _utils.buildPointCardSnapshotFromWalletPassList)(params.availableWalletPassList)
1240
+ ...(currentOrderMetaList || {}),
1241
+ point_cards_snapshot: nextSnapshots
1030
1242
  };
1031
1243
  this.persistTempOrder();
1032
1244
  this.saveDraftInBackground();
1245
+ return nextSnapshots;
1033
1246
  }
1034
1247
  createExternalSaleNumber() {
1035
1248
  const timestamp = (0, _dayjs.default)().format('YYYYMMDDHHmmssSSS');
@@ -1548,7 +1761,8 @@ class OrderModule extends _BaseModule.BaseModule {
1548
1761
  const sku = product?.product_sku || {};
1549
1762
  const metadata = product?.metadata || {};
1550
1763
  const discountTypes = (product?.discount_list || []).map(discount => discount?.type ?? discount?.tag);
1551
- const extensionType = product?.extension_type || (discountTypes.includes('good_pass') ? 'good_pass' : 'normal');
1764
+ const fullCoveragePassType = discountTypes.find(type => ['entitlement_pass', 'good_pass'].includes(type));
1765
+ const extensionType = product?.extension_type || fullCoveragePassType || 'normal';
1552
1766
  return {
1553
1767
  product_id: product?.product_id ?? null,
1554
1768
  product_variant_id: product?.product_variant_id ?? null,
@@ -1601,7 +1815,8 @@ class OrderModule extends _BaseModule.BaseModule {
1601
1815
  const sku = product?.product_sku || {};
1602
1816
  const metadata = product?.metadata || {};
1603
1817
  const discountTypes = (product?.discount_list || []).map(discount => discount?.type ?? discount?.tag);
1604
- const extensionType = product?.extension_type || (discountTypes.includes('good_pass') ? 'good_pass' : 'normal');
1818
+ const fullCoveragePassType = discountTypes.find(type => ['entitlement_pass', 'good_pass'].includes(type));
1819
+ const extensionType = product?.extension_type || fullCoveragePassType || 'normal';
1605
1820
  return {
1606
1821
  product_id: product?.product_id ?? null,
1607
1822
  product_variant_id: product?.product_variant_id ?? null,
@@ -1810,6 +2025,7 @@ class OrderModule extends _BaseModule.BaseModule {
1810
2025
  const freshTempOrder = this.createDefaultTempOrderInstance();
1811
2026
  this.ensureExternalSaleNumber(freshTempOrder);
1812
2027
  this.store.tempOrder = freshTempOrder;
2028
+ this.clearCustomerWalletRuntimeState();
1813
2029
  this.store.summary = (0, _utils.createEmptySummary)();
1814
2030
  this.store.lastOrderInfo = undefined;
1815
2031
  this.store.syncState = undefined;
@@ -1830,6 +2046,7 @@ class OrderModule extends _BaseModule.BaseModule {
1830
2046
  ensureIdentity: false
1831
2047
  });
1832
2048
  this.store.tempOrder = nextTempOrder;
2049
+ this.clearCustomerWalletRuntimeState();
1833
2050
  if (options?.recalculateSummary !== false) {
1834
2051
  await this.recalculateSummary({
1835
2052
  createIfMissing: false
@@ -2614,7 +2831,7 @@ class OrderModule extends _BaseModule.BaseModule {
2614
2831
  }
2615
2832
  isCustomerBoundDiscount(discount) {
2616
2833
  const type = discount?.type ?? discount?.tag;
2617
- return type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
2834
+ return type === 'entitlement_pass' || type === 'good_pass' || type === 'discount_card' || type === 'product_discount_card';
2618
2835
  }
2619
2836
  shouldKeepCustomerBoundDiscountAfterCustomerChange(discount) {
2620
2837
  return Boolean(discount?.isScan || discount?.isEditMode);
@@ -2699,9 +2916,8 @@ class OrderModule extends _BaseModule.BaseModule {
2699
2916
  email: tempOrder.email
2700
2917
  });
2701
2918
  const nextCustomerId = tempOrder.customer_id ?? null;
2702
- if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '') && !hasPersistedOrderId(tempOrder.order_id)) {
2703
- this.store.availableWalletIds = [];
2704
- this.clearTempOrderPointCardSnapshot(tempOrder);
2919
+ if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '')) {
2920
+ this.clearCustomerWalletState(tempOrder);
2705
2921
  }
2706
2922
  this.persistTempOrder();
2707
2923
  return {
@@ -2737,11 +2953,7 @@ class OrderModule extends _BaseModule.BaseModule {
2737
2953
  }
2738
2954
  const nextCustomerId = tempOrder.customer_id ?? null;
2739
2955
  if (String(previousCustomerId ?? '') !== String(nextCustomerId ?? '')) {
2740
- this.store.availableWalletIds = [];
2741
- this.store.virtualCurrencyList = [];
2742
- if (!hasPersistedOrderId(tempOrder.order_id)) {
2743
- this.clearTempOrderPointCardSnapshot(tempOrder);
2744
- }
2956
+ this.clearCustomerWalletState(tempOrder);
2745
2957
  (0, _utils.clearTempOrderHolderAssignments)(tempOrder);
2746
2958
  this.clearCustomerBoundDiscounts(tempOrder);
2747
2959
  this.applyDiscount();
@@ -2779,11 +2991,7 @@ class OrderModule extends _BaseModule.BaseModule {
2779
2991
  tempOrder.email = '';
2780
2992
  tempOrder.customer = null;
2781
2993
  if (tempOrder._extend) delete tempOrder._extend.customerSnapshot;
2782
- this.store.availableWalletIds = [];
2783
- this.store.virtualCurrencyList = [];
2784
- if (!hasPersistedOrderId(tempOrder.order_id)) {
2785
- this.clearTempOrderPointCardSnapshot(tempOrder);
2786
- }
2994
+ this.clearCustomerWalletState(tempOrder);
2787
2995
  this.clearCustomerBoundDiscounts(tempOrder);
2788
2996
  this.applyDiscount();
2789
2997
  this.persistTempOrder();
@@ -3246,17 +3454,35 @@ class OrderModule extends _BaseModule.BaseModule {
3246
3454
  }
3247
3455
  normalizedIncoming.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedIncoming.discount_list);
3248
3456
  const hasIncomingGoodPass = this.hasGoodPassDiscount(normalizedIncoming);
3457
+ const hasIncomingEntitlementPass = this.hasEntitlementPassDiscount(normalizedIncoming);
3458
+ const isIncomingEntitlementAppend = this.isRuntimeEntitlementAppend(productToAdd, tempOrder);
3459
+ const isIncomingPersistedEntitlementPass = hasIncomingEntitlementPass && this.isPersistedProductLine(normalizedIncoming);
3249
3460
  const hasIncomingProductNote = this.hasOrderProductLineNote(normalizedIncoming);
3250
3461
  const hasIncomingManualProductDiscount = isManualProductDiscountProduct(normalizedIncoming);
3251
3462
  const hasIncomingBookingUid = this.hasLinkedBookingUid(productToAdd) || this.hasLinkedBookingUid(normalizedIncoming);
3252
3463
  const isIncomingCustomItem = productToAdd.metadata?.origin?.isCustomItem === true || productToAdd._origin?.isCustomItem === true;
3253
- const shouldForceNewLine = disableMerge || !!booking || hasIncomingProductNote || hasIncomingManualProductDiscount || hasIncomingBookingUid || isIncomingCustomItem || isWeighingProduct;
3254
- const matchedIndex = hasIncomingGoodPass || shouldForceNewLine ? -1 : tempOrder.products.findIndex(item => {
3464
+ const shouldForceNewLine = disableMerge || !!booking || hasIncomingGoodPass || isIncomingPersistedEntitlementPass || hasIncomingBookingUid || isIncomingCustomItem || isWeighingProduct;
3465
+ const matchedIndex = shouldForceNewLine ? -1 : tempOrder.products.findIndex(item => {
3255
3466
  if (this.hasGoodPassDiscount(item)) return false;
3256
- if (this.hasOrderProductLineNote(item)) return false;
3257
- if (isManualProductDiscountProduct(item)) return false;
3258
- if (item.order_detail_id !== undefined && item.order_detail_id !== null) return false;
3259
- if (item.metadata?.is_edit_for_runtime) return false;
3467
+ const isRuntimeEntitlementPass = this.isRuntimeEntitlementAppend(item, tempOrder);
3468
+ if (isIncomingEntitlementAppend !== isRuntimeEntitlementPass) {
3469
+ return false;
3470
+ }
3471
+ if (isIncomingEntitlementAppend && this.isPersistedProductLine(item)) {
3472
+ return false;
3473
+ }
3474
+ const hasExistingProductNote = this.hasOrderProductLineNote(item);
3475
+ if ((hasIncomingProductNote || hasExistingProductNote) && (!isRuntimeEntitlementPass || String(normalizedIncoming.note || '').trim() !== String(item.note || '').trim())) {
3476
+ return false;
3477
+ }
3478
+ const hasExistingManualProductDiscount = isManualProductDiscountProduct(item);
3479
+ if (isRuntimeEntitlementPass && !this.hasSameEntitlementMergePriceConfig(normalizedIncoming, item)) {
3480
+ return false;
3481
+ }
3482
+ if ((hasIncomingManualProductDiscount || hasExistingManualProductDiscount) && !isRuntimeEntitlementPass) {
3483
+ return false;
3484
+ }
3485
+ if (this.isPersistedProductLine(item)) return false;
3260
3486
  if (item.product_id !== productToAdd.product_id) return false;
3261
3487
  if (item.product_variant_id !== productToAdd.product_variant_id) return false;
3262
3488
  const existedFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(item), item.product_bundle);
@@ -3557,17 +3783,51 @@ class OrderModule extends _BaseModule.BaseModule {
3557
3783
  });
3558
3784
  return tempOrder.products;
3559
3785
  }
3560
- hasGoodPassDiscount(product) {
3561
- const hasMainGoodPass = (product.discount_list || []).some(item => item?.type === 'good_pass' || item?.tag === 'good_pass');
3562
- if (hasMainGoodPass) return true;
3563
- const bundleDiscountList = [];
3786
+ getFullCoveragePassDiscounts(product) {
3787
+ const discountList = [...(product.discount_list || [])];
3564
3788
  (product.product_bundle || []).forEach(item => {
3565
- bundleDiscountList.push(...(item?.discount_list || []));
3789
+ discountList.push(...(item?.discount_list || []));
3566
3790
  });
3567
3791
  (product?._extend?.other?.bundle || []).forEach(item => {
3568
- bundleDiscountList.push(...(item?.discount_list || []));
3792
+ discountList.push(...(item?.discount_list || []));
3569
3793
  });
3570
- return bundleDiscountList.some(item => item?.type === 'good_pass' || item?.tag === 'good_pass');
3794
+ return discountList.filter(item => (0, _entitlementPass.isFullCoveragePass)(item));
3795
+ }
3796
+ hasEntitlementPassDiscount(product) {
3797
+ return this.getFullCoveragePassDiscounts(product).some(item => (0, _entitlementPass.isEntitlementPass)(item));
3798
+ }
3799
+ hasGoodPassDiscount(product) {
3800
+ return this.getFullCoveragePassDiscounts(product).some(item => !(0, _entitlementPass.isEntitlementPass)(item));
3801
+ }
3802
+ isPersistedProductLine(product) {
3803
+ return product.order_detail_id !== undefined && product.order_detail_id !== null && product.order_detail_id !== '' || product.metadata?.is_edit_for_runtime === true;
3804
+ }
3805
+ isRuntimeEntitlementAppend(product, tempOrder) {
3806
+ if (this.isPersistedProductLine(product)) return false;
3807
+ const uid = product.metadata?.unique_identification_number ?? product.unique_identification_number;
3808
+ const storedOrigin = uid !== undefined && uid !== null && uid !== '' ? tempOrder._extend?.productsByUid?.[String(uid)]?.origin : undefined;
3809
+ return Boolean(product?._origin?.__entitlementPassRuntimeAppend === true || storedOrigin?.__entitlementPassRuntimeAppend === true);
3810
+ }
3811
+ hasSameEntitlementMergePriceConfig(incoming, existing) {
3812
+ const buildPriceConfig = product => {
3813
+ const metadata = product.metadata || {};
3814
+ const bundle = (product.product_bundle || []).map(item => ({
3815
+ bundle_id: item.bundle_id ?? item.id ?? null,
3816
+ bundle_product_id: item.bundle_product_id ?? item._bundle_product_id ?? item.product_id ?? null,
3817
+ bundle_variant_id: item.bundle_variant_id ?? item.product_variant_id ?? 0,
3818
+ price_type: item.price_type ?? item.custom_price_type ?? '',
3819
+ price_type_ext: item.price_type_ext ?? item.custom_price_type_ext ?? ''
3820
+ })).sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
3821
+ return JSON.stringify({
3822
+ original_price: this.normalizeFingerprintMoney(product.original_price),
3823
+ source_product_price: this.normalizeFingerprintMoney(metadata.source_product_price),
3824
+ main_product_original_price: this.normalizeFingerprintMoney(metadata.main_product_original_price),
3825
+ price_override: this.normalizeFingerprintMoney(metadata.price_override),
3826
+ is_manual_discount: Boolean(metadata.is_manual_discount),
3827
+ bundle
3828
+ });
3829
+ };
3830
+ return buildPriceConfig(incoming) === buildPriceConfig(existing);
3571
3831
  }
3572
3832
  getBundleRuntimeIdentity(bundle) {
3573
3833
  if (!bundle || typeof bundle !== 'object') return '';
@@ -3599,7 +3859,7 @@ class OrderModule extends _BaseModule.BaseModule {
3599
3859
  };
3600
3860
  });
3601
3861
  }
3602
- applyOrderProductUpdateToTempOrder(tempOrder, params) {
3862
+ applyOrderProductUpdateToTempOrder(tempOrder, params, reapplyWeighedDiscountProductUids) {
3603
3863
  const {
3604
3864
  product_id,
3605
3865
  product_variant_id,
@@ -3672,7 +3932,8 @@ class OrderModule extends _BaseModule.BaseModule {
3672
3932
  const previousLineFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(targetProduct), targetProduct.product_bundle);
3673
3933
  const nextLineFingerprint = (0, _utils3.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(nextProduct), nextProduct.product_bundle);
3674
3934
  const callerChangesVariant = Number(product_variant_id || 0) !== Number(targetProduct.product_variant_id || 0);
3675
- const callerChangesLineConfiguration = callerChangesVariant || previousLineFingerprint !== nextLineFingerprint;
3935
+ const callerChangesWeighing = (0, _weighedProduct.getWeighedProductPriceFingerprint)(targetProduct.product_sku) !== (0, _weighedProduct.getWeighedProductPriceFingerprint)(nextProduct.product_sku);
3936
+ const callerChangesLineConfiguration = callerChangesVariant || callerChangesWeighing || previousLineFingerprint !== nextLineFingerprint;
3676
3937
  const isPersistedOrderLine = targetProduct.order_detail_id !== undefined && targetProduct.order_detail_id !== null;
3677
3938
  const callerChangesDiscountList = Array.isArray(updates?.discount_list) && updates.discount_list.length > 0 && !isSameDiscountList(updates.discount_list, targetProduct.discount_list);
3678
3939
  const callerUpdatesManualPrice = updates?.metadata?.price_override !== undefined || updates?.metadata?.is_manual_discount !== undefined || updates?.metadata?.product_discount_reason !== undefined || callerChangesDiscountList;
@@ -3688,6 +3949,23 @@ class OrderModule extends _BaseModule.BaseModule {
3688
3949
  };
3689
3950
  return hasChangedPrice(updates.selling_price, targetProduct.selling_price) || hasChangedPrice(updates.original_price, targetProduct.original_price);
3690
3951
  })();
3952
+ const weighedSourcePrice = callerChangesWeighing ? (0, _weighedProduct.resolveWeighedSourcePrice)(nextProduct.product_sku) : undefined;
3953
+ const hasResolvedWeighedPrice = weighedSourcePrice !== undefined;
3954
+ if (hasResolvedWeighedPrice && !callerUpdatesSourcePrice) {
3955
+ const nextMainOriginal = new _decimal.default(weighedSourcePrice).plus((0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(nextProduct)));
3956
+ const previousMainOriginal = new _decimal.default(targetProduct.metadata?.main_product_original_price ?? targetProduct.original_price ?? 0);
3957
+ const previousMainSelling = new _decimal.default(targetProduct.metadata?.main_product_selling_price ?? targetProduct.selling_price ?? 0);
3958
+ // A manual final price remains fixed; other legacy discounts keep their amount
3959
+ // until Rules reapplies the currently selected wallet discount below.
3960
+ const nextMainSelling = isManualProductDiscountProduct(nextProduct) ? previousMainSelling : nextMainOriginal.minus(previousMainOriginal.minus(previousMainSelling));
3961
+ nextProduct.metadata = {
3962
+ ...nextProduct.metadata,
3963
+ source_product_price: weighedSourcePrice,
3964
+ main_product_original_price: callerUpdatesMainOriginalPrice ? nextProduct.metadata.main_product_original_price : nextMainOriginal.toFixed(2),
3965
+ main_product_selling_price: callerUpdatesMainSellingPrice ? nextProduct.metadata.main_product_selling_price : _decimal.default.max(0, nextMainSelling).toFixed(2),
3966
+ price_schema_version: 2
3967
+ };
3968
+ }
3691
3969
  if (isPersistedOrderLine && !callerChangesLineConfiguration && !callerUpdatesManualPrice && !callerAllowsBookingReprice) {
3692
3970
  nextProduct.selling_price = targetProduct.selling_price;
3693
3971
  nextProduct.original_price = targetProduct.original_price;
@@ -3704,17 +3982,17 @@ class OrderModule extends _BaseModule.BaseModule {
3704
3982
  // 新配置只传了 source 时,不能把未传的 main_product_* 从旧行继承下来。
3705
3983
  // 以新 source + 新 option 重建券前价;券后价未显式提供时保留旧主商品折扣额。
3706
3984
  const previousMainOriginal = new _decimal.default(Number(targetProduct.metadata?.main_product_original_price) || 0);
3707
- const previousMainSelling = new _decimal.default(Number(targetProduct.metadata?.main_product_selling_price) || previousMainOriginal.toNumber());
3985
+ const previousMainSelling = new _decimal.default(Number(targetProduct.metadata?.main_product_selling_price ?? previousMainOriginal.toNumber()));
3708
3986
  const preservedDiscount = previousMainOriginal.minus(previousMainSelling);
3709
3987
  const nextMainOriginal = new _decimal.default(Number(updates.metadata.source_product_price) || 0).plus((0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(nextProduct)));
3710
3988
  nextProduct.metadata = {
3711
3989
  ...(nextProduct.metadata || {}),
3712
3990
  source_product_price: String(updates.metadata.source_product_price),
3713
3991
  main_product_original_price: callerUpdatesMainOriginalPrice ? String(updates.metadata.main_product_original_price) : nextMainOriginal.toDecimalPlaces(2).toFixed(2),
3714
- main_product_selling_price: callerUpdatesMainSellingPrice ? String(updates.metadata.main_product_selling_price) : nextMainOriginal.minus(preservedDiscount).toDecimalPlaces(2).toFixed(2),
3992
+ main_product_selling_price: callerUpdatesMainSellingPrice ? String(updates.metadata.main_product_selling_price) : hasResolvedWeighedPrice && isManualProductDiscountProduct(nextProduct) ? previousMainSelling.toFixed(2) : nextMainOriginal.minus(preservedDiscount).toDecimalPlaces(2).toFixed(2),
3715
3993
  price_schema_version: 2
3716
3994
  };
3717
- } else if (callerChangesVariant && !callerUpdatesTopPrice && !callerUpdatesMainMeta) {
3995
+ } else if (callerChangesVariant && !hasResolvedWeighedPrice && !callerUpdatesTopPrice && !callerUpdatesMainMeta) {
3718
3996
  // variant 已变化但 caller 没有显式报价时,旧 v2 metadata 不能继续充当权威价格源。
3719
3997
  // 清理后由 normalize 优先读取 metadata.origin.variant 中新 variant 的目录价;
3720
3998
  // 顶层 original/selling 的旧差额仍会作为 legacy discount 迁移到新价格。
@@ -3726,7 +4004,7 @@ class OrderModule extends _BaseModule.BaseModule {
3726
4004
  delete nextProduct.metadata.main_product_selling_price;
3727
4005
  delete nextProduct.metadata.main_product_original_price;
3728
4006
  delete nextProduct.metadata.price_schema_version;
3729
- } else if (callerChangesLineConfiguration && !callerUpdatesTopPrice && !callerUpdatesMainMeta) {
4007
+ } else if (callerChangesLineConfiguration && !hasResolvedWeighedPrice && !callerUpdatesTopPrice && !callerUpdatesMainMeta) {
3730
4008
  // 仅 option / bundle 变化时,保留历史 source 和主商品折扣金额,
3731
4009
  // 但必须让 original 与 selling 一起随新的 option 金额移动。
3732
4010
  const previousOptionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(targetProduct));
@@ -3743,7 +4021,7 @@ class OrderModule extends _BaseModule.BaseModule {
3743
4021
  main_product_selling_price: nextMainOriginal.minus(preservedDiscount).toDecimalPlaces(2).toFixed(2),
3744
4022
  price_schema_version: 2
3745
4023
  };
3746
- } else if (callerUpdatesTopPrice && !callerUpdatesMainMeta) {
4024
+ } else if (callerUpdatesTopPrice && !callerUpdatesMainMeta && !hasResolvedWeighedPrice) {
3747
4025
  const existedMeta = nextProduct.metadata || {};
3748
4026
  nextProduct.metadata = {
3749
4027
  ...existedMeta
@@ -3753,10 +4031,41 @@ class OrderModule extends _BaseModule.BaseModule {
3753
4031
  delete nextProduct.metadata.main_product_original_price;
3754
4032
  delete nextProduct.metadata.price_schema_version;
3755
4033
  }
4034
+ if (callerChangesLineConfiguration && (0, _weighedProduct.resolveWeighedSourcePrice)(nextProduct.product_sku) !== undefined && nextProduct.metadata.main_product_original_price != null && nextProduct.metadata.main_product_selling_price != null && isManualProductDiscountProduct(nextProduct)) {
4035
+ const originalPrice = (0, _utils.composeLinePrice)({
4036
+ mainPrice: nextProduct.metadata.main_product_original_price,
4037
+ bundle: (0, _weighedProduct.withOriginalBundleOptionPrices)(nextProduct.product_bundle),
4038
+ useOriginalBundle: true
4039
+ });
4040
+ const sellingPrice = (0, _utils.composeLinePrice)({
4041
+ mainPrice: nextProduct.metadata.main_product_selling_price,
4042
+ bundle: nextProduct.product_bundle
4043
+ });
4044
+ const previousManualDiscount = (0, _manualProductDiscount.findManualProductDiscountItem)(nextProduct.discount_list);
4045
+ nextProduct.original_price = originalPrice;
4046
+ nextProduct.metadata = {
4047
+ ...nextProduct.metadata,
4048
+ origin: {
4049
+ ...nextProduct.metadata.origin,
4050
+ _extend: {
4051
+ ...nextProduct.metadata.origin?._extend,
4052
+ origin_total: Number(originalPrice)
4053
+ }
4054
+ }
4055
+ };
4056
+ nextProduct.discount_list = (0, _manualProductDiscount.mergeManualProductDiscountIntoList)(nextProduct.discount_list, (0, _manualProductDiscount.buildManualProductDiscountItem)({
4057
+ originTotal: Number(originalPrice),
4058
+ sellingTotal: Number(sellingPrice),
4059
+ quantity: nextProduct.num,
4060
+ message: previousManualDiscount?.discount.message,
4061
+ discountway: previousManualDiscount?.discount.discountway,
4062
+ discount_per: previousManualDiscount?.discount.discount_per
4063
+ }));
4064
+ }
3756
4065
  // normalizeOrderProduct 幂等:v2 metadata 存在 → 保留 main_product_* 折扣重算 composite;
3757
4066
  // 否则 → 把顶层 selling_price 视作 source,派生 main_product_* 并写 price_schema_version: 2。
3758
4067
  let normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils.mergeOrderProductDisplayFields)(nextProduct, (0, _utils3.normalizeOrderProduct)(nextProduct)));
3759
- normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
4068
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(isPersistedOrderLine ? syncReducedEntitlementPassUsageCount(normalizedProduct.discount_list, targetProduct.num, normalizedProduct.num) : normalizedProduct.discount_list);
3760
4069
  const preservedBookingUid = nextProduct.booking_uid || nextProduct.metadata?.booking_uid || targetProduct.booking_uid || targetProduct.metadata?.booking_uid;
3761
4070
  if (!booking && preservedBookingUid) {
3762
4071
  normalizedProduct.booking_uid = preservedBookingUid;
@@ -3790,6 +4099,10 @@ class OrderModule extends _BaseModule.BaseModule {
3790
4099
  };
3791
4100
  tempOrder.bookings = [...(tempOrder.bookings || []), linked.booking];
3792
4101
  }
4102
+ if (callerChangesWeighing && !isManualProductDiscountProduct(normalizedProduct)) {
4103
+ const uid = normalizedProduct.metadata?.unique_identification_number;
4104
+ if (uid) reapplyWeighedDiscountProductUids?.push(String(uid));
4105
+ }
3793
4106
  tempOrder.products[productIndex] = normalizedProduct;
3794
4107
  this.syncTempOrderHolderFromBookings(tempOrder);
3795
4108
  this.captureProductRuntime(tempOrder, updates, tempOrder.products[productIndex], tempOrder.products[productIndex].metadata?.unique_identification_number);
@@ -3797,9 +4110,11 @@ class OrderModule extends _BaseModule.BaseModule {
3797
4110
  }
3798
4111
  async updateOrderProduct(params) {
3799
4112
  const tempOrder = this.ensureTempOrder();
3800
- const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
4113
+ const reapplyWeighedDiscountProductUids = [];
4114
+ const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params, reapplyWeighedDiscountProductUids);
3801
4115
  const promotionPayload = await this.evaluatePromotion();
3802
4116
  this.applyDiscount({
4117
+ reapplyWeighedDiscountProductUids,
3803
4118
  reapplyBookingDiscountProductUids: params.allow_booking_discount_reapply && updatedProductUid ? [String(updatedProductUid)] : undefined
3804
4119
  });
3805
4120
  this.sanitizeTempOrderProducts(tempOrder);
@@ -3818,8 +4133,9 @@ class OrderModule extends _BaseModule.BaseModule {
3818
4133
  if (!paramsList.length) {
3819
4134
  return tempOrder.products;
3820
4135
  }
4136
+ const reapplyWeighedDiscountProductUids = [];
3821
4137
  const reapplyBookingDiscountProductUids = paramsList.reduce((acc, params) => {
3822
- const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params);
4138
+ const updatedProductUid = this.applyOrderProductUpdateToTempOrder(tempOrder, params, reapplyWeighedDiscountProductUids);
3823
4139
  // 批量改预约时间时,延迟到最后一次折扣计算里统一恢复已选预约折扣卡。
3824
4140
  if (params.allow_booking_discount_reapply && updatedProductUid) {
3825
4141
  acc.push(String(updatedProductUid));
@@ -3827,6 +4143,7 @@ class OrderModule extends _BaseModule.BaseModule {
3827
4143
  return acc;
3828
4144
  }, []);
3829
4145
  await this.finalizeAfterProductsMutation(tempOrder, {
4146
+ reapplyWeighedDiscountProductUids,
3830
4147
  reapplyBookingDiscountProductUids: reapplyBookingDiscountProductUids.length ? reapplyBookingDiscountProductUids : undefined
3831
4148
  });
3832
4149
  this.logInfo('updateOrderProduct success', {
@@ -3873,6 +4190,7 @@ class OrderModule extends _BaseModule.BaseModule {
3873
4190
  throw new Error('[Order] 目标商品不存在,无法更新数量');
3874
4191
  }
3875
4192
  const targetProduct = tempOrder.products[productIndex];
4193
+ const isPersistedOrderLine = targetProduct.order_detail_id !== undefined && targetProduct.order_detail_id !== null;
3876
4194
  const normalizedProduct = this.normalizeVirtualSettlementProductIfEnabled((0, _utils3.normalizeOrderProduct)({
3877
4195
  ...targetProduct,
3878
4196
  num: (0, _utils3.getSafeProductNum)(num),
@@ -3881,7 +4199,7 @@ class OrderModule extends _BaseModule.BaseModule {
3881
4199
  unique_identification_number: targetProduct.metadata?.unique_identification_number || unique_identification_number
3882
4200
  }
3883
4201
  }));
3884
- normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(normalizedProduct.discount_list);
4202
+ normalizedProduct.discount_list = (0, _utils.normalizeOrderProductDiscountList)(isPersistedOrderLine ? syncReducedEntitlementPassUsageCount(normalizedProduct.discount_list, targetProduct.num, normalizedProduct.num) : normalizedProduct.discount_list);
3885
4203
  const settlementSnapshot = (0, _virtualSettlement.getSelectedSettlementSnapshot)(normalizedProduct);
3886
4204
  if (this.isMultiCurrencySettlementEnabled() && settlementSnapshot?.settlement_type === 'virtual_currency' && tempOrder.customer_id && Number(tempOrder.customer_id) !== 1) {
3887
4205
  const currencyProductId = Number(settlementSnapshot.currency_product_id);
@@ -3958,7 +4276,8 @@ class OrderModule extends _BaseModule.BaseModule {
3958
4276
  this.syncTempOrderHolderFromBookings(tempOrder);
3959
4277
  const promotionPayload = await this.evaluatePromotion();
3960
4278
  this.applyDiscount({
3961
- reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids
4279
+ reapplyBookingDiscountProductUids: options?.reapplyBookingDiscountProductUids,
4280
+ reapplyWeighedDiscountProductUids: options?.reapplyWeighedDiscountProductUids
3962
4281
  });
3963
4282
  this.sanitizeTempOrderProducts(tempOrder);
3964
4283
  await this.recalculateSummary({
@@ -4680,11 +4999,21 @@ class OrderModule extends _BaseModule.BaseModule {
4680
4999
  query
4681
5000
  } = params;
4682
5001
  const fetchUrl = url || '/order/sales/checkout';
4683
- return this.request.post(fetchUrl, query, {
5002
+ const requestQuery = this.isSalesCheckoutHandledByOsServer(fetchUrl) ? query : (0, _osPrivatePayload.stripOsPrivatePayloadFields)(query);
5003
+ return this.request.post(fetchUrl, requestQuery, {
4684
5004
  osServer: true,
4685
5005
  customToast: () => {}
4686
5006
  });
4687
5007
  }
5008
+ isSalesCheckoutHandledByOsServer(fetchUrl) {
5009
+ const server = this.core?.server;
5010
+ if (!server || typeof server.hasRoute !== 'function') return false;
5011
+ const requestBaseUrl = String(this.request?.baseUrl || '/shop').replace(/\/+$/, '');
5012
+ if (!requestBaseUrl.startsWith('/')) return false;
5013
+ const normalizedFetchUrl = fetchUrl.startsWith('/') ? fetchUrl : `/${fetchUrl}`;
5014
+ const routePath = normalizedFetchUrl === requestBaseUrl || normalizedFetchUrl.startsWith(`${requestBaseUrl}/`) ? normalizedFetchUrl : `${requestBaseUrl}${normalizedFetchUrl}`;
5015
+ return server.hasRoute('post', routePath) === true;
5016
+ }
4688
5017
 
4689
5018
  /**
4690
5019
  * 发送支付链接邮件。
@@ -4773,6 +5102,7 @@ class OrderModule extends _BaseModule.BaseModule {
4773
5102
  allowUnresolvedSalesNotes: isSessionStorageHydrate
4774
5103
  });
4775
5104
  this.store.tempOrder = nextTempOrder;
5105
+ this.clearCustomerWalletRuntimeState();
4776
5106
  if (isSessionStorageHydrate) {
4777
5107
  // 会话快照是未提交购物车,不应获得编辑订单的 saved/edit 标记与服务端基线。
4778
5108
  const restoredSessionDiscounts = Array.isArray(raw.discount_list) ? (0, _lodashEs.cloneDeep)(raw.discount_list) : [];