@pisell/pisellos 2.2.283 → 2.2.285

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