@pisell/pisellos 2.3.74 → 2.3.76

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