@pisell/pisellos 2.1.106 → 2.1.108

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.
@@ -41,7 +41,7 @@ export interface Voucher {
41
41
  allowCrossProduct: boolean;
42
42
  /** 可用商品数量上限 (仅多商品) 默认为0,表示不限制商品数量。 */
43
43
  applicableProductLimit: number;
44
- /** 单商品可用卡券上限(同一 Wallet Pass 商品生成的卡券对同一商品行最多抵扣次数)。默认为 0,表示不限制。 */
44
+ /** 单订单行每单位可用同一 Wallet Pass 券次数;该行总券次上限 = maxPassesPerItem × 该行 quantity(按行唯一键区分)。0 表示不限制。 */
45
45
  maxPassesPerItem: number;
46
46
  };
47
47
  }
@@ -50,6 +50,10 @@ export interface Voucher {
50
50
  */
51
51
  export interface Product {
52
52
  product_id: number;
53
+ /** 订单明细 id,参与行唯一键兜底解析 */
54
+ id?: number;
55
+ /** 行级唯一串,参与行唯一键解析 */
56
+ product_unique_string?: string;
53
57
  price: number;
54
58
  quantity: number;
55
59
  name?: string;
@@ -61,6 +65,9 @@ export interface Product {
61
65
  /** 主商品税费 */
62
66
  tax_fee: number;
63
67
  metadata: {
68
+ /** 行唯一标识,优先用于 Wallet Pass 按行配额 */
69
+ product_unique?: string;
70
+ unique_identification_number?: string;
64
71
  main_product_attached_bundle_tax_fee?: number;
65
72
  main_product_attached_bundle_surcharge_fee?: number;
66
73
  surcharge_rounding_remainder?: number;
@@ -129,7 +136,7 @@ export interface EvaluatorInput {
129
136
  allowCrossProduct: boolean;
130
137
  /** 可用商品数量上限 (仅多商品) 默认为0,表示不限制商品数量。 */
131
138
  applicableProductLimit: number;
132
- /** 单商品可用卡券上限(同一 Wallet Pass 商品生成的卡券对同一商品行最多抵扣次数)。默认为 0,表示不限制。 */
139
+ /** 单订单行每单位可用同一 Wallet Pass 券次数;行总上限 = maxPassesPerItem × 该行 quantity(按行唯一键计)。0 表示不限制。 */
133
140
  maxPassesPerItem: number;
134
141
  }>[];
135
142
  }
@@ -1,4 +1,10 @@
1
1
  import { Product, Voucher } from './type';
2
+ /** 订单商品数量 */
3
+ export declare const getProductQuantity: (product: any) => any;
4
+ /**
5
+ * 订单商品行唯一键,用于 maxPassesPerItem 按行、按件配额(同 SPU 不同规格视为不同行)。
6
+ */
7
+ export declare function resolveWalletPassLineKey(product: any, indexInOrder: number): string;
2
8
  export declare const getApplicableProductIds: (voucher: Voucher) => number[] | null;
3
9
  /**
4
10
  * 优惠券处理函数
@@ -24,26 +30,20 @@ export declare function recalculateVouchers(allVouchers: any[], selectedVouchers
24
30
  selectedWithDetails: any[];
25
31
  };
26
32
  /**
27
- * 获取主商品价格
33
+ * 获取主商品价格(单价,不含舍入余数)
28
34
  * @param product 商品
29
35
  * @param isDeductTaxAndFee 是否抵扣税费与附加费
30
- * @returns 商品价格
36
+ * @returns 商品单价
31
37
  */
32
38
  export declare const getMainProductPrice: (product: Product, isDeductTaxAndFee: boolean) => number;
33
39
  /**
34
- * 获取套餐子商品价格
40
+ * 获取套餐子商品价格(不含舍入余数)
35
41
  * @param bundleItem 套餐子商品
36
42
  * @param parentQuantity 父商品数量
37
43
  * @param isDeductTaxAndFee 是否抵扣税费与附加费
38
- * @returns 子商品总价格
44
+ * @returns 子商品总价格(不含舍入余数)
39
45
  */
40
46
  export declare const getBundleItemPrice: (bundleItem: any, parentQuantity: number, isDeductTaxAndFee: boolean) => number;
41
- /**
42
- * 获取商品数量
43
- * @param product 商品
44
- * @returns 商品数量
45
- */
46
- export declare const getProductQuantity: (product: any) => any;
47
47
  export declare const getBundleItemIsOriginalPrice: (item: any) => boolean;
48
48
  export declare const getBundleItemIsMarkupPrice: (item: any) => boolean;
49
49
  export declare const getBundleItemIsDiscountPrice: (item: any) => boolean;
@@ -12,6 +12,63 @@ function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && iter[Symb
12
12
  function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); }
13
13
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
14
14
  import Decimal from 'decimal.js';
15
+ /** 订单商品数量 */
16
+ export var getProductQuantity = function getProductQuantity(product) {
17
+ return product.quantity || product.product_quantity || 1;
18
+ };
19
+
20
+ /**
21
+ * 订单商品行唯一键,用于 maxPassesPerItem 按行、按件配额(同 SPU 不同规格视为不同行)。
22
+ */
23
+ export function resolveWalletPassLineKey(product, indexInOrder) {
24
+ var m = (product === null || product === void 0 ? void 0 : product.metadata) || {};
25
+ var u1 = m.product_unique;
26
+ if (u1 != null && String(u1).length > 0) return String(u1);
27
+ var u2 = m.unique_identification_number;
28
+ if (u2 != null && String(u2).length > 0) return String(u2);
29
+ if ((product === null || product === void 0 ? void 0 : product.id) != null && String(product.id).length > 0) return "order_item:".concat(product.id);
30
+ if ((product === null || product === void 0 ? void 0 : product.product_unique_string) != null && String(product.product_unique_string).length > 0) {
31
+ return String(product.product_unique_string);
32
+ }
33
+ return "order_line_".concat(indexInOrder);
34
+ }
35
+ function getExpandedOrderLineQuantity(p) {
36
+ if ((p === null || p === void 0 ? void 0 : p._orderLineQuantity) != null && p._orderLineQuantity > 0) return p._orderLineQuantity;
37
+ return getProductQuantity(p);
38
+ }
39
+
40
+ /** 该行允许消耗的「券次」上限:maxPassesPerItem × 订单行件数 */
41
+ function getMaxPassSlotsForExpandedLine(maxPassesPerItem, p) {
42
+ if (maxPassesPerItem <= 0) return Infinity;
43
+ return maxPassesPerItem * getExpandedOrderLineQuantity(p);
44
+ }
45
+
46
+ /**
47
+ * 本次抵扣占用多少「券次」配额:有抵扣时至少占 1,并与按金额折算的件数挂钩,不超过剩余额度。
48
+ */
49
+ function computePassSlotsIncrement(deductAmount, deductQty, maxPassesPerItem, orderLineQuantity, currentUsage) {
50
+ if (maxPassesPerItem <= 0 || !deductAmount.greaterThan(0)) return 0;
51
+ var cap = maxPassesPerItem * orderLineQuantity;
52
+ var room = Math.max(0, cap - currentUsage);
53
+ if (room <= 0) return 0;
54
+ var raw = Math.max(1, Math.ceil(Number(deductQty)) || 1);
55
+ return Math.min(raw, room);
56
+ }
57
+ function applyMaxPassUsageIncrements(usageMap, walletPassProductId, maxPassesPerItem, deductionDetails) {
58
+ deductionDetails.forEach(function (detail) {
59
+ var _usageMap$get;
60
+ var lineKey = detail.lineKey;
61
+ if (!lineKey || maxPassesPerItem <= 0) return;
62
+ var orderLineQty = detail.orderLineQuantity != null && detail.orderLineQuantity > 0 ? detail.orderLineQuantity : 1;
63
+ var cur = ((_usageMap$get = usageMap.get(walletPassProductId)) === null || _usageMap$get === void 0 ? void 0 : _usageMap$get.get(lineKey)) || 0;
64
+ var inc = computePassSlotsIncrement(new Decimal(detail.deductAmount), detail.deductQuantity, maxPassesPerItem, orderLineQty, cur);
65
+ if (inc <= 0) return;
66
+ if (!usageMap.has(walletPassProductId)) usageMap.set(walletPassProductId, new Map());
67
+ var inner = usageMap.get(walletPassProductId);
68
+ inner.set(lineKey, cur + inc);
69
+ });
70
+ }
71
+
15
72
  // 辅助函数:根据 deductTaxAndFee 配置获取推荐使用金额
16
73
  var getRecommendedAmount = function getRecommendedAmount(voucher) {
17
74
  var _config$deductTaxAndF;
@@ -118,27 +175,19 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
118
175
  var productsCopy = expandProductsWithBundleItems(products, true);
119
176
  var remainingOrderAmount = new Decimal(orderTotalAmount); // 订单剩余应付金额
120
177
 
121
- // ========== 辅助工具:maxPassesPerItem(单商品可用卡券上限)追踪 ==========
122
- // 获取指定 Wallet Pass 商品对指定订单商品行的已用卡券次数
123
- var getItemPassUsage = function getItemPassUsage(usageMap, walletPassProductId, orderItemProductId) {
124
- var _usageMap$get;
125
- return ((_usageMap$get = usageMap.get(walletPassProductId)) === null || _usageMap$get === void 0 ? void 0 : _usageMap$get.get(orderItemProductId)) || 0;
126
- };
127
-
128
- // 更新指定 Wallet Pass 商品对指定订单商品行的已用卡券次数 +1
129
- var incrementItemPassUsage = function incrementItemPassUsage(usageMap, walletPassProductId, orderItemProductId) {
130
- if (!usageMap.has(walletPassProductId)) {
131
- usageMap.set(walletPassProductId, new Map());
132
- }
133
- var innerMap = usageMap.get(walletPassProductId);
134
- innerMap.set(orderItemProductId, (innerMap.get(orderItemProductId) || 0) + 1);
178
+ // ========== 辅助工具:maxPassesPerItem(按订单行 + 件数)追踪 ==========
179
+ var getItemPassUsage = function getItemPassUsage(usageMap, walletPassProductId, lineKey) {
180
+ var _usageMap$get2;
181
+ return ((_usageMap$get2 = usageMap.get(walletPassProductId)) === null || _usageMap$get2 === void 0 ? void 0 : _usageMap$get2.get(lineKey)) || 0;
135
182
  };
136
183
 
137
- // 按 maxPassesPerItem 过滤商品:排除已达到单商品可用卡券上限的商品
184
+ // 按 maxPassesPerItem 过滤:排除已达到该行「券次」上限的展开行(按 _walletPassLineKey)
138
185
  var filterByMaxPassesPerItem = function filterByMaxPassesPerItem(products, usageMap, walletPassProductId, maxPassesPerItem) {
139
- if (maxPassesPerItem <= 0) return products; // 0 = 不限制
186
+ if (maxPassesPerItem <= 0) return products;
140
187
  return products.filter(function (p) {
141
- return getItemPassUsage(usageMap, walletPassProductId, p.product_id) < maxPassesPerItem;
188
+ var lineKey = p._walletPassLineKey;
189
+ if (!lineKey) return true;
190
+ return getItemPassUsage(usageMap, walletPassProductId, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
142
191
  });
143
192
  };
144
193
  // ================================================================
@@ -217,16 +266,16 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
217
266
  }, new Decimal(0));
218
267
  }
219
268
  } else {
220
- // 非跨商品券:只能抵扣单个商品(剩余金额最高的)
269
+ // 非跨商品券:选择单价最高的商品(优先抵扣高价商品)
221
270
  var maxProduct = applicableProducts.reduce(function (max, p) {
222
- return p[amountField].greaterThan(max[amountField]) ? p : max;
271
+ return p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max;
223
272
  });
224
- // 动态计算当前可抵扣数量
225
- var _currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
226
- // 非跨商品券也受 applicableProductLimit 限制
227
- var _deductQty = applicableProductLimit > 0 ? Math.min(_currentAvailableQty, applicableProductLimit) : _currentAvailableQty;
228
- // 实际可抵扣金额 = min(数量 * 单价, 剩余金额)
229
- finalApplicableAmount = Decimal.min(maxProduct[unitPriceField].times(_deductQty), maxProduct[amountField]);
273
+ // maxPassesPerItem 限制每张券最多抵扣的单位数
274
+ if (maxPassesPerItem > 0) {
275
+ finalApplicableAmount = Decimal.min(maxProduct[unitPriceField].times(maxPassesPerItem), maxProduct[amountField]);
276
+ } else {
277
+ finalApplicableAmount = maxProduct[amountField];
278
+ }
230
279
  }
231
280
 
232
281
  // 返回最小值
@@ -287,7 +336,9 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
287
336
  var availableAfterPassLimit = getApplicableProducts(voucher, productsData).filter(function (p) {
288
337
  return p[amountField].greaterThan(0);
289
338
  }).filter(function (p) {
290
- return getItemPassUsage(itemPassUsage, product_id, p.product_id) < maxPassesPerItem;
339
+ var lineKey = p._walletPassLineKey;
340
+ if (!lineKey) return true;
341
+ return getItemPassUsage(itemPassUsage, product_id, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
291
342
  });
292
343
  if (availableAfterPassLimit.length === 0) {
293
344
  return {
@@ -323,7 +374,7 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
323
374
  // 重置商品余额追踪(同时维护含税和不含税两个金额池)
324
375
  var productsForRecommendation = expandProductsWithBundleItems(products, true);
325
376
  remainingOrderAmount = new Decimal(orderTotalAmount);
326
- // 追踪推荐阶段每个 Wallet Pass 商品对每个订单商品行的已使用卡券次数
377
+ // 追踪推荐阶段每个 Wallet Pass 商品对每个订单商品行的已使用卡券次数(按行唯一键)
327
378
  var itemPassUsageMap = new Map();
328
379
 
329
380
  /**
@@ -426,15 +477,16 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
426
477
  }, new Decimal(0));
427
478
  }
428
479
  } else {
429
- // 非跨商品券:单个剩余金额最高的商品,也受 applicableProductLimit 限制
480
+ // 非跨商品券:选择单价最高的商品(优先抵扣高价商品)
430
481
  var maxProduct = applicableProducts.reduce(function (max, p) {
431
- return p[amountField].greaterThan(max[amountField]) ? p : max;
482
+ return p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max;
432
483
  });
433
- // 动态计算当前可抵扣数量
434
- var _currentAvailableQty2 = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
435
- var _deductQty2 = applicableProductLimit > 0 ? Math.min(_currentAvailableQty2, applicableProductLimit) : _currentAvailableQty2;
436
- // 实际可抵扣金额 = min(数量 * 单价, 剩余金额)
437
- calculatedAvailableMaxAmount = Decimal.min(maxProduct[unitPriceField].times(_deductQty2), maxProduct[amountField]);
484
+ // maxPassesPerItem 限制每张券最多抵扣的单位数
485
+ if (maxPassesPerItem > 0) {
486
+ calculatedAvailableMaxAmount = Decimal.min(maxProduct[unitPriceField].times(maxPassesPerItem), maxProduct[amountField]);
487
+ } else {
488
+ calculatedAvailableMaxAmount = maxProduct[amountField];
489
+ }
438
490
  }
439
491
 
440
492
  // 取最小值:min(recommended_usage_amount, maxDeductionAmount, 适用商品金额, 订单剩余金额)
@@ -459,8 +511,8 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
459
511
  if (deductionLeft.lessThanOrEqualTo(0) || _remainingLimit <= 0) break;
460
512
 
461
513
  // 动态计算当前可抵扣数量 = ceil(剩余金额 / 单价)
462
- var _currentAvailableQty3 = Math.ceil(_product[amountField].dividedBy(_product[unitPriceField]).toNumber());
463
- var availableQty = Math.min(_currentAvailableQty3, _remainingLimit);
514
+ var _currentAvailableQty = Math.ceil(_product[amountField].dividedBy(_product[unitPriceField]).toNumber());
515
+ var availableQty = Math.min(_currentAvailableQty, _remainingLimit);
464
516
 
465
517
  // 计算本商品最大可抵扣金额 = min(数量 * 单价, 剩余金额)
466
518
  var maxDeductForProduct = Decimal.min(_product[unitPriceField].times(availableQty), _product[amountField]);
@@ -477,6 +529,8 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
477
529
  product_id: _product.product_id,
478
530
  parent_product_id: _product.parent_product_id || null,
479
531
  is_bundle_item: _product.is_bundle_item || false,
532
+ lineKey: _product._walletPassLineKey,
533
+ orderLineQuantity: getExpandedOrderLineQuantity(_product),
480
534
  deductAmount: actualDeductAmount.toNumber(),
481
535
  // 转换为数字
482
536
  deductQuantity: actualDeductQty // 抵扣涉及的数量(用于记录)
@@ -488,17 +542,16 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
488
542
  _iterator3.f();
489
543
  }
490
544
  } else {
491
- // 非跨商品券:只抵扣一个商品(剩余金额最高的),也受 applicableProductLimit 限制
545
+ // 非跨商品券:选择单价最高的商品(优先抵扣高价商品)
492
546
  var targetProduct = applicableProducts.reduce(function (max, p) {
493
- return p[amountField].greaterThan(max[amountField]) ? p : max;
547
+ return p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max;
494
548
  });
495
549
 
496
- // 动态计算当前可抵扣数量
497
- var _currentAvailableQty4 = Math.ceil(targetProduct[amountField].dividedBy(targetProduct[unitPriceField]).toNumber());
498
- var _availableQty = applicableProductLimit > 0 ? Math.min(_currentAvailableQty4, applicableProductLimit) : _currentAvailableQty4;
499
-
500
- // 计算本商品最大可抵扣金额 = min(数量 * 单价, 剩余金额)
501
- var _maxDeductForProduct = Decimal.min(targetProduct[unitPriceField].times(_availableQty), targetProduct[amountField]);
550
+ // maxPassesPerItem 限制每张券最多抵扣的单位数
551
+ var _maxDeductForProduct = targetProduct[amountField];
552
+ if (maxPassesPerItem > 0) {
553
+ _maxDeductForProduct = Decimal.min(targetProduct[unitPriceField].times(maxPassesPerItem), targetProduct[amountField]);
554
+ }
502
555
  var _actualDeductAmount = Decimal.min(deductionLeft, _maxDeductForProduct);
503
556
 
504
557
  // 计算实际抵扣的数量
@@ -511,9 +564,10 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
511
564
  product_id: targetProduct.product_id,
512
565
  parent_product_id: targetProduct.parent_product_id || null,
513
566
  is_bundle_item: targetProduct.is_bundle_item || false,
567
+ lineKey: targetProduct._walletPassLineKey,
568
+ orderLineQuantity: getExpandedOrderLineQuantity(targetProduct),
514
569
  deductAmount: _actualDeductAmount.toNumber(),
515
- // 转换为数字
516
- deductQuantity: _actualDeductQty // 抵扣涉及的数量(用于记录)
570
+ deductQuantity: _actualDeductQty
517
571
  });
518
572
  }
519
573
  var totalDeducted = maxDeduction.minus(deductionLeft);
@@ -523,11 +577,7 @@ export function processVouchers(applicableVouchers, orderTotalAmount, products)
523
577
 
524
578
  // 更新券使用次数(按 product_id 统计)
525
579
  usedVoucherCounts.set(product_id, (usedVoucherCounts.get(product_id) || 0) + 1);
526
-
527
- // 更新 maxPassesPerItem 追踪:记录每个被抵扣的商品行
528
- deductionDetails.forEach(function (detail) {
529
- incrementItemPassUsage(itemPassUsageMap, product_id, detail.product_id);
530
- });
580
+ applyMaxPassUsageIncrements(itemPassUsageMap, product_id, maxPassesPerItem, deductionDetails);
531
581
 
532
582
  // 添加到推荐列表(包含基于当前剩余金额计算的 available_max_amount)
533
583
  recommendedVouchers.push(_objectSpread(_objectSpread({}, voucher), {}, {
@@ -610,24 +660,18 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
610
660
  var remainingOrderAmount = new Decimal(orderTotalAmount);
611
661
  var selectedWithDetails = [];
612
662
 
613
- // 追踪每个 Wallet Pass 商品对每个订单商品行的已使用卡券次数
614
- // Map<walletPassProductId, Map<orderItemProductId, usedCount>>
663
+ // 追踪每个 Wallet Pass 商品对每个订单商品行的已使用卡券次数(按行唯一键)
615
664
  var itemPassUsageMap = new Map();
616
- var getItemPassUsage = function getItemPassUsage(walletPassProductId, orderItemProductId) {
665
+ var getItemPassUsageRecalc = function getItemPassUsageRecalc(walletPassProductId, lineKey) {
617
666
  var _itemPassUsageMap$get;
618
- return ((_itemPassUsageMap$get = itemPassUsageMap.get(walletPassProductId)) === null || _itemPassUsageMap$get === void 0 ? void 0 : _itemPassUsageMap$get.get(orderItemProductId)) || 0;
667
+ return ((_itemPassUsageMap$get = itemPassUsageMap.get(walletPassProductId)) === null || _itemPassUsageMap$get === void 0 ? void 0 : _itemPassUsageMap$get.get(lineKey)) || 0;
619
668
  };
620
- var incrementItemPassUsage = function incrementItemPassUsage(walletPassProductId, orderItemProductId) {
621
- if (!itemPassUsageMap.has(walletPassProductId)) {
622
- itemPassUsageMap.set(walletPassProductId, new Map());
623
- }
624
- var innerMap = itemPassUsageMap.get(walletPassProductId);
625
- innerMap.set(orderItemProductId, (innerMap.get(orderItemProductId) || 0) + 1);
626
- };
627
- var filterByMaxPassesPerItem = function filterByMaxPassesPerItem(products, walletPassProductId, maxPassesPerItem) {
628
- if (maxPassesPerItem <= 0) return products; // 0 = 不限制
669
+ var filterByMaxPassesPerItemRecalc = function filterByMaxPassesPerItemRecalc(products, walletPassProductId, maxPassesPerItem) {
670
+ if (maxPassesPerItem <= 0) return products;
629
671
  return products.filter(function (p) {
630
- return getItemPassUsage(walletPassProductId, p.product_id) < maxPassesPerItem;
672
+ var lineKey = p._walletPassLineKey;
673
+ if (!lineKey) return true;
674
+ return getItemPassUsageRecalc(walletPassProductId, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
631
675
  });
632
676
  };
633
677
 
@@ -652,8 +696,8 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
652
696
  return p[amountField].greaterThan(0);
653
697
  });
654
698
 
655
- // 按 maxPassesPerItem 过滤:排除已达到单商品可用卡券上限的商品
656
- applicableProducts = filterByMaxPassesPerItem(applicableProducts, selectedVoucher.product_id, maxPassesPerItem);
699
+ // 按 maxPassesPerItem 过滤:排除已达到该行券次上限的展开行
700
+ applicableProducts = filterByMaxPassesPerItemRecalc(applicableProducts, selectedVoucher.product_id, maxPassesPerItem);
657
701
  if (applicableProducts.length === 0) {
658
702
  // 无适用商品,跳过
659
703
  selectedWithDetails.push(_objectSpread(_objectSpread({}, selectedVoucher), {}, {
@@ -704,6 +748,8 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
704
748
  product_id: product.product_id,
705
749
  parent_product_id: product.parent_product_id || null,
706
750
  is_bundle_item: product.is_bundle_item || false,
751
+ lineKey: product._walletPassLineKey,
752
+ orderLineQuantity: getExpandedOrderLineQuantity(product),
707
753
  deductAmount: actualDeductAmount.toNumber(),
708
754
  // 转换为数字
709
755
  deductQuantity: actualDeductQty // 抵扣涉及的数量(用于记录)
@@ -715,17 +761,16 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
715
761
  _iterator4.f();
716
762
  }
717
763
  } else {
718
- // 非跨商品券:只抵扣一个商品(剩余金额最高的),也受 applicableProductLimit 限制
764
+ // 非跨商品券:选择单价最高的商品(优先抵扣高价商品)
719
765
  var targetProduct = applicableProducts.reduce(function (max, p) {
720
- return p[amountField].greaterThan(max[amountField]) ? p : max;
766
+ return p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max;
721
767
  });
722
768
 
723
- // 动态计算当前可抵扣数量
724
- var _currentAvailableQty5 = Math.ceil(targetProduct[amountField].dividedBy(targetProduct[unitPriceField]).toNumber());
725
- var _availableQty2 = applicableProductLimit > 0 ? Math.min(_currentAvailableQty5, applicableProductLimit) : _currentAvailableQty5;
726
-
727
- // 计算本商品最大可抵扣金额 = min(数量 * 单价, 剩余金额)
728
- var _maxDeductForProduct2 = Decimal.min(targetProduct[unitPriceField].times(_availableQty2), targetProduct[amountField]);
769
+ // maxPassesPerItem 限制每张券最多抵扣的单位数
770
+ var _maxDeductForProduct2 = targetProduct[amountField];
771
+ if (maxPassesPerItem > 0) {
772
+ _maxDeductForProduct2 = Decimal.min(targetProduct[unitPriceField].times(maxPassesPerItem), targetProduct[amountField]);
773
+ }
729
774
  var _actualDeductAmount2 = Decimal.min(deductionLeft, _maxDeductForProduct2);
730
775
 
731
776
  // 计算实际抵扣的数量
@@ -738,20 +783,17 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
738
783
  product_id: targetProduct.product_id,
739
784
  parent_product_id: targetProduct.parent_product_id || null,
740
785
  is_bundle_item: targetProduct.is_bundle_item || false,
786
+ lineKey: targetProduct._walletPassLineKey,
787
+ orderLineQuantity: getExpandedOrderLineQuantity(targetProduct),
741
788
  deductAmount: _actualDeductAmount2.toNumber(),
742
- // 转换为数字
743
- deductQuantity: _actualDeductQty2 // 抵扣涉及的数量(用于记录)
789
+ deductQuantity: _actualDeductQty2
744
790
  });
745
791
  }
746
792
  var totalDeducted = maxDeduction.minus(deductionLeft);
747
793
 
748
794
  // 更新订单剩余金额
749
795
  remainingOrderAmount = remainingOrderAmount.minus(totalDeducted);
750
-
751
- // 更新 maxPassesPerItem 追踪:记录每个被抵扣的商品行
752
- deductionDetails.forEach(function (detail) {
753
- incrementItemPassUsage(selectedVoucher.product_id, detail.product_id);
754
- });
796
+ applyMaxPassUsageIncrements(itemPassUsageMap, selectedVoucher.product_id, maxPassesPerItem, deductionDetails);
755
797
  selectedWithDetails.push(_objectSpread(_objectSpread({}, selectedVoucher), {}, {
756
798
  actualDeduction: totalDeducted.toNumber(),
757
799
  // 转换为数字
@@ -824,7 +866,7 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
824
866
  });
825
867
 
826
868
  // 按 maxPassesPerItem 过滤:排除已达到单商品可用卡券上限的商品
827
- applicableProducts = filterByMaxPassesPerItem(applicableProducts, product_id, maxPassesPerItem);
869
+ applicableProducts = filterByMaxPassesPerItemRecalc(applicableProducts, product_id, maxPassesPerItem);
828
870
  if (applicableProducts.length === 0) {
829
871
  isAvailable = false;
830
872
  reasonCode = 'not_meet_the_required_conditions';
@@ -864,15 +906,16 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
864
906
  }, new Decimal(0));
865
907
  }
866
908
  } else {
867
- // 非跨商品券:单个剩余金额最高的商品,也受 applicableProductLimit 限制
909
+ // 非跨商品券:选择单价最高的商品(优先抵扣高价商品)
868
910
  var maxProduct = applicableProducts.reduce(function (max, p) {
869
- return p[amountField].greaterThan(max[amountField]) ? p : max;
911
+ return p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max;
870
912
  });
871
- // 动态计算当前可抵扣数量
872
- var _currentAvailableQty6 = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
873
- var _deductQty3 = applicableProductLimit > 0 ? Math.min(_currentAvailableQty6, applicableProductLimit) : _currentAvailableQty6;
874
- // 实际可抵扣金额 = min(数量 * 单价, 剩余金额)
875
- calculatedMaxAmount = Decimal.min(maxProduct[unitPriceField].times(_deductQty3), maxProduct[amountField]);
913
+ // maxPassesPerItem 限制每张券最多抵扣的单位数
914
+ if (maxPassesPerItem > 0) {
915
+ calculatedMaxAmount = Decimal.min(maxProduct[unitPriceField].times(maxPassesPerItem), maxProduct[amountField]);
916
+ } else {
917
+ calculatedMaxAmount = maxProduct[amountField];
918
+ }
876
919
  }
877
920
  calculatedMaxAmount = Decimal.min(baseAmount, calculatedMaxAmount, remainingOrderAmount);
878
921
  if (calculatedMaxAmount.lessThanOrEqualTo(0)) {
@@ -897,10 +940,10 @@ export function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmo
897
940
  }
898
941
 
899
942
  /**
900
- * 获取主商品价格
943
+ * 获取主商品价格(单价,不含舍入余数)
901
944
  * @param product 商品
902
945
  * @param isDeductTaxAndFee 是否抵扣税费与附加费
903
- * @returns 商品价格
946
+ * @returns 商品单价
904
947
  */
905
948
  export var getMainProductPrice = function getMainProductPrice(product, isDeductTaxAndFee) {
906
949
  var _product$metadata, _product$metadata2, _product$metadata3, _product$metadata4, _product$metadata5;
@@ -943,11 +986,11 @@ export var getMainProductPrice = function getMainProductPrice(product, isDeductT
943
986
  };
944
987
 
945
988
  /**
946
- * 获取套餐子商品价格
989
+ * 获取套餐子商品价格(不含舍入余数)
947
990
  * @param bundleItem 套餐子商品
948
991
  * @param parentQuantity 父商品数量
949
992
  * @param isDeductTaxAndFee 是否抵扣税费与附加费
950
- * @returns 子商品总价格
993
+ * @returns 子商品总价格(不含舍入余数)
951
994
  */
952
995
  export var getBundleItemPrice = function getBundleItemPrice(bundleItem, parentQuantity, isDeductTaxAndFee) {
953
996
  var _bundleItem$bundle_se2;
@@ -978,8 +1021,9 @@ export var getBundleItemPrice = function getBundleItemPrice(bundleItem, parentQu
978
1021
  */
979
1022
  var expandProductsWithBundleItems = function expandProductsWithBundleItems(products, deductTaxAndFee) {
980
1023
  var expandedProducts = [];
981
- products.forEach(function (product) {
1024
+ products.forEach(function (product, indexInOrder) {
982
1025
  var productQuantity = getProductQuantity(product);
1026
+ var parentLineKey = resolveWalletPassLineKey(product, indexInOrder);
983
1027
 
984
1028
  // 计算主商品单价(含税和不含税)
985
1029
  var unitPriceWithTax = getMainProductPrice(product, true);
@@ -989,6 +1033,8 @@ var expandProductsWithBundleItems = function expandProductsWithBundleItems(produ
989
1033
  expandedProducts.push(_objectSpread(_objectSpread({}, product), {}, {
990
1034
  is_bundle_item: false,
991
1035
  parent_product_id: null,
1036
+ _walletPassLineKey: parentLineKey,
1037
+ _orderLineQuantity: productQuantity,
992
1038
  // 单价(用于按 quantity 计算抵扣)
993
1039
  unitPriceWithTax: new Decimal(unitPriceWithTax),
994
1040
  unitPricePure: new Decimal(unitPricePure),
@@ -1005,6 +1051,7 @@ var expandProductsWithBundleItems = function expandProductsWithBundleItems(produ
1005
1051
  product.product_bundle.forEach(function (bundleItem) {
1006
1052
  if (getBundleItemIsOriginalPrice(bundleItem)) {
1007
1053
  var bundleQuantity = bundleItem.num * productQuantity;
1054
+ var bundleLineKey = "".concat(parentLineKey, "#bundle:").concat(bundleItem.bundle_id, ":").concat(bundleItem.bundle_product_id);
1008
1055
  // 计算子商品单价(注意:getBundleItemPrice 返回的是总价,需要除以数量得到单价)
1009
1056
  var bundleUnitPriceWithTax = new Decimal(getBundleItemPrice(bundleItem, 1, true)).dividedBy(bundleItem.num);
1010
1057
  var bundleUnitPricePure = new Decimal(getBundleItemPrice(bundleItem, 1, false)).dividedBy(bundleItem.num);
@@ -1017,6 +1064,8 @@ var expandProductsWithBundleItems = function expandProductsWithBundleItems(produ
1017
1064
  parent_product_id: product.product_id,
1018
1065
  quantity: bundleQuantity,
1019
1066
  // 子商品数量 * 主商品数量
1067
+ _walletPassLineKey: bundleLineKey,
1068
+ _orderLineQuantity: bundleQuantity,
1020
1069
  // 单价(用于按 quantity 计算抵扣)
1021
1070
  unitPriceWithTax: bundleUnitPriceWithTax,
1022
1071
  unitPricePure: bundleUnitPricePure,
@@ -1034,15 +1083,6 @@ var expandProductsWithBundleItems = function expandProductsWithBundleItems(produ
1034
1083
  return expandedProducts;
1035
1084
  };
1036
1085
 
1037
- /**
1038
- * 获取商品数量
1039
- * @param product 商品
1040
- * @returns 商品数量
1041
- */
1042
- export var getProductQuantity = function getProductQuantity(product) {
1043
- return product.quantity || product.product_quantity || 1;
1044
- };
1045
-
1046
1086
  // bundle商品是否是原价
1047
1087
  export var getBundleItemIsOriginalPrice = function getBundleItemIsOriginalPrice(item) {
1048
1088
  return (item === null || item === void 0 ? void 0 : item.price_type) === 'markup' && (item === null || item === void 0 ? void 0 : item.price_type_ext) === 'product_price';
@@ -49,5 +49,5 @@ export declare class Product extends BaseModule implements Module {
49
49
  getCategories(): ProductCategory[];
50
50
  setOtherParams(key: string, value: any): void;
51
51
  getOtherParams(): any;
52
- getProductType(): "normal" | "duration" | "session";
52
+ getProductType(): "duration" | "session" | "normal";
53
53
  }
@@ -358,7 +358,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
358
358
  };
359
359
  setOtherData(key: string, value: any): void;
360
360
  getOtherData(key: string): any;
361
- getProductTypeById(id: number): Promise<"normal" | "duration" | "session">;
361
+ getProductTypeById(id: number): Promise<"duration" | "session" | "normal">;
362
362
  /**
363
363
  * 提供给 UI 的方法,减轻 UI 层的计算压力,UI 层只需要传递 cartItemId 和 resourceCode 即返回对应的 renderList
364
364
  *
@@ -41,7 +41,7 @@ export interface Voucher {
41
41
  allowCrossProduct: boolean;
42
42
  /** 可用商品数量上限 (仅多商品) 默认为0,表示不限制商品数量。 */
43
43
  applicableProductLimit: number;
44
- /** 单商品可用卡券上限(同一 Wallet Pass 商品生成的卡券对同一商品行最多抵扣次数)。默认为 0,表示不限制。 */
44
+ /** 单订单行每单位可用同一 Wallet Pass 券次数;该行总券次上限 = maxPassesPerItem × 该行 quantity(按行唯一键区分)。0 表示不限制。 */
45
45
  maxPassesPerItem: number;
46
46
  };
47
47
  }
@@ -50,6 +50,10 @@ export interface Voucher {
50
50
  */
51
51
  export interface Product {
52
52
  product_id: number;
53
+ /** 订单明细 id,参与行唯一键兜底解析 */
54
+ id?: number;
55
+ /** 行级唯一串,参与行唯一键解析 */
56
+ product_unique_string?: string;
53
57
  price: number;
54
58
  quantity: number;
55
59
  name?: string;
@@ -61,6 +65,9 @@ export interface Product {
61
65
  /** 主商品税费 */
62
66
  tax_fee: number;
63
67
  metadata: {
68
+ /** 行唯一标识,优先用于 Wallet Pass 按行配额 */
69
+ product_unique?: string;
70
+ unique_identification_number?: string;
64
71
  main_product_attached_bundle_tax_fee?: number;
65
72
  main_product_attached_bundle_surcharge_fee?: number;
66
73
  surcharge_rounding_remainder?: number;
@@ -129,7 +136,7 @@ export interface EvaluatorInput {
129
136
  allowCrossProduct: boolean;
130
137
  /** 可用商品数量上限 (仅多商品) 默认为0,表示不限制商品数量。 */
131
138
  applicableProductLimit: number;
132
- /** 单商品可用卡券上限(同一 Wallet Pass 商品生成的卡券对同一商品行最多抵扣次数)。默认为 0,表示不限制。 */
139
+ /** 单订单行每单位可用同一 Wallet Pass 券次数;行总上限 = maxPassesPerItem × 该行 quantity(按行唯一键计)。0 表示不限制。 */
133
140
  maxPassesPerItem: number;
134
141
  }>[];
135
142
  }
@@ -1,4 +1,10 @@
1
1
  import { Product, Voucher } from './type';
2
+ /** 订单商品数量 */
3
+ export declare const getProductQuantity: (product: any) => any;
4
+ /**
5
+ * 订单商品行唯一键,用于 maxPassesPerItem 按行、按件配额(同 SPU 不同规格视为不同行)。
6
+ */
7
+ export declare function resolveWalletPassLineKey(product: any, indexInOrder: number): string;
2
8
  export declare const getApplicableProductIds: (voucher: Voucher) => number[] | null;
3
9
  /**
4
10
  * 优惠券处理函数
@@ -24,26 +30,20 @@ export declare function recalculateVouchers(allVouchers: any[], selectedVouchers
24
30
  selectedWithDetails: any[];
25
31
  };
26
32
  /**
27
- * 获取主商品价格
33
+ * 获取主商品价格(单价,不含舍入余数)
28
34
  * @param product 商品
29
35
  * @param isDeductTaxAndFee 是否抵扣税费与附加费
30
- * @returns 商品价格
36
+ * @returns 商品单价
31
37
  */
32
38
  export declare const getMainProductPrice: (product: Product, isDeductTaxAndFee: boolean) => number;
33
39
  /**
34
- * 获取套餐子商品价格
40
+ * 获取套餐子商品价格(不含舍入余数)
35
41
  * @param bundleItem 套餐子商品
36
42
  * @param parentQuantity 父商品数量
37
43
  * @param isDeductTaxAndFee 是否抵扣税费与附加费
38
- * @returns 子商品总价格
44
+ * @returns 子商品总价格(不含舍入余数)
39
45
  */
40
46
  export declare const getBundleItemPrice: (bundleItem: any, parentQuantity: number, isDeductTaxAndFee: boolean) => number;
41
- /**
42
- * 获取商品数量
43
- * @param product 商品
44
- * @returns 商品数量
45
- */
46
- export declare const getProductQuantity: (product: any) => any;
47
47
  export declare const getBundleItemIsOriginalPrice: (item: any) => boolean;
48
48
  export declare const getBundleItemIsMarkupPrice: (item: any) => boolean;
49
49
  export declare const getBundleItemIsDiscountPrice: (item: any) => boolean;
@@ -38,10 +38,72 @@ __export(utils_exports, {
38
38
  getMainProductPrice: () => getMainProductPrice,
39
39
  getProductQuantity: () => getProductQuantity,
40
40
  processVouchers: () => processVouchers,
41
- recalculateVouchers: () => recalculateVouchers
41
+ recalculateVouchers: () => recalculateVouchers,
42
+ resolveWalletPassLineKey: () => resolveWalletPassLineKey
42
43
  });
43
44
  module.exports = __toCommonJS(utils_exports);
44
45
  var import_decimal = __toESM(require("decimal.js"));
46
+ var getProductQuantity = (product) => {
47
+ return product.quantity || product.product_quantity || 1;
48
+ };
49
+ function resolveWalletPassLineKey(product, indexInOrder) {
50
+ const m = (product == null ? void 0 : product.metadata) || {};
51
+ const u1 = m.product_unique;
52
+ if (u1 != null && String(u1).length > 0)
53
+ return String(u1);
54
+ const u2 = m.unique_identification_number;
55
+ if (u2 != null && String(u2).length > 0)
56
+ return String(u2);
57
+ if ((product == null ? void 0 : product.id) != null && String(product.id).length > 0)
58
+ return `order_item:${product.id}`;
59
+ if ((product == null ? void 0 : product.product_unique_string) != null && String(product.product_unique_string).length > 0) {
60
+ return String(product.product_unique_string);
61
+ }
62
+ return `order_line_${indexInOrder}`;
63
+ }
64
+ function getExpandedOrderLineQuantity(p) {
65
+ if ((p == null ? void 0 : p._orderLineQuantity) != null && p._orderLineQuantity > 0)
66
+ return p._orderLineQuantity;
67
+ return getProductQuantity(p);
68
+ }
69
+ function getMaxPassSlotsForExpandedLine(maxPassesPerItem, p) {
70
+ if (maxPassesPerItem <= 0)
71
+ return Infinity;
72
+ return maxPassesPerItem * getExpandedOrderLineQuantity(p);
73
+ }
74
+ function computePassSlotsIncrement(deductAmount, deductQty, maxPassesPerItem, orderLineQuantity, currentUsage) {
75
+ if (maxPassesPerItem <= 0 || !deductAmount.greaterThan(0))
76
+ return 0;
77
+ const cap = maxPassesPerItem * orderLineQuantity;
78
+ const room = Math.max(0, cap - currentUsage);
79
+ if (room <= 0)
80
+ return 0;
81
+ const raw = Math.max(1, Math.ceil(Number(deductQty)) || 1);
82
+ return Math.min(raw, room);
83
+ }
84
+ function applyMaxPassUsageIncrements(usageMap, walletPassProductId, maxPassesPerItem, deductionDetails) {
85
+ deductionDetails.forEach((detail) => {
86
+ var _a;
87
+ const lineKey = detail.lineKey;
88
+ if (!lineKey || maxPassesPerItem <= 0)
89
+ return;
90
+ const orderLineQty = detail.orderLineQuantity != null && detail.orderLineQuantity > 0 ? detail.orderLineQuantity : 1;
91
+ const cur = ((_a = usageMap.get(walletPassProductId)) == null ? void 0 : _a.get(lineKey)) || 0;
92
+ const inc = computePassSlotsIncrement(
93
+ new import_decimal.default(detail.deductAmount),
94
+ detail.deductQuantity,
95
+ maxPassesPerItem,
96
+ orderLineQty,
97
+ cur
98
+ );
99
+ if (inc <= 0)
100
+ return;
101
+ if (!usageMap.has(walletPassProductId))
102
+ usageMap.set(walletPassProductId, /* @__PURE__ */ new Map());
103
+ const inner = usageMap.get(walletPassProductId);
104
+ inner.set(lineKey, cur + inc);
105
+ });
106
+ }
45
107
  var getRecommendedAmount = (voucher) => {
46
108
  console.log("voucher312", voucher);
47
109
  const { config, recommended_usage_amount, recommended_pure_product_usage_amount } = voucher;
@@ -86,23 +148,19 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
86
148
  console.log(products, "products123");
87
149
  const productsCopy = expandProductsWithBundleItems(products, true);
88
150
  let remainingOrderAmount = new import_decimal.default(orderTotalAmount);
89
- const getItemPassUsage = (usageMap, walletPassProductId, orderItemProductId) => {
151
+ const getItemPassUsage = (usageMap, walletPassProductId, lineKey) => {
90
152
  var _a;
91
- return ((_a = usageMap.get(walletPassProductId)) == null ? void 0 : _a.get(orderItemProductId)) || 0;
92
- };
93
- const incrementItemPassUsage = (usageMap, walletPassProductId, orderItemProductId) => {
94
- if (!usageMap.has(walletPassProductId)) {
95
- usageMap.set(walletPassProductId, /* @__PURE__ */ new Map());
96
- }
97
- const innerMap = usageMap.get(walletPassProductId);
98
- innerMap.set(orderItemProductId, (innerMap.get(orderItemProductId) || 0) + 1);
153
+ return ((_a = usageMap.get(walletPassProductId)) == null ? void 0 : _a.get(lineKey)) || 0;
99
154
  };
100
155
  const filterByMaxPassesPerItem = (products2, usageMap, walletPassProductId, maxPassesPerItem) => {
101
156
  if (maxPassesPerItem <= 0)
102
157
  return products2;
103
- return products2.filter(
104
- (p) => getItemPassUsage(usageMap, walletPassProductId, p.product_id) < maxPassesPerItem
105
- );
158
+ return products2.filter((p) => {
159
+ const lineKey = p._walletPassLineKey;
160
+ if (!lineKey)
161
+ return true;
162
+ return getItemPassUsage(usageMap, walletPassProductId, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
163
+ });
106
164
  };
107
165
  const calculateAvailableMaxAmount = (voucher, productsData, itemPassUsage) => {
108
166
  const { config } = voucher;
@@ -146,14 +204,16 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
146
204
  }
147
205
  } else {
148
206
  const maxProduct = applicableProducts.reduce(
149
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
150
- );
151
- const currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
152
- const deductQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
153
- finalApplicableAmount = import_decimal.default.min(
154
- maxProduct[unitPriceField].times(deductQty),
155
- maxProduct[amountField]
207
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
156
208
  );
209
+ if (maxPassesPerItem > 0) {
210
+ finalApplicableAmount = import_decimal.default.min(
211
+ maxProduct[unitPriceField].times(maxPassesPerItem),
212
+ maxProduct[amountField]
213
+ );
214
+ } else {
215
+ finalApplicableAmount = maxProduct[amountField];
216
+ }
157
217
  }
158
218
  return import_decimal.default.min(baseAmount, finalApplicableAmount, remainingOrderAmount);
159
219
  };
@@ -180,7 +240,12 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
180
240
  if (maxPassesPerItem > 0 && itemPassUsage) {
181
241
  const deductTaxAndFee = (config == null ? void 0 : config.deductTaxAndFee) ?? true;
182
242
  const amountField = deductTaxAndFee ? "remainingAmountWithTax" : "remainingAmountPure";
183
- const availableAfterPassLimit = getApplicableProducts(voucher, productsData).filter((p) => p[amountField].greaterThan(0)).filter((p) => getItemPassUsage(itemPassUsage, product_id, p.product_id) < maxPassesPerItem);
243
+ const availableAfterPassLimit = getApplicableProducts(voucher, productsData).filter((p) => p[amountField].greaterThan(0)).filter((p) => {
244
+ const lineKey = p._walletPassLineKey;
245
+ if (!lineKey)
246
+ return true;
247
+ return getItemPassUsage(itemPassUsage, product_id, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
248
+ });
184
249
  if (availableAfterPassLimit.length === 0) {
185
250
  return { isAvailable: false, reasonCode: "max_passes_per_item_reached" };
186
251
  }
@@ -263,14 +328,16 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
263
328
  }
264
329
  } else {
265
330
  const maxProduct = applicableProducts.reduce(
266
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
267
- );
268
- const currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
269
- const deductQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
270
- calculatedAvailableMaxAmount = import_decimal.default.min(
271
- maxProduct[unitPriceField].times(deductQty),
272
- maxProduct[amountField]
331
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
273
332
  );
333
+ if (maxPassesPerItem > 0) {
334
+ calculatedAvailableMaxAmount = import_decimal.default.min(
335
+ maxProduct[unitPriceField].times(maxPassesPerItem),
336
+ maxProduct[amountField]
337
+ );
338
+ } else {
339
+ calculatedAvailableMaxAmount = maxProduct[amountField];
340
+ }
274
341
  }
275
342
  const availableMaxAmount = import_decimal.default.min(
276
343
  baseAmount,
@@ -306,6 +373,8 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
306
373
  product_id: product.product_id,
307
374
  parent_product_id: product.parent_product_id || null,
308
375
  is_bundle_item: product.is_bundle_item || false,
376
+ lineKey: product._walletPassLineKey,
377
+ orderLineQuantity: getExpandedOrderLineQuantity(product),
309
378
  deductAmount: actualDeductAmount.toNumber(),
310
379
  // 转换为数字
311
380
  deductQuantity: actualDeductQty
@@ -314,14 +383,15 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
314
383
  }
315
384
  } else {
316
385
  const targetProduct = applicableProducts.reduce(
317
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
318
- );
319
- const currentAvailableQty = Math.ceil(targetProduct[amountField].dividedBy(targetProduct[unitPriceField]).toNumber());
320
- const availableQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
321
- const maxDeductForProduct = import_decimal.default.min(
322
- targetProduct[unitPriceField].times(availableQty),
323
- targetProduct[amountField]
386
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
324
387
  );
388
+ let maxDeductForProduct = targetProduct[amountField];
389
+ if (maxPassesPerItem > 0) {
390
+ maxDeductForProduct = import_decimal.default.min(
391
+ targetProduct[unitPriceField].times(maxPassesPerItem),
392
+ targetProduct[amountField]
393
+ );
394
+ }
325
395
  const actualDeductAmount = import_decimal.default.min(deductionLeft, maxDeductForProduct);
326
396
  const actualDeductQty = Math.ceil(actualDeductAmount.dividedBy(targetProduct[unitPriceField]).toNumber());
327
397
  targetProduct[amountField] = targetProduct[amountField].minus(actualDeductAmount);
@@ -330,19 +400,17 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
330
400
  product_id: targetProduct.product_id,
331
401
  parent_product_id: targetProduct.parent_product_id || null,
332
402
  is_bundle_item: targetProduct.is_bundle_item || false,
403
+ lineKey: targetProduct._walletPassLineKey,
404
+ orderLineQuantity: getExpandedOrderLineQuantity(targetProduct),
333
405
  deductAmount: actualDeductAmount.toNumber(),
334
- // 转换为数字
335
406
  deductQuantity: actualDeductQty
336
- // 抵扣涉及的数量(用于记录)
337
407
  });
338
408
  }
339
409
  const totalDeducted = maxDeduction.minus(deductionLeft);
340
410
  if (totalDeducted.greaterThan(0)) {
341
411
  remainingOrderAmount = remainingOrderAmount.minus(totalDeducted);
342
412
  usedVoucherCounts.set(product_id, (usedVoucherCounts.get(product_id) || 0) + 1);
343
- deductionDetails.forEach((detail) => {
344
- incrementItemPassUsage(itemPassUsageMap, product_id, detail.product_id);
345
- });
413
+ applyMaxPassUsageIncrements(itemPassUsageMap, product_id, maxPassesPerItem, deductionDetails);
346
414
  recommendedVouchers.push({
347
415
  ...voucher,
348
416
  actualDeduction: totalDeducted.toNumber(),
@@ -385,23 +453,19 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
385
453
  let remainingOrderAmount = new import_decimal.default(orderTotalAmount);
386
454
  const selectedWithDetails = [];
387
455
  const itemPassUsageMap = /* @__PURE__ */ new Map();
388
- const getItemPassUsage = (walletPassProductId, orderItemProductId) => {
456
+ const getItemPassUsageRecalc = (walletPassProductId, lineKey) => {
389
457
  var _a;
390
- return ((_a = itemPassUsageMap.get(walletPassProductId)) == null ? void 0 : _a.get(orderItemProductId)) || 0;
391
- };
392
- const incrementItemPassUsage = (walletPassProductId, orderItemProductId) => {
393
- if (!itemPassUsageMap.has(walletPassProductId)) {
394
- itemPassUsageMap.set(walletPassProductId, /* @__PURE__ */ new Map());
395
- }
396
- const innerMap = itemPassUsageMap.get(walletPassProductId);
397
- innerMap.set(orderItemProductId, (innerMap.get(orderItemProductId) || 0) + 1);
458
+ return ((_a = itemPassUsageMap.get(walletPassProductId)) == null ? void 0 : _a.get(lineKey)) || 0;
398
459
  };
399
- const filterByMaxPassesPerItem = (products2, walletPassProductId, maxPassesPerItem) => {
460
+ const filterByMaxPassesPerItemRecalc = (products2, walletPassProductId, maxPassesPerItem) => {
400
461
  if (maxPassesPerItem <= 0)
401
462
  return products2;
402
- return products2.filter(
403
- (p) => getItemPassUsage(walletPassProductId, p.product_id) < maxPassesPerItem
404
- );
463
+ return products2.filter((p) => {
464
+ const lineKey = p._walletPassLineKey;
465
+ if (!lineKey)
466
+ return true;
467
+ return getItemPassUsageRecalc(walletPassProductId, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
468
+ });
405
469
  };
406
470
  selectedVouchers.forEach((selectedVoucher) => {
407
471
  const { config, id } = selectedVoucher;
@@ -412,7 +476,7 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
412
476
  selectedVoucher,
413
477
  productsForCalc
414
478
  ).filter((p) => p[amountField].greaterThan(0));
415
- applicableProducts = filterByMaxPassesPerItem(applicableProducts, selectedVoucher.product_id, maxPassesPerItem);
479
+ applicableProducts = filterByMaxPassesPerItemRecalc(applicableProducts, selectedVoucher.product_id, maxPassesPerItem);
416
480
  if (applicableProducts.length === 0) {
417
481
  selectedWithDetails.push({
418
482
  ...selectedVoucher,
@@ -455,6 +519,8 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
455
519
  product_id: product.product_id,
456
520
  parent_product_id: product.parent_product_id || null,
457
521
  is_bundle_item: product.is_bundle_item || false,
522
+ lineKey: product._walletPassLineKey,
523
+ orderLineQuantity: getExpandedOrderLineQuantity(product),
458
524
  deductAmount: actualDeductAmount.toNumber(),
459
525
  // 转换为数字
460
526
  deductQuantity: actualDeductQty
@@ -463,14 +529,15 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
463
529
  }
464
530
  } else {
465
531
  const targetProduct = applicableProducts.reduce(
466
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
467
- );
468
- const currentAvailableQty = Math.ceil(targetProduct[amountField].dividedBy(targetProduct[unitPriceField]).toNumber());
469
- const availableQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
470
- const maxDeductForProduct = import_decimal.default.min(
471
- targetProduct[unitPriceField].times(availableQty),
472
- targetProduct[amountField]
532
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
473
533
  );
534
+ let maxDeductForProduct = targetProduct[amountField];
535
+ if (maxPassesPerItem > 0) {
536
+ maxDeductForProduct = import_decimal.default.min(
537
+ targetProduct[unitPriceField].times(maxPassesPerItem),
538
+ targetProduct[amountField]
539
+ );
540
+ }
474
541
  const actualDeductAmount = import_decimal.default.min(deductionLeft, maxDeductForProduct);
475
542
  const actualDeductQty = Math.ceil(actualDeductAmount.dividedBy(targetProduct[unitPriceField]).toNumber());
476
543
  targetProduct[amountField] = targetProduct[amountField].minus(actualDeductAmount);
@@ -479,17 +546,15 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
479
546
  product_id: targetProduct.product_id,
480
547
  parent_product_id: targetProduct.parent_product_id || null,
481
548
  is_bundle_item: targetProduct.is_bundle_item || false,
549
+ lineKey: targetProduct._walletPassLineKey,
550
+ orderLineQuantity: getExpandedOrderLineQuantity(targetProduct),
482
551
  deductAmount: actualDeductAmount.toNumber(),
483
- // 转换为数字
484
552
  deductQuantity: actualDeductQty
485
- // 抵扣涉及的数量(用于记录)
486
553
  });
487
554
  }
488
555
  const totalDeducted = maxDeduction.minus(deductionLeft);
489
556
  remainingOrderAmount = remainingOrderAmount.minus(totalDeducted);
490
- deductionDetails.forEach((detail) => {
491
- incrementItemPassUsage(selectedVoucher.product_id, detail.product_id);
492
- });
557
+ applyMaxPassUsageIncrements(itemPassUsageMap, selectedVoucher.product_id, maxPassesPerItem, deductionDetails);
493
558
  selectedWithDetails.push({
494
559
  ...selectedVoucher,
495
560
  actualDeduction: totalDeducted.toNumber(),
@@ -539,7 +604,7 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
539
604
  voucher,
540
605
  productsForCalc
541
606
  ).filter((p) => p[amountField].greaterThan(0));
542
- applicableProducts = filterByMaxPassesPerItem(applicableProducts, product_id, maxPassesPerItem);
607
+ applicableProducts = filterByMaxPassesPerItemRecalc(applicableProducts, product_id, maxPassesPerItem);
543
608
  if (applicableProducts.length === 0) {
544
609
  isAvailable = false;
545
610
  reasonCode = "not_meet_the_required_conditions";
@@ -572,14 +637,16 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
572
637
  }
573
638
  } else {
574
639
  const maxProduct = applicableProducts.reduce(
575
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
576
- );
577
- const currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
578
- const deductQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
579
- calculatedMaxAmount = import_decimal.default.min(
580
- maxProduct[unitPriceField].times(deductQty),
581
- maxProduct[amountField]
640
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
582
641
  );
642
+ if (maxPassesPerItem > 0) {
643
+ calculatedMaxAmount = import_decimal.default.min(
644
+ maxProduct[unitPriceField].times(maxPassesPerItem),
645
+ maxProduct[amountField]
646
+ );
647
+ } else {
648
+ calculatedMaxAmount = maxProduct[amountField];
649
+ }
583
650
  }
584
651
  calculatedMaxAmount = import_decimal.default.min(
585
652
  baseAmount,
@@ -642,14 +709,17 @@ var getBundleItemPrice = (bundleItem, parentQuantity, isDeductTaxAndFee) => {
642
709
  };
643
710
  var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
644
711
  const expandedProducts = [];
645
- products.forEach((product) => {
712
+ products.forEach((product, indexInOrder) => {
646
713
  const productQuantity = getProductQuantity(product);
714
+ const parentLineKey = resolveWalletPassLineKey(product, indexInOrder);
647
715
  const unitPriceWithTax = getMainProductPrice(product, true);
648
716
  const unitPricePure = getMainProductPrice(product, false);
649
717
  expandedProducts.push({
650
718
  ...product,
651
719
  is_bundle_item: false,
652
720
  parent_product_id: null,
721
+ _walletPassLineKey: parentLineKey,
722
+ _orderLineQuantity: productQuantity,
653
723
  // 单价(用于按 quantity 计算抵扣)
654
724
  unitPriceWithTax: new import_decimal.default(unitPriceWithTax),
655
725
  unitPricePure: new import_decimal.default(unitPricePure),
@@ -664,6 +734,7 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
664
734
  product.product_bundle.forEach((bundleItem) => {
665
735
  if (getBundleItemIsOriginalPrice(bundleItem)) {
666
736
  const bundleQuantity = bundleItem.num * productQuantity;
737
+ const bundleLineKey = `${parentLineKey}#bundle:${bundleItem.bundle_id}:${bundleItem.bundle_product_id}`;
667
738
  const bundleUnitPriceWithTax = new import_decimal.default(getBundleItemPrice(bundleItem, 1, true)).dividedBy(bundleItem.num);
668
739
  const bundleUnitPricePure = new import_decimal.default(getBundleItemPrice(bundleItem, 1, false)).dividedBy(bundleItem.num);
669
740
  expandedProducts.push({
@@ -674,6 +745,8 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
674
745
  parent_product_id: product.product_id,
675
746
  quantity: bundleQuantity,
676
747
  // 子商品数量 * 主商品数量
748
+ _walletPassLineKey: bundleLineKey,
749
+ _orderLineQuantity: bundleQuantity,
677
750
  // 单价(用于按 quantity 计算抵扣)
678
751
  unitPriceWithTax: bundleUnitPriceWithTax,
679
752
  unitPricePure: bundleUnitPricePure,
@@ -690,9 +763,6 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
690
763
  });
691
764
  return expandedProducts;
692
765
  };
693
- var getProductQuantity = (product) => {
694
- return product.quantity || product.product_quantity || 1;
695
- };
696
766
  var getBundleItemIsOriginalPrice = (item) => {
697
767
  return (item == null ? void 0 : item.price_type) === "markup" && (item == null ? void 0 : item.price_type_ext) === "product_price";
698
768
  };
@@ -716,5 +786,6 @@ var getBundleItemIsMarkupOrDiscountPrice = (item) => {
716
786
  getMainProductPrice,
717
787
  getProductQuantity,
718
788
  processVouchers,
719
- recalculateVouchers
789
+ recalculateVouchers,
790
+ resolveWalletPassLineKey
720
791
  });
@@ -49,5 +49,5 @@ export declare class Product extends BaseModule implements Module {
49
49
  getCategories(): ProductCategory[];
50
50
  setOtherParams(key: string, value: any): void;
51
51
  getOtherParams(): any;
52
- getProductType(): "normal" | "duration" | "session";
52
+ getProductType(): "duration" | "session" | "normal";
53
53
  }
@@ -358,7 +358,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
358
358
  };
359
359
  setOtherData(key: string, value: any): void;
360
360
  getOtherData(key: string): any;
361
- getProductTypeById(id: number): Promise<"normal" | "duration" | "session">;
361
+ getProductTypeById(id: number): Promise<"duration" | "session" | "normal">;
362
362
  /**
363
363
  * 提供给 UI 的方法,减轻 UI 层的计算压力,UI 层只需要传递 cartItemId 和 resourceCode 即返回对应的 renderList
364
364
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "2.1.106",
4
+ "version": "2.1.108",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",