@pisell/pisellos 2.2.309 → 2.2.311

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +0 -9
  2. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  3. package/dist/model/strategy/adapter/walletPass/evaluator.js +4 -2
  4. package/dist/modules/BookingContext/index.js +4 -4
  5. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +58 -19
  6. package/dist/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +24 -12
  7. package/dist/modules/Customer/index.js +6 -3
  8. package/dist/modules/Customer/types.d.ts +2 -0
  9. package/dist/modules/Discount/availability.d.ts +7 -0
  10. package/dist/modules/Discount/availability.js +49 -0
  11. package/dist/modules/Discount/entitlementPass.d.ts +34 -0
  12. package/dist/modules/Discount/entitlementPass.js +147 -0
  13. package/dist/modules/Discount/index.d.ts +2 -2
  14. package/dist/modules/Discount/index.js +37 -151
  15. package/dist/modules/Discount/types.d.ts +36 -5
  16. package/dist/modules/OpenData/index.d.ts +1 -1
  17. package/dist/modules/OpenData/index.js +21 -12
  18. package/dist/modules/Order/index.d.ts +32 -4
  19. package/dist/modules/Order/index.js +733 -373
  20. package/dist/modules/Order/types.d.ts +17 -0
  21. package/dist/modules/Order/utils/orderCollectionIdentity.js +16 -12
  22. package/dist/modules/Order/utils.d.ts +19 -5
  23. package/dist/modules/Order/utils.js +306 -86
  24. package/dist/modules/Payment/index.d.ts +0 -5
  25. package/dist/modules/Payment/index.js +35 -85
  26. package/dist/modules/Payment/types.d.ts +21 -1
  27. package/dist/modules/Payment/types.js +2 -0
  28. package/dist/modules/Payment/walletpass.d.ts +1 -1
  29. package/dist/modules/Payment/walletpass.js +80 -47
  30. package/dist/modules/ProductList/index.js +18 -5
  31. package/dist/modules/Rules/index.d.ts +2 -2
  32. package/dist/modules/Rules/index.js +480 -186
  33. package/dist/modules/Rules/types.d.ts +4 -0
  34. package/dist/server/index.js +8 -5
  35. package/dist/server/modules/floor-plan/index.js +34 -3
  36. package/dist/server/modules/order/index.js +6 -0
  37. package/dist/server/modules/order/types.d.ts +12 -0
  38. package/dist/server/modules/payment/index.d.ts +25 -6
  39. package/dist/server/modules/payment/index.js +660 -133
  40. package/dist/server/utils/small-ticket.d.ts +8 -5
  41. package/dist/server/utils/small-ticket.js +1269 -199
  42. package/dist/solution/BaseSales/index.d.ts +11 -2
  43. package/dist/solution/BaseSales/index.js +1463 -1173
  44. package/dist/solution/BaseSales/types.d.ts +7 -3
  45. package/dist/solution/BaseSales/utils/cartPromotion.js +141 -126
  46. package/dist/solution/BookingTicket/index.d.ts +11 -2
  47. package/dist/solution/BookingTicket/index.js +663 -542
  48. package/dist/solution/BookingTicket/types.d.ts +5 -1
  49. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  50. package/dist/solution/BookingTicket/utils/scan/applyGlobalScan.js +338 -147
  51. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  52. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  53. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.js +19 -6
  54. package/dist/solution/ShopDiscount/index.d.ts +2 -0
  55. package/dist/solution/ShopDiscount/index.js +99 -42
  56. package/dist/solution/ShopDiscount/utils.d.ts +4 -2
  57. package/dist/solution/ShopDiscount/utils.js +43 -23
  58. package/dist/solution/UnifiedBookingSales/index.d.ts +2 -2
  59. package/dist/solution/UnifiedBookingSales/index.js +5 -5
  60. package/dist/utils/os-private-payload.d.ts +5 -0
  61. package/dist/utils/os-private-payload.js +17 -0
  62. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  63. package/lib/model/strategy/adapter/walletPass/evaluator.js +2 -0
  64. package/lib/modules/BookingContext/index.js +3 -3
  65. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +35 -6
  66. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +13 -6
  67. package/lib/modules/Customer/index.js +5 -1
  68. package/lib/modules/Customer/types.d.ts +2 -0
  69. package/lib/modules/Discount/availability.d.ts +7 -0
  70. package/lib/modules/Discount/availability.js +43 -0
  71. package/lib/modules/Discount/entitlementPass.d.ts +34 -0
  72. package/lib/modules/Discount/entitlementPass.js +141 -0
  73. package/lib/modules/Discount/index.d.ts +2 -2
  74. package/lib/modules/Discount/index.js +23 -129
  75. package/lib/modules/Discount/types.d.ts +36 -5
  76. package/lib/modules/OpenData/index.d.ts +1 -1
  77. package/lib/modules/OpenData/index.js +11 -2
  78. package/lib/modules/Order/index.d.ts +32 -4
  79. package/lib/modules/Order/index.js +341 -71
  80. package/lib/modules/Order/types.d.ts +17 -0
  81. package/lib/modules/Order/utils/orderCollectionIdentity.js +14 -11
  82. package/lib/modules/Order/utils.d.ts +19 -5
  83. package/lib/modules/Order/utils.js +215 -13
  84. package/lib/modules/Payment/index.d.ts +0 -5
  85. package/lib/modules/Payment/index.js +3 -28
  86. package/lib/modules/Payment/types.d.ts +21 -1
  87. package/lib/modules/Payment/types.js +1 -0
  88. package/lib/modules/Payment/walletpass.d.ts +1 -1
  89. package/lib/modules/Payment/walletpass.js +29 -9
  90. package/lib/modules/ProductList/index.js +14 -1
  91. package/lib/modules/Rules/index.d.ts +2 -2
  92. package/lib/modules/Rules/index.js +357 -110
  93. package/lib/modules/Rules/types.d.ts +4 -0
  94. package/lib/server/index.js +8 -5
  95. package/lib/server/modules/floor-plan/index.js +29 -1
  96. package/lib/server/modules/order/index.js +6 -0
  97. package/lib/server/modules/order/types.d.ts +12 -0
  98. package/lib/server/modules/payment/index.d.ts +25 -6
  99. package/lib/server/modules/payment/index.js +350 -78
  100. package/lib/server/utils/small-ticket.d.ts +8 -5
  101. package/lib/server/utils/small-ticket.js +1009 -121
  102. package/lib/solution/BaseSales/index.d.ts +11 -2
  103. package/lib/solution/BaseSales/index.js +164 -30
  104. package/lib/solution/BaseSales/types.d.ts +7 -3
  105. package/lib/solution/BaseSales/utils/cartPromotion.js +12 -4
  106. package/lib/solution/BookingTicket/index.d.ts +11 -2
  107. package/lib/solution/BookingTicket/index.js +103 -41
  108. package/lib/solution/BookingTicket/types.d.ts +5 -1
  109. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.d.ts +2 -0
  110. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +174 -42
  111. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +8 -4
  112. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +6 -0
  113. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +11 -3
  114. package/lib/solution/ShopDiscount/index.d.ts +2 -0
  115. package/lib/solution/ShopDiscount/index.js +55 -9
  116. package/lib/solution/ShopDiscount/utils.d.ts +4 -2
  117. package/lib/solution/ShopDiscount/utils.js +42 -25
  118. package/lib/solution/UnifiedBookingSales/index.d.ts +2 -2
  119. package/lib/solution/UnifiedBookingSales/index.js +5 -5
  120. package/lib/utils/os-private-payload.d.ts +5 -0
  121. package/lib/utils/os-private-payload.js +22 -0
  122. package/package.json +1 -1
@@ -11,6 +11,8 @@ var _utils2 = require("../Cart/utils");
11
11
  var _decimal = _interopRequireDefault(require("decimal.js"));
12
12
  var _lodashEs = require("lodash-es");
13
13
  var _dayjs = _interopRequireDefault(require("dayjs"));
