@pisell/pisellos 0.0.571 → 0.0.572

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 (143) hide show
  1. package/dist/effects/index.d.ts +2 -2
  2. package/dist/model/strategy/adapter/dataVariant/type.d.ts +3 -3
  3. package/dist/model/strategy/adapter/itemRule/type.d.ts +6 -6
  4. package/dist/model/strategy/adapter/promotion/index.d.ts +4 -0
  5. package/dist/model/strategy/adapter/promotion/type.d.ts +5 -5
  6. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  7. package/dist/model/strategy/adapter/walletPass/evaluator.js +4 -2
  8. package/dist/model/strategy/type.d.ts +1 -1
  9. package/dist/modules/BookingContext/types.d.ts +4 -4
  10. package/dist/modules/BookingContext/utils/resourceErrors.d.ts +1 -1
  11. package/dist/modules/Customer/types.d.ts +1 -1
  12. package/dist/modules/Discount/availability.d.ts +7 -0
  13. package/dist/modules/Discount/availability.js +52 -0
  14. package/dist/modules/Discount/entitlementPass.d.ts +34 -0
  15. package/dist/modules/Discount/entitlementPass.js +147 -0
  16. package/dist/modules/Discount/entitlementState.d.ts +8 -0
  17. package/dist/modules/Discount/entitlementState.js +205 -0
  18. package/dist/modules/Discount/index.d.ts +5 -2
  19. package/dist/modules/Discount/index.js +124 -25
  20. package/dist/modules/Discount/types.d.ts +29 -6
  21. package/dist/modules/Holder/types.d.ts +1 -1
  22. package/dist/modules/OpenData/types.d.ts +1 -1
  23. package/dist/modules/Order/types.d.ts +5 -5
  24. package/dist/modules/Order/utils/bundleDiscountHydration.d.ts +1 -1
  25. package/dist/modules/Order/utils/orderCollectionIdentity.d.ts +1 -1
  26. package/dist/modules/Order/utils.d.ts +1 -1
  27. package/dist/modules/Payment/types.d.ts +7 -7
  28. package/dist/modules/Product/index.d.ts +1 -1
  29. package/dist/modules/ResourcePlanner/types.d.ts +4 -4
  30. package/dist/modules/Rules/entitlementLines.d.ts +8 -0
  31. package/dist/modules/Rules/entitlementLines.js +159 -0
  32. package/dist/modules/Rules/index.d.ts +13 -3
  33. package/dist/modules/Rules/index.js +617 -342
  34. package/dist/modules/Rules/types.d.ts +4 -4
  35. package/dist/modules/ScanOrderLogger/types.d.ts +2 -2
  36. package/dist/modules/Schedule/getDateIsInSchedule.d.ts +1 -1
  37. package/dist/modules/Schedule/type.d.ts +7 -7
  38. package/dist/modules/Schedule/types.d.ts +9 -9
  39. package/dist/plugins/app.d.ts +1 -1
  40. package/dist/plugins/request.d.ts +2 -2
  41. package/dist/plugins/window.d.ts +2 -2
  42. package/dist/server/modules/floor-plan/types.d.ts +1 -1
  43. package/dist/server/modules/menu/types.d.ts +1 -1
  44. package/dist/server/modules/order/types.d.ts +16 -16
  45. package/dist/server/modules/products/types.d.ts +1 -1
  46. package/dist/server/modules/resource/types.d.ts +1 -1
  47. package/dist/server/modules/schedule/utils.d.ts +1 -1
  48. package/dist/server/types.d.ts +4 -4
  49. package/dist/server/utils/small-ticket.d.ts +1 -1
  50. package/dist/solution/BaseSales/index.d.ts +1 -1
  51. package/dist/solution/BaseSales/types.d.ts +7 -7
  52. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -1
  53. package/dist/solution/BookingByStep/index.d.ts +1 -1
  54. package/dist/solution/BookingByStep/utils/resources.d.ts +1 -1
  55. package/dist/solution/BookingTicket/index.d.ts +13 -1
  56. package/dist/solution/BookingTicket/types.d.ts +3 -3
  57. package/dist/solution/BookingTicket/utils/addProductDecision.d.ts +1 -1
  58. package/dist/solution/BookingTicket/utils/bookingStatus.d.ts +1 -1
  59. package/dist/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +1 -1
  60. package/dist/solution/Sales/types.d.ts +4 -4
  61. package/dist/solution/ScanOrder/types.d.ts +5 -5
  62. package/dist/solution/ShopDiscount/index.d.ts +3 -0
  63. package/dist/solution/ShopDiscount/index.js +161 -78
  64. package/dist/solution/ShopDiscount/types.d.ts +1 -1
  65. package/dist/solution/ShopDiscount/utils.d.ts +3 -2
  66. package/dist/solution/ShopDiscount/utils.js +40 -1
  67. package/dist/solution/UnifiedBookingSales/types.d.ts +1 -1
  68. package/dist/solution/VenueBooking/types.d.ts +1 -1
  69. package/dist/types/index.d.ts +1 -1
  70. package/dist/utils/task.d.ts +2 -2
  71. package/dist/utils/watch.d.ts +2 -2
  72. package/lib/effects/index.d.ts +2 -2
  73. package/lib/model/strategy/adapter/dataVariant/type.d.ts +3 -3
  74. package/lib/model/strategy/adapter/itemRule/type.d.ts +6 -6
  75. package/lib/model/strategy/adapter/promotion/index.d.ts +4 -0
  76. package/lib/model/strategy/adapter/promotion/type.d.ts +5 -5
  77. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  78. package/lib/model/strategy/adapter/walletPass/evaluator.js +2 -0
  79. package/lib/model/strategy/type.d.ts +1 -1
  80. package/lib/modules/BookingContext/types.d.ts +4 -4
  81. package/lib/modules/BookingContext/utils/resourceErrors.d.ts +1 -1
  82. package/lib/modules/Customer/types.d.ts +1 -1
  83. package/lib/modules/Discount/availability.d.ts +7 -0
  84. package/lib/modules/Discount/availability.js +46 -0
  85. package/lib/modules/Discount/entitlementPass.d.ts +34 -0
  86. package/lib/modules/Discount/entitlementPass.js +141 -0
  87. package/lib/modules/Discount/entitlementState.d.ts +8 -0
  88. package/lib/modules/Discount/entitlementState.js +150 -0
  89. package/lib/modules/Discount/index.d.ts +5 -2
  90. package/lib/modules/Discount/index.js +59 -14
  91. package/lib/modules/Discount/types.d.ts +29 -6
  92. package/lib/modules/Holder/types.d.ts +1 -1
  93. package/lib/modules/OpenData/types.d.ts +1 -1
  94. package/lib/modules/Order/types.d.ts +5 -5
  95. package/lib/modules/Order/utils/bundleDiscountHydration.d.ts +1 -1
  96. package/lib/modules/Order/utils/orderCollectionIdentity.d.ts +1 -1
  97. package/lib/modules/Order/utils.d.ts +1 -1
  98. package/lib/modules/Payment/types.d.ts +7 -7
  99. package/lib/modules/Product/index.d.ts +1 -1
  100. package/lib/modules/ResourcePlanner/types.d.ts +4 -4
  101. package/lib/modules/Rules/entitlementLines.d.ts +8 -0
  102. package/lib/modules/Rules/entitlementLines.js +128 -0
  103. package/lib/modules/Rules/index.d.ts +13 -3
  104. package/lib/modules/Rules/index.js +284 -61
  105. package/lib/modules/Rules/types.d.ts +4 -4
  106. package/lib/modules/ScanOrderLogger/types.d.ts +2 -2
  107. package/lib/modules/Schedule/getDateIsInSchedule.d.ts +1 -1
  108. package/lib/modules/Schedule/type.d.ts +7 -7
  109. package/lib/modules/Schedule/types.d.ts +9 -9
  110. package/lib/plugins/app.d.ts +1 -1
  111. package/lib/plugins/request.d.ts +2 -2
  112. package/lib/plugins/window.d.ts +2 -2
  113. package/lib/server/modules/floor-plan/types.d.ts +1 -1
  114. package/lib/server/modules/menu/types.d.ts +1 -1
  115. package/lib/server/modules/order/types.d.ts +16 -16
  116. package/lib/server/modules/products/types.d.ts +1 -1
  117. package/lib/server/modules/resource/types.d.ts +1 -1
  118. package/lib/server/modules/schedule/utils.d.ts +1 -1
  119. package/lib/server/types.d.ts +4 -4
  120. package/lib/server/utils/small-ticket.d.ts +1 -1
  121. package/lib/solution/BaseSales/index.d.ts +1 -1
  122. package/lib/solution/BaseSales/types.d.ts +7 -7
  123. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -1
  124. package/lib/solution/BookingByStep/index.d.ts +1 -1
  125. package/lib/solution/BookingByStep/utils/resources.d.ts +1 -1
  126. package/lib/solution/BookingTicket/index.d.ts +13 -1
  127. package/lib/solution/BookingTicket/types.d.ts +3 -3
  128. package/lib/solution/BookingTicket/utils/addProductDecision.d.ts +1 -1
  129. package/lib/solution/BookingTicket/utils/bookingStatus.d.ts +1 -1
  130. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.d.ts +1 -1
  131. package/lib/solution/Sales/types.d.ts +4 -4
  132. package/lib/solution/ScanOrder/types.d.ts +5 -5
  133. package/lib/solution/ShopDiscount/index.d.ts +3 -0
  134. package/lib/solution/ShopDiscount/index.js +55 -18
  135. package/lib/solution/ShopDiscount/types.d.ts +1 -1
  136. package/lib/solution/ShopDiscount/utils.d.ts +3 -2
  137. package/lib/solution/ShopDiscount/utils.js +39 -3
  138. package/lib/solution/UnifiedBookingSales/types.d.ts +1 -1
  139. package/lib/solution/VenueBooking/types.d.ts +1 -1
  140. package/lib/types/index.d.ts +1 -1
  141. package/lib/utils/task.d.ts +2 -2
  142. package/lib/utils/watch.d.ts +2 -2
  143. package/package.json +19 -18
