@pisell/pisellos 2.3.74 → 2.3.75

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 (276) hide show
  1. package/dist/core/index.js +6 -10
  2. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  3. package/dist/model/strategy/adapter/walletPass/example.js +3 -26
  4. package/dist/model/strategy/strategy-example.js +5 -19
  5. package/dist/modules/Cart/index.js +0 -3
  6. package/dist/modules/Cart/utils/cartProduct.js +0 -1
  7. package/dist/modules/Date/index.js +1 -5
  8. package/dist/modules/Discount/index.js +2 -4
  9. package/dist/modules/OpenData/index.js +0 -2
  10. package/dist/modules/Order/index.js +36 -64
  11. package/dist/modules/Payment/index.js +97 -162
  12. package/dist/modules/Rules/index.js +1 -4
  13. package/dist/modules/Schedule/index.js +0 -1
  14. package/dist/modules/Schedule/utils.js +0 -1
  15. package/dist/modules/Step/index.js +0 -1
  16. package/dist/plugins/request.js +1 -4
  17. package/dist/plugins/window.js +2 -6
  18. package/dist/server/index.js +97 -129
  19. package/dist/server/modules/menu/index.js +32 -41
  20. package/dist/server/modules/order/index.js +2 -4
  21. package/dist/server/modules/products/index.js +24 -42
  22. package/dist/server/modules/quotation/index.js +29 -38
  23. package/dist/server/modules/resource/index.js +3 -4
  24. package/dist/server/modules/schedule/index.js +27 -35
  25. package/dist/server/utils/product.js +0 -1
  26. package/dist/solution/BaseSales/index.js +38 -41
  27. package/dist/solution/BookingByStep/index.js +7 -59
  28. package/dist/solution/BookingByStep/utils/capacity.js +1 -11
  29. package/dist/solution/BookingByStep/utils/resources.js +1 -3
  30. package/dist/solution/BookingByStep/utils/timeslots.js +2 -43
  31. package/dist/solution/BookingTicket/index.js +0 -2
  32. package/dist/solution/BookingTicket/utils/scan/cloudSearch.js +6 -7
  33. package/dist/solution/BookingTicket/utils/scan/handleScan.js +25 -31
  34. package/dist/solution/BookingTicket/utils/scan/index.js +6 -10
  35. package/dist/solution/BuyTickets/index.js +9 -12
  36. package/dist/solution/Checkout/index.js +177 -252
  37. package/dist/solution/Checkout/utils/index.js +4 -16
  38. package/dist/solution/RegisterAndLogin/index.js +30 -76
  39. package/dist/solution/ScanOrder/index.js +50 -60
  40. package/dist/solution/ShopDiscount/index.js +2 -7
  41. package/dist/solution/VenueBooking/index.js +45 -55
  42. package/dist/utils/task.js +15 -18
  43. package/dist/utils/watch.js +0 -1
  44. package/lib/apis/picoding.js +2 -0
  45. package/lib/core/index.js +134 -134
  46. package/lib/effects/index.js +46 -57
  47. package/lib/index.js +82 -49
  48. package/lib/model/index.js +14 -21
  49. package/lib/model/strategy/adapter/dataVariant/adapter.js +38 -50
  50. package/lib/model/strategy/adapter/dataVariant/evaluator.js +162 -236
  51. package/lib/model/strategy/adapter/dataVariant/examples.js +229 -237
  52. package/lib/model/strategy/adapter/dataVariant/index.js +39 -34
  53. package/lib/model/strategy/adapter/dataVariant/type.js +57 -28
  54. package/lib/model/strategy/adapter/index.js +100 -64
  55. package/lib/model/strategy/adapter/itemRule/adapter.js +156 -144
  56. package/lib/model/strategy/adapter/itemRule/evaluator.js +36 -48
  57. package/lib/model/strategy/adapter/itemRule/examples.js +295 -225
  58. package/lib/model/strategy/adapter/itemRule/index.js +83 -57
  59. package/lib/model/strategy/adapter/itemRule/type.js +97 -36
  60. package/lib/model/strategy/adapter/promotion/adapter.js +91 -69
  61. package/lib/model/strategy/adapter/promotion/evaluator.js +444 -310
  62. package/lib/model/strategy/adapter/promotion/examples.js +222 -234
  63. package/lib/model/strategy/adapter/promotion/index.js +1 -0
  64. package/lib/model/strategy/adapter/promotion/type.js +243 -36
  65. package/lib/model/strategy/adapter/type.js +4 -16
  66. package/lib/model/strategy/adapter/walletPass/evaluator.js +180 -150
  67. package/lib/model/strategy/adapter/walletPass/example.js +217 -190
  68. package/lib/model/strategy/adapter/walletPass/index.js +75 -51
  69. package/lib/model/strategy/adapter/walletPass/locales.js +36 -58
  70. package/lib/model/strategy/adapter/walletPass/type.js +4 -16
  71. package/lib/model/strategy/adapter/walletPass/utils.js +588 -378
  72. package/lib/model/strategy/engine.js +270 -168
  73. package/lib/model/strategy/index.js +49 -34
  74. package/lib/model/strategy/strategy-example.js +243 -239
  75. package/lib/model/strategy/type.js +100 -40
  76. package/lib/modules/Account/index.js +39 -53
  77. package/lib/modules/Account/types.js +20 -33
  78. package/lib/modules/AccountList/index.js +116 -154
  79. package/lib/modules/AccountList/types.js +30 -31
  80. package/lib/modules/AccountList/utils.js +18 -30
  81. package/lib/modules/BaseModule.js +23 -42
  82. package/lib/modules/BookingContext/index.js +158 -136
  83. package/lib/modules/BookingContext/types.js +46 -32
  84. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +138 -146
  85. package/lib/modules/BookingContext/utils/buildNormalProductCacheItemFromOrderLine.js +78 -83
  86. package/lib/modules/BookingContext/utils/cacheItemToBookingInput.js +170 -150
  87. package/lib/modules/BookingContext/utils/flexible.js +38 -55
  88. package/lib/modules/BookingContext/utils/formatResourceList.js +19 -34
  89. package/lib/modules/BookingContext/utils/index.js +124 -41
  90. package/lib/modules/BookingContext/utils/noResource.js +58 -70
  91. package/lib/modules/BookingContext/utils/orderLineDisplay.js +14 -41
  92. package/lib/modules/BookingContext/utils/productExtend.js +202 -155
  93. package/lib/modules/BookingContext/utils/resourceErrors.js +53 -53
  94. package/lib/modules/BookingContext/utils/resourceSelection.js +19 -41
  95. package/lib/modules/BookingContext/utils/resources.js +209 -167
  96. package/lib/modules/BookingContext/utils/serviceTimes.js +115 -116
  97. package/lib/modules/BookingContext/utils/timeSlices.js +76 -86
  98. package/lib/modules/Cart/index.js +170 -124
  99. package/lib/modules/Cart/types.js +46 -51
  100. package/lib/modules/Cart/utils/cartAccount.js +40 -39
  101. package/lib/modules/Cart/utils/cartDate.js +56 -61
  102. package/lib/modules/Cart/utils/cartDiscount.js +28 -33
  103. package/lib/modules/Cart/utils/cartNote.js +27 -32
  104. package/lib/modules/Cart/utils/cartProduct.js +321 -251
  105. package/lib/modules/Cart/utils/cartRelationForms.js +29 -35
  106. package/lib/modules/Cart/utils/cartResource.js +70 -78
  107. package/lib/modules/Cart/utils/changePrice.js +22 -50
  108. package/lib/modules/Cart/utils/index.js +106 -48
  109. package/lib/modules/Customer/constants.js +13 -34
  110. package/lib/modules/Customer/index.js +235 -172
  111. package/lib/modules/Customer/types.js +38 -35
  112. package/lib/modules/Date/index.js +116 -115
  113. package/lib/modules/Date/types.js +16 -28
  114. package/lib/modules/Date/utils.js +174 -123
  115. package/lib/modules/Discount/index.js +122 -127
  116. package/lib/modules/Discount/types.js +9 -31
  117. package/lib/modules/Guests/index.js +30 -56
  118. package/lib/modules/Guests/types.js +23 -33
  119. package/lib/modules/Holder/index.js +209 -189
  120. package/lib/modules/Holder/types.js +4 -16
  121. package/lib/modules/Holder/utils.js +20 -49
  122. package/lib/modules/OpenData/index.js +70 -76
  123. package/lib/modules/OpenData/types.js +4 -16
  124. package/lib/modules/OpenData/utils.js +44 -78
  125. package/lib/modules/Order/index.js +1713 -2118
  126. package/lib/modules/Order/payment-utils.js +77 -120
  127. package/lib/modules/Order/types.js +88 -35
  128. package/lib/modules/Order/utils/bundleDiscountHydration.js +41 -74
  129. package/lib/modules/Order/utils/discountProductLineIdentity.js +142 -100
  130. package/lib/modules/Order/utils/manualProductDiscount.js +124 -123
  131. package/lib/modules/Order/utils/orderCollectionIdentity.js +184 -244
  132. package/lib/modules/Order/utils.js +814 -647
  133. package/lib/modules/Payment/cash.js +20 -33
  134. package/lib/modules/Payment/cashRecommendationAlgorithm.js +145 -76
  135. package/lib/modules/Payment/eftpos.js +16 -30
  136. package/lib/modules/Payment/index.js +532 -399
  137. package/lib/modules/Payment/mx51.js +1 -0
  138. package/lib/modules/Payment/types.js +230 -108
  139. package/lib/modules/Payment/utils.js +52 -51
  140. package/lib/modules/Payment/walletpass.js +485 -660
  141. package/lib/modules/Product/index.js +51 -46
  142. package/lib/modules/Product/types.js +4 -16
  143. package/lib/modules/Product/utils.js +32 -33
  144. package/lib/modules/ProductList/index.js +113 -123
  145. package/lib/modules/ProductList/types.js +9 -31
  146. package/lib/modules/Quotation/index.js +81 -118
  147. package/lib/modules/Quotation/types.js +4 -16
  148. package/lib/modules/Resource/index.js +27 -59
  149. package/lib/modules/Resource/types.js +22 -32
  150. package/lib/modules/Resource/utils.js +30 -38
  151. package/lib/modules/ResourcePlanner/index.js +25 -23
  152. package/lib/modules/ResourcePlanner/localClock.js +54 -119
  153. package/lib/modules/ResourcePlanner/planner.js +589 -770
  154. package/lib/modules/ResourcePlanner/types.js +10 -29
  155. package/lib/modules/Rules/index.js +1218 -959
  156. package/lib/modules/Rules/types.js +17 -40
  157. package/lib/modules/SalesSummary/index.js +85 -90
  158. package/lib/modules/SalesSummary/types.js +4 -16
  159. package/lib/modules/SalesSummary/utils.js +355 -430
  160. package/lib/modules/ScanOrderLogger/index.js +59 -79
  161. package/lib/modules/ScanOrderLogger/providers/feishu.js +54 -78
  162. package/lib/modules/ScanOrderLogger/providers/grafana.js +13 -36
  163. package/lib/modules/ScanOrderLogger/types.js +4 -16
  164. package/lib/modules/Schedule/getDateIsInSchedule.js +334 -163
  165. package/lib/modules/Schedule/index.js +100 -114
  166. package/lib/modules/Schedule/type.js +4 -16
  167. package/lib/modules/Schedule/types.js +4 -16
  168. package/lib/modules/Schedule/utils.js +433 -291
  169. package/lib/modules/Step/index.js +40 -52
  170. package/lib/modules/Step/tyeps.js +4 -16
  171. package/lib/modules/Summary/index.js +66 -71
  172. package/lib/modules/Summary/types.js +4 -16
  173. package/lib/modules/Summary/utils.js +773 -418
  174. package/lib/modules/SurchargeList/index.js +46 -60
  175. package/lib/modules/SurchargeList/types.js +4 -16
  176. package/lib/modules/index.js +245 -63
  177. package/lib/plugins/app.js +4 -16
  178. package/lib/plugins/index.js +36 -25
  179. package/lib/plugins/request.js +137 -126
  180. package/lib/plugins/shopStore.js +4 -16
  181. package/lib/plugins/user.js +4 -16
  182. package/lib/plugins/window.js +171 -179
  183. package/lib/server/index.js +3050 -2665
  184. package/lib/server/modules/floor-plan/index.js +118 -134
  185. package/lib/server/modules/floor-plan/types.js +15 -30
  186. package/lib/server/modules/index.js +106 -68
  187. package/lib/server/modules/menu/index.js +142 -122
  188. package/lib/server/modules/menu/types.js +18 -31
  189. package/lib/server/modules/order/index.js +784 -1002
  190. package/lib/server/modules/order/types.js +122 -33
  191. package/lib/server/modules/order/utils/filterBookings.js +219 -194
  192. package/lib/server/modules/order/utils/filterOrders.js +118 -92
  193. package/lib/server/modules/payment/index.js +59 -83
  194. package/lib/server/modules/payment/types.js +4 -16
  195. package/lib/server/modules/products/index.js +583 -471
  196. package/lib/server/modules/products/types.js +44 -34
  197. package/lib/server/modules/quotation/index.js +137 -172
  198. package/lib/server/modules/quotation/types.js +21 -31
  199. package/lib/server/modules/resource/index.js +220 -186
  200. package/lib/server/modules/resource/types.js +42 -33
  201. package/lib/server/modules/schedule/index.js +135 -123
  202. package/lib/server/modules/schedule/types.js +14 -21
  203. package/lib/server/modules/schedule/utils.js +334 -163
  204. package/lib/server/types.js +4 -16
  205. package/lib/server/utils/index.js +25 -23
  206. package/lib/server/utils/product.js +196 -139
  207. package/lib/server/utils/schedule.js +34 -41
  208. package/lib/server/utils/small-ticket.js +479 -456
  209. package/lib/server/utils/time.js +40 -49
  210. package/lib/solution/BaseSales/index.js +1186 -1410
  211. package/lib/solution/BaseSales/types.js +42 -38
  212. package/lib/solution/BaseSales/utils/cartPromotion.js +642 -498
  213. package/lib/solution/BaseSales/utils/parseSalesResponse.js +85 -105
  214. package/lib/solution/BaseSales/utils/quotationPrice.js +56 -114
  215. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +108 -145
  216. package/lib/solution/BaseSales/utils.js +158 -143
  217. package/lib/solution/BookingByStep/index.js +1527 -1340
  218. package/lib/solution/BookingByStep/types.js +40 -99
  219. package/lib/solution/BookingByStep/utils/capacity.js +192 -160
  220. package/lib/solution/BookingByStep/utils/products.js +42 -52
  221. package/lib/solution/BookingByStep/utils/resources.js +527 -330
  222. package/lib/solution/BookingByStep/utils/stock.js +65 -44
  223. package/lib/solution/BookingByStep/utils/timeslots.js +129 -131
  224. package/lib/solution/BookingTicket/index.js +607 -522
  225. package/lib/solution/BookingTicket/types.js +99 -77
  226. package/lib/solution/BookingTicket/utils/addProductDecision.js +226 -189
  227. package/lib/solution/BookingTicket/utils/addTimeAvailability.js +58 -89
  228. package/lib/solution/BookingTicket/utils/bookingStatus.js +107 -78
  229. package/lib/solution/BookingTicket/utils/cartView.js +114 -99
  230. package/lib/solution/BookingTicket/utils/exampleData.js +185 -0
  231. package/lib/solution/BookingTicket/utils/orderCustomer.js +18 -41
  232. package/lib/solution/BookingTicket/utils/resolveBestAddTimePlan.js +219 -86
  233. package/lib/solution/BookingTicket/utils/scan/applyGlobalScan.js +168 -116
  234. package/lib/solution/BookingTicket/utils/scan/cloudSearch.js +79 -91
  235. package/lib/solution/BookingTicket/utils/scan/formatGlobalScan.js +54 -41
  236. package/lib/solution/BookingTicket/utils/scan/handleScan.js +84 -81
  237. package/lib/solution/BookingTicket/utils/scan/index.js +98 -104
  238. package/lib/solution/BookingTicket/utils/scan/scanCache.js +90 -43
  239. package/lib/solution/BookingTicket/utils/scan/scanProductValues.js +52 -64
  240. package/lib/solution/BookingTicket/utils/sessionCatalogStale.js +56 -58
  241. package/lib/solution/BookingTicket/utils/weighingProductSku.js +34 -48
  242. package/lib/solution/BuyTickets/index.js +67 -70
  243. package/lib/solution/BuyTickets/types.js +22 -38
  244. package/lib/solution/Checkout/index.js +1451 -1158
  245. package/lib/solution/Checkout/types.js +91 -61
  246. package/lib/solution/Checkout/utils/index.js +228 -156
  247. package/lib/solution/PlaceOrder/index.js +55 -0
  248. package/lib/solution/RegisterAndLogin/config.js +493 -460
  249. package/lib/solution/RegisterAndLogin/index.js +633 -630
  250. package/lib/solution/RegisterAndLogin/types.js +189 -76
  251. package/lib/solution/RegisterAndLogin/utils.js +183 -125
  252. package/lib/solution/Sales/index.js +328 -324
  253. package/lib/solution/Sales/types.js +27 -33
  254. package/lib/solution/ScanOrder/index.js +832 -864
  255. package/lib/solution/ScanOrder/types.js +33 -34
  256. package/lib/solution/ScanOrder/utils.js +409 -374
  257. package/lib/solution/ShopDiscount/index.js +226 -232
  258. package/lib/solution/ShopDiscount/types.js +15 -37
  259. package/lib/solution/ShopDiscount/utils.js +300 -172
  260. package/lib/solution/UnifiedBookingSales/index.js +1035 -1201
  261. package/lib/solution/UnifiedBookingSales/remoteOccupancyCache.js +99 -159
  262. package/lib/solution/UnifiedBookingSales/types.js +10 -32
  263. package/lib/solution/VenueBooking/index.js +811 -879
  264. package/lib/solution/VenueBooking/types.js +19 -39
  265. package/lib/solution/VenueBooking/utils/dateSummary.js +27 -56
  266. package/lib/solution/VenueBooking/utils/resource.js +31 -54
  267. package/lib/solution/VenueBooking/utils/slotMerge.js +94 -107
  268. package/lib/solution/VenueBooking/utils/timeSlot.js +132 -132
  269. package/lib/solution/VenueBooking/utils.js +130 -67
  270. package/lib/solution/index.js +124 -41
  271. package/lib/store/createStore.js +32 -29
  272. package/lib/types/index.js +4 -16
  273. package/lib/utils/payment-number.js +26 -46
  274. package/lib/utils/task.js +36 -36
  275. package/lib/utils/watch.js +81 -47
  276. package/package.json +2 -1