14
+ var _entitlementPass = require("../Discount/entitlementPass");
15
+ var _availability = require("../Discount/availability");
14
16
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
17
  class RulesModule extends _BaseModule.BaseModule {
16
18
  defaultName = 'rules';
@@ -49,12 +51,20 @@ class RulesModule extends _BaseModule.BaseModule {
49
51
  }
50
52
 
51
53
  /**
52
- * 特定 form Holder 卡只授权给绑定同一 Holder Record 的商品行。
53
- * required/isNeedHolder 仅控制是否强制选择 Holder,不参与优惠资格。
54
+ * 商品未开启 Holder 选择时跳过匹配;已开启时,特定 form Holder 卡
55
+ * 只适用于同一 Holder Record,是否必填不影响匹配规则。
54
56
  */
55
- checkHolderMatch(discount, product, holders) {
57
+ checkHolderMatch(discount, product, holderSource, holders) {
56
58
  // 非表单类型 holder 视为匹配
57
59
  if (discount.holder?.holder_type !== 'form') return true;
60
+
61
+ // getProduct 会裁剪商品字段,从原始商品读取配置,套餐子项沿用父商品。
62
+ const holderConfig = holderSource?.holder_config ?? holderSource?.metadata?.holder_config ?? holderSource?.origin?.holder_config ?? holderSource?.metadata?.origin?.holder_config ?? holderSource?._origin?.holder_config ?? holderSource?._extend?.holder_config ?? holderSource?._extend?.product?.holder_config ?? holderSource?._extend?.product?.metadata?.holder_config;
63
+ const holderStatus = holderConfig?.status;
64
+ const holderEnabled = holderStatus === true || ['enable', '1'].includes(String(holderStatus).trim().toLowerCase());
65
+ if (!holderEnabled || holderConfig?.resource_id === undefined || holderConfig?.resource_id === null || holderConfig?.resource_id === '') {
66
+ return true;
67
+ }
58
68
  const discountHolderIds = this.normalizeHolderRecordIds(discount.holder.holder_id);
59
69
  if (!discountHolderIds.length) return false;
60
70
  const productHolderIds = this.normalizeHolderRecordIds(product.holder_id);
@@ -94,10 +104,12 @@ class RulesModule extends _BaseModule.BaseModule {
94
104
  unavailableReason: _types.UnavailableReason.AlreadyUsed
95
105
  };
96
106
  }
107
+ const scannedEntitlementPassIds = newDiscountList.filter(discount => (0, _entitlementPass.isEntitlementPass)(discount)).map(discount => discount.id);
108
+ const scannedEntitlementPassIdSet = new Set(scannedEntitlementPassIds.map(String));
97
109
 
98
- // 合并新旧折扣列表,并计算折扣结果,注意,如果旧折扣里有 isEditMode 为 true 的优惠券,则不合并
99
- // 保留之前扫码得到的商品券(isScan: true)
100
- const filteredOldDiscountList = oldDiscountList.filter(discount => !discount.isEditMode && (discount.tag !== 'good_pass' || discount.isScan));
110
+ // 客户次卡必须保留在完整列表中;同一实例以本次扫码数据更新。
111
+ // 历史折扣不参与试算,商品券继续沿用仅保留扫码来源的规则。
112
+ const filteredOldDiscountList = oldDiscountList.filter(discount => !discount.isEditMode && (!(0, _entitlementPass.isFullCoveragePass)(discount) || (0, _entitlementPass.isEntitlementPass)(discount) || discount.isScan) && !scannedEntitlementPassIdSet.has(String(discount.id)));
101
113
  const mergedDiscountList = (0, _utils.uniqueById)([...filteredOldDiscountList, ...newDiscountList]);
102
114
  const result = this.calcDiscount({
103
115
  discountList: mergedDiscountList,
@@ -106,7 +118,8 @@ class RulesModule extends _BaseModule.BaseModule {
106
118
  holders,
107
119
  isFormSubject
108
120
  }, {
109
- scan: true
121
+ scan: true,
122
+ preferredEntitlementPassIds: scannedEntitlementPassIds
110
123
  });
111
124
 
112
125
  // 检查是否有产品使用了新折扣
@@ -206,6 +219,7 @@ class RulesModule extends _BaseModule.BaseModule {
206
219
  isFormSubject,
207
220
  orderTotalAmount
208
221
  }, options) {
222
+ const preferredEntitlementPassIds = new Set((options?.preferredEntitlementPassIds || []).map(String));
209
223
  const resolveIsFormSubject = product => typeof isFormSubject === 'function' ? Boolean(isFormSubject(product)) : Boolean(isFormSubject);
210
224
 
211
225
  // 识别出来是不是在编辑的界面里又新增了商品
@@ -223,7 +237,7 @@ class RulesModule extends _BaseModule.BaseModule {
223
237
  const uid = getOriginProductUid(originProduct);
224
238
  if (!uid || !reapplyBookingDiscountProductUids.has(uid)) return false;
225
239
  const discountType = selectedDiscount.tag || selectedDiscount.type;
226
- return selectedDiscount.isEditMode === true && selectedDiscount.isSelected === true && ['good_pass', 'discount_card', 'product_discount_card'].includes(discountType);
240
+ return selectedDiscount.isEditMode === true && selectedDiscount.isSelected === true && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType ?? '');
227
241
  };
228
242
  const resolveReapplyBookingDiscountPercent = (originProduct, selectedDiscount) => {
229
243
  if (!shouldReapplyBookingDiscount(originProduct, selectedDiscount)) return undefined;
@@ -240,7 +254,13 @@ class RulesModule extends _BaseModule.BaseModule {
240
254
  if (discount.isEditMode) {
241
255
  editModeDiscount.push(discount);
242
256
  } else {
243
- addModeDiscount.push(discount);
257
+ // 试算不能把 config / 不可用原因写回原有选券;每次重新计算以清除旧原因。
258
+ addModeDiscount.push({
259
+ ...discount,
260
+ usageLimitFailKey: (0, _availability.getDiscountUsageLimitFailKey)(discount),
261
+ reason: undefined,
262
+ reasonCode: (0, _availability.getDiscountUsageBlockReason)(discount)
263
+ });
244
264
  }
245
265
  });
246
266
 
@@ -269,7 +289,7 @@ class RulesModule extends _BaseModule.BaseModule {
269
289
  }
270
290
  const canUseEditModeDiscountForReapply = discount => {
271
291
  const discountType = discount.tag || discount.type;
272
- return reapplyBookingDiscountProductUids.size > 0 && discount.isEditMode === true && discount.isSelected === true && ['good_pass', 'discount_card', 'product_discount_card'].includes(discountType);
292
+ return reapplyBookingDiscountProductUids.size > 0 && discount.isEditMode === true && discount.isSelected === true && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType ?? '');
273
293
  };
274
294
 
275
295
  // 过滤掉手动取消false的优惠券,但仅用于应用优惠逻辑。
@@ -343,6 +363,7 @@ class RulesModule extends _BaseModule.BaseModule {
343
363
 
344
364
  // 辅助函数:判断是否是商品券
345
365
  const isGoodPass = discount => discount.tag === 'good_pass';
366
+ const isEntitlement = discount => (0, _entitlementPass.isEntitlementPass)(discount);
346
367
 
347
368
  // 辅助函数:判断是否是订单级折扣卡
348
369
  const isOrderLevelDiscount = discount => discount.tag === 'product_discount_card' && discount.metadata?.discount_calculation_mode === 'order_level';
@@ -404,6 +425,14 @@ class RulesModule extends _BaseModule.BaseModule {
404
425
  }
405
426
  return 0;
406
427
  }
428
+ const entitlementA = isEntitlement(a);
429
+ const entitlementB = isEntitlement(b);
430
+ if (entitlementA || entitlementB) {
431
+ if (entitlementA && entitlementB) {
432
+ return (0, _entitlementPass.compareEntitlementPassPriority)(a, b);
433
+ }
434
+ return entitlementA ? -1 : 1;
435
+ }
407
436
 
408
437
  // 1. 先按优先级排序
409
438
  const priorityA = getPriority(a);
@@ -472,19 +501,34 @@ class RulesModule extends _BaseModule.BaseModule {
472
501
 
473
502
  // 🔥 为每个折扣卡/商品券执行策略检查并附加config
474
503
  const evaluator = this.getWalletPassEvaluator();
504
+ addModeDiscount.forEach(discount => {
505
+ if (discount.reasonCode) discount.config = {
506
+ isAvailable: false
507
+ };
508
+ });
475
509
  if (evaluator) {
476
510
  addModeDiscount.forEach(discount => {
511
+ // 即使没有匹配的动态策略,服务端状态和次数限制也不能被覆盖为可用。
512
+ if (discount.reasonCode) return;
477
513
  const discountType = discount.tag || discount.type;
478
- // 只对 good_pass 和 discount_card 类型执行策略检查
479
- if (['good_pass', 'discount_card', 'product_discount_card'].includes(discountType)) {
514
+ // 对两类全额抵扣卡和折扣卡执行附加策略检查。
515
+ if (['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType ?? '')) {
516
+ const entitlementQuota = (0, _entitlementPass.isEntitlementPass)(discount) ? (0, _entitlementPass.resolveEntitlementQuota)(discount) : null;
517
+ if ((0, _entitlementPass.isEntitlementPass)(discount) && !entitlementQuota) {
518
+ discount.config = {
519
+ isAvailable: false
520
+ };
521
+ return;
522
+ }
523
+
480
524
  // 转换 discount 为 Voucher 格式
481
525
  const voucher = {
482
526
  id: discount.id,
483
527
  amount: Number(discount.par_value || 0),
484
- balance: Number(discount.balance || 0),
528
+ balance: entitlementQuota?.type === 'unlimited' ? Number.MAX_SAFE_INTEGER : entitlementQuota?.type === 'limited' ? entitlementQuota.balance : Number(discount.balance || 0),
485
529
  type: discountType,
486
530
  product_id: discount.product_id,
487
- unified_available_status: 1,
531
+ unified_available_status: discount.unified_available_status ?? 1,
488
532
  available_product_type: discount.limited_relation_product_data?.type,
489
533
  available_product_ids: discount.limited_relation_product_data?.product_ids
490
534
  };
@@ -515,6 +559,8 @@ class RulesModule extends _BaseModule.BaseModule {
515
559
  discount.config = {
516
560
  isAvailable: false
517
561
  };
562
+ discount.reason = result.reason;
563
+ discount.reasonCode = result.reasonCode || 'not_meet_the_required_conditions';
518
564
  }
519
565
  }
520
566
  });
@@ -537,17 +583,126 @@ class RulesModule extends _BaseModule.BaseModule {
537
583
  });
538
584
  */
539
585
 
540
- // 标记已使用的优惠券
586
+ // good_pass / 折扣卡保持原有的一次性使用标记。
541
587
  const usedDiscounts = new Map();
542
588
 
589
+ // Entitlement Pass 单独记录本次重算新增分配的次数。
590
+ const entitlementUsageCounts = new Map();
591
+
543
592
  // 🔥 统计每个 product_id(商品券商品id)已使用的数量,用于 maxUsagePerOrder 限制
544
593
  const usedProductIdCounts = new Map();
594
+ const markDiscountUsed = (discount, count = 1) => {
595
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
596
+ entitlementUsageCounts.set(discount.id, (entitlementUsageCounts.get(discount.id) || 0) + count);
597
+ return;
598
+ }
599
+ usedDiscounts.set(discount.id, true);
600
+ };
601
+ const isDiscountUsed = discount => {
602
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
603
+ return (entitlementUsageCounts.get(discount.id) || 0) > 0;
604
+ }
605
+ return usedDiscounts.get(discount.id) === true;
606
+ };
607
+ const buildFullCoveragePassUsageGroups = (discounts, requestedCount) => {
608
+ const groups = [];
609
+ const pendingUsedDiscounts = new Set(Array.from(usedDiscounts.entries()).filter(([, used]) => used).map(([discountId]) => discountId));
610
+ const pendingEntitlementCounts = new Map(entitlementUsageCounts);
611
+ const pendingProductCounts = new Map(usedProductIdCounts);
612
+ const firstGoodPass = discounts.find(discount => (0, _entitlementPass.isFullCoveragePass)(discount) && !(0, _entitlementPass.isEntitlementPass)(discount));
613
+ const legacyGoodPassPerLineLimit = firstGoodPass ? Number(firstGoodPass.config?.maxUsagePerOrder || 0) : 0;
614
+ let allocatedGoodPassCount = 0;
615
+ let remaining = Math.max(Math.floor(requestedCount), 0);
616
+ for (const discount of discounts) {
617
+ if (remaining <= 0) break;
618
+ if (!(0, _entitlementPass.isFullCoveragePass)(discount)) continue;
619
+ const currentEntitlementCount = pendingEntitlementCounts.get(discount.id) || 0;
620
+ let availableCount = (0, _entitlementPass.isEntitlementPass)(discount) ? (0, _entitlementPass.getRemainingEntitlementUses)(discount, currentEntitlementCount, remaining) : pendingUsedDiscounts.has(discount.id) ? 0 : 1;
621
+ if (!(0, _entitlementPass.isEntitlementPass)(discount) && legacyGoodPassPerLineLimit > 0) {
622
+ availableCount = Math.min(availableCount, Math.max(Math.floor(legacyGoodPassPerLineLimit) - allocatedGoodPassCount, 0));
623
+ }
624
+ const maxUsagePerOrder = Number(discount.config?.maxUsagePerOrder || 0);
625
+ if ((0, _entitlementPass.isEntitlementPass)(discount) && maxUsagePerOrder > 0) {
626
+ const currentProductCount = pendingProductCounts.get(discount.product_id) || 0;
627
+ availableCount = Math.min(availableCount, Math.max(Math.floor(maxUsagePerOrder) - currentProductCount, 0));
628
+ }
629
+ const allocatedCount = Math.min(availableCount, remaining);
630
+ if (allocatedCount <= 0) continue;
631
+ const existingGroup = groups.find(group => String(group.discount.id) === String(discount.id));
632
+ if (existingGroup) {
633
+ existingGroup.count += allocatedCount;
634
+ } else {
635
+ groups.push({
636
+ discount,
637
+ count: allocatedCount
638
+ });
639
+ }
640
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
641
+ pendingEntitlementCounts.set(discount.id, currentEntitlementCount + allocatedCount);
642
+ } else {
643
+ pendingUsedDiscounts.add(discount.id);
644
+ allocatedGoodPassCount += allocatedCount;
645
+ }
646
+ pendingProductCounts.set(discount.product_id, (pendingProductCounts.get(discount.product_id) || 0) + allocatedCount);
647
+ remaining -= allocatedCount;
648
+ }
649
+ return groups;
650
+ };
651
+ const mergeEntitlementAppliedProductDetails = details => {
652
+ const mergedDetails = [];
653
+ const mergedIndexByKey = new Map();
654
+ const toComparableNumber = value => {
655
+ const numberValue = Number(value);
656
+ return Number.isFinite(numberValue) ? String(numberValue) : String(value ?? '');
657
+ };
658
+ for (const detail of details || []) {
659
+ if (!(0, _entitlementPass.isEntitlementPass)(detail)) {
660
+ mergedDetails.push(detail);
661
+ continue;
662
+ }
663
+ const resourceId = detail?.discount?.resource_id ?? detail?.resource_id;
664
+ if (resourceId === undefined || resourceId === null) {
665
+ mergedDetails.push(detail);
666
+ continue;
667
+ }
668
+ const key = [resourceId, detail?.discount?.product_id ?? '', detail?.metadata?.custom_product_bundle_map_id ?? '', toComparableNumber(detail?.amount), toComparableNumber(detail?.discount?.fixed_amount)].join('|');
669
+ const rawUsageCount = Number(detail?.metadata?.num ?? detail?._num ?? 1);
670
+ const usageCount = Number.isFinite(rawUsageCount) && rawUsageCount > 0 ? rawUsageCount : 1;
671
+ const mergedIndex = mergedIndexByKey.get(key);
672
+ if (mergedIndex === undefined) {
673
+ mergedIndexByKey.set(key, mergedDetails.length);
674
+ mergedDetails.push({
675
+ ...detail,
676
+ metadata: {
677
+ ...(detail?.metadata || {}),
678
+ num: usageCount
679
+ },
680
+ _num: usageCount
681
+ });
682
+ continue;
683
+ }
684
+ const existed = mergedDetails[mergedIndex];
685
+ const existedUsageCount = Number(existed?.metadata?.num ?? existed?._num ?? 1);
686
+ const totalUsageCount = existedUsageCount + usageCount;
687
+ mergedDetails[mergedIndex] = {
688
+ ...existed,
689
+ metadata: {
690
+ ...(existed?.metadata || {}),
691
+ num: totalUsageCount
692
+ },
693
+ _num: totalUsageCount
694
+ };
695
+ }
696
+ return mergedDetails;
697
+ };
545
698
 
546
699
  // 记录每个优惠券适用的商品ID
547
700
  const discountApplicability = new Map();
548
701
 
549
702
  // 记录每个优惠券适用的商品详细信息
550
703
  const discountApplicableProducts = new Map();
704
+ const discountTimeReasons = new Map();
705
+ const discountHolderMismatch = new Set();
551
706
 
552
707
  // 初始化每个优惠券的适用性和可抵扣商品
553
708
  addModeDiscount.forEach(discount => {
@@ -567,6 +722,7 @@ class RulesModule extends _BaseModule.BaseModule {
567
722
 
568
723
  // 辅助函数:检查商品是否对某个折扣卡可用
569
724
  const checkItemApplicableForDiscount = (flatItem, discount) => {
725
+ if (discount.config?.isAvailable === false) return false;
570
726
  let product;
571
727
  if (flatItem.type === 'main') {
572
728
  product = flatItem.product;
@@ -587,7 +743,7 @@ class RulesModule extends _BaseModule.BaseModule {
587
743
  }
588
744
 
589
745
  // 编辑的商品使用了优惠券不可用
590
- const isAvailableProduct = flatItem.type === 'main' ? !(product?.booking_id && product?.discount_list?.length && product?.discount_list?.every(d => d.id && ['good_pass', 'discount_card', 'product_discount_card'].includes(d.tag || d.type))) : !(flatItem?.booking_id && !!flatItem?.bundleItem?.discount_list?.length && flatItem?.bundleItem?.discount_list?.every(d => d.id));
746
+ const isAvailableProduct = flatItem.type === 'main' ? !(product?.booking_id && product?.discount_list?.length && product?.discount_list?.every(d => d.id && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(d.tag || d.type))) : !(flatItem?.booking_id && !!flatItem?.bundleItem?.discount_list?.length && flatItem?.bundleItem?.discount_list?.every(d => d.id));
591
747
  if (!isAvailableProduct) {
592
748
  return false;
593
749
  }
@@ -771,30 +927,37 @@ class RulesModule extends _BaseModule.BaseModule {
771
927
  const isHolderMatch = this.checkHolderMatch(discount, {
772
928
  isNeedHolder: resolveIsFormSubject(_tempVar) && !_tempVar?.isNormalProduct,
773
929
  holder_id: _tempVar?.holder_id || product.holder_id
774
- }, holders);
775
- let timeLimit = true;
776
- timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss')).length;
930
+ }, flatItem.originProduct, holders);
931
+ const timeReason = (0, _utils.getDiscountTimeUnavailableReason)(discount, (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss'));
932
+ const timeLimit = !timeReason;
777
933
  // 是符合折扣的商品
778
- const isLimitedProduct = (limitedData.type === 'product_all' && limitedData.filter !== 1 || limitedData.type === 'product_all' && limitedData.filter === 1 && !limitedData.exclude_product_ids.includes(product.id) || limitedData.product_ids && limitedData.product_ids.includes(product.id)) && isHolderMatch;
934
+ const isProductInScope = limitedData.type === 'product_all' && limitedData.filter !== 1 || limitedData.type === 'product_all' && limitedData.filter === 1 && !limitedData.exclude_product_ids.includes(product.id) || limitedData.product_ids && limitedData.product_ids.includes(product.id);
935
+ const isLimitedProduct = isProductInScope && isHolderMatch;
779
936
 
780
937
  // 编辑的商品 使用了优惠券不可用
781
- const isAvailableProduct = flatItem.type === 'main' ? !(product?.booking_id && product?.discount_list?.length && product?.discount_list?.every(discount => discount.id && ['good_pass', 'discount_card', 'product_discount_card'].includes(discount.tag || discount.type))) : !(flatItem?.booking_id && !!flatItem?.bundleItem?.discount_list?.length && flatItem?.bundleItem?.discount_list?.every(discount => discount.id));
938
+ const isAvailableProduct = flatItem.type === 'main' ? !(product?.booking_id && product?.discount_list?.length && product?.discount_list?.every(discount => discount.id && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discount.tag || discount.type))) : !(flatItem?.booking_id && !!flatItem?.bundleItem?.discount_list?.length && flatItem?.bundleItem?.discount_list?.every(discount => discount.id));
782
939
 
783
940
  // 套餐是否可用判断
784
941
  const isBundleAvailable = this.checkPackageSubItemUsageRules(discount, flatItem);
942
+ if (isProductInScope && isBundleAvailable && !isItemRewardProductDiscount) {
943
+ const timeReasons = discountTimeReasons.get(discount.id) || [];
944
+ timeReasons.push(timeReason);
945
+ discountTimeReasons.set(discount.id, timeReasons);
946
+ if (!isHolderMatch && timeLimit) discountHolderMismatch.add(discount.id);
947
+ }
785
948
 
786
949
  // 判断优惠券是否适用于该商品
787
- if (isAvailableProduct && !isItemRewardProductDiscount && isLimitedProduct && timeLimit && isBundleAvailable && discount.config?.isAvailable) {
950
+ if (isAvailableProduct && !isItemRewardProductDiscount && isLimitedProduct && timeLimit && isBundleAvailable && discount.config?.isAvailable && (!(0, _entitlementPass.isEntitlementPass)(discount) || (0, _entitlementPass.getRemainingEntitlementUses)(discount, 0, 1) > 0)) {
788
951
  // 记录此优惠券适用的商品
789
952
  discountApplicability.get(discount.id)?.push(product.id);
790
953
 
791
954
  // 记录可抵扣的商品详情
792
955
  const applicableProducts = discountApplicableProducts.get(discount.id) || [];
793
956
  const discountType = discount.tag || discount.type;
794
- const isGoodPass = discountType === 'good_pass';
957
+ const isFullCoverage = (0, _entitlementPass.isFullCoveragePass)(discount);
795
958
 
796
959
  // 使用数量
797
- const num = isGoodPass || flatItem?.type === 'main' ? 1 : product.num;
960
+ const num = isFullCoverage || flatItem?.type === 'main' ? 1 : product.num;
798
961
  const productData = {
799
962
  amount: product.price * num,
800
963
  type: discountType,
@@ -811,6 +974,7 @@ class RulesModule extends _BaseModule.BaseModule {
811
974
  discount_product_id: discount.product_id
812
975
  },
813
976
  metadata: {
977
+ encoded: discount.metadata?.encoded || discount.encoded,
814
978
  num: num,
815
979
  discount_rule_uncheck_flag: discount?.discount_rule_uncheck_flag
816
980
  }
@@ -912,7 +1076,7 @@ class RulesModule extends _BaseModule.BaseModule {
912
1076
  const hasExistingWalletDiscount = product.discount_list?.length && product?.discount_list?.every(discount => {
913
1077
  const discountIdentity = discount.id ?? discount.discount?.resource_id;
914
1078
  const discountType = discount.tag || discount.type;
915
- return Boolean(discountIdentity && ['good_pass', 'discount_card', 'product_discount_card'].includes(discountType));
1079
+ return Boolean(discountIdentity && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType));
916
1080
  });
917
1081
 
918
1082
  // 已持久化且已有钱包券/折扣卡的商品行跳过,保留后端历史折扣结果。
@@ -930,7 +1094,7 @@ class RulesModule extends _BaseModule.BaseModule {
930
1094
  // 找到适用于此商品的所有优惠券,仅考虑isSelected不为false的优惠券
931
1095
  const applicableDiscounts = sortedDiscountList.filter(discount => {
932
1096
  if (this.isItemRewardProductDiscount(product)) return false;
933
- // 🔥 检查策略可用性(针对 good_pass 和折扣卡)
1097
+ // 🔥 检查策略可用性(针对全额抵扣卡和折扣卡)
934
1098
  const discountType = discount.tag || discount.type;
935
1099
  const currentOriginUid = getOriginProductUid(originProduct);
936
1100
  const isReapplyEditModeDiscountForProduct = discount.isEditMode && reapplyBookingDiscountProductUids.size > 0 && currentOriginUid && reapplyBookingDiscountProductUids.has(String(currentOriginUid)) && discount.isSelected === true;
@@ -939,7 +1103,7 @@ class RulesModule extends _BaseModule.BaseModule {
939
1103
  if (!uid || !reapplyBookingDiscountProductUids.has(uid)) return false;
940
1104
  if (!discount.isSelected) return false;
941
1105
  }
942
- if (['good_pass', 'discount_card', 'product_discount_card'].includes(discountType) && !isReapplyEditModeDiscountForProduct) {
1106
+ if (['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType ?? '') && !isReapplyEditModeDiscountForProduct) {
943
1107
  // 如果策略检查后没有config,说明不可用
944
1108
  if (discount.config === undefined || !discount?.config?.isAvailable) {
945
1109
  return false;
@@ -947,17 +1111,22 @@ class RulesModule extends _BaseModule.BaseModule {
947
1111
  }
948
1112
 
949
1113
  // 如果商品价格为 0,其实不需要使用任何优惠券,直接 return true
950
- // 商品券时主商品价格为0不可用
951
- if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.length && (discount.tag || discount.type) === 'good_pass') return false;
1114
+ // 全额抵扣卡在主商品价格为 0 时不可用。
1115
+ if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.length && (0, _entitlementPass.isFullCoveragePass)(discount)) return false;
952
1116
 
953
1117
  // 折扣卡商品价格为0时不可用
954
- if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.find(n => n.discount?.resource_id === discount.id) && (discount.tag || discount.type) !== 'good_pass') return false;
955
- // 如果优惠券已被使用,则跳过
956
- const targetUsedDiscounts = usedDiscounts.get(discount.id);
957
- if (targetUsedDiscounts && (discount.tag || discount.type) === 'good_pass') return false;
1118
+ if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.find(n => n.discount?.resource_id === discount.id) && !(0, _entitlementPass.isFullCoveragePass)(discount)) return false;
1119
+ // good_pass 保持一张实例只使用一次。
1120
+ if ((discount.tag || discount.type) === 'good_pass' && usedDiscounts.get(discount.id)) {
1121
+ return false;
1122
+ }
1123
+ // 有限次数卡按后端实时 balance 约束本次新增分配;无限卡不看余额。
1124
+ if ((0, _entitlementPass.isEntitlementPass)(discount) && (0, _entitlementPass.getRemainingEntitlementUses)(discount, entitlementUsageCounts.get(discount.id) || 0, 1) <= 0) {
1125
+ return false;
1126
+ }
958
1127
 
959
1128
  // 🔥 检查 maxUsagePerOrder 限制:同一 product_id 的商品券使用数量不能超过配置值
960
- if ((discount.tag || discount.type) === 'good_pass') {
1129
+ if ((0, _entitlementPass.isFullCoveragePass)(discount)) {
961
1130
  const maxUsagePerOrder = discount.config?.maxUsagePerOrder;
962
1131
  if (maxUsagePerOrder && maxUsagePerOrder > 0) {
963
1132
  const currentUsedCount = usedProductIdCounts.get(discount.product_id) || 0;
@@ -973,7 +1142,7 @@ class RulesModule extends _BaseModule.BaseModule {
973
1142
  const isHolderMatch = this.checkHolderMatch(discount, {
974
1143
  isNeedHolder: resolveIsFormSubject(_tempVar) && !_tempVar?.isNormalProduct,
975
1144
  holder_id: _tempVar?.holder_id || product.holder_id
976
- }, holders);
1145
+ }, flatItem.originProduct, holders);
977
1146
  // 如果 holder 不匹配,则不适用
978
1147
  if (!isHolderMatch) return false;
979
1148
  let timeLimit = true;
@@ -1002,31 +1171,34 @@ class RulesModule extends _BaseModule.BaseModule {
1002
1171
  return false;
1003
1172
  });
1004
1173
 
1005
- // 用户显式点击某张卡券时,本次选择优先于自动排序和扫码默认选择。
1174
+ // 识别用户本次显式选择的卡券,供次卡和折扣卡各自的优先级使用。
1006
1175
  // 目标仍必须在当前商品的 applicableDiscounts 中,因此不会绕过 Holder、时间或商品范围校验。
1007
1176
  const explicitlySelectedDiscount = options?.isSelected === true && options.discountId !== undefined ? applicableDiscounts.find(discount => {
1008
1177
  const discountType = discount.tag || discount.type;
1009
- return String(discount.id) === String(options.discountId) && ['good_pass', 'discount_card', 'product_discount_card'].includes(discountType);
1178
+ return String(discount.id) === String(options.discountId) && ['entitlement_pass', 'good_pass', 'discount_card', 'product_discount_card'].includes(discountType ?? '');
1010
1179
  }) : undefined;
1011
1180
  // 折扣卡会复用于整条商品行;商品券必须继续按单张拆分,不能复用该分支。
1012
- const explicitlySelectedDiscountCard = explicitlySelectedDiscount && (explicitlySelectedDiscount.tag || explicitlySelectedDiscount.type) !== 'good_pass' ? explicitlySelectedDiscount : undefined;
1181
+ const explicitlySelectedDiscountCard = explicitlySelectedDiscount && !(0, _entitlementPass.isFullCoveragePass)(explicitlySelectedDiscount) ? explicitlySelectedDiscount : undefined;
1013
1182
 
1014
- // 商品数量等变化会触发无 discountId 的重算。只要当前行已应用的折扣卡仍在
1183
+ // 商品数量等变化会触发无 discountId 的重算。只要当前行已应用的卡仍在
1015
1184
  // applicableDiscounts 中,就应保留该行选择,避免被自动排序中力度更大的卡替换。
1016
- const appliedDiscountCardIds = new Set((product.discount_list || []).filter(item => ['discount_card', 'product_discount_card'].includes(item?.tag || item?.type)).map(item => item?.discount?.resource_id ?? item?.resource_id ?? item?.id).filter(id => id !== undefined && id !== null).map(String));
1185
+ const appliedWalletDiscountIds = new Set((product.discount_list || []).filter(item => ['entitlement_pass', 'discount_card', 'product_discount_card'].includes(item?.tag || item?.type)).map(item => item?.discount?.resource_id ?? item?.resource_id ?? item?.id).filter(id => id !== undefined && id !== null).map(String));
1017
1186
  const appliedDiscountCard = options?.scan ? undefined : applicableDiscounts.find(discount => {
1018
1187
  const discountType = discount.tag || discount.type;
1019
- return discount.isSelected === true && ['discount_card', 'product_discount_card'].includes(discountType) && appliedDiscountCardIds.has(String(discount.id));
1188
+ return discount.isSelected === true && ['discount_card', 'product_discount_card'].includes(discountType ?? '') && appliedWalletDiscountIds.has(String(discount.id));
1020
1189
  });
1190
+ const appliedEntitlementPass = options?.scan || options?.preserveEntitlementSelection === false ? undefined : applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount) && discount.isSelected === true && appliedWalletDiscountIds.has(String(discount.id)));
1021
1191
 
1022
- // 优先级:本次显式选择 > 当前行既有选择 > 扫码选择 > 自动排序第一项。
1023
- const scannedSelectedDiscountCard = applicableDiscounts.find(n => n.isScan && n.isSelected && (n.tag || n.type) !== 'good_pass');
1192
+ // 次卡内部按本次手选/扫码、当前行已应用、默认排序选择。
1193
+ const scannedSelectedDiscountCard = applicableDiscounts.find(n => n.isScan && n.isSelected && !(0, _entitlementPass.isFullCoveragePass)(n));
1024
1194
  const selectedDiscountCard = explicitlySelectedDiscountCard || appliedDiscountCard || scannedSelectedDiscountCard;
1025
- const selectedDiscount = explicitlySelectedDiscount || selectedDiscountCard || applicableDiscounts[0];
1026
- const prioritizedApplicableDiscounts = explicitlySelectedDiscount && (explicitlySelectedDiscount.tag || explicitlySelectedDiscount.type) === 'good_pass' ? [explicitlySelectedDiscount, ...applicableDiscounts.filter(discount => {
1027
- const discountType = discount.tag || discount.type;
1028
- return discountType === 'good_pass' && String(discount.id) !== String(explicitlySelectedDiscount.id);
1029
- }), ...applicableDiscounts.filter(discount => (discount.tag || discount.type) !== 'good_pass')] : applicableDiscounts;
1195
+ const selectedEntitlementPass = (explicitlySelectedDiscount && (0, _entitlementPass.isEntitlementPass)(explicitlySelectedDiscount) ? explicitlySelectedDiscount : undefined) || applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount) && preferredEntitlementPassIds.has(String(discount.id))) || appliedEntitlementPass || applicableDiscounts.find(discount => (0, _entitlementPass.isEntitlementPass)(discount));
1196
+ // 手选优先;普通重算保留当前折扣卡,扫码和最优选择仍使用原有优先级。
1197
+ const selectedDiscount = explicitlySelectedDiscount || (options?.preserveEntitlementSelection !== false && preferredEntitlementPassIds.size === 0 ? appliedDiscountCard : undefined) || selectedEntitlementPass || selectedDiscountCard || applicableDiscounts[0];
1198
+ // 实际次数分配必须使用同一顺序,避免选中状态切换后仍将次数分配给默认卡。
1199
+ const prioritizedApplicableDiscounts = selectedDiscount && (0, _entitlementPass.isFullCoveragePass)(selectedDiscount) ? [selectedDiscount, ...applicableDiscounts.filter(discount => {
1200
+ return (0, _entitlementPass.isFullCoveragePass)(discount) && String(discount.id) !== String(selectedDiscount.id);
1201
+ }), ...applicableDiscounts.filter(discount => !(0, _entitlementPass.isFullCoveragePass)(discount))] : applicableDiscounts;
1030
1202
 
1031
1203
  // 如果是手动折扣,则不适用优惠券
1032
1204
  let isManualDiscount = false;
@@ -1142,19 +1314,13 @@ class RulesModule extends _BaseModule.BaseModule {
1142
1314
  return;
1143
1315
  }
1144
1316
 
1145
- // 是否需要拆分(商品券需要拆分)
1146
- const isNeedSplit = (selectedDiscount.tag || selectedDiscount.type) === 'good_pass';
1317
+ // good_pass 与 Entitlement Pass 都沿用全额抵扣卡的商品行拆分机制。
1318
+ const isNeedSplit = (0, _entitlementPass.isFullCoveragePass)(selectedDiscount);
1147
1319
 
1148
1320
  // 需要拆分出来的数量
1149
1321
  const totalQuantity = product.quantity || product.num || 1;
1150
- const availableGoodPassCount = applicableDiscounts.filter(item => (item.tag || item.type) === 'good_pass').length;
1151
-
1152
- // 🔥 如果selectedDiscount有config配置,则使用config.maxUsagePerOrder限制使用次数
1153
- let maxUsageLimit = availableGoodPassCount;
1154
- if (selectedDiscount.config && selectedDiscount.config.maxUsagePerOrder) {
1155
- maxUsageLimit = Math.min(availableGoodPassCount, selectedDiscount.config.maxUsagePerOrder);
1156
- }
1157
- const splitCount = isNeedSplit ? Math.min(product.quantity || product.num || 1, maxUsageLimit) : 1;
1322
+ const fullCoveragePassUsageGroups = isNeedSplit ? buildFullCoveragePassUsageGroups(prioritizedApplicableDiscounts, totalQuantity) : [];
1323
+ const splitCount = isNeedSplit ? fullCoveragePassUsageGroups.reduce((total, group) => total + group.count, 0) : 1;
1158
1324
  const arr = [];
1159
1325
 
1160
1326
  // 🔥 主商品和bundle子商品分别处理
@@ -1162,22 +1328,22 @@ class RulesModule extends _BaseModule.BaseModule {
1162
1328
  // 主商品:保持原有逻辑
1163
1329
  if (splitCount < totalQuantity && isNeedSplit) {
1164
1330
  const remainingQuantity = totalQuantity - splitCount;
1165
- const applicableNonGoodPassById = new Map(applicableDiscounts.filter(discount => (discount.tag || discount.type) !== 'good_pass').map(discount => [String(discount.id), discount]));
1331
+ const applicableNonFullCoveragePassById = new Map(applicableDiscounts.filter(discount => !(0, _entitlementPass.isFullCoveragePass)(discount)).map(discount => [String(discount.id), discount]));
1166
1332
  const remainderDiscountList = (product.discount_list || []).filter(discount => {
1167
1333
  const discountType = discount.tag || discount.type;
1168
1334
  if (discountType === 'promotion') return true;
1169
- if (discountType === 'good_pass') return false;
1335
+ if ((0, _entitlementPass.isFullCoveragePass)(discount)) return false;
1170
1336
  const resourceId = discount.discount?.resource_id ?? discount.id;
1171
- return resourceId !== undefined && resourceId !== null && applicableNonGoodPassById.has(String(resourceId));
1337
+ return resourceId !== undefined && resourceId !== null && applicableNonFullCoveragePassById.has(String(resourceId));
1172
1338
  }).map(discount => {
1173
1339
  if ((discount.tag || discount.type) === 'promotion') return discount;
1174
1340
  const resourceId = discount.discount?.resource_id ?? discount.id;
1175
- const matchedDiscount = applicableNonGoodPassById.get(String(resourceId));
1341
+ const matchedDiscount = applicableNonFullCoveragePassById.get(String(resourceId));
1176
1342
  if (matchedDiscount) {
1177
1343
  // 实际场景:3 件折扣卡商品中拆出 1 件改用商品券后,
1178
1344
  // 剩余 2 件仍在使用原折扣卡;必须同步登记折扣卡的使用状态,
1179
1345
  // 否则 updatedDiscountList 会把它改成未选中并清空 savedAmount。
1180
- usedDiscounts.set(matchedDiscount.id, true);
1346
+ markDiscountUsed(matchedDiscount);
1181
1347
  const appliedProducts = appliedDiscountProducts.get(matchedDiscount.id) || [];
1182
1348
  appliedProducts.push({
1183
1349
  ...discount,
@@ -1191,14 +1357,14 @@ class RulesModule extends _BaseModule.BaseModule {
1191
1357
  _num: remainingQuantity
1192
1358
  };
1193
1359
  });
1194
- const hasRetainedWalletDiscount = remainderDiscountList.some(discount => !['promotion', 'good_pass'].includes(discount.tag || discount.type));
1360
+ const hasRetainedWalletDiscount = remainderDiscountList.some(discount => (discount.tag || discount.type) !== 'promotion' && !(0, _entitlementPass.isFullCoveragePass)(discount));
1195
1361
  let total = product.origin_total ?? product.total;
1196
1362
  if ((product.discount_list || []).some(item => item.type === 'promotion')) {
1197
1363
  total = product.total ?? product.origin_total;
1198
1364
  }
1199
1365
  arr.push(this.hooks.setProduct(originProduct, {
1200
1366
  // 商品券只替换被拆出的 splitCount 件。余量行原本仍有效的折扣卡
1201
- // 不能随 good_pass 拆分一起清除;已取消或已失效的折扣不会进入此列表。
1367
+ // 不能随全额抵扣卡拆分一起清除;已取消或已失效的折扣不会进入此列表。
1202
1368
  discount_list: remainderDiscountList,
1203
1369
  quantity: remainingQuantity,
1204
1370
  _id: product._id.split('___')[0],
@@ -1210,14 +1376,21 @@ class RulesModule extends _BaseModule.BaseModule {
1210
1376
  } : {})
1211
1377
  }));
1212
1378
  }
1213
- for (let i = 0; i < splitCount; i++) {
1214
- const selectedDiscount = isNeedSplit ? prioritizedApplicableDiscounts[i] : selectedDiscountCard || applicableDiscounts[i];
1379
+ const discountUsageGroups = isNeedSplit ? fullCoveragePassUsageGroups : [{
1380
+ discount: selectedDiscount,
1381
+ count: 1
1382
+ }];
1383
+ for (let i = 0; i < discountUsageGroups.length; i++) {
1384
+ const {
1385
+ discount: selectedDiscount,
1386
+ count: appliedUseCount
1387
+ } = discountUsageGroups[i];
1215
1388
  // 标记优惠券为已使用
1216
- usedDiscounts.set(selectedDiscount.id, true);
1389
+ markDiscountUsed(selectedDiscount, appliedUseCount);
1217
1390
  // 🔥 更新 product_id 使用计数
1218
- if ((selectedDiscount.tag || selectedDiscount.type) === 'good_pass') {
1391
+ if ((0, _entitlementPass.isFullCoveragePass)(selectedDiscount)) {
1219
1392
  const currentCount = usedProductIdCounts.get(selectedDiscount.product_id) || 0;
1220
- usedProductIdCounts.set(selectedDiscount.product_id, currentCount + 1);
1393
+ usedProductIdCounts.set(selectedDiscount.product_id, currentCount + appliedUseCount);
1221
1394
  }
1222
1395
 
1223
1396
  // 记录实际应用了优惠券的商品信息
@@ -1279,7 +1452,7 @@ class RulesModule extends _BaseModule.BaseModule {
1279
1452
  const mainProductDiscountAmount = amount;
1280
1453
  const lineDiscountAmount = new _decimal.default(mainProductDiscountAmount).plus(optionDiscountAmount).toNumber();
1281
1454
  const discountType = selectedDiscount.tag || selectedDiscount.type;
1282
- const isGoodPass = discountType === 'good_pass';
1455
+ const isFullCoverage = (0, _entitlementPass.isFullCoveragePass)(selectedDiscount);
1283
1456
  const discountDetail = {
1284
1457
  amount: lineDiscountAmount,
1285
1458
  type: selectedDiscount.tag === 'product_discount_card' ? 'discount_card' : discountType,
@@ -1295,10 +1468,11 @@ class RulesModule extends _BaseModule.BaseModule {
1295
1468
  discount_product_id: selectedDiscount.product_id
1296
1469
  },
1297
1470
  // 前端使用的num数量,为了计算优惠金额
1298
- _num: isGoodPass ? 1 : product.num,
1471
+ _num: isFullCoverage ? appliedUseCount : product.num,
1299
1472
  config: selectedDiscount?.config,
1300
1473
  metadata: {
1301
- num: 1,
1474
+ encoded: selectedDiscount.metadata?.encoded || selectedDiscount.encoded,
1475
+ num: isFullCoverage ? appliedUseCount : 1,
1302
1476
  discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,
1303
1477
  // 🔥 order_level 分摊差值
1304
1478
  ...(productDiscountDifference !== undefined && {
@@ -1317,9 +1491,9 @@ class RulesModule extends _BaseModule.BaseModule {
1317
1491
  if (product.isClient) {
1318
1492
  arr.push(this.hooks.setProduct(originProduct, {
1319
1493
  discount_list: [discountDetail],
1320
- // good_pass:主商品价以折后价为准;勿用 product.price - amount(amount 含 option 优惠时会算错)
1321
- price: isGoodPass ? mainProductSellingPrice : product.price,
1322
- quantity: isNeedSplit ? 1 : product.quantity,
1494
+ // 全额抵扣卡:主商品价以折后价为准;勿用 product.price - amount(amount 含 option 优惠时会算错)
1495
+ price: isFullCoverage ? mainProductSellingPrice : product.price,
1496
+ quantity: isNeedSplit ? appliedUseCount : product.quantity,
1323
1497
  origin_total: (0, _utils2.getProductOriginTotalPrice)({
1324
1498
  product: {
1325
1499
  original_price: product.original_price
@@ -1336,8 +1510,8 @@ class RulesModule extends _BaseModule.BaseModule {
1336
1510
  arr.push(this.hooks.setProduct(originProduct, {
1337
1511
  discount_list: this.filterDiscountListByType(product.discount_list, 'promotion').concat([discountDetail]),
1338
1512
  _id: product._id.split('___')[0] + '___' + selectedDiscount.id + index,
1339
- price: selectedDiscount.tag === 'good_pass' ? 0 : product.price,
1340
- quantity: isNeedSplit ? 1 : product.quantity,
1513
+ price: isFullCoverage ? 0 : product.price,
1514
+ quantity: isNeedSplit ? appliedUseCount : product.quantity,
1341
1515
  total: total,
1342
1516
  origin_total: productOriginTotal,
1343
1517
  main_product_selling_price: mainProductSellingPrice,
@@ -1354,14 +1528,17 @@ class RulesModule extends _BaseModule.BaseModule {
1354
1528
  const discountNum = splitCount;
1355
1529
  const normalNum = totalQuantity - discountNum;
1356
1530
 
1357
- // 生成有折扣的商品(每张商品券对应 num: 1)
1358
- for (let i = 0; i < discountNum; i++) {
1359
- const selectedDiscount = prioritizedApplicableDiscounts[i];
1360
- usedDiscounts.set(selectedDiscount.id, true);
1531
+ // 同一卡实例的本次新增使用合并为一条,metadata.num 记录实际次数。
1532
+ for (let i = 0; i < fullCoveragePassUsageGroups.length; i++) {
1533
+ const {
1534
+ discount: selectedDiscount,
1535
+ count: appliedUseCount
1536
+ } = fullCoveragePassUsageGroups[i];
1537
+ markDiscountUsed(selectedDiscount, appliedUseCount);
1361
1538
  // 🔥 更新 product_id 使用计数
1362
- if ((selectedDiscount.tag || selectedDiscount.type) === 'good_pass') {
1539
+ if ((0, _entitlementPass.isFullCoveragePass)(selectedDiscount)) {
1363
1540
  const currentCount = usedProductIdCounts.get(selectedDiscount.product_id) || 0;
1364
- usedProductIdCounts.set(selectedDiscount.product_id, currentCount + 1);
1541
+ usedProductIdCounts.set(selectedDiscount.product_id, currentCount + appliedUseCount);
1365
1542
  }
1366
1543
 
1367
1544
  // 🔥 生成唯一的 _id
@@ -1369,11 +1546,12 @@ class RulesModule extends _BaseModule.BaseModule {
1369
1546
 
1370
1547
  // 计算折扣后的价格(使用 getDiscountAmount,内部已处理 maxDeductionAmount)
1371
1548
  const discountedPrice = (0, _utils.getDiscountAmount)(selectedDiscount, product.origin_total, product.origin_total);
1372
- const bundleDiscountAmount = new _decimal.default(product.origin_total || 0).minus(discountedPrice).toNumber();
1549
+ const bundleDiscountAmountPerItem = new _decimal.default(product.origin_total || 0).minus(discountedPrice).toNumber();
1550
+ const bundleDiscountAmount = new _decimal.default(bundleDiscountAmountPerItem).mul(appliedUseCount).toNumber();
1373
1551
  debugger;
1374
1552
  const discountDetail = {
1375
1553
  amount: bundleDiscountAmount,
1376
- type: 'good_pass',
1554
+ type: selectedDiscount.tag || selectedDiscount.type,
1377
1555
  discount: {
1378
1556
  fixed_amount: product.origin_total,
1379
1557
  discount_calculation_mode: selectedDiscount?.metadata?.discount_calculation_mode,
@@ -1384,12 +1562,13 @@ class RulesModule extends _BaseModule.BaseModule {
1384
1562
  discount_product_id: selectedDiscount.product_id
1385
1563
  },
1386
1564
  metadata: {
1565
+ encoded: selectedDiscount.metadata?.encoded || selectedDiscount.encoded,
1387
1566
  // 🔥 使用拆分后的唯一 _id
1388
1567
  custom_product_bundle_map_id: uniqueId,
1389
1568
  discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,
1390
- num: 1
1569
+ num: appliedUseCount
1391
1570
  },
1392
- _num: 1,
1571
+ _num: appliedUseCount,
1393
1572
  config: selectedDiscount?.config
1394
1573
  };
1395
1574
 
@@ -1401,11 +1580,11 @@ class RulesModule extends _BaseModule.BaseModule {
1401
1580
  ...flatItem,
1402
1581
  // 🔥 使用唯一的 _id
1403
1582
  _id: uniqueId,
1404
- num: 1,
1405
- quantity: 1,
1583
+ num: appliedUseCount,
1584
+ quantity: appliedUseCount,
1406
1585
  price: discountedPrice,
1407
1586
  // 折扣后的价格
1408
- total: discountedPrice,
1587
+ total: new _decimal.default(discountedPrice).mul(appliedUseCount).toNumber(),
1409
1588
  discount_list: [discountDetail],
1410
1589
  processed: true,
1411
1590
  _discountId: selectedDiscount.id
@@ -1427,9 +1606,9 @@ class RulesModule extends _BaseModule.BaseModule {
1427
1606
  } else {
1428
1607
  // 折扣卡:不拆分数量,直接应用
1429
1608
  const selectedDiscount = selectedDiscountCard || applicableDiscounts[0];
1430
- usedDiscounts.set(selectedDiscount.id, true);
1609
+ markDiscountUsed(selectedDiscount);
1431
1610
  // 🔥 更新 product_id 使用计数(虽然此处主要是折扣卡,但保持一致性)
1432
- if ((selectedDiscount.tag || selectedDiscount.type) === 'good_pass') {
1611
+ if ((0, _entitlementPass.isFullCoveragePass)(selectedDiscount)) {
1433
1612
  const currentCount = usedProductIdCounts.get(selectedDiscount.product_id) || 0;
1434
1613
  usedProductIdCounts.set(selectedDiscount.product_id, currentCount + 1);
1435
1614
  }
@@ -1479,6 +1658,7 @@ class RulesModule extends _BaseModule.BaseModule {
1479
1658
  discount_product_id: selectedDiscount.product_id
1480
1659
  },
1481
1660
  metadata: {
1661
+ encoded: selectedDiscount.metadata?.encoded || selectedDiscount.encoded,
1482
1662
  // 🔥 使用唯一的 _id
1483
1663
  custom_product_bundle_map_id: uniqueId,
1484
1664
  discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,
@@ -1562,11 +1742,10 @@ class RulesModule extends _BaseModule.BaseModule {
1562
1742
  // 没有bundle,直接使用主商品处理结果
1563
1743
  result.push(...mainProductArr);
1564
1744
  } else {
1565
- // 🔥 有bundle,需要检查子商品是否应用了商品券
1745
+ // 🔥 有 bundle,需要检查子商品是否应用了全额抵扣卡
1566
1746
  // 1. 先收集所有bundle子商品的处理结果
1567
1747
  const bundleProcessingInfo = new Map();
1568
- let hasGoodPassApplied = false; // 标记是否有子商品应用了商品券
1569
-
1748
+ let hasFullCoveragePassApplied = false;
1570
1749
  if (product.bundle && Array.isArray(product.bundle)) {
1571
1750
  product.bundle.forEach((bundleItem, bundleIndex) => {
1572
1751
  const bundleItemId = `${product._id}_bundle_${bundleIndex}`;
@@ -1577,10 +1756,9 @@ class RulesModule extends _BaseModule.BaseModule {
1577
1756
  } else {
1578
1757
  // 处理过的bundle item
1579
1758
  bundleProcessingInfo.set(bundleIndex, processedBundleItems);
1580
- // 🔥 检查是否应用了商品券(good_pass)
1581
- const hasGoodPass = processedBundleItems.some(item => item.discount_list?.some(discount => discount.type === 'good_pass' || discount.tag === 'good_pass'));
1582
- if (hasGoodPass) {
1583
- hasGoodPassApplied = true;
1759
+ const hasFullCoveragePass = processedBundleItems.some(item => item.discount_list?.some(discount => (0, _entitlementPass.isFullCoveragePass)(discount)));
1760
+ if (hasFullCoveragePass) {
1761
+ hasFullCoveragePassApplied = true;
1584
1762
  }
1585
1763
  }
1586
1764
  });
@@ -1588,7 +1766,7 @@ class RulesModule extends _BaseModule.BaseModule {
1588
1766
 
1589
1767
  // 🔥 2. 如果有子商品应用了商品券,且主商品数量大于1,需要拆分主商品
1590
1768
  const mainProductQuantity = mainProductArr[0] ? this.hooks.getProduct(mainProductArr[0]).quantity : 1;
1591
- if (hasGoodPassApplied && mainProductArr.length === 1 && mainProductQuantity > 1) {
1769
+ if (hasFullCoveragePassApplied && mainProductArr.length === 1 && mainProductQuantity > 1) {
1592
1770
  const mainProduct = mainProductArr[0];
1593
1771
  const mainProductData = this.hooks.getProduct(mainProduct);
1594
1772
 
@@ -1892,27 +2070,39 @@ class RulesModule extends _BaseModule.BaseModule {
1892
2070
 
1893
2071
  // 获取此优惠券实际应用的商品详情
1894
2072
  const appliedProductDetails = appliedDiscountProducts.get(discount.id) || [];
2073
+ const committedAppliedProductDetails = (0, _entitlementPass.isEntitlementPass)(discount) ? mergeEntitlementAppliedProductDetails(discount.committedAppliedProductDetails || []) : [];
2074
+ const allAppliedProductDetails = (0, _entitlementPass.isEntitlementPass)(discount) ? mergeEntitlementAppliedProductDetails([...committedAppliedProductDetails, ...appliedProductDetails]) : appliedProductDetails;
1895
2075
 
1896
2076
  // 根据是否有适用商品决定isAvailable
1897
2077
  const isAvailable = applicableProducts.length > 0;
2078
+ const timeReasons = discountTimeReasons.get(discount.id) || [];
2079
+ const timeReason = timeReasons.length ? timeReasons.every(Boolean) ? timeReasons[0] : undefined : (0, _utils.getDiscountTimeUnavailableReason)(discount, (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss'));
2080
+ const reasonCode = isAvailable ? undefined : (0, _availability.getDiscountUsageBlockReason)(discount) || timeReason || discount.reasonCode || (discountHolderMismatch.has(discount.id) ? 'holder_not_matched' : 'product_not_applicable');
2081
+ const availabilityReason = {
2082
+ reasonCode,
2083
+ reason: reasonCode && reasonCode === discount.reasonCode ? discount.reason : undefined
2084
+ };
1898
2085
 
1899
2086
  // 如果此优惠券被明确设置为不选中,保持其状态,但正确设置isAvailable
1900
2087
  if (typeof discount.isSelected === 'boolean' && discount.isSelected === false && discount.isManualSelect) {
1901
2088
  return {
1902
2089
  ...discount,
2090
+ ...availabilityReason,
1903
2091
  isSelected: false,
1904
2092
  isAvailable: isAvailable,
1905
2093
  // 正确设置可用性
1906
2094
  applicableProductIds: applicableProducts,
1907
2095
  // 添加可抵扣的商品详情
1908
2096
  applicableProductDetails: applicableProductDetails,
1909
- // 添加实际抵扣的商品详情(对于未选中的优惠券,为空数组)
1910
- appliedProductDetails: []
2097
+ // 已提交的历史使用不可撤销;只取消本次新增分配。
2098
+ appliedProductDetails: committedAppliedProductDetails
1911
2099
  };
1912
2100
  }
1913
- if (usedDiscounts.get(discount.id)) {
2101
+ if (isDiscountUsed(discount)) {
1914
2102
  return {
1915
2103
  ...discount,
2104
+ reason: undefined,
2105
+ reasonCode: undefined,
1916
2106
  isSelected: true,
1917
2107
  isManualSelect: options?.scan ? false : discount.isManualSelect,
1918
2108
  // 标记为可用,因为它已被应用
@@ -1922,7 +2112,7 @@ class RulesModule extends _BaseModule.BaseModule {
1922
2112
  // 添加可抵扣的商品详情
1923
2113
  applicableProductDetails: applicableProductDetails,
1924
2114
  // 添加实际抵扣的商品详情
1925
- appliedProductDetails: appliedProductDetails
2115
+ appliedProductDetails: allAppliedProductDetails
1926
2116
  };
1927
2117
  }
1928
2118
  let isSelected = typeof discount.isSelected === 'boolean' ? discount.isSelected : undefined;
@@ -1930,11 +2120,12 @@ class RulesModule extends _BaseModule.BaseModule {
1930
2120
  // 如果是可用的 并且没有手动标记 则设置为选中
1931
2121
  if (isAvailable && appliedProductDetails.length && !discount.isManualSelect) {
1932
2122
  isSelected = true;
1933
- } else if (!appliedProductDetails.length) {
2123
+ } else if (!appliedProductDetails.length && !committedAppliedProductDetails.length) {
1934
2124
  isSelected = false;
1935
2125
  }
1936
2126
  return {
1937
2127
  ...discount,
2128
+ ...availabilityReason,
1938
2129
  // 如果有适用的商品,标记为可用
1939
2130
  isAvailable: isAvailable,
1940
2131
  isSelected,
@@ -1944,13 +2135,69 @@ class RulesModule extends _BaseModule.BaseModule {
1944
2135
  applicableProductIds: applicableProducts,
1945
2136
  // 添加可抵扣的商品详情
1946
2137
  applicableProductDetails: applicableProductDetails,
1947
- // 添加实际抵扣的商品详情(对于未使用的优惠券,为空数组)
1948
- appliedProductDetails: []
2138
+ // 编辑订单的历史使用继续保留,本次没有新增使用。
2139
+ appliedProductDetails: committedAppliedProductDetails
2140
+ };
2141
+ });
2142
+
2143
+ // 整单分配完成后统一记录卡余额;历史使用已包含在服务端余额中,只扣本次新增次数。
2144
+ const entitlementBalances = new Map();
2145
+ addModeDiscount.forEach(discount => {
2146
+ const quota = (0, _entitlementPass.resolveEntitlementQuota)(discount);
2147
+ if (!quota) return;
2148
+ entitlementBalances.set(String(discount.id), quota.type === 'unlimited' ? null : Math.max(quota.balance - (entitlementUsageCounts.get(discount.id) || 0), 0));
2149
+ });
2150
+ const getEntitlementBalance = detail => {
2151
+ if (!(0, _entitlementPass.isEntitlementPass)(detail)) return undefined;
2152
+ const resourceId = detail?.discount?.resource_id ?? detail?.resource_id;
2153
+ return entitlementBalances.get(String(resourceId));
2154
+ };
2155
+ const needsEntitlementBalance = detail => {
2156
+ const balance = getEntitlementBalance(detail);
2157
+ return balance !== undefined && detail?.metadata?.balance !== balance;
2158
+ };
2159
+ const withEntitlementBalance = detail => {
2160
+ if (!needsEntitlementBalance(detail)) return detail;
2161
+ return {
2162
+ ...detail,
2163
+ metadata: {
2164
+ ...(detail.metadata || {}),
2165
+ balance: getEntitlementBalance(detail)
2166
+ }
2167
+ };
2168
+ };
2169
+ const getProductDiscountDetails = originProduct => {
2170
+ const product = this.hooks.getProduct(originProduct);
2171
+ return [...(product.discount_list || []), ...(product.bundle || []).flatMap(bundle => bundle.discount_list || [])];
2172
+ };
2173
+ const productsWithEntitlementBalances = entitlementBalances.size ? processedProductList.map(product => {
2174
+ if (!getProductDiscountDetails(product).some(needsEntitlementBalance)) {
2175
+ return product;
2176
+ }
2177
+ // 历史商品及套餐可能仍引用输入快照;仅复制有变化的行,并直接更新 metadata。
2178
+ // 不再次调用价格换算钩子,避免只补余额时改变商品价格。
2179
+ const nextProduct = (0, _lodashEs.cloneDeep)(product);
2180
+ getProductDiscountDetails(nextProduct).forEach(detail => {
2181
+ if (!needsEntitlementBalance(detail)) return;
2182
+ detail.metadata = withEntitlementBalance(detail).metadata;
2183
+ });
2184
+ return nextProduct;
2185
+ }) : processedProductList;
2186
+ const discountsWithEntitlementBalances = [...editModeDiscount, ...updatedDiscountList].map(discount => {
2187
+ if (!(0, _entitlementPass.isEntitlementPass)(discount) || !entitlementBalances.has(String(discount.id))) {
2188
+ return discount;
2189
+ }
2190
+ return {
2191
+ ...discount,
2192
+ appliedProductDetails: (discount.appliedProductDetails || []).map(withEntitlementBalance),
2193
+ ...(discount.committedAppliedProductDetails ? {
2194
+ committedAppliedProductDetails: discount.committedAppliedProductDetails.map(withEntitlementBalance)
2195
+ } : {})
1949
2196
  };
1950
2197
  });
1951
2198
  return {
1952
- productList: processedProductList,
1953
- discountList: [...editModeDiscount, ...updatedDiscountList]
2199
+ productList: productsWithEntitlementBalances,
2200
+ discountList: discountsWithEntitlementBalances
1954
2201
  };
1955
2202
  }
1956
2203