@@ -11,6 +11,11 @@ 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 _evaluator = require("../../model/strategy/adapter/walletPass/evaluator");
15
+ var _entitlementPass = require("../Discount/entitlementPass");
16
+ var _availability = require("../Discount/availability");
17
+ var _entitlementState = require("../Discount/entitlementState");
18
+ var _entitlementLines = require("./entitlementLines");
14
19
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
15
20
  class RulesModule extends _BaseModule.BaseModule {
16
21
  defaultName = 'rules';
@@ -18,6 +23,11 @@ class RulesModule extends _BaseModule.BaseModule {
18
23
  store;
19
24
  window; // WindowPlugin 实例
20
25
  hooks;
26
+ entitlementPassEnabled = false;
27
+ defaultWalletPassEvaluator;
28
+ setEntitlementPassEnabled(enabled) {
29
+ this.entitlementPassEnabled = enabled;
30
+ }
21
31
  constructor(name, version) {
22
32
  super(name, version);
23
33
  this.hooks = {};
@@ -38,21 +48,48 @@ class RulesModule extends _BaseModule.BaseModule {
38
48
  getWalletPassEvaluator() {
39
49
  return this.window.getWalletPassEvaluator?.();
40
50
  }
51
+ normalizeHolderRecordIds(value) {
52
+ const records = Array.isArray(value) ? value : [value];
53
+ return Array.from(new Set(records.map(record => {
54
+ if (record && typeof record === 'object') {
55
+ return record.form_record_id;
56
+ }
57
+ return record;
58
+ }).filter(record => record !== undefined && record !== null && record !== '' && record !== 0 && record !== '0').map(String)));
59
+ }
41
60
 
42
- // 商品不需要holder,则不需要判断,直接返回true,商品需要holder但是还没填写,那么暂时不使用带有holder的券,直到填写才去匹配
43
- checkHolderMatch(discount, product, holders) {
61
+ /**
62
+ * 商品未开启 Holder 选择时跳过匹配;已开启时,特定 form Holder 卡
63
+ * 只适用于同一 Holder Record,是否必填不影响匹配规则。
64
+ */
65
+ checkHolderMatch(discount, product, holderSource, holders) {
44
66
  // 非表单类型 holder 视为匹配
45
67
  if (discount.holder?.holder_type !== 'form') return true;
46
- // 主预约holder, 目前(20251124)默认只考虑单个holder的情况
47
- // 旧版 holders[0].form_record_id 新版 holders[0]
48
- const orderHolderId = Array.isArray(holders) && holders.length > 0 ? holders[0].form_record_id || holders[0] : undefined;
49
- const productHolderId = Array.isArray(product.holder_id) ? product.holder_id[0] : product.holder_id;
50
- // 商品不需要选holder,则不对比holder,直接返回true,
51
- if (!product.isNeedHolder) return true;
52
- // 商品需要holder,但父预约及商品都无holder,返回false
53
- if (!orderHolderId && !productHolderId) return false;
54
- // 最终直接匹配 holder 是否相同
55
- return (productHolderId || orderHolderId) === discount.holder.holder_id;
68
+
69
+ // 旧券沿用预约主体和单个 Holder 的匹配口径。
70
+ if (!(0, _entitlementPass.isEntitlementPass)(discount)) {
71
+ if (!product.isNeedHolder) return true;
72
+ const holder = holders?.[0];
73
+ const orderHolderId = holder && typeof holder === 'object' ? holder.form_record_id : holder;
74
+ const productHolderId = Array.isArray(product.holder_id) ? product.holder_id[0] : product.holder_id;
75
+ if (!orderHolderId && !productHolderId) return false;
76
+ return (productHolderId || orderHolderId) === discount.holder.holder_id;
77
+ }
78
+
79
+ // getProduct 会裁剪商品字段,从原始商品读取配置,套餐子项沿用父商品。
80
+ const holderConfig = holderSource?.holder_config ?? holderSource?.metadata?.holder_config ?? holderSource?.origin?.holder_config ?? holderSource?.metadata?.origin?.holder_config ?? holderSource?._origin?.holder_config ?? holderSource?._origin?.product?.holder_config ?? holderSource?._originData?.product?.holder_config ?? holderSource?._productInit?.holder_config ?? holderSource?._productOrigin?.holder_config ?? holderSource?._extend?.holder_config ?? holderSource?._extend?.product?.holder_config ?? holderSource?._extend?.product?.metadata?.holder_config;
81
+ const holderStatus = holderConfig?.status;
82
+ const holderEnabled = holderStatus == null || holderStatus === '' || holderStatus === true || ['enable', '1'].includes(String(holderStatus).trim().toLowerCase());
83
+ if (!holderEnabled || !holderConfig?.resource_id) {
84
+ return true;
85
+ }
86
+ const discountHolderIds = this.normalizeHolderRecordIds(discount.holder.holder_id);
87
+ if (!discountHolderIds.length) return false;
88
+ const productHolderIds = this.normalizeHolderRecordIds(product.holder_id);
89
+ const candidateHolderIds = productHolderIds.length || Array.isArray(product.holder_id) ? productHolderIds : this.normalizeHolderRecordIds(holders);
90
+ if (!candidateHolderIds.length) return false;
91
+ const discountHolderIdSet = new Set(discountHolderIds);
92
+ return candidateHolderIds.every(holderId => discountHolderIdSet.has(holderId));
56
93
  }
57
94
 
58
95
  // 判断discountList 是否可以对当前productList生效
@@ -74,7 +111,7 @@ class RulesModule extends _BaseModule.BaseModule {
74
111
  }
75
112
 
76
113
  // 编辑商品已有商品券
77
- if (productList.every(item => {
114
+ if (!newDiscountList.some(_entitlementPass.isEntitlementPass) && productList.length > 0 && productList.every(item => {
78
115
  const product = this.hooks.getProduct(item);
79
116
  return product.booking_id && (product.discount_list?.length || product.total == 0);
80
117
  })) {
@@ -89,10 +126,10 @@ class RulesModule extends _BaseModule.BaseModule {
89
126
  // 合并新旧折扣列表,并计算折扣结果,注意,如果旧折扣里有 isEditMode 为 true 的优惠券,则不合并
90
127
  // 保留之前扫码得到的商品券(isScan: true)
91
128
  const filteredOldDiscountList = oldDiscountList.filter(discount => !discount.isEditMode && (discount.tag !== 'good_pass' || discount.isScan));
92
- const mergedDiscountList = (0, _utils.uniqueById)([...filteredOldDiscountList, ...newDiscountList]);
129
+ const mergedDiscountList = (0, _utils.uniqueById)([...newDiscountList.filter(_entitlementPass.isEntitlementPass), ...filteredOldDiscountList, ...newDiscountList]);
93
130
  const result = this.calcDiscount({
94
131
  discountList: mergedDiscountList,
95
- productList: [...productList],
132
+ productList: this.entitlementPassEnabled ? (0, _lodashEs.cloneDeep)(productList) : [...productList],
96
133
  orderTotalAmount,
97
134
  holders,
98
135
  isFormSubject
@@ -102,21 +139,23 @@ class RulesModule extends _BaseModule.BaseModule {
102
139
 
103
140
  // 检查是否有产品使用了新折扣
104
141
  let hasApplicableDiscount = false;
105
- const newDiscountIds = newDiscountList.map(discount => discount.id);
142
+ const newDiscountIds = new Set(newDiscountList.map(discount => String(discount.id)));
106
143
 
107
144
  // 遍历处理后的产品列表,检查是否有产品使用了新折扣
108
145
  result.productList.forEach(product => {
146
+ const mappedProduct = this.hooks.getProduct(product);
147
+ if ((0, _entitlementState.isPersistedEntitlementLine)(product, mappedProduct)) return;
109
148
  const {
110
149
  discount_list,
111
150
  bundle
112
- } = this.hooks.getProduct(product);
151
+ } = mappedProduct;
113
152
  const allDiscountList = [...(discount_list || [])];
114
153
  (bundle || []).forEach(item => {
115
154
  allDiscountList.push(...(item?.discount_list || []));
116
155
  });
117
156
  if (allDiscountList && allDiscountList.length > 0) {
118
157
  // 检查是否有使用新折扣的情况
119
- const usedNewDiscount = allDiscountList.some(discount => newDiscountIds.includes(discount?.discount?.resource_id));
158
+ const usedNewDiscount = allDiscountList.some(discount => newDiscountIds.has(String(discount?.discount?.resource_id)));
120
159
  if (usedNewDiscount) {
121
160
  hasApplicableDiscount = true;
122
161
  }
@@ -130,6 +169,7 @@ class RulesModule extends _BaseModule.BaseModule {
130
169
  }
131
170
  return {
132
171
  isAvailable: hasApplicableDiscount,
172
+ evaluatedDiscountList: result.discountList,
133
173
  discountList: hasApplicableDiscount ? result.discountList : oldDiscountList,
134
174
  productList: hasApplicableDiscount ? result.productList : productList,
135
175
  unavailableReason
@@ -154,7 +194,7 @@ class RulesModule extends _BaseModule.BaseModule {
154
194
  for (const discount of discountList) {
155
195
  for (const item of productList) {
156
196
  const product = this.hooks.getProduct(item);
157
- const bookingTime = (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss');
197
+ const bookingTime = (0, _dayjs.default)(product?.startDate || undefined).format('YYYY-MM-DD HH:mm:ss');
158
198
  const filteredList = (0, _utils.filterDiscountListByBookingTime)([discount], bookingTime);
159
199
  if (filteredList.length === 0) {
160
200
  return _types.UnavailableReason.TimeLimit;
@@ -165,6 +205,7 @@ class RulesModule extends _BaseModule.BaseModule {
165
205
  // 检查商品适用性
166
206
  for (const discount of discountList) {
167
207
  const limitedData = discount.limited_relation_product_data;
208
+ if (!limitedData) return _types.UnavailableReason.ProductNotApplicable;
168
209
  let hasApplicableProduct = false;
169
210
  for (const item of productList) {
170
211
  const product = this.hooks.getProduct(item);
@@ -190,6 +231,37 @@ class RulesModule extends _BaseModule.BaseModule {
190
231
  orderTotalAmount
191
232
  }, options) {
192
233
  const resolveIsFormSubject = product => typeof isFormSubject === 'function' ? Boolean(isFormSubject(product)) : Boolean(isFormSubject);
234
+ discountList = this.entitlementPassEnabled ? (0, _entitlementState.withCommittedEntitlementDetails)(discountList, productList, this.hooks.getProduct) : discountList.filter(item => !(0, _entitlementPass.isEntitlementPass)(item));
235
+ const usableEntitlements = discountList.filter(discount => (0, _entitlementPass.isEntitlementPass)(discount) && !discount.isEditMode && !discount.isManualSelect && !(0, _availability.getDiscountUsageBlockReason)(discount));
236
+ const canUseEntitlement = originProduct => {
237
+ const product = this.hooks.getProduct(originProduct);
238
+ if (product.vouchersApplicable === false) return false;
239
+ // 预判任一份可用就按份拆开,实际抵扣仍逐份核对 Holder。
240
+ const holderProducts = Array.isArray(product.holder_id) && product.holder_id.length > 1 ? product.holder_id.map(holder_id => ({
241
+ ...product,
242
+ holder_id
243
+ })) : [product];
244
+ const items = [{
245
+ type: 'main',
246
+ id: product.id,
247
+ price: product.original_price ?? product.price
248
+ }, ...(product.bundle || []).map(bundleItem => ({
249
+ type: 'bundle',
250
+ id: bundleItem._bundle_product_id ?? bundleItem.bundle_product_id ?? bundleItem.product_id,
251
+ price: bundleItem.original_price ?? bundleItem.price,
252
+ bundleItem
253
+ }))];
254
+ return usableEntitlements.some(discount => {
255
+ const scope = discount.limited_relation_product_data;
256
+ if (!scope || !holderProducts.some(item => this.checkHolderMatch(discount, item, originProduct, holders)) || !(0, _utils.filterDiscountListByBookingTime)([discount], (0, _dayjs.default)(product.startDate || undefined).format('YYYY-MM-DD HH:mm:ss')).length) return false;
257
+ return items.some(item => Number(item.price) > 0 && (scope.type === 'product_all' ? !(scope.filter === 1 && scope.exclude_product_ids?.some(id => String(id) === String(item.id))) : !!scope.product_ids?.includes(item.id)) && this.checkPackageSubItemUsageRules(discount, {
258
+ ...item,
259
+ originProduct,
260
+ parentProduct: product
261
+ }));
262
+ });
263
+ };
264
+ if (this.entitlementPassEnabled) productList = (0, _entitlementLines.prepareEntitlementLines)(productList, this.hooks, canUseEntitlement);
193
265
 
194
266
  // 识别出来是不是在编辑的界面里又新增了商品
195
267
  // 这种情况下,如果有可用的优惠券,也会自动勾选上
@@ -222,7 +294,12 @@ class RulesModule extends _BaseModule.BaseModule {
222
294
  if (discount.isEditMode) {
223
295
  editModeDiscount.push(discount);
224
296
  } else {
225
- addModeDiscount.push(discount);
297
+ addModeDiscount.push((0, _entitlementPass.isEntitlementPass)(discount) ? {
298
+ ...discount,
299
+ config: undefined,
300
+ reason: undefined,
301
+ reasonCode: undefined
302
+ } : discount);
226
303
  }
227
304
  });
228
305
 
@@ -289,8 +366,7 @@ class RulesModule extends _BaseModule.BaseModule {
289
366
  bundleIndex,
290
367
  // 虚拟商品属性
291
368
  price: Number(bundleItem.price || 0),
292
- id: bundleItem._bundle_product_id,
293
- // 🔥 使用 _bundle_product_id
369
+ id: bundleItem._bundle_product_id ?? bundleItem.bundle_product_id ?? bundleItem.product_id,
294
370
  _id: `${product._id}_bundle_${bundleIndex}`,
295
371
  parentId: product._id,
296
372
  num: bundleItem.num || 1,
@@ -384,6 +460,9 @@ class RulesModule extends _BaseModule.BaseModule {
384
460
  }
385
461
  return 0;
386
462
  }
463
+ if ((0, _entitlementPass.isEntitlementPass)(a) || (0, _entitlementPass.isEntitlementPass)(b)) {
464
+ return (0, _entitlementPass.isEntitlementPass)(a) && (0, _entitlementPass.isEntitlementPass)(b) ? (0, _entitlementPass.compareEntitlementPassPriority)(a, b) : (0, _entitlementPass.isEntitlementPass)(a) ? -1 : 1;
465
+ }
387
466
 
388
467
  // 1. 先按优先级排序
389
468
  const priorityA = getPriority(a);
@@ -540,6 +619,78 @@ class RulesModule extends _BaseModule.BaseModule {
540
619
  usedDiscountCardLimitCounts.set(discount.id, currentCount + (discount.metadata?.num || 1));
541
620
  }
542
621
  });
622
+ const entitlementUsageCounts = new Map();
623
+ const markDiscountUsed = discount => {
624
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
625
+ entitlementUsageCounts.set(discount.id, (entitlementUsageCounts.get(discount.id) || 0) + 1);
626
+ usedProductIdCounts.set(discount.product_id, (usedProductIdCounts.get(discount.product_id) || 0) + 1);
627
+ } else {
628
+ usedDiscounts.set(discount.id, true);
629
+ }
630
+ };
631
+
632
+ // 沿用商品券逐份拆分,同一多次卡可重复分配,数量受本单剩余额度限制。
633
+ const getFullCoveragePasses = (discounts, quantity) => {
634
+ const allocated = [];
635
+ const productCounts = new Map(usedProductIdCounts);
636
+ for (const discount of discounts) {
637
+ if (!(0, _entitlementPass.isFullCoveragePass)(discount) || allocated.length >= quantity) continue;
638
+ let count = (0, _entitlementPass.isEntitlementPass)(discount) ? (0, _entitlementPass.getRemainingEntitlementUses)(discount, entitlementUsageCounts.get(discount.id) || 0, quantity - allocated.length) : usedDiscounts.get(discount.id) ? 0 : 1;
639
+ const limit = Number(discount.config?.maxUsagePerOrder || 0);
640
+ if (limit > 0) {
641
+ count = Math.min(count, Math.max(0, Math.floor(limit) - (productCounts.get(discount.product_id) || 0)));
642
+ }
643
+ for (let i = 0; i < count; i++) allocated.push(discount);
644
+ productCounts.set(discount.product_id, (productCounts.get(discount.product_id) || 0) + count);
645
+ }
646
+ return allocated;
647
+ };
648
+ addModeDiscount.filter(_entitlementPass.isEntitlementPass).forEach(discount => {
649
+ discount.usageLimitFailKey = (0, _availability.getDiscountUsageLimitFailKey)(discount);
650
+ discount.reasonCode = (0, _availability.getDiscountUsageBlockReason)(discount);
651
+ const quota = (0, _entitlementPass.resolveEntitlementQuota)(discount);
652
+ if (discount.reasonCode || !quota) {
653
+ discount.config = {
654
+ isAvailable: false
655
+ };
656
+ return;
657
+ }
658
+ const entitlementEvaluator = evaluator || (this.defaultWalletPassEvaluator ?? (this.defaultWalletPassEvaluator = new _evaluator.WalletPassEvaluator()));
659
+ const result = entitlementEvaluator.checkVoucherAvailability({
660
+ // 兼容尚未传入订单总额的预约组件,保留多次卡原有的金额计算。
661
+ orderTotalAmount: orderTotalAmount || productList.reduce((sum, item) => {
662
+ const product = this.hooks.getProduct(item);
663
+ return sum.plus(new _decimal.default(product.origin_total || 0).times(product.quantity || product.num || 1));
664
+ }, new _decimal.default(0)).toNumber(),
665
+ // 策略按统一商品结构读主价与数量,套餐次数还需乘父商品份数。
666
+ products: sortedFlattenedList.map(item => ({
667
+ product_id: item.id,
668
+ price: item.price || 0,
669
+ main_product_selling_price: item.price || 0,
670
+ quantity: (item.quantity || item.num || 1) * (item.type === 'bundle' ? item.parentProduct?.quantity || item.parentProduct?.num || 1 : 1),
671
+ product_options: item.type === 'main' ? item.product?.options : undefined
672
+ })),
673
+ vouchers: [{
674
+ ...discount,
675
+ amount: Number(discount.par_value || 0),
676
+ balance: quota.type === 'unlimited' ? Number.MAX_SAFE_INTEGER : quota.balance,
677
+ type: 'entitlement_pass',
678
+ unified_available_status: discount.unified_available_status ?? 1,
679
+ available_product_type: discount.limited_relation_product_data?.type,
680
+ available_product_ids: discount.limited_relation_product_data?.product_ids
681
+ }]
682
+ });
683
+ discount.config = {
684
+ ...result.config,
685
+ isAvailable: result.isAvailable
686
+ };
687
+ if (!result.isAvailable) {
688
+ discount.reason = result.reason;
689
+ discount.reasonCode = result.reasonCode || 'not_meet_the_required_conditions';
690
+ }
691
+ });
692
+ const entitlementTimeReasons = new Map();
693
+ const entitlementHolderMismatch = new Set();
543
694
 
544
695
  // 记录每个优惠券适用的商品ID
545
696
  const discountApplicability = new Map();
@@ -582,7 +733,7 @@ class RulesModule extends _BaseModule.BaseModule {
582
733
  }
583
734
 
584
735
  // 编辑的商品使用了优惠券不可用
585
- 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));
736
+ 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));
586
737
  if (!isAvailableProduct) {
587
738
  return false;
588
739
  }
@@ -599,7 +750,7 @@ class RulesModule extends _BaseModule.BaseModule {
599
750
  const limitedData = discount.limited_relation_product_data;
600
751
 
601
752
  // 时间限制检查
602
- const timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss')).length;
753
+ const timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (0, _dayjs.default)(product?.startDate || undefined).format('YYYY-MM-DD HH:mm:ss')).length;
603
754
  if (!timeLimit) {
604
755
  return false;
605
756
  }
@@ -760,16 +911,24 @@ class RulesModule extends _BaseModule.BaseModule {
760
911
  }
761
912
  addModeDiscount.forEach(discount => {
762
913
  const limitedData = discount?.limited_relation_product_data;
914
+ if (!limitedData || (0, _entitlementPass.isEntitlementPass)(discount) && discount.config?.isAvailable === false) return;
763
915
  // 拿到discount配置的holder信息 product信息 product.holder 加在 isLimitedProduct
764
916
  const _tempVar = flatItem?.type === 'bundle' ? flatItem?.parentProduct : flatItem?.product;
765
917
  const isHolderMatch = this.checkHolderMatch(discount, {
766
918
  isNeedHolder: resolveIsFormSubject(_tempVar) && !_tempVar?.isNormalProduct,
767
919
  holder_id: _tempVar?.holder_id || product.holder_id
768
- }, holders);
920
+ }, flatItem.originProduct, holders);
769
921
  let timeLimit = true;
770
- timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (product?.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss')).length;
922
+ const useTime = (0, _dayjs.default)(product?.startDate || undefined).format('YYYY-MM-DD HH:mm:ss');
923
+ timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], useTime).length;
924
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
925
+ const reasons = entitlementTimeReasons.get(discount.id) || [];
926
+ reasons.push((0, _utils.getDiscountTimeUnavailableReason)(discount, useTime));
927
+ entitlementTimeReasons.set(discount.id, reasons);
928
+ if (!isHolderMatch) entitlementHolderMismatch.add(discount.id);
929
+ }
771
930
  // 是符合折扣的商品
772
- 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;
931
+ const isLimitedProduct = (limitedData.type === 'product_all' && isProductIncludedByLimitedData(limitedData, product.id) || limitedData.product_ids && limitedData.product_ids.includes(product.id)) && isHolderMatch;
773
932
 
774
933
  // 编辑的商品 使用了优惠券不可用
775
934
  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));
@@ -778,14 +937,14 @@ class RulesModule extends _BaseModule.BaseModule {
778
937
  const isBundleAvailable = this.checkPackageSubItemUsageRules(discount, flatItem);
779
938
 
780
939
  // 判断优惠券是否适用于该商品
781
- if (isAvailableProduct && isLimitedProduct && timeLimit && isBundleAvailable && discount.config?.isAvailable) {
940
+ if (isAvailableProduct && !(0, _entitlementState.isPersistedEntitlementLine)(originProduct, flatItem.type === 'main' ? flatItem.product : flatItem.parentProduct) && (!(0, _entitlementPass.isEntitlementPass)(discount) || (flatItem.type === 'main' ? flatItem.product : flatItem.parentProduct)?.vouchersApplicable !== false) && isLimitedProduct && timeLimit && isBundleAvailable && discount.config?.isAvailable) {
782
941
  // 记录此优惠券适用的商品
783
942
  discountApplicability.get(discount.id)?.push(product.id);
784
943
 
785
944
  // 记录可抵扣的商品详情
786
945
  const applicableProducts = discountApplicableProducts.get(discount.id) || [];
787
946
  const discountType = discount.tag || discount.type;
788
- const isGoodPass = discountType === 'good_pass';
947
+ const isGoodPass = (0, _entitlementPass.isFullCoveragePass)(discount);
789
948
 
790
949
  // 使用数量
791
950
  const num = isGoodPass || flatItem?.type === 'main' ? 1 : product.num;
@@ -801,7 +960,7 @@ class RulesModule extends _BaseModule.BaseModule {
801
960
  title: discount.format_title,
802
961
  original_amount: product.price || product.origin_total,
803
962
  pre_value: discount.par_value,
804
- product_id: originProduct.id || originProduct.product_id,
963
+ product_id: (0, _entitlementPass.isEntitlementPass)(discount) ? product.id : originProduct.id || originProduct.product_id,
805
964
  discount_product_id: discount.product_id
806
965
  },
807
966
  metadata: {
@@ -910,7 +1069,7 @@ class RulesModule extends _BaseModule.BaseModule {
910
1069
  });
911
1070
 
912
1071
  // 已持久化且已有钱包券/折扣卡的商品行跳过,保留后端历史折扣结果。
913
- if (isPersistedProduct && hasExistingWalletDiscount) {
1072
+ if ((0, _entitlementState.isPersistedEntitlementLine)(originProduct, flatItem.type === 'main' ? flatItem.product : flatItem.parentProduct) || isPersistedProduct && hasExistingWalletDiscount) {
914
1073
  if (flatItem.type === 'main') {
915
1074
  processedProductsMap.set(product._id, [originProduct]);
916
1075
  } else {
@@ -924,6 +1083,7 @@ class RulesModule extends _BaseModule.BaseModule {
924
1083
 
925
1084
  // 找到适用于此商品的所有优惠券,仅考虑isSelected不为false的优惠券
926
1085
  const applicableDiscounts = sortedDiscountList.filter(discount => {
1086
+ if ((0, _entitlementPass.isEntitlementPass)(discount) && (discount.config?.isAvailable !== true || (flatItem.type === 'main' ? flatItem.product : flatItem.parentProduct)?.vouchersApplicable === false || (0, _entitlementPass.getRemainingEntitlementUses)(discount, entitlementUsageCounts.get(discount.id) || 0, 1) <= 0)) return false;
927
1087
  // 🔥 检查策略可用性(针对 good_pass 和折扣卡)
928
1088
  const discountType = discount.tag || discount.type;
929
1089
  const currentOriginUid = getOriginProductUid(originProduct);
@@ -942,16 +1102,16 @@ class RulesModule extends _BaseModule.BaseModule {
942
1102
 
943
1103
  // 如果商品价格为 0,其实不需要使用任何优惠券,直接 return true
944
1104
  // 商品券时主商品价格为0不可用
945
- if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.length && (discount.tag || discount.type) === 'good_pass') return false;
1105
+ if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.length && (0, _entitlementPass.isFullCoveragePass)(discount)) return false;
946
1106
 
947
1107
  // 折扣卡商品价格为0时不可用
948
- 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;
1108
+ if ((Number(product.price) <= 0 || !product.price) && !product.discount_list?.find(n => n.discount?.resource_id === discount.id) && !(0, _entitlementPass.isFullCoveragePass)(discount)) return false;
949
1109
  // 如果优惠券已被使用,则跳过
950
1110
  const targetUsedDiscounts = usedDiscounts.get(discount.id);
951
1111
  if (targetUsedDiscounts && (discount.tag || discount.type) === 'good_pass') return false;
952
1112
 
953
1113
  // 🔥 检查 maxUsagePerOrder 限制:同一 product_id 的商品券使用数量不能超过配置值
954
- if ((discount.tag || discount.type) === 'good_pass') {
1114
+ if ((0, _entitlementPass.isFullCoveragePass)(discount)) {
955
1115
  const maxUsagePerOrder = discount.config?.maxUsagePerOrder;
956
1116
  if (maxUsagePerOrder && maxUsagePerOrder > 0) {
957
1117
  const currentUsedCount = usedProductIdCounts.get(discount.product_id) || 0;
@@ -973,17 +1133,18 @@ class RulesModule extends _BaseModule.BaseModule {
973
1133
  }
974
1134
  }
975
1135
  const limitedData = discount.limited_relation_product_data;
1136
+ if (!limitedData) return false;
976
1137
 
977
1138
  // 拿到discount配置的holder信息 product信息 product.holder 不可用return false
978
1139
  const _tempVar = flatItem?.type === 'bundle' ? flatItem?.parentProduct : flatItem?.product;
979
1140
  const isHolderMatch = this.checkHolderMatch(discount, {
980
1141
  isNeedHolder: resolveIsFormSubject(_tempVar) && !_tempVar?.isNormalProduct,
981
1142
  holder_id: _tempVar?.holder_id || product.holder_id
982
- }, holders);
1143
+ }, flatItem.originProduct, holders);
983
1144
  // 如果 holder 不匹配,则不适用
984
1145
  if (!isHolderMatch) return false;
985
1146
  let timeLimit = true;
986
- timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (product.startDate || (0, _dayjs.default)()).format('YYYY-MM-DD HH:mm:ss')).length;
1147
+ timeLimit = !!(0, _utils.filterDiscountListByBookingTime)([discount], (0, _dayjs.default)(product.startDate || undefined).format('YYYY-MM-DD HH:mm:ss')).length;
987
1148
  if (!timeLimit) {
988
1149
  return false;
989
1150
  }
@@ -1012,8 +1173,9 @@ class RulesModule extends _BaseModule.BaseModule {
1012
1173
  // 选择第一个适用的优惠券(已经按过期时间排序)
1013
1174
  // 如果有 isScan 且 isSelected 的折扣卡,则优先选中,否则再选择第一个
1014
1175
  // 用于处理:先手动扫描了一张折扣卡,扫描完以后带入客户信息,但是之前那张折扣卡不是最优惠的情况
1015
- const selectedDiscountCard = applicableDiscounts.find(n => n.isScan && n.isSelected && (n.tag || n.type) !== 'good_pass');
1016
- const selectedDiscount = selectedDiscountCard || applicableDiscounts[0];
1176
+ const selectedDiscountCard = applicableDiscounts.find(n => n.isScan && n.isSelected && !(0, _entitlementPass.isFullCoveragePass)(n));
1177
+ const explicitlySelected = options?.isSelected === true ? applicableDiscounts.find(item => (0, _entitlementPass.isEntitlementPass)(item) && String(item.id) === String(options.discountId)) : undefined;
1178
+ const selectedDiscount = explicitlySelected || selectedDiscountCard || applicableDiscounts[0];
1017
1179
 
1018
1180
  // 如果是手动折扣,则不适用优惠券
1019
1181
  let isManualDiscount = false;
@@ -1124,7 +1286,7 @@ class RulesModule extends _BaseModule.BaseModule {
1124
1286
 
1125
1287
  // 是否需要拆分(商品券需要拆分,折扣卡在 applicableProductLimit > 0 时也需要拆分)
1126
1288
  const discountType = selectedDiscount.tag || selectedDiscount.type;
1127
- const isGoodPass = discountType === 'good_pass';
1289
+ const isGoodPass = (0, _entitlementPass.isFullCoveragePass)(selectedDiscount);
1128
1290
  const isDiscountCard = ['discount_card', 'product_discount_card'].includes(discountType);
1129
1291
  const applicableProductLimit = selectedDiscount.config?.applicableProductLimit || 0;
1130
1292
 
@@ -1134,10 +1296,13 @@ class RulesModule extends _BaseModule.BaseModule {
1134
1296
  // 需要拆分出来的数量
1135
1297
  const totalQuantity = product.quantity || product.num || 1;
1136
1298
  const availableGoodPassCount = applicableDiscounts.filter(item => (item.tag || item.type) === 'good_pass').length;
1299
+ const allocatedPasses = isGoodPass && applicableDiscounts.some(_entitlementPass.isEntitlementPass) ? getFullCoveragePasses([selectedDiscount, ...applicableDiscounts.filter(item => item !== selectedDiscount)], totalQuantity) : undefined;
1137
1300
 
1138
1301
  // 🔥 计算最大使用次数限制
1139
1302
  let maxUsageLimit;
1140
- if (isGoodPass) {
1303
+ if (allocatedPasses) {
1304
+ maxUsageLimit = allocatedPasses.length;
1305
+ } else if (isGoodPass) {
1141
1306
  // 商品券:使用 maxUsagePerOrder 限制
1142
1307
  maxUsageLimit = availableGoodPassCount;
1143
1308
  if (selectedDiscount.config && selectedDiscount.config.maxUsagePerOrder) {
@@ -1173,9 +1338,10 @@ class RulesModule extends _BaseModule.BaseModule {
1173
1338
  for (let i = 0; i < splitCount; i++) {
1174
1339
  // 商品券:每个拆分商品使用不同的商品券(从 applicableDiscounts 按索引取)
1175
1340
  // 折扣卡:所有拆分商品使用同一张折扣卡(selectedDiscountCard 或 applicableDiscounts[0])
1176
- const currentSelectedDiscount = isGoodPass ? selectedDiscountCard || applicableDiscounts[i] : selectedDiscountCard || applicableDiscounts[0];
1341
+ const currentSelectedDiscount = allocatedPasses?.[i] || (isGoodPass ? selectedDiscountCard || applicableDiscounts[i] : selectedDiscountCard || applicableDiscounts[0]);
1342
+ const selectedDiscount = currentSelectedDiscount;
1177
1343
  // 标记优惠券为已使用
1178
- usedDiscounts.set(currentSelectedDiscount.id, true);
1344
+ markDiscountUsed(currentSelectedDiscount);
1179
1345
  // 🔥 更新 product_id 使用计数
1180
1346
  if ((currentSelectedDiscount.tag || currentSelectedDiscount.type) === 'good_pass') {
1181
1347
  const currentCount = usedProductIdCounts.get(currentSelectedDiscount.product_id) || 0;
@@ -1247,7 +1413,7 @@ class RulesModule extends _BaseModule.BaseModule {
1247
1413
  const mainProductDiscountAmount = amount;
1248
1414
  const lineDiscountAmount = new _decimal.default(mainProductDiscountAmount).plus(optionDiscountAmount).toNumber();
1249
1415
  const discountType = selectedDiscount.tag || selectedDiscount.type;
1250
- const currentIsGoodPass = discountType === 'good_pass';
1416
+ const currentIsGoodPass = (0, _entitlementPass.isFullCoveragePass)(selectedDiscount);
1251
1417
  const discountDetail = {
1252
1418
  amount: lineDiscountAmount,
1253
1419
  type: selectedDiscount.tag === 'product_discount_card' ? 'discount_card' : discountType,
@@ -1258,7 +1424,7 @@ class RulesModule extends _BaseModule.BaseModule {
1258
1424
  resource_id: selectedDiscount.id,
1259
1425
  title: selectedDiscount.format_title,
1260
1426
  original_amount: product.price,
1261
- product_id: originProduct.id || originProduct.product_id,
1427
+ product_id: (0, _entitlementPass.isEntitlementPass)(selectedDiscount) ? product.id : originProduct.id || originProduct.product_id,
1262
1428
  percent: reapplyDiscountPercent ?? selectedDiscount.par_value,
1263
1429
  discount_product_id: selectedDiscount.product_id
1264
1430
  },
@@ -1326,8 +1492,8 @@ class RulesModule extends _BaseModule.BaseModule {
1326
1492
  for (let i = 0; i < discountNum; i++) {
1327
1493
  // 商品券:每个拆分商品使用不同的商品券
1328
1494
  // 折扣卡:所有拆分商品使用同一张折扣卡
1329
- const currentBundleDiscount = isGoodPass ? selectedDiscountCard || applicableDiscounts[i] : selectedDiscountCard || applicableDiscounts[0];
1330
- usedDiscounts.set(currentBundleDiscount.id, true);
1495
+ const currentBundleDiscount = allocatedPasses?.[i] || (isGoodPass ? selectedDiscountCard || applicableDiscounts[i] : selectedDiscountCard || applicableDiscounts[0]);
1496
+ markDiscountUsed(currentBundleDiscount);
1331
1497
  // 🔥 更新 product_id 使用计数
1332
1498
  if ((currentBundleDiscount.tag || currentBundleDiscount.type) === 'good_pass') {
1333
1499
  const currentCount = usedProductIdCounts.get(currentBundleDiscount.product_id) || 0;
@@ -1349,7 +1515,7 @@ class RulesModule extends _BaseModule.BaseModule {
1349
1515
  debugger;
1350
1516
 
1351
1517
  // 确定折扣类型
1352
- const bundleDiscountType = currentBundleDiscount.tag === 'product_discount_card' ? 'discount_card' : currentBundleDiscount.tag;
1518
+ const bundleDiscountType = currentBundleDiscount.tag === 'product_discount_card' ? 'discount_card' : currentBundleDiscount.tag || currentBundleDiscount.type;
1353
1519
  const discountDetail = {
1354
1520
  amount: bundleDiscountAmount,
1355
1521
  type: bundleDiscountType,
@@ -1361,12 +1527,12 @@ class RulesModule extends _BaseModule.BaseModule {
1361
1527
  title: currentBundleDiscount.format_title,
1362
1528
  original_amount: product.origin_total,
1363
1529
  product_id: product.id || product.product_id,
1364
- discount_product_id: selectedDiscount.product_id
1530
+ discount_product_id: currentBundleDiscount.product_id
1365
1531
  },
1366
1532
  metadata: {
1367
1533
  // 🔥 使用拆分后的唯一 _id
1368
1534
  custom_product_bundle_map_id: uniqueId,
1369
- discount_rule_uncheck_flag: selectedDiscount?.discount_rule_uncheck_flag,
1535
+ discount_rule_uncheck_flag: currentBundleDiscount?.discount_rule_uncheck_flag,
1370
1536
  num: 1
1371
1537
  },
1372
1538
  _num: 1,
@@ -1399,6 +1565,9 @@ class RulesModule extends _BaseModule.BaseModule {
1399
1565
  // 重新查找适用于剩余商品的可用折扣(已使用的会被自动过滤掉)
1400
1566
  const nextApplicableDiscounts = sortedDiscountList.filter(nextDiscount => {
1401
1567
  const nextDiscountType = nextDiscount.tag || nextDiscount.type;
1568
+ if ((0, _entitlementPass.isEntitlementPass)(nextDiscount)) {
1569
+ if (nextDiscount.config?.isAvailable !== true || flatItem.parentProduct?.vouchersApplicable === false || (0, _entitlementPass.getRemainingEntitlementUses)(nextDiscount, entitlementUsageCounts.get(nextDiscount.id) || 0, 1) <= 0 || !this.checkHolderMatch(nextDiscount, flatItem.parentProduct, originProduct, holders) || !(0, _utils.filterDiscountListByBookingTime)([nextDiscount], (0, _dayjs.default)(product.startDate || undefined).format('YYYY-MM-DD HH:mm:ss')).length) return false;
1570
+ }
1402
1571
  // 检查策略可用性
1403
1572
  if (['good_pass', 'discount_card', 'product_discount_card'].includes(nextDiscountType)) {
1404
1573
  if (nextDiscount.config === undefined || !nextDiscount?.config?.isAvailable) {
@@ -1406,13 +1575,13 @@ class RulesModule extends _BaseModule.BaseModule {
1406
1575
  }
1407
1576
  }
1408
1577
  // 折扣卡时总价为0时不可用
1409
- if ((Number(product.total) <= 0 || !product.total) && !product.discount_list?.find(n => n.discount?.resource_id === nextDiscount.id) && nextDiscountType !== 'good_pass') return false;
1578
+ if ((Number(product.total) <= 0 || !product.total) && !product.discount_list?.find(n => n.discount?.resource_id === nextDiscount.id) && !(0, _entitlementPass.isFullCoveragePass)(nextDiscount)) return false;
1410
1579
 
1411
1580
  // 商品券已被使用则跳过
1412
1581
  if (usedDiscounts.get(nextDiscount.id) && nextDiscountType === 'good_pass') return false;
1413
1582
 
1414
1583
  // 检查 maxUsagePerOrder 限制
1415
- if (nextDiscountType === 'good_pass') {
1584
+ if ((0, _entitlementPass.isFullCoveragePass)(nextDiscount)) {
1416
1585
  const maxUsagePerOrder = nextDiscount.config?.maxUsagePerOrder;
1417
1586
  if (maxUsagePerOrder && maxUsagePerOrder > 0) {
1418
1587
  const currentUsedCount = usedProductIdCounts.get(nextDiscount.product_id) || 0;
@@ -1433,8 +1602,9 @@ class RulesModule extends _BaseModule.BaseModule {
1433
1602
  }
1434
1603
  }
1435
1604
  const limitedData = nextDiscount.limited_relation_product_data;
1605
+ if (!limitedData) return false;
1436
1606
  if (limitedData.type === 'product_all') {
1437
- if (limitedData.filter === 1 && limitedData.exclude_product_ids.includes(product.id)) {
1607
+ if (!isProductIncludedByLimitedData(limitedData, product.id)) {
1438
1608
  return false;
1439
1609
  }
1440
1610
  if (!this.checkPackageSubItemUsageRules(nextDiscount, flatItem)) {
@@ -1464,12 +1634,12 @@ class RulesModule extends _BaseModule.BaseModule {
1464
1634
  }
1465
1635
 
1466
1636
  // 选择最优折扣
1467
- const nextSelectedDiscountCard = nextApplicableDiscounts.find(n => n.isScan && n.isSelected && (n.tag || n.type) !== 'good_pass');
1637
+ const nextSelectedDiscountCard = nextApplicableDiscounts.find(n => n.isScan && n.isSelected && !(0, _entitlementPass.isFullCoveragePass)(n));
1468
1638
  const nextSelectedDiscount = nextSelectedDiscountCard || nextApplicableDiscounts[0];
1469
1639
 
1470
1640
  // 判断新折扣是否需要拆分
1471
1641
  const nextDiscType = nextSelectedDiscount.tag || nextSelectedDiscount.type;
1472
- const nextIsGoodPass = nextDiscType === 'good_pass';
1642
+ const nextIsGoodPass = (0, _entitlementPass.isFullCoveragePass)(nextSelectedDiscount);
1473
1643
  const nextIsDiscountCard = ['discount_card', 'product_discount_card'].includes(nextDiscType);
1474
1644
  const nextApplicableProductLimit = nextSelectedDiscount.config?.applicableProductLimit || 0;
1475
1645
  const nextIsNeedSplit = nextIsGoodPass || nextIsDiscountCard && nextApplicableProductLimit > 0;
@@ -1490,7 +1660,7 @@ class RulesModule extends _BaseModule.BaseModule {
1490
1660
  const nextTargetTotal = (0, _utils.getDiscountAmount)(nextBundleDiscount, nextOriginTotal, nextOriginTotal);
1491
1661
  const nextDiscountAmount = new _decimal.default(nextOriginTotal).minus(nextTargetTotal).toNumber();
1492
1662
  const nextUniqueId = `${flatItem._id}_split_${continueSplitIndex}`;
1493
- const nextBundleDiscType = nextBundleDiscount.tag === 'product_discount_card' ? 'discount_card' : nextBundleDiscount.tag;
1663
+ const nextBundleDiscType = nextBundleDiscount.tag === 'product_discount_card' ? 'discount_card' : nextBundleDiscount.tag || nextBundleDiscount.type;
1494
1664
  const nextDiscountDetail = {
1495
1665
  amount: nextDiscountAmount * remainingNum,
1496
1666
  type: nextBundleDiscType,
@@ -1529,8 +1699,11 @@ class RulesModule extends _BaseModule.BaseModule {
1529
1699
  }
1530
1700
 
1531
1701
  // 🔥 需要拆分:计算最大使用次数
1702
+ const nextAllocatedPasses = nextIsGoodPass && nextApplicableDiscounts.some(_entitlementPass.isEntitlementPass) ? getFullCoveragePasses([nextSelectedDiscount, ...nextApplicableDiscounts.filter(item => item !== nextSelectedDiscount)], remainingNum) : undefined;
1532
1703
  let nextMaxUsageLimit;
1533
- if (nextIsGoodPass) {
1704
+ if (nextAllocatedPasses) {
1705
+ nextMaxUsageLimit = nextAllocatedPasses.length;
1706
+ } else if (nextIsGoodPass) {
1534
1707
  const nextGoodPassCount = nextApplicableDiscounts.filter(item => (item.tag || item.type) === 'good_pass').length;
1535
1708
  nextMaxUsageLimit = nextGoodPassCount;
1536
1709
  if (nextSelectedDiscount.config?.maxUsagePerOrder) {
@@ -1559,8 +1732,8 @@ class RulesModule extends _BaseModule.BaseModule {
1559
1732
 
1560
1733
  // 生成有折扣的商品
1561
1734
  for (let j = 0; j < nextSplitCount; j++) {
1562
- const nextBundleDiscount = nextIsGoodPass ? nextSelectedDiscountCard || nextApplicableDiscounts[j] : nextSelectedDiscountCard || nextApplicableDiscounts[0];
1563
- usedDiscounts.set(nextBundleDiscount.id, true);
1735
+ const nextBundleDiscount = nextAllocatedPasses?.[j] || (nextIsGoodPass ? nextSelectedDiscountCard || nextApplicableDiscounts[j] : nextSelectedDiscountCard || nextApplicableDiscounts[0]);
1736
+ markDiscountUsed(nextBundleDiscount);
1564
1737
  if ((nextBundleDiscount.tag || nextBundleDiscount.type) === 'good_pass') {
1565
1738
  const curCount = usedProductIdCounts.get(nextBundleDiscount.product_id) || 0;
1566
1739
  usedProductIdCounts.set(nextBundleDiscount.product_id, curCount + 1);
@@ -1573,7 +1746,7 @@ class RulesModule extends _BaseModule.BaseModule {
1573
1746
  const nextUniqueId = `${flatItem._id}_split_${continueSplitIndex}`;
1574
1747
  const nextDiscountedPrice = (0, _utils.getDiscountAmount)(nextBundleDiscount, product.origin_total, product.origin_total);
1575
1748
  const nextBundleDiscAmount = new _decimal.default(product.origin_total || 0).minus(nextDiscountedPrice).toNumber();
1576
- const nextBundleDiscTypeStr = nextBundleDiscount.tag === 'product_discount_card' ? 'discount_card' : nextBundleDiscount.tag;
1749
+ const nextBundleDiscTypeStr = nextBundleDiscount.tag === 'product_discount_card' ? 'discount_card' : nextBundleDiscount.tag || nextBundleDiscount.type;
1577
1750
  const nextDiscountDetail = {
1578
1751
  amount: nextBundleDiscAmount,
1579
1752
  type: nextBundleDiscTypeStr,
@@ -1704,8 +1877,37 @@ class RulesModule extends _BaseModule.BaseModule {
1704
1877
  originalProductList.forEach(originProduct => {
1705
1878
  const product = this.hooks.getProduct(originProduct);
1706
1879
 
1880
+ // 已提交的扣次不参与运行时重组,避免价格或使用明细被二次改写。
1881
+ if ((0, _entitlementState.isPersistedEntitlementLine)(originProduct, product)) {
1882
+ result.push(originProduct);
1883
+ return;
1884
+ }
1885
+
1707
1886
  // 获取主商品处理结果
1708
1887
  const mainProductArr = processedProductsMap.get(product._id);
1888
+ if ((0, _entitlementLines.isEntitlementRuntimeLine)(originProduct, product) && !product.isManualDiscount && mainProductArr?.length) {
1889
+ const bundle = (product.bundle || []).flatMap((item, index) => {
1890
+ const processed = processedFlatItemsMap.get(`${product._id}_bundle_${index}`);
1891
+ if (!processed?.length) return [item];
1892
+ return processed.map(child => ({
1893
+ ...item,
1894
+ _id: child._id,
1895
+ price: child.price,
1896
+ bundle_selling_price: child.price,
1897
+ num: child.num,
1898
+ discount_list: (child.discount_list || []).map(detail => ({
1899
+ ...detail,
1900
+ metadata: {
1901
+ ...detail.metadata,
1902
+ num: child.num,
1903
+ custom_product_bundle_map_id: child._id
1904
+ }
1905
+ }))
1906
+ }));
1907
+ });
1908
+ result.push(...mainProductArr.map(item => (0, _entitlementLines.rebuildEntitlementLine)(item, bundle, this.hooks)));
1909
+ return;
1910
+ }
1709
1911
  if (!mainProductArr || mainProductArr.length === 0) {
1710
1912
  // 如果没有处理结果,返回默认商品(还原 option 价格)
1711
1913
  const getDefaultProduct = () => {
@@ -1768,7 +1970,7 @@ class RulesModule extends _BaseModule.BaseModule {
1768
1970
  // 处理过的bundle item
1769
1971
  bundleProcessingInfo.set(bundleIndex, processedBundleItems);
1770
1972
  // 🔥 检查是否应用了商品券(good_pass)
1771
- const hasGoodPass = processedBundleItems.some(item => item.discount_list?.some(discount => discount.type === 'good_pass' || discount.tag === 'good_pass'));
1973
+ const hasGoodPass = processedBundleItems.some(item => item.discount_list?.some(discount => (0, _entitlementPass.isFullCoveragePass)(discount)));
1772
1974
  if (hasGoodPass) {
1773
1975
  hasGoodPassApplied = true;
1774
1976
  }
@@ -2084,6 +2286,27 @@ class RulesModule extends _BaseModule.BaseModule {
2084
2286
 
2085
2287
  // 获取此优惠券实际应用的商品详情
2086
2288
  const appliedProductDetails = appliedDiscountProducts.get(discount.id) || [];
2289
+ if ((0, _entitlementPass.isEntitlementPass)(discount)) {
2290
+ const committed = discount.committedAppliedProductDetails || [];
2291
+ const details = [...committed, ...appliedProductDetails];
2292
+ const isAvailable = applicableProducts.length > 0;
2293
+ const timeReasons = entitlementTimeReasons.get(discount.id) || [];
2294
+ const timeReason = timeReasons.length ? timeReasons.every(Boolean) ? timeReasons[0] : undefined : (0, _utils.getDiscountTimeUnavailableReason)(discount, (0, _dayjs.default)().format('YYYY-MM-DD HH:mm:ss'));
2295
+ const reasonCode = isAvailable ? undefined : discount.reasonCode || timeReason || (entitlementHolderMismatch.has(discount.id) ? 'holder_not_matched' : 'product_not_applicable');
2296
+ return {
2297
+ ...discount,
2298
+ isAvailable: isAvailable || committed.length > 0,
2299
+ isSelected: details.length > 0,
2300
+ isUsed: details.length > 0,
2301
+ reasonCode,
2302
+ reason: reasonCode === discount.reasonCode ? discount.reason : undefined,
2303
+ isManualSelect: options?.scan ? false : discount.isManualSelect,
2304
+ applicableProductIds: applicableProducts,
2305
+ applicableProductDetails,
2306
+ appliedProductDetails: details,
2307
+ savedAmount: details.reduce((sum, detail) => new _decimal.default(sum).plus(new _decimal.default(detail.discount?.fixed_amount || 0).mul(detail._num || 1)).toNumber(), 0)
2308
+ };
2309
+ }
2087
2310
 
2088
2311
  // 根据是否有适用商品决定isAvailable
2089
2312
  const isAvailable = applicableProducts.length > 0;
@@ -2141,7 +2364,7 @@ class RulesModule extends _BaseModule.BaseModule {
2141
2364
  };
2142
2365
  });
2143
2366
  return {
2144
- productList: processedProductList,
2367
+ productList: this.entitlementPassEnabled ? (0, _entitlementLines.finalizeEntitlementLines)(processedProductList, this.hooks) : processedProductList,
2145
2368
  discountList: [...editModeDiscount, ...updatedDiscountList]
2146
2369
  };
2147
2370
  }