@@ -1,104 +1,153 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
1
+ "use strict";
28
2
 
29
- // src/solution/BaseSales/utils/cartPromotion.ts
30
- var cartPromotion_exports = {};
31
- __export(cartPromotion_exports, {
32
- appendPromotionTags: () => appendPromotionTags,
33
- buildConsolidateKeyForPromotion: () => buildConsolidateKeyForPromotion,
34
- calculateUnfulfilledPromotions: () => calculateUnfulfilledPromotions,
35
- extractProductMatchIdentifiersFromStrategyConfig: () => extractProductMatchIdentifiersFromStrategyConfig,
36
- getOrderProductBasePrice: () => getOrderProductBasePrice,
37
- getOrderProductOriginalPriceForPromotion: () => getOrderProductOriginalPriceForPromotion,
38
- getOrderProductUid: () => getOrderProductUid,
39
- mergeIdenticalPromotionCartLines: () => mergeIdenticalPromotionCartLines,
40
- processCartPromotion: () => processCartPromotion,
41
- resolveStrategyEligibleProducts: () => resolveStrategyEligibleProducts,
42
- resolveStrategyRequiredQuantity: () => resolveStrategyRequiredQuantity
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
43
5
  });
44
- module.exports = __toCommonJS(cartPromotion_exports);
45
- var import_decimal = __toESM(require("decimal.js"));
46
- var import_lodash_es = require("lodash-es");
47
- var import_utils = require("../../../modules/Order/utils");
48
- var import_utils2 = require("../../ScanOrder/utils");
6
+ exports.appendPromotionTags = appendPromotionTags;
7
+ exports.buildConsolidateKeyForPromotion = buildConsolidateKeyForPromotion;
8
+ exports.calculateUnfulfilledPromotions = calculateUnfulfilledPromotions;
9
+ exports.extractProductMatchIdentifiersFromStrategyConfig = extractProductMatchIdentifiersFromStrategyConfig;
10
+ exports.getOrderProductBasePrice = getOrderProductBasePrice;
11
+ exports.getOrderProductOriginalPriceForPromotion = getOrderProductOriginalPriceForPromotion;
12
+ exports.getOrderProductUid = getOrderProductUid;
13
+ exports.mergeIdenticalPromotionCartLines = mergeIdenticalPromotionCartLines;
14
+ exports.processCartPromotion = processCartPromotion;
15
+ exports.resolveStrategyEligibleProducts = resolveStrategyEligibleProducts;
16
+ exports.resolveStrategyRequiredQuantity = resolveStrategyRequiredQuantity;
17
+ var _decimal = _interopRequireDefault(require("decimal.js"));
18
+ var _lodashEs = require("lodash-es");
19
+ var _utils = require("../../../modules/Order/utils");
20
+ var _utils2 = require("../../ScanOrder/utils");
21
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
22
+ /**
23
+ * Cart promotion 工具集(OS 内部纯函数)
24
+ *
25
+ * 这是 `private-materials/components/booking/hooks/usePromotion.ts` 中促销/赠品
26
+ * 核心计算(evaluator → 拆分 / 计价 → 赠品差异化 / 未满足提示 / 标签)的 OS 等价实现。
27
+ *
28
+ * 与 legacy 的关键差异:
29
+ * - legacy 操作 booking `_extend.*` 形状;OS 操作 `OrderProduct.metadata.*` 形状。
30
+ * - 行唯一标识不再用 `_id`,统一改用 `metadata.unique_identification_number`。
31
+ * - 赠品标签存到 `metadata._giftInfo`;主商品促销信息存到 `metadata._promotion`;
32
+ * 商品卡列表的促销 tag 存到 `metadata._promotionTags`。
33
+ *
34
+ * 仅暴露纯函数,不持有任何状态;评估器以参数注入。
35
+ *
36
+ * @example
37
+ * const evaluator = appHelper.utils.promotionEvaluator;
38
+ * const result = processCartPromotion(tempOrder.products, evaluator);
39
+ * // result.products: 处理后的商品(带 _promotion / 拆分)
40
+ * // result.giftActions: 需要 add / reduce / remove 的赠品操作
41
+ * // result.unfulfilledPromotions: 未满足策略列表(购物车底部 alert)
42
+ */
43
+
44
+ // ============================================
45
+ // 类型定义
46
+ // ============================================
47
+
48
+ /** Cart 评估器协议(只描述本文件用到的方法,避免循环依赖) */
49
+
50
+ /** 单个赠品减量项 */
51
+
52
+ /** 赠品新增项 */
53
+
54
+ /** 赠品操作集合(OS 侧根据它落地 tempOrder.products) */
55
+
56
+ /** 未满足促销提示项(购物车底部 alert 数据源) */
57
+
58
+ /** 商品卡促销标签结构(写入 `metadata._promotionTags`) */
59
+
60
+ /** processCartPromotion 输出 */
61
+
62
+ // ============================================
63
+ // 字段适配 helper
64
+ // ============================================
65
+
66
+ /**
67
+ * 获取商品的「主商品基础价」用于评估器入参。
68
+ *
69
+ * 优先级(与 OS v2 schema 一致):
70
+ * 1. `metadata.source_product_price`
71
+ * 2. `metadata.main_product_original_price` 减去 option 合计
72
+ * 3. `original_price`
73
+ */
49
74
  function getOrderProductBasePrice(product) {
50
- const metadata = (product == null ? void 0 : product.metadata) || {};
51
- const optionSum = (0, import_utils.sumOptionUnitPrice)((0, import_utils.getProductSkuOptions)(product)).toNumber();
52
- if (metadata.source_product_price !== void 0) {
53
- const v2 = Number(metadata.source_product_price);
54
- return Number.isFinite(v2) ? v2 : 0;
75
+ const metadata = product?.metadata || {};
76
+ const optionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(product)).toNumber();
77
+ if (metadata.source_product_price !== undefined) {
78
+ const v = Number(metadata.source_product_price);
79
+ return Number.isFinite(v) ? v : 0;
55
80
  }
56
- if (metadata.main_product_original_price !== void 0) {
57
- const v2 = Number(metadata.main_product_original_price) - optionSum;
58
- return Number.isFinite(v2) ? v2 : 0;
81
+ if (metadata.main_product_original_price !== undefined) {
82
+ const v = Number(metadata.main_product_original_price) - optionSum;
83
+ return Number.isFinite(v) ? v : 0;
59
84
  }
60
- const v = Number((product == null ? void 0 : product.original_price) ?? (product == null ? void 0 : product.selling_price) ?? 0);
85
+ const v = Number(product?.original_price ?? product?.selling_price ?? 0);
61
86
  return Number.isFinite(v) ? v : 0;
62
87
  }
88
+
89
+ /**
90
+ * 促销均摊专用原价(对齐 legacy `getMainOriginalPrice` 语义)。
91
+ *
92
+ * 必须用券前原价做 X_ITEMS_FOR_Y_PRICE 比例拆分;`source_product_price` 在上一轮 applyPromotion
93
+ * 后可能已被写成促销价(如 5 / 9 / 1),若继续用它会导致同商品两行出现 9+1 而非 5+5。
94
+ *
95
+ * @example
96
+ * getOrderProductOriginalPriceForPromotion({ metadata: { source_product_price: '9', main_product_original_price: '5.80' } });
97
+ * // => 5.8
98
+ */
63
99
  function getOrderProductOriginalPriceForPromotion(product) {
64
- var _a;
65
- const metadata = (product == null ? void 0 : product.metadata) || {};
66
- const optionSum = (0, import_utils.sumOptionUnitPrice)((0, import_utils.getProductSkuOptions)(product)).toNumber();
67
- if (metadata.main_product_original_price !== void 0 && metadata.main_product_original_price !== null && metadata.main_product_original_price !== "") {
100
+ const metadata = product?.metadata || {};
101
+ const optionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(product)).toNumber();
102
+ if (metadata.main_product_original_price !== undefined && metadata.main_product_original_price !== null && metadata.main_product_original_price !== '') {
68
103
  const v = Number(metadata.main_product_original_price) - optionSum;
69
- if (Number.isFinite(v) && v >= 0)
70
- return v;
104
+ if (Number.isFinite(v) && v >= 0) return v;
71
105
  }
72
- const promoOriginal = (_a = metadata._promotion) == null ? void 0 : _a.originalPrice;
73
- if (promoOriginal !== void 0 && promoOriginal !== null && promoOriginal !== "") {
106
+ const promoOriginal = metadata._promotion?.originalPrice;
107
+ if (promoOriginal !== undefined && promoOriginal !== null && promoOriginal !== '') {
74
108
  const v = Number(promoOriginal);
75
- if (Number.isFinite(v) && v >= 0)
76
- return v;
109
+ if (Number.isFinite(v) && v >= 0) return v;
77
110
  }
78
111
  return getOrderProductBasePrice(product);
79
112
  }
80
- var X_ITEMS_FOR_Y_PRICE = "X_ITEMS_FOR_Y_PRICE";
81
- var ITEM_REWARD = "ITEM_REWARD";
113
+ const X_ITEMS_FOR_Y_PRICE = 'X_ITEMS_FOR_Y_PRICE';
114
+ const BUY_X_GET_Y_FREE = 'BUY_X_GET_Y_FREE';
115
+ const ITEM_REWARD = 'ITEM_REWARD';
82
116
  function isItemRewardPromotionDiscountItem(item) {
83
- var _a, _b;
84
- return (item == null ? void 0 : item.type) === "product" && ((_a = item == null ? void 0 : item.metadata) == null ? void 0 : _a.source) === "promotion" && ((_b = item == null ? void 0 : item.metadata) == null ? void 0 : _b.actionType) === ITEM_REWARD;
117
+ return item?.type === 'product' && item?.metadata?.source === 'promotion' && item?.metadata?.actionType === ITEM_REWARD;
85
118
  }
86
119
  function isManagedPromotionDiscountItem(item) {
87
- if ((item == null ? void 0 : item.type) === "promotion")
88
- return true;
120
+ if (item?.type === 'promotion') return true;
89
121
  return isItemRewardPromotionDiscountItem(item);
90
122
  }
123
+
124
+ /**
125
+ * 判断商品行在本轮评估前是否已享受促销。
126
+ *
127
+ * `_promotion` 仅存在于当前运行时;订单保存后会被裁剪,因此编辑态需通过
128
+ * ITEM_REWARD 写入的受管商品折扣恢复上一轮状态。
129
+ */
91
130
  function wasInPromotionBeforeEvaluation(item) {
92
- var _a, _b;
93
- if (((_b = (_a = item == null ? void 0 : item.metadata) == null ? void 0 : _a._promotion) == null ? void 0 : _b.inPromotion) === true)
94
- return true;
95
- return Array.isArray(item == null ? void 0 : item.discount_list) && item.discount_list.some(isItemRewardPromotionDiscountItem);
131
+ if (item?.metadata?._promotion?.inPromotion === true) return true;
132
+ return Array.isArray(item?.discount_list) && item.discount_list.some(isItemRewardPromotionDiscountItem);
96
133
  }
134
+
135
+ /**
136
+ * 写入/清除促销 discount_list。
137
+ *
138
+ * applyDiscount → applyProductDiscountPrices 会从 source_product_price 减去 discount_list 合计
139
+ * 重算 main_product_selling_price;保留券前 source 时必须同步写入促销差额,否则会还原成原价。
140
+ *
141
+ * ITEM_REWARD 使用 type=product,复用商品级折扣排他规则,避免客户折扣继续叠加到奖励商品行。
142
+ *
143
+ * @example
144
+ * updatePromotionDiscountList(line, { title: '2 items for $10', amount: '17.00', original_amount: '22.00', fixed_amount: '17.00' });
145
+ */
97
146
  function updatePromotionDiscountList(item, promotionDiscount, options) {
98
147
  let discountList = Array.isArray(item.discount_list) ? [...item.discount_list] : [];
99
- discountList = discountList.filter((d) => !isManagedPromotionDiscountItem(d));
148
+ discountList = discountList.filter(d => !isManagedPromotionDiscountItem(d));
100
149
  if (promotionDiscount) {
101
- const discountType = (options == null ? void 0 : options.discountType) || "promotion";
150
+ const discountType = options?.discountType || 'promotion';
102
151
  discountList.push({
103
152
  type: discountType,
104
153
  amount: promotionDiscount.amount,
@@ -106,14 +155,14 @@ function updatePromotionDiscountList(item, promotionDiscount, options) {
106
155
  original_amount: promotionDiscount.original_amount,
107
156
  fixed_amount: promotionDiscount.fixed_amount,
108
157
  title: promotionDiscount.title,
109
- resource_id: ""
158
+ resource_id: ''
110
159
  },
111
- ...discountType === "product" ? {
160
+ ...(discountType === 'product' ? {
112
161
  metadata: {
113
- source: "promotion",
114
- actionType: (options == null ? void 0 : options.actionType) || ITEM_REWARD
162
+ source: 'promotion',
163
+ actionType: options?.actionType || ITEM_REWARD
115
164
  }
116
- } : {}
165
+ } : {})
117
166
  });
118
167
  }
119
168
  if (discountList.length > 0) {
@@ -123,133 +172,132 @@ function updatePromotionDiscountList(item, promotionDiscount, options) {
123
172
  }
124
173
  }
125
174
  function evaluatorHasActionType(evaluator, actionType) {
126
- var _a;
127
- const configs = (_a = evaluator == null ? void 0 : evaluator.getStrategyConfigs) == null ? void 0 : _a.call(evaluator);
128
- if (!Array.isArray(configs))
129
- return false;
130
- return configs.some(
131
- (config) => ((config == null ? void 0 : config.actions) || []).some((action) => (action == null ? void 0 : action.type) === actionType)
132
- );
175
+ const configs = evaluator?.getStrategyConfigs?.();
176
+ if (!Array.isArray(configs)) return false;
177
+ return configs.some(config => (config?.actions || []).some(action => action?.type === actionType));
133
178
  }
179
+
180
+ /**
181
+ * 将促销单价写入 OrderProduct(含 metadata 三字段 + 行内折扣)。
182
+ *
183
+ * @example
184
+ * applyPromoUnitPriceToLine(newItem, { promoUnitPrice: '5.00', originalBasePrice: 1, strategyTitle: '2 for 10' });
185
+ */
134
186
  function applyPromoUnitPriceToLine(item, input) {
135
187
  const metadata = item.metadata || (item.metadata = {});
136
- const optionSum = (0, import_utils.sumOptionUnitPrice)((0, import_utils.getProductSkuOptions)(item)).toNumber();
137
- const promoUnitPrice = new import_decimal.default(Number(input.promoUnitPrice) || 0).toDecimalPlaces(2).toFixed(2);
138
- const mainSelling = new import_decimal.default(Number(promoUnitPrice) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
139
- const catalogSource = new import_decimal.default(input.originalBasePrice).toDecimalPlaces(2).toFixed(2);
188
+ const optionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(item)).toNumber();
189
+ const promoUnitPrice = new _decimal.default(Number(input.promoUnitPrice) || 0).toDecimalPlaces(2).toFixed(2);
190
+ const mainSelling = new _decimal.default(Number(promoUnitPrice) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
191
+ const catalogSource = new _decimal.default(input.originalBasePrice).toDecimalPlaces(2).toFixed(2);
140
192
  metadata.source_product_price = catalogSource;
141
- metadata.main_product_original_price = new import_decimal.default(Number(catalogSource) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
193
+ metadata.main_product_original_price = new _decimal.default(Number(catalogSource) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
142
194
  metadata.main_product_selling_price = mainSelling;
143
195
  metadata.price_schema_version = 2;
144
- item.selling_price = (0, import_utils.composeLinePrice)({
196
+ item.selling_price = (0, _utils.composeLinePrice)({
145
197
  mainPrice: mainSelling,
146
198
  bundle: item.product_bundle
147
199
  });
148
- item.original_price = (0, import_utils.composeLinePrice)({
200
+ item.original_price = (0, _utils.composeLinePrice)({
149
201
  mainPrice: metadata.main_product_original_price,
150
202
  bundle: item.product_bundle,
151
203
  useOriginalBundle: true
152
204
  });
153
- const discountAmount = new import_decimal.default(input.originalBasePrice).minus(Number(promoUnitPrice) || 0).toDecimalPlaces(2).toFixed(2);
205
+ const discountAmount = new _decimal.default(input.originalBasePrice).minus(Number(promoUnitPrice) || 0).toDecimalPlaces(2).toFixed(2);
154
206
  if (Number(catalogSource) !== Number(promoUnitPrice)) {
155
207
  updatePromotionDiscountList(item, {
156
- title: input.strategyTitle || "",
208
+ title: input.strategyTitle || '',
157
209
  amount: discountAmount,
158
210
  original_amount: catalogSource,
159
211
  fixed_amount: discountAmount
160
212
  }, {
161
213
  actionType: input.actionType,
162
- discountType: input.actionType === ITEM_REWARD ? "product" : "promotion"
214
+ discountType: input.actionType === ITEM_REWARD ? 'product' : 'promotion'
163
215
  });
164
216
  } else {
165
217
  updatePromotionDiscountList(item, null);
166
218
  }
167
219
  }
168
220
  function getPromotionActionType(promotion) {
169
- var _a, _b;
170
- if (promotion == null ? void 0 : promotion.actionType)
171
- return String(promotion.actionType);
172
- const actions = (_a = promotion == null ? void 0 : promotion.strategyMetadata) == null ? void 0 : _a.actions;
173
- if (Array.isArray(actions) && ((_b = actions[0]) == null ? void 0 : _b.type))
174
- return String(actions[0].type);
175
- return void 0;
221
+ if (promotion?.actionType) return String(promotion.actionType);
222
+ const actions = promotion?.strategyMetadata?.actions;
223
+ if (Array.isArray(actions) && actions[0]?.type) return String(actions[0].type);
224
+ return undefined;
176
225
  }
226
+
227
+ /**
228
+ * 同款行凑满 X件Y元 且仅该行参与时,按 Y/X 计算期望单价(2 件 $10 → $5)。
229
+ *
230
+ * @example
231
+ * resolveXItemsExpectedUnitPrice({ actionType: 'X_ITEMS_FOR_Y_PRICE', actionDetail: { x: 2, price: 10 } }, 2);
232
+ */
177
233
  function resolveXItemsExpectedUnitPrice(promotion, sourceCartNum) {
178
- if (getPromotionActionType(promotion) !== X_ITEMS_FOR_Y_PRICE)
179
- return void 0;
180
- const detail = promotion == null ? void 0 : promotion.actionDetail;
181
- const x = Number(detail == null ? void 0 : detail.x);
182
- const y = Number(detail == null ? void 0 : detail.price);
183
- if (!Number.isFinite(x) || x <= 0 || !Number.isFinite(y))
184
- return void 0;
185
- if (sourceCartNum < x)
186
- return void 0;
187
- return Number(new import_decimal.default(y).div(x).toDecimalPlaces(2).toFixed(2));
234
+ if (getPromotionActionType(promotion) !== X_ITEMS_FOR_Y_PRICE) return undefined;
235
+ const detail = promotion?.actionDetail;
236
+ const x = Number(detail?.x);
237
+ const y = Number(detail?.price);
238
+ if (!Number.isFinite(x) || x <= 0 || !Number.isFinite(y)) return undefined;
239
+ if (sourceCartNum < x) return undefined;
240
+ return Number(new _decimal.default(y).div(x).toDecimalPlaces(2).toFixed(2));
188
241
  }
242
+
243
+ /**
244
+ * 合并「同商品 + 同规格指纹 + 同促销策略 + 同促销单价」的参与促销行(num 累加)。
245
+ * 避免 Browse Items 连续加购同款时拆成多行且单价被错误均摊。
246
+ *
247
+ * @example
248
+ * mergeIdenticalPromotionCartLines(processedProducts);
249
+ */
189
250
  function mergeIdenticalPromotionCartLines(products) {
190
- var _a, _b, _c, _d;
191
251
  if (!Array.isArray(products) || products.length <= 1) {
192
- return [...products || []];
252
+ return [...(products || [])];
193
253
  }
194
254
  const merged = [];
195
- const indexByKey = /* @__PURE__ */ new Map();
255
+ const indexByKey = new Map();
196
256
  for (const line of products) {
197
257
  if (getGiftInfo(line)) {
198
258
  merged.push(line);
199
259
  continue;
200
260
  }
201
- const promotion = (_a = line == null ? void 0 : line.metadata) == null ? void 0 : _a._promotion;
202
- if (!(promotion == null ? void 0 : promotion.inPromotion) || !promotion.strategyId) {
261
+ const promotion = line?.metadata?._promotion;
262
+ if (!promotion?.inPromotion || !promotion.strategyId) {
203
263
  merged.push(line);
204
264
  continue;
205
265
  }
206
- const fingerprint = (0, import_utils2.buildProductLineFingerprint)(
207
- (0, import_utils.getProductSkuOptions)(line),
208
- line.product_bundle
209
- );
210
- const unitPrice = String(
211
- ((_b = line.metadata) == null ? void 0 : _b.source_product_price) ?? line.selling_price ?? ""
212
- );
213
- const promoUnitPrice = String(
214
- promotion.finalPrice ?? ((_c = line.metadata) == null ? void 0 : _c.main_product_selling_price) ?? ""
215
- );
266
+ const fingerprint = (0, _utils2.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(line), line.product_bundle);
267
+ const unitPrice = String(line.metadata?.source_product_price ?? line.selling_price ?? '');
268
+ const promoUnitPrice = String(promotion.finalPrice ?? line.metadata?.main_product_selling_price ?? '');
216
269
  const noteSignature = getProductLineNoteSignature(line);
217
270
  const lineMergeBoundary = getPromotionLineMergeBoundary(line);
218
- const bookingUid = (line == null ? void 0 : line.booking_uid) ?? ((_d = line == null ? void 0 : line.metadata) == null ? void 0 : _d.booking_uid);
219
- const orderDetailId = (line == null ? void 0 : line.order_detail_id) ?? (line == null ? void 0 : line.orderDetailId);
220
- const mergeKey = [
221
- line.product_id,
222
- line.product_variant_id ?? 0,
223
- fingerprint,
224
- noteSignature,
225
- lineMergeBoundary,
226
- bookingUid ? String(bookingUid) : "",
227
- orderDetailId !== void 0 && orderDetailId !== null && orderDetailId !== "" ? String(orderDetailId) : "",
228
- promotion.strategyId,
229
- unitPrice,
230
- promoUnitPrice
231
- ].join("#");
271
+ const bookingUid = line?.booking_uid ?? line?.metadata?.booking_uid;
272
+ const orderDetailId = line?.order_detail_id ?? line?.orderDetailId;
273
+ const mergeKey = [line.product_id, line.product_variant_id ?? 0, fingerprint, noteSignature, lineMergeBoundary, bookingUid ? String(bookingUid) : '', orderDetailId !== undefined && orderDetailId !== null && orderDetailId !== '' ? String(orderDetailId) : '', promotion.strategyId, unitPrice, promoUnitPrice].join('#');
232
274
  const hitIndex = indexByKey.get(mergeKey);
233
- if (hitIndex === void 0) {
275
+ if (hitIndex === undefined) {
234
276
  indexByKey.set(mergeKey, merged.length);
235
277
  merged.push(line);
236
278
  continue;
237
279
  }
238
280
  const target = merged[hitIndex];
239
- target.num = (0, import_utils2.getSafeProductNum)(Number(target.num || 0) + Number(line.num || 0));
281
+ target.num = (0, _utils2.getSafeProductNum)(Number(target.num || 0) + Number(line.num || 0));
240
282
  }
241
283
  return merged;
242
284
  }
285
+
286
+ /**
287
+ * 同一原购物车行被多个「inPromotion」评估结果命中时(如买赠不改价 + X件Y元),
288
+ * 优先保留 X件Y元 定价,并恢复加购前的行数量。
289
+ *
290
+ * @example
291
+ * dedupePromotionProcessedLines(processedProducts);
292
+ */
243
293
  function dedupePromotionProcessedLines(lines) {
244
- var _a, _b, _c, _d, _e, _f, _g, _h;
245
- if (!Array.isArray(lines) || lines.length <= 1)
246
- return [...lines || []];
294
+ if (!Array.isArray(lines) || lines.length <= 1) return [...(lines || [])];
247
295
  const passthrough = [];
248
- const buckets = /* @__PURE__ */ new Map();
296
+ const buckets = new Map();
249
297
  for (const line of lines) {
250
- const promo = (_a = line == null ? void 0 : line.metadata) == null ? void 0 : _a._promotion;
251
- const originalUid = String(line._originalProductUid || getOrderProductUid(line) || "");
252
- if (!(promo == null ? void 0 : promo.inPromotion) || !originalUid) {
298
+ const promo = line?.metadata?._promotion;
299
+ const originalUid = String(line._originalProductUid || getOrderProductUid(line) || '');
300
+ if (!promo?.inPromotion || !originalUid) {
253
301
  passthrough.push(line);
254
302
  continue;
255
303
  }
@@ -257,13 +305,11 @@ function dedupePromotionProcessedLines(lines) {
257
305
  group.push(line);
258
306
  buckets.set(originalUid, group);
259
307
  }
260
- const promotionDelta = (line) => {
261
- var _a2;
262
- const promo = (_a2 = line.metadata) == null ? void 0 : _a2._promotion;
263
- const original = Number(promo == null ? void 0 : promo.originalPrice);
264
- const final = Number(promo == null ? void 0 : promo.finalPrice);
265
- if (!Number.isFinite(original) || !Number.isFinite(final))
266
- return 0;
308
+ const promotionDelta = line => {
309
+ const promo = line.metadata?._promotion;
310
+ const original = Number(promo?.originalPrice);
311
+ const final = Number(promo?.finalPrice);
312
+ if (!Number.isFinite(original) || !Number.isFinite(final)) return 0;
267
313
  return Math.abs(original - final);
268
314
  };
269
315
  const deduped = [...passthrough];
@@ -272,73 +318,53 @@ function dedupePromotionProcessedLines(lines) {
272
318
  deduped.push(group[0]);
273
319
  continue;
274
320
  }
275
- const sourceCartNum = Math.max(
276
- ...group.map((line) => Number(line._sourceCartNum || line.num || 0))
277
- );
278
- const pricingLines = group.filter((line) => promotionDelta(line) > 0);
279
- const xItemsLines = (pricingLines.length ? pricingLines : group).filter(
280
- (line) => {
281
- var _a2;
282
- return getPromotionActionType((_a2 = line.metadata) == null ? void 0 : _a2._promotion) === X_ITEMS_FOR_Y_PRICE;
283
- }
284
- );
321
+ const sourceCartNum = Math.max(...group.map(line => Number(line._sourceCartNum || line.num || 0)));
322
+ const pricingLines = group.filter(line => promotionDelta(line) > 0);
323
+ const xItemsLines = (pricingLines.length ? pricingLines : group).filter(line => getPromotionActionType(line.metadata?._promotion) === X_ITEMS_FOR_Y_PRICE);
285
324
  const pool = xItemsLines.length ? xItemsLines : pricingLines.length ? pricingLines : group;
286
325
  let winner = pool[0];
287
- const expectedUnit = resolveXItemsExpectedUnitPrice(
288
- (_b = winner.metadata) == null ? void 0 : _b._promotion,
289
- sourceCartNum
290
- );
326
+ const expectedUnit = resolveXItemsExpectedUnitPrice(winner.metadata?._promotion, sourceCartNum);
291
327
  if (expectedUnit != null && pool.length > 1) {
292
328
  winner = pool.reduce((best, line) => {
293
- var _a2, _b2, _c2, _d2;
294
- const bestDiff = Math.abs(Number((_b2 = (_a2 = best.metadata) == null ? void 0 : _a2._promotion) == null ? void 0 : _b2.finalPrice) - expectedUnit);
295
- const lineDiff = Math.abs(Number((_d2 = (_c2 = line.metadata) == null ? void 0 : _c2._promotion) == null ? void 0 : _d2.finalPrice) - expectedUnit);
329
+ const bestDiff = Math.abs(Number(best.metadata?._promotion?.finalPrice) - expectedUnit);
330
+ const lineDiff = Math.abs(Number(line.metadata?._promotion?.finalPrice) - expectedUnit);
296
331
  return lineDiff < bestDiff ? line : best;
297
332
  }, pool[0]);
298
333
  } else if (pool.length > 1 && !expectedUnit) {
299
334
  winner = pool.reduce((best, line) => {
300
- var _a2, _b2, _c2, _d2, _e2, _f2;
301
- const bestFinal = Number((_b2 = (_a2 = best.metadata) == null ? void 0 : _a2._promotion) == null ? void 0 : _b2.finalPrice);
302
- const lineFinal = Number((_d2 = (_c2 = line.metadata) == null ? void 0 : _c2._promotion) == null ? void 0 : _d2.finalPrice);
303
- const original = Number((_f2 = (_e2 = line.metadata) == null ? void 0 : _e2._promotion) == null ? void 0 : _f2.originalPrice);
335
+ const bestFinal = Number(best.metadata?._promotion?.finalPrice);
336
+ const lineFinal = Number(line.metadata?._promotion?.finalPrice);
337
+ const original = Number(line.metadata?._promotion?.originalPrice);
304
338
  if (Number.isFinite(original) && lineFinal >= original && bestFinal >= original) {
305
339
  return lineFinal < bestFinal ? line : best;
306
340
  }
307
341
  return promotionDelta(line) > promotionDelta(best) ? line : best;
308
342
  }, pool[0]);
309
343
  }
310
- const winnerIsXItems = getPromotionActionType((_c = winner.metadata) == null ? void 0 : _c._promotion) === X_ITEMS_FOR_Y_PRICE;
344
+
345
+ // X件Y元:评估器会按单件均摊产出多条 inPromotion 行(如 $1 + $9),
346
+ // dedupe 应合并为「同组 X件Y元 行 num 之和」,不能用 sourceCartNum(整单 3 件)。
347
+ const winnerIsXItems = getPromotionActionType(winner.metadata?._promotion) === X_ITEMS_FOR_Y_PRICE;
311
348
  if (winnerIsXItems) {
312
- const xItemsGroupLines = group.filter(
313
- (line) => {
314
- var _a2;
315
- return getPromotionActionType((_a2 = line.metadata) == null ? void 0 : _a2._promotion) === X_ITEMS_FOR_Y_PRICE;
316
- }
317
- );
318
- const evaluatedPromoQty = xItemsGroupLines.reduce(
319
- (sum, line) => sum + Number(line.num || 0),
320
- 0
321
- );
322
- winner.num = (0, import_utils2.getSafeProductNum)(evaluatedPromoQty || winner.num || 1);
349
+ const xItemsGroupLines = group.filter(line => getPromotionActionType(line.metadata?._promotion) === X_ITEMS_FOR_Y_PRICE);
350
+ const evaluatedPromoQty = xItemsGroupLines.reduce((sum, line) => sum + Number(line.num || 0), 0);
351
+ winner.num = (0, _utils2.getSafeProductNum)(evaluatedPromoQty || winner.num || 1);
323
352
  } else {
324
- winner.num = (0, import_utils2.getSafeProductNum)(sourceCartNum || winner.num || 1);
353
+ winner.num = (0, _utils2.getSafeProductNum)(sourceCartNum || winner.num || 1);
325
354
  }
326
- const recomputeUnit = resolveXItemsExpectedUnitPrice(
327
- (_d = winner.metadata) == null ? void 0 : _d._promotion,
328
- sourceCartNum
329
- );
355
+ const recomputeUnit = resolveXItemsExpectedUnitPrice(winner.metadata?._promotion, sourceCartNum);
330
356
  if (recomputeUnit != null) {
331
357
  const promo = winner.metadata._promotion;
332
358
  applyPromoUnitPriceToLine(winner, {
333
359
  promoUnitPrice: recomputeUnit,
334
360
  originalBasePrice: Number(promo.originalPrice),
335
- strategyTitle: (_e = promo.strategyMetadata) == null ? void 0 : _e.name
361
+ strategyTitle: promo.strategyMetadata?.name
336
362
  });
337
363
  promo.finalPrice = recomputeUnit;
338
364
  }
339
365
  for (const line of group) {
340
- const giftInfo = (_g = (_f = line.metadata) == null ? void 0 : _f._promotion) == null ? void 0 : _g.giftInfo;
341
- if (giftInfo && ((_h = winner.metadata) == null ? void 0 : _h._promotion) && !winner.metadata._promotion.giftInfo) {
366
+ const giftInfo = line.metadata?._promotion?.giftInfo;
367
+ if (giftInfo && winner.metadata?._promotion && !winner.metadata._promotion.giftInfo) {
342
368
  winner.metadata._promotion.giftInfo = giftInfo;
343
369
  }
344
370
  }
@@ -346,155 +372,141 @@ function dedupePromotionProcessedLines(lines) {
346
372
  }
347
373
  return deduped;
348
374
  }
375
+
376
+ /** 获取行 uid(统一以 `metadata.unique_identification_number` 为唯一键) */
349
377
  function getOrderProductUid(product) {
350
- var _a;
351
- return String(
352
- ((_a = product == null ? void 0 : product.metadata) == null ? void 0 : _a.unique_identification_number) ?? (product == null ? void 0 : product.unique_identification_number) ?? ""
353
- );
378
+ return String(product?.metadata?.unique_identification_number ?? product?.unique_identification_number ?? '');
354
379
  }
355
380
  function getGiftInfo(product) {
356
- var _a;
357
- return ((_a = product == null ? void 0 : product.metadata) == null ? void 0 : _a._giftInfo) || (product == null ? void 0 : product._giftInfo) || null;
381
+ return product?.metadata?._giftInfo || product?._giftInfo || null;
358
382
  }
359
383
  function getProductLineNoteSignature(product) {
360
- const note = product == null ? void 0 : product.note;
361
- if (typeof note !== "string")
362
- return "";
384
+ const note = product?.note;
385
+ if (typeof note !== 'string') return '';
363
386
  const normalizedNote = note.trim();
364
- if (!normalizedNote)
365
- return "";
387
+ if (!normalizedNote) return '';
366
388
  const uid = getOrderProductUid(product);
367
389
  return uid ? `note:${uid}` : `note:${normalizedNote}`;
368
390
  }
369
391
  function hasGoodPassDiscount(product) {
370
- var _a, _b;
371
- const productBundle = Array.isArray(product == null ? void 0 : product.product_bundle) ? product.product_bundle : [];
372
- const extendedBundle = Array.isArray((_b = (_a = product == null ? void 0 : product._extend) == null ? void 0 : _a.other) == null ? void 0 : _b.bundle) ? product._extend.other.bundle : [];
373
- const discountLists = [
374
- product == null ? void 0 : product.discount_list,
375
- ...productBundle.map((item) => item == null ? void 0 : item.discount_list),
376
- ...extendedBundle.map((item) => item == null ? void 0 : item.discount_list)
377
- ];
378
- return discountLists.some(
379
- (discountList) => Array.isArray(discountList) && discountList.some(
380
- (item) => (item == null ? void 0 : item.type) === "good_pass" || (item == null ? void 0 : item.tag) === "good_pass"
381
- )
382
- );
392
+ const productBundle = Array.isArray(product?.product_bundle) ? product.product_bundle : [];
393
+ const extendedBundle = Array.isArray(product?._extend?.other?.bundle) ? product._extend.other.bundle : [];
394
+ const discountLists = [product?.discount_list, ...productBundle.map(item => item?.discount_list), ...extendedBundle.map(item => item?.discount_list)];
395
+ return discountLists.some(discountList => Array.isArray(discountList) && discountList.some(item => item?.type === 'good_pass' || item?.tag === 'good_pass'));
383
396
  }
397
+
398
+ /**
399
+ * 商品券、自定义商品、称重行以及编辑订单历史行都有各自的促销合并边界。
400
+ */
384
401
  function getPromotionLineMergeBoundary(product) {
385
- var _a, _b;
386
- if ((product == null ? void 0 : product.product_id) === void 0 || (product == null ? void 0 : product.product_id) === null) {
402
+ if (product?.product_id === undefined || product?.product_id === null) {
387
403
  return `custom:${getOrderProductUid(product)}`;
388
404
  }
389
405
  if (hasGoodPassDiscount(product)) {
390
406
  return `good-pass:${getOrderProductUid(product)}`;
391
407
  }
392
- if (Number(((_a = product == null ? void 0 : product.product_sku) == null ? void 0 : _a.open_sold_weight) || 0) === 1) {
408
+ if (Number(product?.product_sku?.open_sold_weight || 0) === 1) {
393
409
  return `weighing:${getOrderProductUid(product)}`;
394
410
  }
395
- if (((_b = product == null ? void 0 : product.metadata) == null ? void 0 : _b.is_edit_for_runtime) !== true)
396
- return "";
411
+ if (product?.metadata?.is_edit_for_runtime !== true) return '';
397
412
  return `edit:${getOrderProductUid(product)}`;
398
413
  }
399
414
  function isPersistedGeneratedVoucher(product) {
400
- const orderDetailId = (product == null ? void 0 : product.order_detail_id) ?? (product == null ? void 0 : product.orderDetailId);
401
- if (orderDetailId === void 0 || orderDetailId === null || orderDetailId === "") {
415
+ const orderDetailId = product?.order_detail_id ?? product?.orderDetailId;
416
+ if (orderDetailId === undefined || orderDetailId === null || orderDetailId === '') {
402
417
  return false;
403
418
  }
404
- const metadata = (product == null ? void 0 : product.metadata) || {};
405
- return (product == null ? void 0 : product.extension_type) === "product_voucher" || metadata.parent_order_detail_id !== void 0 || metadata.gift_card_type !== void 0;
419
+ const metadata = product?.metadata || {};
420
+ return product?.extension_type === 'product_voucher' || metadata.parent_order_detail_id !== undefined || metadata.gift_card_type !== undefined;
406
421
  }
407
422
  function getProductBundleForEvaluator(product) {
408
- const bundles = Array.isArray(product == null ? void 0 : product.product_bundle) ? product.product_bundle : [];
409
- return bundles.map((b) => ({
410
- product_id: (b == null ? void 0 : b.bundle_product_id) ?? (b == null ? void 0 : b._bundle_product_id) ?? (b == null ? void 0 : b.product_id),
411
- product_variant_id: (b == null ? void 0 : b.bundle_variant_id) ?? (b == null ? void 0 : b.product_variant_id) ?? 0,
412
- quantity: Number((b == null ? void 0 : b.num) ?? (b == null ? void 0 : b.quantity) ?? 1) || 1,
413
- price: Number((b == null ? void 0 : b.bundle_selling_price) ?? (b == null ? void 0 : b.price) ?? 0) || 0,
414
- original_price: Number((b == null ? void 0 : b.original_price) ?? (b == null ? void 0 : b.product_price) ?? (b == null ? void 0 : b.price) ?? 0) || 0,
415
- id: (b == null ? void 0 : b.bundle_id) ?? (b == null ? void 0 : b.id) ?? 0,
416
- _bundle_product_id: (b == null ? void 0 : b._bundle_product_id) ?? (b == null ? void 0 : b.bundle_product_id) ?? (b == null ? void 0 : b.product_id)
423
+ const bundles = Array.isArray(product?.product_bundle) ? product.product_bundle : [];
424
+ return bundles.map(b => ({
425
+ product_id: b?.bundle_product_id ?? b?._bundle_product_id ?? b?.product_id,
426
+ product_variant_id: b?.bundle_variant_id ?? b?.product_variant_id ?? 0,
427
+ quantity: Number(b?.num ?? b?.quantity ?? 1) || 1,
428
+ price: Number(b?.bundle_selling_price ?? b?.price ?? 0) || 0,
429
+ original_price: Number(b?.original_price ?? b?.product_price ?? b?.price ?? 0) || 0,
430
+ id: b?.bundle_id ?? b?.id ?? 0,
431
+ _bundle_product_id: b?._bundle_product_id ?? b?.bundle_product_id ?? b?.product_id
417
432
  }));
418
433
  }
434
+
435
+ /**
436
+ * 促销评估前合并同 SKU 拆分行(含促销/原价行),避免加购后仅前 2 件享受优惠。
437
+ * 带 `booking_uid` 的预约行不参与合并(每次加车独立 booking + 商品行)。
438
+ *
439
+ * @example
440
+ * consolidateMainProductsForPromotion([
441
+ * { item: promoLineNum2, originalListIndex: 0 },
442
+ * { item: regularLineNum2, originalListIndex: 1 },
443
+ * ]);
444
+ * // => 单行 num=4,清除 _promotion 后重新评估
445
+ */
419
446
  function buildConsolidateKeyForPromotion(item) {
420
- var _a;
421
- const fingerprint = (0, import_utils2.buildProductLineFingerprint)(
422
- (0, import_utils.getProductSkuOptions)(item),
423
- item.product_bundle
424
- );
447
+ const fingerprint = (0, _utils2.buildProductLineFingerprint)((0, _utils.getProductSkuOptions)(item), item.product_bundle);
425
448
  const noteSignature = getProductLineNoteSignature(item);
426
449
  const lineMergeBoundary = getPromotionLineMergeBoundary(item);
427
- const bookingUid = (item == null ? void 0 : item.booking_uid) ?? ((_a = item == null ? void 0 : item.metadata) == null ? void 0 : _a.booking_uid);
428
- const orderDetailId = (item == null ? void 0 : item.order_detail_id) ?? (item == null ? void 0 : item.orderDetailId);
429
- if (orderDetailId !== void 0 && orderDetailId !== null && orderDetailId !== "") {
430
- return [
431
- item.product_id,
432
- item.product_variant_id ?? 0,
433
- fingerprint,
434
- noteSignature,
435
- lineMergeBoundary,
436
- String(orderDetailId)
437
- ].join("#");
450
+ const bookingUid = item?.booking_uid ?? item?.metadata?.booking_uid;
451
+ const orderDetailId = item?.order_detail_id ?? item?.orderDetailId;
452
+ if (orderDetailId !== undefined && orderDetailId !== null && orderDetailId !== '') {
453
+ return [item.product_id, item.product_variant_id ?? 0, fingerprint, noteSignature, lineMergeBoundary, String(orderDetailId)].join('#');
438
454
  }
439
- if (bookingUid !== void 0 && bookingUid !== null && String(bookingUid) !== "") {
440
- return [
441
- item.product_id,
442
- item.product_variant_id ?? 0,
443
- fingerprint,
444
- noteSignature,
445
- lineMergeBoundary,
446
- String(bookingUid)
447
- ].join("#");
455
+ if (bookingUid !== undefined && bookingUid !== null && String(bookingUid) !== '') {
456
+ return [item.product_id, item.product_variant_id ?? 0, fingerprint, noteSignature, lineMergeBoundary, String(bookingUid)].join('#');
448
457
  }
449
- return [
450
- item.product_id,
451
- item.product_variant_id ?? 0,
452
- fingerprint,
453
- noteSignature,
454
- lineMergeBoundary
455
- ].join("#");
458
+ return [item.product_id, item.product_variant_id ?? 0, fingerprint, noteSignature, lineMergeBoundary].join('#');
456
459
  }
457
460
  function consolidateMainProductsForPromotion(entries) {
458
461
  if (!Array.isArray(entries) || entries.length <= 1) {
459
- return [...entries || []];
462
+ return [...(entries || [])];
460
463
  }
461
- const groups = /* @__PURE__ */ new Map();
462
- for (const { item, originalListIndex } of entries) {
464
+ const groups = new Map();
465
+ for (const {
466
+ item,
467
+ originalListIndex
468
+ } of entries) {
463
469
  const key = buildConsolidateKeyForPromotion(item);
464
470
  const wasInPromotion = wasInPromotionBeforeEvaluation(item);
465
471
  const existing = groups.get(key);
466
472
  if (!existing) {
467
- const mergedItem = (0, import_lodash_es.cloneDeep)(item);
473
+ const mergedItem = (0, _lodashEs.cloneDeep)(item);
468
474
  const metadata = mergedItem.metadata || {};
469
475
  delete metadata._promotion;
470
476
  mergedItem.metadata = metadata;
471
477
  if (Array.isArray(mergedItem.product_bundle)) {
472
478
  for (const bundle of mergedItem.product_bundle) {
473
- if (bundle && typeof bundle === "object")
474
- delete bundle._promotion;
479
+ if (bundle && typeof bundle === 'object') delete bundle._promotion;
475
480
  }
476
481
  }
477
482
  if (wasInPromotion) {
478
483
  mergedItem._wasInPromotionBeforeConsolidate = true;
479
484
  }
480
- groups.set(key, { item: mergedItem, originalListIndex });
485
+ groups.set(key, {
486
+ item: mergedItem,
487
+ originalListIndex
488
+ });
481
489
  continue;
482
490
  }
483
- existing.item.num = (0, import_utils2.getSafeProductNum)(
484
- Number(existing.item.num || 0) + Number(item.num || 0)
485
- );
491
+ existing.item.num = (0, _utils2.getSafeProductNum)(Number(existing.item.num || 0) + Number(item.num || 0));
486
492
  if (wasInPromotion) {
487
493
  existing.item._wasInPromotionBeforeConsolidate = true;
488
494
  }
489
495
  }
490
496
  return Array.from(groups.values());
491
497
  }
498
+
499
+ /**
500
+ * X件Y元 整组凑满时按每组 x 件拆开展示(4 件 → 2 行各 2 件 $10)。
501
+ *
502
+ * @example
503
+ * splitXItemsPromoLinesByGroup(mergedLines, strategyActionMap);
504
+ */
492
505
  function splitXItemsPromoLinesByGroup(lines, strategyActionMap) {
493
- var _a;
494
506
  const result = [];
495
507
  for (const line of lines) {
496
- const promo = (_a = line == null ? void 0 : line.metadata) == null ? void 0 : _a._promotion;
497
- if (!(promo == null ? void 0 : promo.inPromotion) || !promo.strategyId) {
508
+ const promo = line?.metadata?._promotion;
509
+ if (!promo?.inPromotion || !promo.strategyId) {
498
510
  result.push(line);
499
511
  continue;
500
512
  }
@@ -503,44 +515,53 @@ function splitXItemsPromoLinesByGroup(lines, strategyActionMap) {
503
515
  continue;
504
516
  }
505
517
  const actionMeta = strategyActionMap.get(promo.strategyId);
506
- const detail = (actionMeta == null ? void 0 : actionMeta.actionDetail) ?? promo.actionDetail;
507
- const x = Number(detail == null ? void 0 : detail.x);
518
+ const detail = actionMeta?.actionDetail ?? promo.actionDetail;
519
+ const x = Number(detail?.x);
508
520
  const num = Number(line.num || 0);
509
- const isCumulative = (detail == null ? void 0 : detail.cumulative) !== false;
521
+ const isCumulative = detail?.cumulative !== false;
510
522
  if (!isCumulative || !Number.isFinite(x) || x <= 0 || num <= x || num % x !== 0) {
511
523
  result.push(line);
512
524
  continue;
513
525
  }
514
526
  const groupCount = num / x;
515
527
  for (let i = 0; i < groupCount; i++) {
516
- const splitLine = (0, import_lodash_es.cloneDeep)(line);
517
- splitLine.num = (0, import_utils2.getSafeProductNum)(x);
528
+ const splitLine = (0, _lodashEs.cloneDeep)(line);
529
+ splitLine.num = (0, _utils2.getSafeProductNum)(x);
518
530
  splitLine.metadata = {
519
- ...splitLine.metadata || {},
520
- unique_identification_number: (0, import_utils.createUuidV4)()
531
+ ...(splitLine.metadata || {}),
532
+ unique_identification_number: (0, _utils.createUuidV4)()
521
533
  };
522
534
  result.push(splitLine);
523
535
  }
524
536
  }
525
537
  return result;
526
538
  }
539
+
540
+ /** 生成新 numeric id(仅用于评估器内部追踪) */
527
541
  function generateNumericId() {
528
- return Math.floor(Math.random() * 1e9) + Date.now();
542
+ return Math.floor(Math.random() * 1_000_000_000) + Date.now();
529
543
  }
530
544
  function isBookingEditProductLine(product) {
531
- return (product == null ? void 0 : product.booking_id) !== void 0 && (product == null ? void 0 : product.booking_id) !== null && product.booking_id !== 0 && product.booking_id !== "0";
545
+ return product?.booking_id !== undefined && product?.booking_id !== null && product.booking_id !== 0 && product.booking_id !== '0';
532
546
  }
547
+
548
+ /**
549
+ * 把 OrderProduct 转成 evaluator 的 PromotionProduct(仅评估期使用)。
550
+ *
551
+ * 注意:评估器是「无状态、按 product_id + variant 组」工作的;这里给每行分配一个
552
+ * numericId 用于关联回原 OrderProduct(通过 originalItemMap)。
553
+ */
533
554
  function convertToPromotionProduct(product, index) {
534
- const metadata = (product == null ? void 0 : product.metadata) || {};
555
+ const metadata = product?.metadata || {};
535
556
  const numericId = generateNumericId();
536
557
  const originalItem = product;
537
558
  return {
538
559
  id: numericId,
539
- product_id: Number((product == null ? void 0 : product.product_id) ?? 0) || 0,
540
- product_variant_id: Number((product == null ? void 0 : product.product_variant_id) ?? 0) || 0,
541
- name: metadata.product_name || (product == null ? void 0 : product.title) || "",
560
+ product_id: Number(product?.product_id ?? 0) || 0,
561
+ product_variant_id: Number(product?.product_variant_id ?? 0) || 0,
562
+ name: metadata.product_name || product?.title || '',
542
563
  price: getOrderProductOriginalPriceForPromotion(product),
543
- quantity: Number((product == null ? void 0 : product.num) ?? 1) || 1,
564
+ quantity: Number(product?.num ?? 1) || 1,
544
565
  bundle: getProductBundleForEvaluator(product),
545
566
  _promotionOnlyForItemReward: isBookingEditProductLine(product),
546
567
  _originalItem: originalItem,
@@ -548,35 +569,55 @@ function convertToPromotionProduct(product, index) {
548
569
  _originalIndex: index
549
570
  };
550
571
  }
572
+
573
+ // ============================================
574
+ // 主流程:processCartPromotion
575
+ // ============================================
576
+
577
+ /**
578
+ * 计算赠品差异化操作(toAdd / toReduce / toRemove)。
579
+ *
580
+ * 与 legacy `calculateGiftActions` 等价;唯一差别是行键从 `_id` 改成 `metadata.unique_identification_number`。
581
+ */
551
582
  function calculateGiftActions(gifts, existingGifts) {
552
- var _a, _b, _c;
553
583
  const toAdd = [];
554
584
  const toReduce = [];
555
585
  const toRemove = [];
556
- const existingByStrategy = /* @__PURE__ */ new Map();
586
+
587
+ // 按 strategyId 分组现有赠品
588
+ const existingByStrategy = new Map();
557
589
  for (const gift of existingGifts) {
558
- const strategyId = (_a = getGiftInfo(gift)) == null ? void 0 : _a.strategyId;
559
- if (!strategyId)
560
- continue;
590
+ const strategyId = getGiftInfo(gift)?.strategyId;
591
+ if (!strategyId) continue;
561
592
  const existing = existingByStrategy.get(strategyId);
562
- const qty = Number((gift == null ? void 0 : gift.num) ?? 1) || 1;
593
+ const qty = Number(gift?.num ?? 1) || 1;
563
594
  if (existing) {
564
595
  existing.items.push(gift);
565
596
  existing.totalQuantity += qty;
566
597
  } else {
567
- existingByStrategy.set(strategyId, { items: [gift], totalQuantity: qty });
598
+ existingByStrategy.set(strategyId, {
599
+ items: [gift],
600
+ totalQuantity: qty
601
+ });
568
602
  }
569
603
  }
570
- const processedStrategyIds = /* @__PURE__ */ new Set();
604
+ const processedStrategyIds = new Set();
571
605
  for (const giftInfo of gifts) {
572
- const { strategyId, strategyName, giftCount, giftOptions, sourceProductIds } = giftInfo;
606
+ const {
607
+ strategyId,
608
+ strategyName,
609
+ giftCount,
610
+ giftOptions,
611
+ sourceProductIds
612
+ } = giftInfo;
573
613
  processedStrategyIds.add(strategyId);
574
614
  const existing = existingByStrategy.get(strategyId);
575
- const existingCount = (existing == null ? void 0 : existing.totalQuantity) ?? 0;
615
+ const existingCount = existing?.totalQuantity ?? 0;
576
616
  const diff = giftCount - existingCount;
577
617
  if (diff > 0) {
578
- const lastEvaluated = (_c = getGiftInfo((_b = existing == null ? void 0 : existing.items) == null ? void 0 : _b[0])) == null ? void 0 : _c.lastEvaluatedGiftCount;
579
- const strategyChanged = lastEvaluated === void 0 || giftCount !== lastEvaluated;
618
+ // 判断「用户手动减赠品」: 若评估器的 giftCount 没变,diff>0 说明是手动减,跳过补回
619
+ const lastEvaluated = getGiftInfo(existing?.items?.[0])?.lastEvaluatedGiftCount;
620
+ const strategyChanged = lastEvaluated === undefined || giftCount !== lastEvaluated;
580
621
  if (strategyChanged) {
581
622
  toAdd.push({
582
623
  strategyId,
@@ -587,102 +628,127 @@ function calculateGiftActions(gifts, existingGifts) {
587
628
  });
588
629
  }
589
630
  } else if (diff < 0) {
631
+ // 减赠品:后进先出
590
632
  let remaining = Math.abs(diff);
591
633
  const reduceItems = [];
592
634
  const removeIds = [];
593
- const reversed = [...(existing == null ? void 0 : existing.items) || []].reverse();
635
+ const reversed = [...(existing?.items || [])].reverse();
594
636
  for (const giftItem of reversed) {
595
- if (remaining <= 0)
596
- break;
637
+ if (remaining <= 0) break;
597
638
  const uid = getOrderProductUid(giftItem);
598
- const qty = Number((giftItem == null ? void 0 : giftItem.num) ?? 1) || 1;
639
+ const qty = Number(giftItem?.num ?? 1) || 1;
599
640
  if (qty <= remaining) {
600
641
  removeIds.push(uid);
601
642
  remaining -= qty;
602
643
  } else {
603
- reduceItems.push({ uid, currentQuantity: qty, newQuantity: qty - remaining });
644
+ reduceItems.push({
645
+ uid,
646
+ currentQuantity: qty,
647
+ newQuantity: qty - remaining
648
+ });
604
649
  remaining = 0;
605
650
  }
606
651
  }
607
- if (reduceItems.length > 0)
608
- toReduce.push({ strategyId, items: reduceItems });
652
+ if (reduceItems.length > 0) toReduce.push({
653
+ strategyId,
654
+ items: reduceItems
655
+ });
609
656
  toRemove.push(...removeIds);
610
657
  }
611
658
  }
659
+
660
+ // 失效策略的赠品:全部 remove
612
661
  for (const [strategyId, existing] of existingByStrategy) {
613
- if (processedStrategyIds.has(strategyId))
614
- continue;
662
+ if (processedStrategyIds.has(strategyId)) continue;
615
663
  for (const giftItem of existing.items) {
616
664
  toRemove.push(getOrderProductUid(giftItem));
617
665
  }
618
666
  }
619
- return { toAdd, toReduce, toRemove };
667
+ return {
668
+ toAdd,
669
+ toReduce,
670
+ toRemove
671
+ };
620
672
  }
673
+
674
+ /**
675
+ * 从策略配置的 product_match 规则提取可参与商品列表。
676
+ *
677
+ * @example
678
+ * extractProductMatchIdentifiersFromStrategyConfig(strategyConfig);
679
+ * // => [{ product_id: 60736, product_variant_id: 0 }]
680
+ */
621
681
  function extractProductMatchIdentifiersFromStrategyConfig(strategyConfig) {
622
- var _a;
623
- const rules = (_a = strategyConfig == null ? void 0 : strategyConfig.conditions) == null ? void 0 : _a.rules;
624
- if (!Array.isArray(rules))
625
- return [];
682
+ const rules = strategyConfig?.conditions?.rules;
683
+ if (!Array.isArray(rules)) return [];
626
684
  for (const rule of rules) {
627
- if ((rule == null ? void 0 : rule.field) === "productIdAndVariantId" && ((rule == null ? void 0 : rule.operator) === "product_match" || (rule == null ? void 0 : rule.operator) === "object_in") && Array.isArray(rule == null ? void 0 : rule.value)) {
628
- return rule.value.map((item) => ({
629
- product_id: Number(item == null ? void 0 : item.product_id) || 0,
630
- product_variant_id: Number(item == null ? void 0 : item.product_variant_id) || 0
631
- })).filter((item) => item.product_id > 0);
685
+ if (rule?.field === 'productIdAndVariantId' && (rule?.operator === 'product_match' || rule?.operator === 'object_in') && Array.isArray(rule?.value)) {
686
+ return rule.value.map(item => ({
687
+ product_id: Number(item?.product_id) || 0,
688
+ product_variant_id: Number(item?.product_variant_id) || 0
689
+ })).filter(item => item.product_id > 0);
632
690
  }
633
691
  }
634
692
  return [];
635
693
  }
694
+
695
+ /**
696
+ * 解析策略凑单所需件数(evaluator 原始返回可能不含 requiredQuantity)。
697
+ *
698
+ * @example
699
+ * resolveStrategyRequiredQuantity({ actionType: 'X_ITEMS_FOR_Y_PRICE', actionDetail: { x: 2 } });
700
+ */
636
701
  function resolveStrategyRequiredQuantity(matched) {
637
702
  const preset = Number(matched.requiredQuantity);
638
- if (Number.isFinite(preset) && preset > 0)
639
- return preset;
703
+ if (Number.isFinite(preset) && preset > 0) return preset;
640
704
  const detail = matched.actionDetail || {};
641
- if (matched.actionType === "X_ITEMS_FOR_Y_PRICE") {
705
+ if (matched.actionType === 'X_ITEMS_FOR_Y_PRICE') {
642
706
  return Number(detail.x) || 2;
643
707
  }
644
- if (matched.actionType === "BUY_X_GET_Y_FREE") {
708
+ if (matched.actionType === 'BUY_X_GET_Y_FREE') {
645
709
  return Number(detail.buyQuantity) || 1;
646
710
  }
647
- if (matched.actionType === "ITEM_REWARD") {
711
+ if (matched.actionType === 'ITEM_REWARD') {
648
712
  return Number(detail.triggerQuantity) || 1;
649
713
  }
650
714
  return 1;
651
715
  }
716
+
717
+ /**
718
+ * 解析 Browse Items 弹窗候选商品(优先 eligibleProducts,否则读 strategyConfig)。
719
+ *
720
+ * @example
721
+ * resolveStrategyEligibleProducts(matchedStrategy);
722
+ */
652
723
  function resolveStrategyEligibleProducts(matched) {
653
724
  if (Array.isArray(matched.eligibleProducts) && matched.eligibleProducts.length > 0) {
654
725
  return matched.eligibleProducts;
655
726
  }
656
727
  return extractProductMatchIdentifiersFromStrategyConfig(matched.strategyConfig);
657
728
  }
729
+
730
+ /**
731
+ * 计算未满足促销条件(用于购物车底部「再买 N 件即可享受 XX」alert)。
732
+ *
733
+ * 数据源:商品的 `metadata._promotion.inPromotion === false`(来自 processCartPromotion
734
+ * 第 8 步追加的「有适用策略但当前未参与促销」标记)。
735
+ */
658
736
  function calculateUnfulfilledPromotions(processedProducts, applicableStrategiesResult, mainProducts) {
659
- var _a, _b;
660
- const unfulfilledMap = /* @__PURE__ */ new Map();
737
+ const unfulfilledMap = new Map();
661
738
  for (let i = 0; i < processedProducts.length; i++) {
662
739
  const product = processedProducts[i];
663
- const promotion = (_a = product == null ? void 0 : product.metadata) == null ? void 0 : _a._promotion;
664
- if (!promotion || promotion.inPromotion !== false)
665
- continue;
740
+ const promotion = product?.metadata?._promotion;
741
+ if (!promotion || promotion.inPromotion !== false) continue;
666
742
  const strategyId = promotion.strategyId;
667
- if (!strategyId)
668
- continue;
669
- const lookupUid = String(
670
- (product == null ? void 0 : product._originalProductUid) ?? ((_b = product == null ? void 0 : product.metadata) == null ? void 0 : _b.unique_identification_number) ?? ""
671
- );
672
- const originalIndex = mainProducts.findIndex(
673
- (item) => getOrderProductUid(item) === lookupUid
674
- );
675
- if (originalIndex === -1)
676
- continue;
743
+ if (!strategyId) continue;
744
+ const lookupUid = String(product?._originalProductUid ?? product?.metadata?.unique_identification_number ?? '');
745
+ const originalIndex = mainProducts.findIndex(item => getOrderProductUid(item) === lookupUid);
746
+ if (originalIndex === -1) continue;
677
747
  const strategyData = applicableStrategiesResult[originalIndex];
678
- if (!strategyData || !strategyData.hasApplicableStrategy)
679
- continue;
680
- const matched = strategyData.applicableStrategies.find(
681
- (s) => s.strategyId === strategyId
682
- );
683
- if (!matched)
684
- continue;
685
- const quantity = Number((product == null ? void 0 : product.num) ?? 1) || 1;
748
+ if (!strategyData || !strategyData.hasApplicableStrategy) continue;
749
+ const matched = strategyData.applicableStrategies.find(s => s.strategyId === strategyId);
750
+ if (!matched) continue;
751
+ const quantity = Number(product?.num ?? 1) || 1;
686
752
  if (unfulfilledMap.has(strategyId)) {
687
753
  unfulfilledMap.get(strategyId).currentQuantity += quantity;
688
754
  } else {
@@ -703,32 +769,47 @@ function calculateUnfulfilledPromotions(processedProducts, applicableStrategiesR
703
769
  for (const data of unfulfilledMap.values()) {
704
770
  const needQuantity = data.requiredQuantity - data.currentQuantity;
705
771
  if (needQuantity > 0) {
706
- result.push({ ...data, needQuantity });
772
+ result.push({
773
+ ...data,
774
+ needQuantity
775
+ });
707
776
  }
708
777
  }
709
778
  return result;
710
779
  }
780
+
781
+ /**
782
+ * 给商品列表追加促销标签(写入 `metadata._promotionTags`),供 ProductCard 渲染。
783
+ *
784
+ * - 没有适用策略的商品会清除 `_promotionTags`(避免脏数据)。
785
+ * - 仅基于 product_id 匹配(matchVariant=false),与商品卡通用场景对齐。
786
+ *
787
+ * @example
788
+ * const list = appendPromotionTags(catalog, evaluator);
789
+ * // list[i].metadata._promotionTags = [{ text, type, strategyId }]
790
+ */
711
791
  function appendPromotionTags(products, evaluator) {
712
792
  if (!Array.isArray(products) || products.length === 0 || !evaluator) {
713
793
  return products || [];
714
794
  }
795
+
796
+ // 评估器期望的 product shape,最少字段集
715
797
  const evaluatorInput = products.map((item, index) => ({
716
798
  id: index,
717
- product_id: Number((item == null ? void 0 : item.id) ?? (item == null ? void 0 : item.product_id) ?? 0) || 0,
718
- product_variant_id: Number((item == null ? void 0 : item.product_variant_id) ?? 0) || 0,
719
- name: (item == null ? void 0 : item.title) || (item == null ? void 0 : item.name) || "",
720
- price: Number((item == null ? void 0 : item.price) ?? 0) || 0,
721
- quantity: Number((item == null ? void 0 : item.quantity) ?? 1) || 1,
799
+ product_id: Number(item?.id ?? item?.product_id ?? 0) || 0,
800
+ product_variant_id: Number(item?.product_variant_id ?? 0) || 0,
801
+ name: item?.title || item?.name || '',
802
+ price: Number(item?.price ?? 0) || 0,
803
+ quantity: Number(item?.quantity ?? 1) || 1,
722
804
  bundle: []
723
805
  }));
724
- const strategies = evaluator.getProductsApplicableStrategies(
725
- { products: evaluatorInput },
726
- false
727
- );
806
+ const strategies = evaluator.getProductsApplicableStrategies({
807
+ products: evaluatorInput
808
+ }, false);
728
809
  return products.map((item, index) => {
729
810
  const data = strategies[index];
730
811
  const tags = [];
731
- if (data == null ? void 0 : data.hasApplicableStrategy) {
812
+ if (data?.hasApplicableStrategy) {
732
813
  for (const strategy of data.applicableStrategies) {
733
814
  if (strategy.display) {
734
815
  tags.push({
@@ -739,28 +820,60 @@ function appendPromotionTags(products, evaluator) {
739
820
  }
740
821
  }
741
822
  }
823
+ // 商品卡兼容两种位置:顶层 `_promotionTags` 与 `metadata._promotionTags`
824
+ // ProductCard 现有实现读顶层(详见 productSelect/ProductCard/index.tsx),
825
+ // 此处同时写顶层以保持视图直连;OrderProduct 行版本走 metadata(避免顶层非协议字段)。
742
826
  if (tags.length === 0) {
743
- const { _promotionTags, ...rest } = item;
827
+ const {
828
+ _promotionTags,
829
+ ...rest
830
+ } = item;
831
+ void _promotionTags;
744
832
  return rest;
745
833
  }
746
- return { ...item, _promotionTags: tags };
834
+ return {
835
+ ...item,
836
+ _promotionTags: tags
837
+ };
747
838
  });
748
839
  }
840
+
841
+ /**
842
+ * 处理购物车的促销计算与赠品差异化。
843
+ *
844
+ * 流程:
845
+ * 1. 分离 main / gift / edit-product 三类(普通促销下带 `booking_id` 的编辑态商品不参与;
846
+ * 配置 ITEM_REWARD 时,编辑态商品也参与商品奖励重算)
847
+ * 2. 转 PromotionProduct 喂评估器 → 拿到 `evaluateCartWithPricing` 与 `getProductsApplicableStrategies`
848
+ * 3. 把 PricedProduct 映射回 OrderProduct 形态:
849
+ * - 参与促销的,写 `metadata._promotion` + 调整 `selling_price` / `main_product_selling_price`
850
+ * - 拆分的,生成新 uid,复制原行其它字段
851
+ * 4. 计算 `_giftInfo` 挂主商品(每个策略首个主商品)
852
+ * 5. 计算 `giftActions`(toAdd / toReduce / toRemove)
853
+ * 6. 计算 `unfulfilledPromotions`
854
+ *
855
+ * @example
856
+ * const { products, giftActions, unfulfilledPromotions } = processCartPromotion(
857
+ * tempOrder.products,
858
+ * evaluator,
859
+ * );
860
+ */
749
861
  function processCartPromotion(list, evaluator, options) {
750
- var _a, _b, _c, _d, _e, _f, _g, _h;
751
862
  const empty = {
752
863
  products: Array.isArray(list) ? [...list] : [],
753
864
  totalDiscount: 0,
754
865
  gifts: [],
755
- giftActions: { toAdd: [], toReduce: [], toRemove: [] },
866
+ giftActions: {
867
+ toAdd: [],
868
+ toReduce: [],
869
+ toRemove: []
870
+ },
756
871
  unfulfilledPromotions: []
757
872
  };
758
- if (!list || list.length === 0 || !evaluator)
759
- return empty;
760
- const shouldIncludeEditProductsForItemReward = evaluatorHasActionType(
761
- evaluator,
762
- ITEM_REWARD
763
- );
873
+ if (!list || list.length === 0 || !evaluator) return empty;
874
+
875
+ // 0. 分类
876
+ const shouldIncludeEditProductsForItemReward = evaluatorHasActionType(evaluator, ITEM_REWARD);
764
877
  const mainProductsWithIndex = [];
765
878
  const existingGiftsWithIndex = [];
766
879
  const editProductsWithIndex = [];
@@ -768,38 +881,54 @@ function processCartPromotion(list, evaluator, options) {
768
881
  const item = list[i];
769
882
  const giftInfo = getGiftInfo(item);
770
883
  if (isPersistedGeneratedVoucher(item)) {
771
- editProductsWithIndex.push({ item, originalListIndex: i });
884
+ editProductsWithIndex.push({
885
+ item,
886
+ originalListIndex: i
887
+ });
772
888
  } else if (giftInfo) {
773
- existingGiftsWithIndex.push({ item, originalListIndex: i });
889
+ existingGiftsWithIndex.push({
890
+ item,
891
+ originalListIndex: i
892
+ });
774
893
  } else if (isBookingEditProductLine(item)) {
775
894
  if (shouldIncludeEditProductsForItemReward) {
776
- mainProductsWithIndex.push({ item, originalListIndex: i });
895
+ mainProductsWithIndex.push({
896
+ item,
897
+ originalListIndex: i
898
+ });
777
899
  } else {
778
- editProductsWithIndex.push({ item, originalListIndex: i });
900
+ editProductsWithIndex.push({
901
+ item,
902
+ originalListIndex: i
903
+ });
779
904
  }
780
905
  } else {
781
- mainProductsWithIndex.push({ item, originalListIndex: i });
906
+ mainProductsWithIndex.push({
907
+ item,
908
+ originalListIndex: i
909
+ });
782
910
  }
783
911
  }
784
- const consolidatedMainProductsWithIndex = consolidateMainProductsForPromotion(
785
- mainProductsWithIndex
786
- );
787
- const mainProducts = consolidatedMainProductsWithIndex.map((x) => x.item);
788
- const existingGifts = existingGiftsWithIndex.map((x) => x.item);
912
+ const consolidatedMainProductsWithIndex = consolidateMainProductsForPromotion(mainProductsWithIndex);
913
+ const mainProducts = consolidatedMainProductsWithIndex.map(x => x.item);
914
+ const existingGifts = existingGiftsWithIndex.map(x => x.item);
789
915
  if (mainProducts.length === 0) {
790
- const editProducts = editProductsWithIndex.map((x) => x.item);
916
+ const editProducts = editProductsWithIndex.map(x => x.item);
791
917
  if (existingGifts.length > 0) {
792
918
  const toRemove = [];
793
919
  for (const gift of existingGifts) {
794
920
  const uid = getOrderProductUid(gift);
795
- if (uid)
796
- toRemove.push(uid);
921
+ if (uid) toRemove.push(uid);
797
922
  }
798
923
  return {
799
924
  products: editProducts,
800
925
  totalDiscount: 0,
801
926
  gifts: [],
802
- giftActions: { toAdd: [], toReduce: [], toRemove },
927
+ giftActions: {
928
+ toAdd: [],
929
+ toReduce: [],
930
+ toRemove
931
+ },
803
932
  unfulfilledPromotions: []
804
933
  };
805
934
  }
@@ -808,93 +937,109 @@ function processCartPromotion(list, evaluator, options) {
808
937
  products: editProducts.length > 0 ? editProducts : [...list]
809
938
  };
810
939
  }
940
+
941
+ // 1. 转 PromotionProduct
811
942
  const promotionProducts = [];
812
- const originalItemMap = /* @__PURE__ */ new Map();
813
- const productsForStrategies = consolidatedMainProductsWithIndex.map(({ item }, i) => {
814
- var _a2;
815
- return {
816
- id: i,
817
- product_id: Number((item == null ? void 0 : item.product_id) ?? 0) || 0,
818
- product_variant_id: Number((item == null ? void 0 : item.product_variant_id) ?? 0) || 0,
819
- name: ((_a2 = item == null ? void 0 : item.metadata) == null ? void 0 : _a2.product_name) || (item == null ? void 0 : item.title) || "",
820
- price: getOrderProductBasePrice(item),
821
- quantity: Number((item == null ? void 0 : item.num) ?? 1) || 1,
822
- bundle: getProductBundleForEvaluator(item),
823
- _promotionOnlyForItemReward: isBookingEditProductLine(item)
824
- };
825
- });
943
+ const originalItemMap = new Map();
944
+ const productsForStrategies = consolidatedMainProductsWithIndex.map(({
945
+ item
946
+ }, i) => ({
947
+ id: i,
948
+ product_id: Number(item?.product_id ?? 0) || 0,
949
+ product_variant_id: Number(item?.product_variant_id ?? 0) || 0,
950
+ name: item?.metadata?.product_name || item?.title || '',
951
+ price: getOrderProductBasePrice(item),
952
+ quantity: Number(item?.num ?? 1) || 1,
953
+ bundle: getProductBundleForEvaluator(item),
954
+ _promotionOnlyForItemReward: isBookingEditProductLine(item)
955
+ }));
826
956
  for (let i = 0; i < consolidatedMainProductsWithIndex.length; i++) {
827
- const { item, originalListIndex } = consolidatedMainProductsWithIndex[i];
957
+ const {
958
+ item,
959
+ originalListIndex
960
+ } = consolidatedMainProductsWithIndex[i];
828
961
  const promotionProduct = convertToPromotionProduct(item, originalListIndex);
829
962
  promotionProducts.push(promotionProduct);
830
- originalItemMap.set(promotionProduct.id, { item, index: originalListIndex });
963
+ originalItemMap.set(promotionProduct.id, {
964
+ item,
965
+ index: originalListIndex
966
+ });
831
967
  }
968
+
969
+ // 2. 拿适用策略
832
970
  const applicableStrategiesResult = evaluator.getProductsApplicableStrategies({
833
971
  products: productsForStrategies,
834
- channel: options == null ? void 0 : options.channel
972
+ channel: options?.channel
835
973
  });
836
- const strategyActionMap = /* @__PURE__ */ new Map();
974
+ const strategyActionMap = new Map();
837
975
  for (const row of applicableStrategiesResult || []) {
838
- for (const strategy of (row == null ? void 0 : row.applicableStrategies) || []) {
839
- if (!(strategy == null ? void 0 : strategy.strategyId))
840
- continue;
976
+ for (const strategy of row?.applicableStrategies || []) {
977
+ if (!strategy?.strategyId) continue;
841
978
  strategyActionMap.set(strategy.strategyId, {
842
979
  actionType: strategy.actionType,
843
980
  actionDetail: strategy.actionDetail
844
981
  });
845
982
  }
846
983
  }
984
+
985
+ // 3. 评估器
847
986
  const result = evaluator.evaluateCartWithPricing({
848
987
  products: promotionProducts,
849
- channel: options == null ? void 0 : options.channel
988
+ channel: options?.channel
850
989
  });
851
- const strategySourceProductIds = /* @__PURE__ */ new Map();
990
+
991
+ // 4. 收集每个策略的 sourceProductIds(参与策略的主商品 uid)
992
+ const strategySourceProductIds = new Map();
852
993
  for (const priced of result.products) {
853
994
  if (priced.inPromotion && priced.strategyId) {
854
995
  const sourceId = priced.originalId ?? priced.id;
855
996
  const originalData = originalItemMap.get(sourceId);
856
997
  if (originalData) {
857
998
  const uid = getOrderProductUid(originalData.item);
858
- if (!uid)
859
- continue;
999
+ if (!uid) continue;
860
1000
  const arr = strategySourceProductIds.get(priced.strategyId) || [];
861
- if (!arr.includes(uid))
862
- arr.push(uid);
1001
+ if (!arr.includes(uid)) arr.push(uid);
863
1002
  strategySourceProductIds.set(priced.strategyId, arr);
864
1003
  }
865
1004
  }
866
1005
  }
867
- const extendedGifts = result.gifts.map((gift) => ({
1006
+
1007
+ // 5. 扩展 gifts,加 sourceProductIds
1008
+ const extendedGifts = result.gifts.map(gift => ({
868
1009
  ...gift,
869
1010
  sourceProductIds: strategySourceProductIds.get(gift.strategyId) || []
870
1011
  }));
871
- const strategyGiftInfoMap = /* @__PURE__ */ new Map();
872
- for (const gift of extendedGifts)
873
- strategyGiftInfoMap.set(gift.strategyId, gift);
874
- const strategyGiftInfoOwner = /* @__PURE__ */ new Map();
1012
+
1013
+ // 6. PricedProduct OrderProduct(含拆分与价格调整)
1014
+ const strategyGiftInfoMap = new Map();
1015
+ for (const gift of extendedGifts) strategyGiftInfoMap.set(gift.strategyId, gift);
1016
+ const strategyGiftInfoOwner = new Map();
875
1017
  const processedProducts = [];
876
1018
  for (const priced of result.products) {
877
1019
  const rawSourceId = priced.originalId ?? priced.id;
878
1020
  const sourceId = Number(rawSourceId);
879
- const originalData = Number.isFinite(sourceId) ? originalItemMap.get(sourceId) : void 0;
880
- if (!originalData)
881
- continue;
882
- const { item: originalItem, index: originalIndex } = originalData;
1021
+ const originalData = Number.isFinite(sourceId) ? originalItemMap.get(sourceId) : undefined;
1022
+ if (!originalData) continue;
1023
+ const {
1024
+ item: originalItem,
1025
+ index: originalIndex
1026
+ } = originalData;
883
1027
  const pricedProductId = Number(priced.product_id ?? 0);
884
1028
  const originalProductId = Number(originalItem.product_id ?? 0);
885
1029
  if (pricedProductId > 0 && originalProductId > 0 && pricedProductId !== originalProductId) {
886
1030
  continue;
887
1031
  }
888
- const newItem = (0, import_lodash_es.cloneDeep)(originalItem);
1032
+ const newItem = (0, _lodashEs.cloneDeep)(originalItem);
889
1033
  const metadata = newItem.metadata || {};
890
- const sourceCartNum = (0, import_utils2.getSafeProductNum)(originalItem.num);
1034
+ const sourceCartNum = (0, _utils2.getSafeProductNum)(originalItem.num);
891
1035
  newItem._sourceCartNum = sourceCartNum;
1036
+
1037
+ // 清掉历史促销字段
892
1038
  const wasInPromotion = wasInPromotionBeforeEvaluation(originalItem) || originalItem._wasInPromotionBeforeConsolidate === true;
893
1039
  delete metadata._promotion;
894
1040
  if (Array.isArray(newItem.product_bundle)) {
895
1041
  for (const b of newItem.product_bundle) {
896
- if (b && typeof b === "object")
897
- delete b._promotion;
1042
+ if (b && typeof b === 'object') delete b._promotion;
898
1043
  }
899
1044
  }
900
1045
  const originalBasePrice = getOrderProductOriginalPriceForPromotion(originalItem);
@@ -902,30 +1047,33 @@ function processCartPromotion(list, evaluator, options) {
902
1047
  const priceUnchangedPromo = priced.inPromotion && Number(priced.finalPrice) === Number(originalBasePrice);
903
1048
  if (shouldModifyPrice) {
904
1049
  if (priced.inPromotion && !priceUnchangedPromo) {
905
- const promoUnitPrice = new import_decimal.default(priced.finalPrice).toDecimalPlaces(2).toFixed(2);
1050
+ const promoUnitPrice = new _decimal.default(priced.finalPrice).toDecimalPlaces(2).toFixed(2);
906
1051
  applyPromoUnitPriceToLine(newItem, {
907
1052
  promoUnitPrice,
908
1053
  originalBasePrice,
909
- strategyTitle: (_a = priced.strategyMetadata) == null ? void 0 : _a.name,
910
- actionType: priced.strategyId ? (_b = strategyActionMap.get(priced.strategyId)) == null ? void 0 : _b.actionType : void 0
1054
+ strategyTitle: priced.strategyMetadata?.name,
1055
+ actionType: priced.strategyId ? strategyActionMap.get(priced.strategyId)?.actionType : undefined
911
1056
  });
912
1057
  } else {
913
- const optionSum = (0, import_utils.sumOptionUnitPrice)((0, import_utils.getProductSkuOptions)(newItem)).toNumber();
914
- const sourceRestore = new import_decimal.default(originalBasePrice).toDecimalPlaces(2).toFixed(2);
915
- const mainRestore = new import_decimal.default(Number(sourceRestore) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
1058
+ // 撤销上一轮促销:还原 selling_price main_product_original_price 派生值
1059
+ const optionSum = (0, _utils.sumOptionUnitPrice)((0, _utils.getProductSkuOptions)(newItem)).toNumber();
1060
+ const sourceRestore = new _decimal.default(originalBasePrice).toDecimalPlaces(2).toFixed(2);
1061
+ const mainRestore = new _decimal.default(Number(sourceRestore) || 0).plus(optionSum).toDecimalPlaces(2).toFixed(2);
916
1062
  metadata.source_product_price = sourceRestore;
917
1063
  metadata.main_product_selling_price = mainRestore;
918
1064
  metadata.price_schema_version = 2;
919
- newItem.selling_price = (0, import_utils.composeLinePrice)({
1065
+ newItem.selling_price = (0, _utils.composeLinePrice)({
920
1066
  mainPrice: mainRestore,
921
1067
  bundle: newItem.product_bundle
922
1068
  });
923
1069
  updatePromotionDiscountList(newItem, null);
924
1070
  }
925
1071
  }
926
- newItem.num = priced.isSplit ? (0, import_utils2.getSafeProductNum)(priced.quantity) : sourceCartNum;
1072
+ newItem.num = priced.isSplit ? (0, _utils2.getSafeProductNum)(priced.quantity) : sourceCartNum;
1073
+
1074
+ // 拆分行:生成新 uid,复制原行
927
1075
  if (priced.isSplit) {
928
- metadata.unique_identification_number = (0, import_utils.createUuidV4)();
1076
+ metadata.unique_identification_number = (0, _utils.createUuidV4)();
929
1077
  }
930
1078
  if (priced.strategyId) {
931
1079
  const actionMeta = strategyActionMap.get(priced.strategyId);
@@ -935,17 +1083,19 @@ function processCartPromotion(list, evaluator, options) {
935
1083
  inPromotion: priced.inPromotion,
936
1084
  originalPrice: originalBasePrice,
937
1085
  finalPrice: priced.finalPrice,
938
- actionType: actionMeta == null ? void 0 : actionMeta.actionType,
939
- actionDetail: actionMeta == null ? void 0 : actionMeta.actionDetail
1086
+ actionType: actionMeta?.actionType,
1087
+ actionDetail: actionMeta?.actionDetail
940
1088
  };
941
1089
  const bundleIdx = priced.matchedBundleIndex;
942
- if (bundleIdx !== void 0 && Array.isArray(newItem.product_bundle) && newItem.product_bundle[bundleIdx]) {
1090
+ if (bundleIdx !== undefined && Array.isArray(newItem.product_bundle) && newItem.product_bundle[bundleIdx]) {
943
1091
  newItem.product_bundle[bundleIdx]._promotion = promotionInfo;
944
1092
  } else {
945
1093
  metadata._promotion = promotionInfo;
946
1094
  }
947
1095
  }
948
1096
  newItem.metadata = metadata;
1097
+
1098
+ // 5.1 第一个参与「买X送Y」策略的主商品,挂 giftInfo
949
1099
  if (priced.inPromotion && priced.strategyId) {
950
1100
  const gift = strategyGiftInfoMap.get(priced.strategyId);
951
1101
  if (gift && Array.isArray(gift.giftOptions) && gift.giftOptions.length > 0 && !strategyGiftInfoOwner.has(priced.strategyId)) {
@@ -955,74 +1105,79 @@ function processCartPromotion(list, evaluator, options) {
955
1105
  giftOptions: gift.giftOptions
956
1106
  };
957
1107
  const bundleIdx = priced.matchedBundleIndex;
958
- if (bundleIdx !== void 0 && Array.isArray(newItem.product_bundle) && ((_c = newItem.product_bundle[bundleIdx]) == null ? void 0 : _c._promotion)) {
1108
+ if (bundleIdx !== undefined && Array.isArray(newItem.product_bundle) && newItem.product_bundle[bundleIdx]?._promotion) {
959
1109
  newItem.product_bundle[bundleIdx]._promotion.giftInfo = giftInfoData;
960
1110
  } else if (metadata._promotion) {
961
1111
  metadata._promotion.giftInfo = giftInfoData;
962
1112
  }
963
- strategyGiftInfoOwner.set(
964
- priced.strategyId,
965
- getOrderProductUid(newItem)
966
- );
1113
+ strategyGiftInfoOwner.set(priced.strategyId, getOrderProductUid(newItem));
967
1114
  }
968
1115
  }
1116
+
1117
+ // 记录原商品 uid(用于排序与未满足匹配)
969
1118
  newItem._sortIndex = originalIndex;
970
1119
  newItem._sortSubIndex = priced.inPromotion ? 0 : 1;
971
1120
  newItem._originalProductUid = getOrderProductUid(originalItem);
972
1121
  processedProducts.push(newItem);
973
1122
  }
974
- for (const { item, originalListIndex } of existingGiftsWithIndex) {
1123
+
1124
+ // 7. 赠品/编辑商品原样拼回;同时同步赠品的 sourceProductIds / sourceProductId
1125
+ for (const {
1126
+ item,
1127
+ originalListIndex
1128
+ } of existingGiftsWithIndex) {
975
1129
  const next = item;
976
1130
  next._sortIndex = originalListIndex;
977
1131
  next._sortSubIndex = 0;
978
- const strategyId = (_d = getGiftInfo(next)) == null ? void 0 : _d.strategyId;
1132
+ const strategyId = getGiftInfo(next)?.strategyId;
979
1133
  if (strategyId) {
980
1134
  const latest = strategyGiftInfoMap.get(strategyId);
981
1135
  const owner = strategyGiftInfoOwner.get(strategyId);
982
- if (latest && ((_e = next.metadata) == null ? void 0 : _e._giftInfo)) {
1136
+ if (latest && next.metadata?._giftInfo) {
983
1137
  next.metadata._giftInfo.sourceProductIds = latest.sourceProductIds;
984
1138
  }
985
- if (owner && ((_f = next.metadata) == null ? void 0 : _f._giftInfo)) {
1139
+ if (owner && next.metadata?._giftInfo) {
986
1140
  next.metadata._giftInfo.sourceProductId = owner;
987
1141
  }
988
1142
  }
989
1143
  processedProducts.push(next);
990
1144
  }
991
- for (const { item, originalListIndex } of editProductsWithIndex) {
1145
+ for (const {
1146
+ item,
1147
+ originalListIndex
1148
+ } of editProductsWithIndex) {
992
1149
  const next = item;
993
1150
  next._sortIndex = originalListIndex;
994
1151
  next._sortSubIndex = 0;
995
1152
  processedProducts.push(next);
996
1153
  }
1154
+
1155
+ // 8. 排序回原顺序,参与促销的拆分行排在前
997
1156
  processedProducts.sort((a, b) => {
998
- if (a._sortIndex !== b._sortIndex)
999
- return a._sortIndex - b._sortIndex;
1157
+ if (a._sortIndex !== b._sortIndex) return a._sortIndex - b._sortIndex;
1000
1158
  return a._sortSubIndex - b._sortSubIndex;
1001
1159
  });
1160
+
1161
+ // 8.1 同款参与促销行:先去重(买赠+满减并存)再合并 num
1002
1162
  const dedupedPromotionLines = dedupePromotionProcessedLines(processedProducts);
1003
1163
  processedProducts.splice(0, processedProducts.length, ...dedupedPromotionLines);
1004
1164
  const mergedPromotionLines = mergeIdenticalPromotionCartLines(processedProducts);
1005
1165
  processedProducts.splice(0, processedProducts.length, ...mergedPromotionLines);
1006
1166
  const splitPromoLines = splitXItemsPromoLinesByGroup(processedProducts, strategyActionMap);
1007
1167
  processedProducts.splice(0, processedProducts.length, ...splitPromoLines);
1168
+
1169
+ // 9. 给「有适用策略但未参与促销」的主商品打 inPromotion:false
1008
1170
  for (const product of processedProducts) {
1009
1171
  const metadata = product.metadata || {};
1010
- if (metadata._promotion)
1011
- continue;
1012
- if (getGiftInfo(product))
1013
- continue;
1172
+ if (metadata._promotion) continue;
1173
+ if (getGiftInfo(product)) continue;
1014
1174
  const lookupUid = String(product._originalProductUid ?? getOrderProductUid(product));
1015
- const originalIndex = mainProducts.findIndex(
1016
- (item) => getOrderProductUid(item) === lookupUid
1017
- );
1018
- if (originalIndex === -1)
1019
- continue;
1175
+ const originalIndex = mainProducts.findIndex(item => getOrderProductUid(item) === lookupUid);
1176
+ if (originalIndex === -1) continue;
1020
1177
  const strategyData = applicableStrategiesResult[originalIndex];
1021
- if (!strategyData || !strategyData.hasApplicableStrategy)
1022
- continue;
1178
+ if (!strategyData || !strategyData.hasApplicableStrategy) continue;
1023
1179
  const first = strategyData.applicableStrategies[0];
1024
- if (!first)
1025
- continue;
1180
+ if (!first) continue;
1026
1181
  const originalPrice = getOrderProductOriginalPriceForPromotion(mainProducts[originalIndex]);
1027
1182
  metadata._promotion = {
1028
1183
  strategyId: first.strategyId,
@@ -1033,21 +1188,24 @@ function processCartPromotion(list, evaluator, options) {
1033
1188
  };
1034
1189
  product.metadata = metadata;
1035
1190
  }
1191
+
1192
+ // 10. 赠品操作(在更新 lastEvaluatedGiftCount 前做对比)
1036
1193
  const giftActions = calculateGiftActions(extendedGifts, existingGifts);
1194
+
1195
+ // 10.1 更新 lastEvaluatedGiftCount,便于下次评估时判别「策略已变」
1037
1196
  for (const gift of existingGifts) {
1038
- const strategyId = (_g = getGiftInfo(gift)) == null ? void 0 : _g.strategyId;
1039
- if (!strategyId)
1040
- continue;
1197
+ const strategyId = getGiftInfo(gift)?.strategyId;
1198
+ if (!strategyId) continue;
1041
1199
  const latest = strategyGiftInfoMap.get(strategyId);
1042
- if (latest && ((_h = gift.metadata) == null ? void 0 : _h._giftInfo)) {
1200
+ if (latest && gift.metadata?._giftInfo) {
1043
1201
  gift.metadata._giftInfo.lastEvaluatedGiftCount = latest.giftCount;
1044
1202
  }
1045
1203
  }
1046
- const unfulfilledPromotions = calculateUnfulfilledPromotions(
1047
- processedProducts,
1048
- applicableStrategiesResult,
1049
- mainProducts
1050
- );
1204
+
1205
+ // 11. 未满足促销
1206
+ const unfulfilledPromotions = calculateUnfulfilledPromotions(processedProducts, applicableStrategiesResult, mainProducts);
1207
+
1208
+ // 12. 清理排序辅助字段
1051
1209
  for (const product of processedProducts) {
1052
1210
  delete product._sortIndex;
1053
1211
  delete product._sortSubIndex;
@@ -1062,18 +1220,4 @@ function processCartPromotion(list, evaluator, options) {
1062
1220
  giftActions,
1063
1221
  unfulfilledPromotions
1064
1222
  };
1065
- }
1066
- // Annotate the CommonJS export names for ESM import in node:
1067
- 0 && (module.exports = {
1068
- appendPromotionTags,
1069
- buildConsolidateKeyForPromotion,
1070
- calculateUnfulfilledPromotions,
1071
- extractProductMatchIdentifiersFromStrategyConfig,
1072
- getOrderProductBasePrice,
1073
- getOrderProductOriginalPriceForPromotion,
1074
- getOrderProductUid,
1075
- mergeIdenticalPromotionCartLines,
1076
- processCartPromotion,
1077
- resolveStrategyEligibleProducts,
1078
- resolveStrategyRequiredQuantity
1079
- });
1223
+ }