@pisell/pisellos 1.0.142 → 1.0.143

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 (72) hide show
  1. package/dist/model/strategy/adapter/index.d.ts +4 -0
  2. package/dist/model/strategy/adapter/index.js +4 -1
  3. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +67 -0
  4. package/dist/model/strategy/adapter/itemRule/adapter.js +483 -0
  5. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +43 -0
  6. package/dist/model/strategy/adapter/itemRule/evaluator.js +169 -0
  7. package/dist/model/strategy/adapter/itemRule/examples.d.ts +79 -0
  8. package/dist/model/strategy/adapter/itemRule/examples.js +382 -0
  9. package/dist/model/strategy/adapter/itemRule/index.d.ts +5 -0
  10. package/dist/model/strategy/adapter/itemRule/index.js +5 -0
  11. package/dist/model/strategy/adapter/itemRule/type.d.ts +225 -0
  12. package/dist/model/strategy/adapter/itemRule/type.js +101 -0
  13. package/dist/model/strategy/adapter/promotion/adapter.d.ts +66 -0
  14. package/dist/model/strategy/adapter/promotion/adapter.js +271 -0
  15. package/dist/model/strategy/adapter/promotion/evaluator.d.ts +213 -0
  16. package/dist/model/strategy/adapter/promotion/evaluator.js +1279 -0
  17. package/dist/model/strategy/adapter/promotion/examples.d.ts +138 -0
  18. package/dist/model/strategy/adapter/promotion/examples.js +166 -0
  19. package/dist/model/strategy/adapter/promotion/index.d.ts +4 -0
  20. package/dist/model/strategy/adapter/promotion/index.js +0 -0
  21. package/dist/model/strategy/adapter/promotion/type.d.ts +447 -0
  22. package/dist/model/strategy/adapter/promotion/type.js +209 -0
  23. package/dist/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  24. package/dist/model/strategy/adapter/walletPass/evaluator.js +67 -10
  25. package/dist/model/strategy/adapter/walletPass/locales.js +12 -3
  26. package/dist/model/strategy/adapter/walletPass/type.d.ts +20 -2
  27. package/dist/model/strategy/adapter/walletPass/utils.d.ts +6 -6
  28. package/dist/model/strategy/adapter/walletPass/utils.js +143 -103
  29. package/dist/model/strategy/engine.d.ts +119 -0
  30. package/dist/model/strategy/engine.js +711 -0
  31. package/dist/model/strategy/index.d.ts +2 -93
  32. package/dist/model/strategy/index.js +6 -549
  33. package/dist/model/strategy/type.d.ts +17 -0
  34. package/dist/model/strategy/type.js +4 -0
  35. package/dist/solution/BookingByStep/index.d.ts +1 -1
  36. package/dist/solution/BookingTicket/index.d.ts +1 -1
  37. package/lib/model/strategy/adapter/index.d.ts +4 -0
  38. package/lib/model/strategy/adapter/index.js +12 -0
  39. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +67 -0
  40. package/lib/model/strategy/adapter/itemRule/adapter.js +377 -0
  41. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +43 -0
  42. package/lib/model/strategy/adapter/itemRule/evaluator.js +124 -0
  43. package/lib/model/strategy/adapter/itemRule/examples.d.ts +79 -0
  44. package/lib/model/strategy/adapter/itemRule/examples.js +316 -0
  45. package/lib/model/strategy/adapter/itemRule/index.d.ts +5 -0
  46. package/lib/model/strategy/adapter/itemRule/index.js +60 -0
  47. package/lib/model/strategy/adapter/itemRule/type.d.ts +225 -0
  48. package/lib/model/strategy/adapter/itemRule/type.js +46 -0
  49. package/lib/model/strategy/adapter/promotion/adapter.d.ts +66 -0
  50. package/lib/model/strategy/adapter/promotion/adapter.js +217 -0
  51. package/lib/model/strategy/adapter/promotion/evaluator.d.ts +213 -0
  52. package/lib/model/strategy/adapter/promotion/evaluator.js +893 -0
  53. package/lib/model/strategy/adapter/promotion/examples.d.ts +138 -0
  54. package/lib/model/strategy/adapter/promotion/examples.js +192 -0
  55. package/lib/model/strategy/adapter/promotion/index.d.ts +4 -0
  56. package/lib/model/strategy/adapter/promotion/index.js +0 -0
  57. package/lib/model/strategy/adapter/promotion/type.d.ts +447 -0
  58. package/lib/model/strategy/adapter/promotion/type.js +51 -0
  59. package/lib/model/strategy/adapter/walletPass/evaluator.d.ts +2 -0
  60. package/lib/model/strategy/adapter/walletPass/evaluator.js +38 -5
  61. package/lib/model/strategy/adapter/walletPass/locales.js +12 -3
  62. package/lib/model/strategy/adapter/walletPass/type.d.ts +20 -2
  63. package/lib/model/strategy/adapter/walletPass/utils.d.ts +6 -6
  64. package/lib/model/strategy/adapter/walletPass/utils.js +153 -82
  65. package/lib/model/strategy/engine.d.ts +119 -0
  66. package/lib/model/strategy/engine.js +514 -0
  67. package/lib/model/strategy/index.d.ts +2 -93
  68. package/lib/model/strategy/index.js +6 -381
  69. package/lib/model/strategy/type.d.ts +17 -0
  70. package/lib/solution/BookingByStep/index.d.ts +1 -1
  71. package/lib/solution/BookingTicket/index.d.ts +1 -1
  72. package/package.json +1 -1
