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