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