@@ -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
  * 优惠券处理函数
@@ -64,12 +70,6 @@ export declare const getBundleItemDiscountDifference: (bundleItem: any) => numbe
64
70
  * @returns 子商品总价格(不含舍入余数)
65
71
  */
66
72
  export declare const getBundleItemPrice: (bundleItem: any, parentQuantity: number, isDeductTaxAndFee: boolean) => number;
67
- /**
68
- * 获取商品数量
69
- * @param product 商品
70
- * @returns 商品数量
71
- */
72
- export declare const getProductQuantity: (product: any) => any;
73
73
  export declare const getBundleItemIsOriginalPrice: (item: any) => boolean;
74
74
  export declare const getBundleItemIsMarkupPrice: (item: any) => boolean;
75
75
  export declare const getBundleItemIsDiscountPrice: (item: any) => boolean;
@@ -42,10 +42,72 @@ __export(utils_exports, {
42
42
  getProductQuantity: () => getProductQuantity,
43
43
  getTaxAndFeeRoundingRemainder: () => getTaxAndFeeRoundingRemainder,
44
44
  processVouchers: () => processVouchers,
45
- recalculateVouchers: () => recalculateVouchers
45
+ recalculateVouchers: () => recalculateVouchers,
46
+ resolveWalletPassLineKey: () => resolveWalletPassLineKey
46
47
  });
47
48
  module.exports = __toCommonJS(utils_exports);
48
49
  var import_decimal = __toESM(require("decimal.js"));
