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