50
+ var getProductQuantity = (product) => {
51
+ return product.quantity || product.product_quantity || 1;
52
+ };
53
+ function resolveWalletPassLineKey(product, indexInOrder) {
54
+ const m = (product == null ? void 0 : product.metadata) || {};
55
+ const u1 = m.product_unique;
56
+ if (u1 != null && String(u1).length > 0)
57
+ return String(u1);
58
+ const u2 = m.unique_identification_number;
59
+ if (u2 != null && String(u2).length > 0)
60
+ return String(u2);
61
+ if ((product == null ? void 0 : product.id) != null && String(product.id).length > 0)
62
+ return `order_item:${product.id}`;
63
+ if ((product == null ? void 0 : product.product_unique_string) != null && String(product.product_unique_string).length > 0) {
64
+ return String(product.product_unique_string);
65
+ }
66
+ return `order_line_${indexInOrder}`;
67
+ }
68
+ function getExpandedOrderLineQuantity(p) {
69
+ if ((p == null ? void 0 : p._orderLineQuantity) != null && p._orderLineQuantity > 0)
70
+ return p._orderLineQuantity;
71
+ return getProductQuantity(p);
72
+ }
73
+ function getMaxPassSlotsForExpandedLine(maxPassesPerItem, p) {
74
+ if (maxPassesPerItem <= 0)
75
+ return Infinity;
76
+ return maxPassesPerItem * getExpandedOrderLineQuantity(p);
77
+ }
78
+ function computePassSlotsIncrement(deductAmount, deductQty, maxPassesPerItem, orderLineQuantity, currentUsage) {
79
+ if (maxPassesPerItem <= 0 || !deductAmount.greaterThan(0))
80
+ return 0;
81
+ const cap = maxPassesPerItem * orderLineQuantity;
82
+ const room = Math.max(0, cap - currentUsage);
83
+ if (room <= 0)
84
+ return 0;
85
+ const raw = Math.max(1, Math.ceil(Number(deductQty)) || 1);
86
+ return Math.min(raw, room);
87
+ }
88
+ function applyMaxPassUsageIncrements(usageMap, walletPassProductId, maxPassesPerItem, deductionDetails) {
89
+ deductionDetails.forEach((detail) => {
90
+ var _a;
91
+ const lineKey = detail.lineKey;
92
+ if (!lineKey || maxPassesPerItem <= 0)
93
+ return;
94
+ const orderLineQty = detail.orderLineQuantity != null && detail.orderLineQuantity > 0 ? detail.orderLineQuantity : 1;
95
+ const cur = ((_a = usageMap.get(walletPassProductId)) == null ? void 0 : _a.get(lineKey)) || 0;
96
+ const inc = computePassSlotsIncrement(
97
+ new import_decimal.default(detail.deductAmount),
98
+ detail.deductQuantity,
99
+ maxPassesPerItem,
100
+ orderLineQty,
101
+ cur
102
+ );
103
+ if (inc <= 0)
104
+ return;
105
+ if (!usageMap.has(walletPassProductId))
106
+ usageMap.set(walletPassProductId, /* @__PURE__ */ new Map());
107
+ const inner = usageMap.get(walletPassProductId);
108
+ inner.set(lineKey, cur + inc);
109
+ });
110
+ }
49
111
  var getRecommendedAmount = (voucher) => {
50
112
  console.log("voucher312", voucher);
51
113
  const { config, recommended_usage_amount, recommended_pure_product_usage_amount } = voucher;
@@ -90,23 +152,19 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
90
152
  console.log(products, "products123");
91
153
  const productsCopy = expandProductsWithBundleItems(products, true);
92
154
  let remainingOrderAmount = new import_decimal.default(orderTotalAmount);
93
- const getItemPassUsage = (usageMap, walletPassProductId, orderItemProductId) => {
155
+ const getItemPassUsage = (usageMap, walletPassProductId, lineKey) => {
94
156
  var _a;
95
- return ((_a = usageMap.get(walletPassProductId)) == null ? void 0 : _a.get(orderItemProductId)) || 0;
96
- };
97
- const incrementItemPassUsage = (usageMap, walletPassProductId, orderItemProductId) => {
98
- if (!usageMap.has(walletPassProductId)) {
99
- usageMap.set(walletPassProductId, /* @__PURE__ */ new Map());
100
- }
101
- const innerMap = usageMap.get(walletPassProductId);
102
- innerMap.set(orderItemProductId, (innerMap.get(orderItemProductId) || 0) + 1);
157
+ return ((_a = usageMap.get(walletPassProductId)) == null ? void 0 : _a.get(lineKey)) || 0;
103
158
  };
104
159
  const filterByMaxPassesPerItem = (products2, usageMap, walletPassProductId, maxPassesPerItem) => {
105
160
  if (maxPassesPerItem <= 0)
106
161
  return products2;
107
- return products2.filter(
108
- (p) => getItemPassUsage(usageMap, walletPassProductId, p.product_id) < maxPassesPerItem
109
- );
162
+ return products2.filter((p) => {
163
+ const lineKey = p._walletPassLineKey;
164
+ if (!lineKey)
165
+ return true;
166
+ return getItemPassUsage(usageMap, walletPassProductId, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
167
+ });
110
168
  };
111
169
  const calculateAvailableMaxAmount = (voucher, productsData, itemPassUsage) => {
112
170
  const { config } = voucher;
@@ -150,14 +208,16 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
150
208
  }
151
209
  } else {
152
210
  const maxProduct = applicableProducts.reduce(
153
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
154
- );
155
- const currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
156
- const deductQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
157
- finalApplicableAmount = import_decimal.default.min(
158
- maxProduct[unitPriceField].times(deductQty),
159
- maxProduct[amountField]
211
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
160
212
  );
213
+ if (maxPassesPerItem > 0) {
214
+ finalApplicableAmount = import_decimal.default.min(
215
+ maxProduct[unitPriceField].times(maxPassesPerItem),
216
+ maxProduct[amountField]
217
+ );
218
+ } else {
219
+ finalApplicableAmount = maxProduct[amountField];
220
+ }
161
221
  }
162
222
  return import_decimal.default.min(baseAmount, finalApplicableAmount, remainingOrderAmount);
163
223
  };
@@ -184,7 +244,12 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
184
244
  if (maxPassesPerItem > 0 && itemPassUsage) {
185
245
  const deductTaxAndFee = (config == null ? void 0 : config.deductTaxAndFee) ?? true;
186
246
  const amountField = deductTaxAndFee ? "remainingAmountWithTax" : "remainingAmountPure";
187
- const availableAfterPassLimit = getApplicableProducts(voucher, productsData).filter((p) => p[amountField].greaterThan(0)).filter((p) => getItemPassUsage(itemPassUsage, product_id, p.product_id) < maxPassesPerItem);
247
+ const availableAfterPassLimit = getApplicableProducts(voucher, productsData).filter((p) => p[amountField].greaterThan(0)).filter((p) => {
248
+ const lineKey = p._walletPassLineKey;
249
+ if (!lineKey)
250
+ return true;
251
+ return getItemPassUsage(itemPassUsage, product_id, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
252
+ });
188
253
  if (availableAfterPassLimit.length === 0) {
189
254
  return { isAvailable: false, reasonCode: "max_passes_per_item_reached" };
190
255
  }
@@ -267,14 +332,16 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
267
332
  }
268
333
  } else {
269
334
  const maxProduct = applicableProducts.reduce(
270
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
271
- );
272
- const currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
273
- const deductQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
274
- calculatedAvailableMaxAmount = import_decimal.default.min(
275
- maxProduct[unitPriceField].times(deductQty),
276
- maxProduct[amountField]
335
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
277
336
  );
337
+ if (maxPassesPerItem > 0) {
338
+ calculatedAvailableMaxAmount = import_decimal.default.min(
339
+ maxProduct[unitPriceField].times(maxPassesPerItem),
340
+ maxProduct[amountField]
341
+ );
342
+ } else {
343
+ calculatedAvailableMaxAmount = maxProduct[amountField];
344
+ }
278
345
  }
279
346
  const availableMaxAmount = import_decimal.default.min(
280
347
  baseAmount,
@@ -310,6 +377,8 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
310
377
  product_id: product.product_id,
311
378
  parent_product_id: product.parent_product_id || null,
312
379
  is_bundle_item: product.is_bundle_item || false,
380
+ lineKey: product._walletPassLineKey,
381
+ orderLineQuantity: getExpandedOrderLineQuantity(product),
313
382
  deductAmount: actualDeductAmount.toNumber(),
314
383
  // 转换为数字
315
384
  deductQuantity: actualDeductQty
@@ -318,14 +387,15 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
318
387
  }
319
388
  } else {
320
389
  const targetProduct = applicableProducts.reduce(
321
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
322
- );
323
- const currentAvailableQty = Math.ceil(targetProduct[amountField].dividedBy(targetProduct[unitPriceField]).toNumber());
324
- const availableQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
325
- const maxDeductForProduct = import_decimal.default.min(
326
- targetProduct[unitPriceField].times(availableQty),
327
- targetProduct[amountField]
390
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
328
391
  );
392
+ let maxDeductForProduct = targetProduct[amountField];
393
+ if (maxPassesPerItem > 0) {
394
+ maxDeductForProduct = import_decimal.default.min(
395
+ targetProduct[unitPriceField].times(maxPassesPerItem),
396
+ targetProduct[amountField]
397
+ );
398
+ }
329
399
  const actualDeductAmount = import_decimal.default.min(deductionLeft, maxDeductForProduct);
330
400
  const actualDeductQty = Math.ceil(actualDeductAmount.dividedBy(targetProduct[unitPriceField]).toNumber());
331
401
  targetProduct[amountField] = targetProduct[amountField].minus(actualDeductAmount);
@@ -334,19 +404,17 @@ function processVouchers(applicableVouchers, orderTotalAmount, products) {
334
404
  product_id: targetProduct.product_id,
335
405
  parent_product_id: targetProduct.parent_product_id || null,
336
406
  is_bundle_item: targetProduct.is_bundle_item || false,
407
+ lineKey: targetProduct._walletPassLineKey,
408
+ orderLineQuantity: getExpandedOrderLineQuantity(targetProduct),
337
409
  deductAmount: actualDeductAmount.toNumber(),
338
- // 转换为数字
339
410
  deductQuantity: actualDeductQty
340
- // 抵扣涉及的数量(用于记录)
341
411
  });
342
412
  }
343
413
  const totalDeducted = maxDeduction.minus(deductionLeft);
344
414
  if (totalDeducted.greaterThan(0)) {
345
415
  remainingOrderAmount = remainingOrderAmount.minus(totalDeducted);
346
416
  usedVoucherCounts.set(product_id, (usedVoucherCounts.get(product_id) || 0) + 1);
347
- deductionDetails.forEach((detail) => {
348
- incrementItemPassUsage(itemPassUsageMap, product_id, detail.product_id);
349
- });
417
+ applyMaxPassUsageIncrements(itemPassUsageMap, product_id, maxPassesPerItem, deductionDetails);
350
418
  recommendedVouchers.push({
351
419
  ...voucher,
352
420
  actualDeduction: totalDeducted.toNumber(),
@@ -389,23 +457,19 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
389
457
  let remainingOrderAmount = new import_decimal.default(orderTotalAmount);
390
458
  const selectedWithDetails = [];
391
459
  const itemPassUsageMap = /* @__PURE__ */ new Map();
392
- const getItemPassUsage = (walletPassProductId, orderItemProductId) => {
460
+ const getItemPassUsageRecalc = (walletPassProductId, lineKey) => {
393
461
  var _a;
394
- return ((_a = itemPassUsageMap.get(walletPassProductId)) == null ? void 0 : _a.get(orderItemProductId)) || 0;
462
+ return ((_a = itemPassUsageMap.get(walletPassProductId)) == null ? void 0 : _a.get(lineKey)) || 0;
395
463
  };
396
- const incrementItemPassUsage = (walletPassProductId, orderItemProductId) => {
397
- if (!itemPassUsageMap.has(walletPassProductId)) {
398
- itemPassUsageMap.set(walletPassProductId, /* @__PURE__ */ new Map());
399
- }
400
- const innerMap = itemPassUsageMap.get(walletPassProductId);
401
- innerMap.set(orderItemProductId, (innerMap.get(orderItemProductId) || 0) + 1);
402
- };
403
- const filterByMaxPassesPerItem = (products2, walletPassProductId, maxPassesPerItem) => {
464
+ const filterByMaxPassesPerItemRecalc = (products2, walletPassProductId, maxPassesPerItem) => {
404
465
  if (maxPassesPerItem <= 0)
405
466
  return products2;
406
- return products2.filter(
407
- (p) => getItemPassUsage(walletPassProductId, p.product_id) < maxPassesPerItem
408
- );
467
+ return products2.filter((p) => {
468
+ const lineKey = p._walletPassLineKey;
469
+ if (!lineKey)
470
+ return true;
471
+ return getItemPassUsageRecalc(walletPassProductId, lineKey) < getMaxPassSlotsForExpandedLine(maxPassesPerItem, p);
472
+ });
409
473
  };
410
474
  selectedVouchers.forEach((selectedVoucher) => {
411
475
  const { config, id } = selectedVoucher;
@@ -416,7 +480,7 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
416
480
  selectedVoucher,
417
481
  productsForCalc
418
482
  ).filter((p) => p[amountField].greaterThan(0));
419
- applicableProducts = filterByMaxPassesPerItem(applicableProducts, selectedVoucher.product_id, maxPassesPerItem);
483
+ applicableProducts = filterByMaxPassesPerItemRecalc(applicableProducts, selectedVoucher.product_id, maxPassesPerItem);
420
484
  if (applicableProducts.length === 0) {
421
485
  selectedWithDetails.push({
422
486
  ...selectedVoucher,
@@ -459,6 +523,8 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
459
523
  product_id: product.product_id,
460
524
  parent_product_id: product.parent_product_id || null,
461
525
  is_bundle_item: product.is_bundle_item || false,
526
+ lineKey: product._walletPassLineKey,
527
+ orderLineQuantity: getExpandedOrderLineQuantity(product),
462
528
  deductAmount: actualDeductAmount.toNumber(),
463
529
  // 转换为数字
464
530
  deductQuantity: actualDeductQty
@@ -467,14 +533,15 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
467
533
  }
468
534
  } else {
469
535
  const targetProduct = applicableProducts.reduce(
470
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
471
- );
472
- const currentAvailableQty = Math.ceil(targetProduct[amountField].dividedBy(targetProduct[unitPriceField]).toNumber());
473
- const availableQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
474
- const maxDeductForProduct = import_decimal.default.min(
475
- targetProduct[unitPriceField].times(availableQty),
476
- targetProduct[amountField]
536
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
477
537
  );
538
+ let maxDeductForProduct = targetProduct[amountField];
539
+ if (maxPassesPerItem > 0) {
540
+ maxDeductForProduct = import_decimal.default.min(
541
+ targetProduct[unitPriceField].times(maxPassesPerItem),
542
+ targetProduct[amountField]
543
+ );
544
+ }
478
545
  const actualDeductAmount = import_decimal.default.min(deductionLeft, maxDeductForProduct);
479
546
  const actualDeductQty = Math.ceil(actualDeductAmount.dividedBy(targetProduct[unitPriceField]).toNumber());
480
547
  targetProduct[amountField] = targetProduct[amountField].minus(actualDeductAmount);
@@ -483,17 +550,15 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
483
550
  product_id: targetProduct.product_id,
484
551
  parent_product_id: targetProduct.parent_product_id || null,
485
552
  is_bundle_item: targetProduct.is_bundle_item || false,
553
+ lineKey: targetProduct._walletPassLineKey,
554
+ orderLineQuantity: getExpandedOrderLineQuantity(targetProduct),
486
555
  deductAmount: actualDeductAmount.toNumber(),
487
- // 转换为数字
488
556
  deductQuantity: actualDeductQty
489
- // 抵扣涉及的数量(用于记录)
490
557
  });
491
558
  }
492
559
  const totalDeducted = maxDeduction.minus(deductionLeft);
493
560
  remainingOrderAmount = remainingOrderAmount.minus(totalDeducted);
494
- deductionDetails.forEach((detail) => {
495
- incrementItemPassUsage(selectedVoucher.product_id, detail.product_id);
496
- });
561
+ applyMaxPassUsageIncrements(itemPassUsageMap, selectedVoucher.product_id, maxPassesPerItem, deductionDetails);
497
562
  selectedWithDetails.push({
498
563
  ...selectedVoucher,
499
564
  actualDeduction: totalDeducted.toNumber(),
@@ -543,7 +608,7 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
543
608
  voucher,
544
609
  productsForCalc
545
610
  ).filter((p) => p[amountField].greaterThan(0));
546
- applicableProducts = filterByMaxPassesPerItem(applicableProducts, product_id, maxPassesPerItem);
611
+ applicableProducts = filterByMaxPassesPerItemRecalc(applicableProducts, product_id, maxPassesPerItem);
547
612
  if (applicableProducts.length === 0) {
548
613
  isAvailable = false;
549
614
  reasonCode = "not_meet_the_required_conditions";
@@ -576,14 +641,16 @@ function recalculateVouchers(allVouchers, selectedVouchers, orderTotalAmount, pr
576
641
  }
577
642
  } else {
578
643
  const maxProduct = applicableProducts.reduce(
579
- (max, p) => p[amountField].greaterThan(max[amountField]) ? p : max
580
- );
581
- const currentAvailableQty = Math.ceil(maxProduct[amountField].dividedBy(maxProduct[unitPriceField]).toNumber());
582
- const deductQty = applicableProductLimit > 0 ? Math.min(currentAvailableQty, applicableProductLimit) : currentAvailableQty;
583
- calculatedMaxAmount = import_decimal.default.min(
584
- maxProduct[unitPriceField].times(deductQty),
585
- maxProduct[amountField]
644
+ (max, p) => p[unitPriceField].greaterThan(max[unitPriceField]) ? p : max
586
645
  );
646
+ if (maxPassesPerItem > 0) {
647
+ calculatedMaxAmount = import_decimal.default.min(
648
+ maxProduct[unitPriceField].times(maxPassesPerItem),
649
+ maxProduct[amountField]
650
+ );
651
+ } else {
652
+ calculatedMaxAmount = maxProduct[amountField];
653
+ }
587
654
  }
588
655
  calculatedMaxAmount = import_decimal.default.min(
589
656
  baseAmount,
@@ -627,7 +694,7 @@ var getProductDiscountDifference = (product) => {
627
694
  return new import_decimal.default(((_a = product == null ? void 0 : product.metadata) == null ? void 0 : _a.product_discount_difference) || 0).toNumber();
628
695
  };
629
696
  var getMainProductPrice = (product, isDeductTaxAndFee) => {
630
- var _a, _b, _c;
697
+ var _a, _b, _c, _d, _e;
631
698
  let mainProductPrice = new import_decimal.default((product == null ? void 0 : product.main_product_selling_price) || ((_a = product.metadata) == null ? void 0 : _a.main_product_selling_price) || 0);
632
699
  for (let bundleItem of (product == null ? void 0 : product.product_bundle) || []) {
633
700
  if (getBundleItemIsMarkupOrDiscountPrice(bundleItem)) {
@@ -635,11 +702,11 @@ var getMainProductPrice = (product, isDeductTaxAndFee) => {
635
702
  mainProductPrice = mainProductPrice.add(bundleItemPrice.times(bundleItem.num));
636
703
  }
637
704
  }
638
- let taxFee = new import_decimal.default((product == null ? void 0 : product.tax_fee) || ((_b = product == null ? void 0 : product.metadata) == null ? void 0 : _b.main_product_attached_bundle_tax_fee) || 0);
705
+ let taxFee = new import_decimal.default((product == null ? void 0 : product.tax_fee) || ((_b = product == null ? void 0 : product.metadata) == null ? void 0 : _b.main_product_attached_bundle_tax_fee) || 0).add(((_c = product == null ? void 0 : product.metadata) == null ? void 0 : _c.tax_fee_rounding_remainder) || 0);
639
706
  if (product.is_price_include_tax === 1) {
640
707
  taxFee = new import_decimal.default(0);
641
708
  }
642
- const surchargeFee = new import_decimal.default(((_c = product == null ? void 0 : product.metadata) == null ? void 0 : _c.main_product_attached_bundle_surcharge_fee) || 0);
709
+ const surchargeFee = new import_decimal.default(((_d = product == null ? void 0 : product.metadata) == null ? void 0 : _d.main_product_attached_bundle_surcharge_fee) || 0).add(((_e = product == null ? void 0 : product.metadata) == null ? void 0 : _e.surcharge_rounding_remainder) || 0);
643
710
  const taxAndFeeTotal = taxFee.add(surchargeFee);
644
711
  if (isDeductTaxAndFee) {
645
712
  mainProductPrice = mainProductPrice.add(taxAndFeeTotal);
@@ -678,8 +745,9 @@ var getBundleItemPrice = (bundleItem, parentQuantity, isDeductTaxAndFee) => {
678
745
  };
679
746
  var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
680
747
  const expandedProducts = [];
681
- products.forEach((product) => {
748
+ products.forEach((product, indexInOrder) => {
682
749
  const productQuantity = getProductQuantity(product);
750
+ const parentLineKey = resolveWalletPassLineKey(product, indexInOrder);
683
751
  const unitPriceWithTax = getMainProductPrice(product, true);
684
752
  const unitPricePure = getMainProductPrice(product, false);
685
753
  const roundingRemainderWithTax = getTaxAndFeeRoundingRemainder(product, true);
@@ -688,6 +756,8 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
688
756
  ...product,
689
757
  is_bundle_item: false,
690
758
  parent_product_id: null,
759
+ _walletPassLineKey: parentLineKey,
760
+ _orderLineQuantity: productQuantity,
691
761
  // 单价(用于按 quantity 计算抵扣)
692
762
  unitPriceWithTax: new import_decimal.default(unitPriceWithTax),
693
763
  unitPricePure: new import_decimal.default(unitPricePure),
@@ -702,6 +772,7 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
702
772
  product.product_bundle.forEach((bundleItem) => {
703
773
  if (getBundleItemIsOriginalPrice(bundleItem)) {
704
774
  const bundleQuantity = bundleItem.num * productQuantity;
775
+ const bundleLineKey = `${parentLineKey}#bundle:${bundleItem.bundle_id}:${bundleItem.bundle_product_id}`;
705
776
  const bundleUnitPriceWithTax = new import_decimal.default(getBundleItemPrice(bundleItem, 1, true)).dividedBy(bundleItem.num);
706
777
  const bundleUnitPricePure = new import_decimal.default(getBundleItemPrice(bundleItem, 1, false)).dividedBy(bundleItem.num);
707
778
  const bundleRoundingRemainder = getBundleItemTaxAndFeeRoundingRemainder(bundleItem, true);
@@ -714,6 +785,8 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
714
785
  parent_product_id: product.product_id,
715
786
  quantity: bundleQuantity,
716
787
  // 子商品数量 * 主商品数量
788
+ _walletPassLineKey: bundleLineKey,
789
+ _orderLineQuantity: bundleQuantity,
717
790
  // 单价(用于按 quantity 计算抵扣)
718
791
  unitPriceWithTax: bundleUnitPriceWithTax,
719
792
  unitPricePure: bundleUnitPricePure,
@@ -730,9 +803,6 @@ var expandProductsWithBundleItems = (products, deductTaxAndFee) => {
730
803
  });
731
804
  return expandedProducts;
732
805
  };
733
- var getProductQuantity = (product) => {
734
- return product.quantity || product.product_quantity || 1;
735
- };
736
806
  var getBundleItemIsOriginalPrice = (item) => {
737
807
  return (item == null ? void 0 : item.price_type) === "markup" && (item == null ? void 0 : item.price_type_ext) === "product_price";
738
808
  };
@@ -760,5 +830,6 @@ var getBundleItemIsMarkupOrDiscountPrice = (item) => {
760
830
  getProductQuantity,
761
831
  getTaxAndFeeRoundingRemainder,
762
832
  processVouchers,
763
- recalculateVouchers
833
+ recalculateVouchers,
834
+ resolveWalletPassLineKey
764
835
  });
@@ -0,0 +1,119 @@
1
+ import { StrategyConfig, RuntimeContext, EvaluationResult, EngineOptions, OperatorHandler } from './type';
2
+ /**
3
+ * 策略模型 - 核心实现
4
+ *
5
+ * 完全业务无关的通用策略引擎
6
+ * 支持条件评估、动作匹配、结果返回
7
+ */
8
+ /**
9
+ * 策略引擎
10
+ *
11
+ * 核心职责:
12
+ * 1. 递归评估条件组
13
+ * 2. 收集满足条件的 actionIds
14
+ * 3. 根据 actionIds 获取 ActionEffect 对象
15
+ * 4. 按 priority 排序返回
16
+ */
17
+ export declare class StrategyEngine {
18
+ private options;
19
+ private operatorHandlers;
20
+ constructor(options?: EngineOptions);
21
+ /**
22
+ * 评估策略
23
+ */
24
+ evaluate(config: StrategyConfig, context: RuntimeContext): EvaluationResult;
25
+ /**
26
+ * 递归评估条件组
27
+ *
28
+ * @param group 条件组
29
+ * @param context 运行时上下文
30
+ * @returns 评估结果对象,包含条件是否满足、收集到的 actionIds、以及失败的原子规则信息
31
+ */
32
+ private evaluateConditionGroup;
33
+ /**
34
+ * 评估条件组的逻辑运算(带失败规则收集)
35
+ *
36
+ * 失败规则收集策略:
37
+ * - `and`:返回第一个失败的原子规则(嵌套 group 递归取其首个失败原子规则)
38
+ * - `or`:所有分支失败时,聚合每个分支的失败规则
39
+ * - `not`:被取反规则实际通过时,记录该规则
40
+ */
41
+ private evaluateGroupLogicDetailed;
42
+ /**
43
+ * 评估单个规则(带失败规则收集)
44
+ */
45
+ private evaluateRuleDetailed;
46
+ /**
47
+ * 评估原子规则(不收集失败信息,仅返回布尔结果)
48
+ */
49
+ private evaluateAtomicRule;
50
+ /**
51
+ * 构建失败规则信息
52
+ */
53
+ private buildFailedRuleInfo;
54
+ /**
55
+ * 评估代码模式条件
56
+ */
57
+ private evaluateCodeCondition;
58
+ /**
59
+ * 评估运算符模式条件
60
+ */
61
+ private evaluateOperatorCondition;
62
+ /**
63
+ * 获取字段值
64
+ */
65
+ private getFieldValue;
66
+ /**
67
+ * 评估内置运算符
68
+ */
69
+ private evaluateBuiltInOperator;
70
+ /**
71
+ * 根据 actionIds 获取 ActionEffect 对象
72
+ */
73
+ private getActionsByIds;
74
+ /**
75
+ * 按 priority 排序(降序)
76
+ */
77
+ private sortActionsByPriority;
78
+ /**
79
+ * 判断是否为条件组
80
+ */
81
+ private isConditionGroup;
82
+ /**
83
+ * 验证配置
84
+ */
85
+ private validateConfig;
86
+ /**
87
+ * 初始化内置运算符处理器
88
+ */
89
+ private initializeBuiltInOperators;
90
+ /**
91
+ * 评估商品匹配运算符
92
+ *
93
+ * 匹配规则:
94
+ * - 首先匹配 product_id
95
+ * - 如果配置的 product_variant_id = 0,只需要 product_id 匹配(表示任意变体)
96
+ * - 如果配置的 product_variant_id != 0,还需要 product_variant_id 精确匹配
97
+ *
98
+ * @param fieldValue 实际商品 { product_id: number, product_variant_id: number }
99
+ * @param configValue 配置的商品列表 Array<{ product_id: number, product_variant_id: number }>
100
+ * @returns 是否匹配
101
+ */
102
+ private evaluateProductMatch;
103
+ /**
104
+ * 注册自定义运算符
105
+ */
106
+ registerOperator(operator: string, handler: OperatorHandler): void;
107
+ /**
108
+ * 获取调试信息
109
+ */
110
+ getDebugInfo(): Record<string, any>;
111
+ }
112
+ /**
113
+ * 创建策略引擎实例
114
+ */
115
+ export declare function createStrategyEngine(options?: EngineOptions): StrategyEngine;
116
+ /**
117
+ * 快速评估(使用默认引擎配置)
118
+ */
119
+ export declare function evaluate(config: StrategyConfig, context: RuntimeContext): EvaluationResult;