@pisell/pisellos 2.2.174 → 2.2.176

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.
@@ -883,6 +883,10 @@ export var BookingTicketImpl = /*#__PURE__*/function (_BaseSalesImpl) {
883
883
  var next = this.buildOrderCustomerPayload();
884
884
  this.core.effects.emit("".concat(this.name, ":onOrderCustomerChange"), next);
885
885
  this.refreshDiscountConfigAfterOrderCustomerChange(patch.customer_id);
886
+ // 客户切换可能改变可用促销/赠品;触发一次重算(recursion-safe)。
887
+ void this.store.order.applyPromotion().catch(function (error) {
888
+ console.error('[BookingTicket] applyPromotion after customer change failed', error);
889
+ });
886
890
  }
887
891
 
888
892
  /**
@@ -67,8 +67,10 @@ export function buildCartView(lines, bookings) {
67
67
  });
68
68
  });
69
69
  var items = productLines.filter(function (line) {
70
- var _line$metadata2;
71
- var uid = (_line$metadata2 = line.metadata) === null || _line$metadata2 === void 0 ? void 0 : _line$metadata2.unique_identification_number;
70
+ var _line$metadata2, _line$metadata3;
71
+ // 促销赠品行仅通过主商品 Gift 行展示,不在 items 单独占一行(对齐 legacy `isGift: !!_giftInfo`)
72
+ if ((_line$metadata2 = line.metadata) !== null && _line$metadata2 !== void 0 && _line$metadata2._giftInfo) return false;
73
+ var uid = (_line$metadata3 = line.metadata) === null || _line$metadata3 === void 0 ? void 0 : _line$metadata3.unique_identification_number;
72
74
  if (uid === undefined || uid === null || uid === '') return true;
73
75
  return !claimedUids.has(String(uid));
74
76
  });
@@ -1,49 +0,0 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __export = (target, all) => {
8
- for (var name in all)
9
- __defProp(target, name, { get: all[name], enumerable: true });
10
- };
11
- var __copyProps = (to, from, except, desc) => {
12
- if (from && typeof from === "object" || typeof from === "function") {
13
- for (let key of __getOwnPropNames(from))
14
- if (!__hasOwnProp.call(to, key) && key !== except)
15
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
- }
17
- return to;
18
- };
19
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
- // If the importer is in node compatibility mode or this is not an ESM
21
- // file that has been converted to a CommonJS file using a Babel-
22
- // compatible transform (i.e. "__esModule" has not been set), then set
23
- // "default" to the CommonJS "module.exports" for node compatibility.
24
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
- mod
26
- ));
27
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
-
29
- // src/model/strategy/adapter/promotion/index.ts
30
- var promotion_exports = {};
31
- __export(promotion_exports, {
32
- BUY_X_GET_Y_FREE_STRATEGY: () => import_examples.BUY_X_GET_Y_FREE_STRATEGY,
33
- PromotionAdapter: () => import_adapter.PromotionAdapter,
34
- PromotionEvaluator: () => import_evaluator.PromotionEvaluator,
35
- X_ITEMS_FOR_Y_PRICE_STRATEGY: () => import_examples.X_ITEMS_FOR_Y_PRICE_STRATEGY,
36
- default: () => import_adapter2.default
37
- });
38
- module.exports = __toCommonJS(promotion_exports);
39
- var import_evaluator = require("./evaluator");
40
- var import_adapter = require("./adapter");
41
- var import_adapter2 = __toESM(require("./adapter"));
42
- var import_examples = require("./examples");
43
- // Annotate the CommonJS export names for ESM import in node:
44
- 0 && (module.exports = {
45
- BUY_X_GET_Y_FREE_STRATEGY,
46
- PromotionAdapter,
47
- PromotionEvaluator,
48
- X_ITEMS_FOR_Y_PRICE_STRATEGY
49
- });
@@ -3,7 +3,7 @@ import { BaseModule } from '../BaseModule';
3
3
  import { OrderModuleAPI, CommitOrderParams, UpdateOrderBookingParams, UpdateOrderProductParams, UpdateOrderProductQuantityParams, CheckoutOrderParams, CancelOrderParams, ChangeBookingStatusParams } from './types';
4
4
  import { CartItem } from '../Cart/types';
5
5
  import { type SubmitPayloadEnhancer } from './utils';
6
- import type { OrderAmountSnapshot, AddProductBookingInput, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, OrderScanCodeResult, ReplaceTempOrderOptions } from './types';
6
+ import type { OrderAmountSnapshot, AddProductBookingInput, OrderIdentitySnapshot, OrderPaymentSource, OrderSnapshot, OrderSyncState, SyncPaymentsToOrderParams, SyncPaymentsToOrderResult, SubmitSalesOrderParams, OrderProduct, OrderProductIdentity, OrderSummary, OrderTempOrder, OrderPaymentData, SendCustomerPayLinkParams, UpdateTempOrderCustomerInput, OrderCustomerPatch, OrderCustomerView, OrderScanCodeResult, ReplaceTempOrderOptions, OrderPromotionEvaluator, OrderGiftSelectResolver, OrderUnfulfilledPromotion, OrderLastGiftActions } from './types';
7
7
  import type { ICustomer } from '../AccountList/types';
8
8
  import type { Discount } from '../Discount/types';
9
9
  export declare class OrderModule extends BaseModule implements Module, OrderModuleAPI {
@@ -19,6 +19,16 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
19
19
  private rulesHooksOverride?;
20
20
  private orderHooks?;
21
21
  private otherParams?;
22
+ /** 评估器引用;由 SalesSdkProvider 注入,未注入时 applyPromotion 静默跳过 */
23
+ private promotionEvaluator;
24
+ /** 赠品选择 resolver;由 SDK host bridge 注入 */
25
+ private giftSelectResolver;
26
+ /** applyPromotion 递归保护 flag(写路径同步调 applyPromotion,禁止重入) */
27
+ private isApplyingPromotion;
28
+ /** 最近一次 applyPromotion 的未满足促销提示 */
29
+ private lastUnfulfilledPromotions;
30
+ /** 最近一次 applyPromotion 的赠品操作 diff(debug / SDK 读取使用) */
31
+ private lastGiftActions;
22
32
  /**
23
33
  * Populate `savedAmount` on each discount based on product-level discount details.
24
34
  * Only selected discounts get a non-zero value.
@@ -50,6 +60,46 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
50
60
  getDiscountList(): Discount[];
51
61
  scanCode(code: string, customerId?: number): Promise<OrderScanCodeResult>;
52
62
  applyDiscount(): void;
63
+ /**
64
+ * 注入促销评估器。
65
+ *
66
+ * @example
67
+ * order.setPromotionEvaluator(appHelper.utils.promotionEvaluator);
68
+ */
69
+ setPromotionEvaluator(evaluator: OrderPromotionEvaluator | null): void;
70
+ /**
71
+ * 注入赠品选择 resolver。OS 需要补赠品时会 await 调用 resolver;缺省时跳过新增并 console.warn。
72
+ *
73
+ * @example
74
+ * order.setGiftSelectResolver(async (ctx) => {
75
+ * // SDK 内部决定弹窗 or 自动选第一项,返回完整 OrderProduct[]
76
+ * return giftSelectBridge.request(ctx);
77
+ * });
78
+ */
79
+ setGiftSelectResolver(resolver: OrderGiftSelectResolver | null): void;
80
+ /**
81
+ * 为商品目录追加促销标签,供上层 Sales 门面复用。
82
+ *
83
+ * @example
84
+ * const products = order.appendPromotionTags(catalogProducts);
85
+ */
86
+ appendPromotionTags<T extends Record<string, any>>(products: T[]): T[];
87
+ /**
88
+ * 应用购物车促销:计算 → 差异化赠品 → 写回 tempOrder.products → emit onPromotionApplied。
89
+ *
90
+ * 调用时机:写路径(add/update/remove/clear/setCustomer)末尾自动触发;外部一般无需手动调。
91
+ *
92
+ * 关键约束:
93
+ * - 不调用任何公开 CRUD API(如 removeProductFromOrder),全部内部 mutate tempOrder.products,避免事件风暴;
94
+ * - 用 `isApplyingPromotion` 防递归;
95
+ * - 多选项赠品依赖 giftSelectResolver,未注入则跳过且 console.warn;
96
+ * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进。
97
+ */
98
+ applyPromotion(): Promise<void>;
99
+ /** 最近一次 applyPromotion 输出的未满足促销列表 */
100
+ getUnfulfilledPromotions(): OrderUnfulfilledPromotion[];
101
+ /** 最近一次 applyPromotion 输出的赠品操作 diff */
102
+ getLastGiftActions(): OrderLastGiftActions | null;
53
103
  initTempOrder(params: {
54
104
  cacheId?: string;
55
105
  salesSummaryModuleName?: string;
@@ -64,6 +114,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
64
114
  private createExternalSaleNumber;
65
115
  private ensureExternalSaleNumber;
66
116
  private ensureRequestUniqueIdempotencyToken;
117
+ private buildPaymentSyncIdempotencyToken;
67
118
  hasLocalDatabase(): boolean;
68
119
  private buildLocalOrderRecord;
69
120
  getLocalOrderByOrderId(orderId: number | string): Promise<Record<string, any> | null>;
@@ -37,6 +37,7 @@ var import_types = require("./types");
37
37
  var import_utils = require("./utils");
38
38
  var import_utils2 = require("../Product/utils");
39
39
  var import_dayjs = __toESM(require("dayjs"));
40
+ var import_cartPromotion = require("../../solution/BaseSales/utils/cartPromotion");
40
41
  var import_utils3 = require("../../solution/ScanOrder/utils");
41
42
  var import_manualProductDiscount = require("./utils/manualProductDiscount");
42
43
  var import_Discount = require("../Discount");
@@ -50,6 +51,17 @@ var OrderModule = class extends import_BaseModule.BaseModule {
50
51
  super(name || "order", version);
51
52
  this.defaultName = "order";
52
53
  this.defaultVersion = "1.0.0";
54
+ // ─── 促销/赠品 ────────────────────────────────────
55
+ /** 评估器引用;由 SalesSdkProvider 注入,未注入时 applyPromotion 静默跳过 */
56
+ this.promotionEvaluator = null;
57
+ /** 赠品选择 resolver;由 SDK host bridge 注入 */
58
+ this.giftSelectResolver = null;
59
+ /** applyPromotion 递归保护 flag(写路径同步调 applyPromotion,禁止重入) */
60
+ this.isApplyingPromotion = false;
61
+ /** 最近一次 applyPromotion 的未满足促销提示 */
62
+ this.lastUnfulfilledPromotions = [];
63
+ /** 最近一次 applyPromotion 的赠品操作 diff(debug / SDK 读取使用) */
64
+ this.lastGiftActions = null;
53
65
  }
54
66
  /**
55
67
  * Populate `savedAmount` on each discount based on product-level discount details.
@@ -81,10 +93,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
81
93
  var _a, _b, _c;
82
94
  if ((_a = product.metadata) == null ? void 0 : _a.is_manual_discount)
83
95
  return product;
84
- const totalPerUnitDiscount = (product.discount_list || []).reduce(
85
- (sum, discount) => sum + Number(discount.amount || 0),
86
- 0
87
- );
96
+ const totalPerUnitDiscount = (0, import_utils.resolveEffectivePerUnitDiscount)(product);
88
97
  const optionSum = (0, import_utils.sumOptionUnitPrice)(product.product_option_item);
89
98
  const sourcePrice = ((_b = product.metadata) == null ? void 0 : _b.source_product_price) ?? (((_c = product.metadata) == null ? void 0 : _c.main_product_original_price) != null ? new import_decimal.default(Number(product.metadata.main_product_original_price) || 0).minus(optionSum).toFixed(2) : product.original_price ?? "0");
90
99
  const newSourceSellingPrice = new import_decimal.default(Number(sourcePrice) || 0).minus(totalPerUnitDiscount).toDecimalPlaces(2).toString();
@@ -317,6 +326,160 @@ var OrderModule = class extends import_BaseModule.BaseModule {
317
326
  (_e = this.store.discount) == null ? void 0 : _e.setDiscountList(result.discountList);
318
327
  }
319
328
  }
329
+ // ─── 促销/赠品 ──────────────────────────────────────
330
+ /**
331
+ * 注入促销评估器。
332
+ *
333
+ * @example
334
+ * order.setPromotionEvaluator(appHelper.utils.promotionEvaluator);
335
+ */
336
+ setPromotionEvaluator(evaluator) {
337
+ this.promotionEvaluator = evaluator || null;
338
+ }
339
+ /**
340
+ * 注入赠品选择 resolver。OS 需要补赠品时会 await 调用 resolver;缺省时跳过新增并 console.warn。
341
+ *
342
+ * @example
343
+ * order.setGiftSelectResolver(async (ctx) => {
344
+ * // SDK 内部决定弹窗 or 自动选第一项,返回完整 OrderProduct[]
345
+ * return giftSelectBridge.request(ctx);
346
+ * });
347
+ */
348
+ setGiftSelectResolver(resolver) {
349
+ this.giftSelectResolver = resolver || null;
350
+ }
351
+ /**
352
+ * 为商品目录追加促销标签,供上层 Sales 门面复用。
353
+ *
354
+ * @example
355
+ * const products = order.appendPromotionTags(catalogProducts);
356
+ */
357
+ appendPromotionTags(products) {
358
+ return (0, import_cartPromotion.appendPromotionTags)(products, this.promotionEvaluator);
359
+ }
360
+ /**
361
+ * 应用购物车促销:计算 → 差异化赠品 → 写回 tempOrder.products → emit onPromotionApplied。
362
+ *
363
+ * 调用时机:写路径(add/update/remove/clear/setCustomer)末尾自动触发;外部一般无需手动调。
364
+ *
365
+ * 关键约束:
366
+ * - 不调用任何公开 CRUD API(如 removeProductFromOrder),全部内部 mutate tempOrder.products,避免事件风暴;
367
+ * - 用 `isApplyingPromotion` 防递归;
368
+ * - 多选项赠品依赖 giftSelectResolver,未注入则跳过且 console.warn;
369
+ * - 不主动调用 applyDiscount / recalculateSummary,调用方负责跟进。
370
+ */
371
+ async applyPromotion() {
372
+ if (this.isApplyingPromotion)
373
+ return;
374
+ if (!this.promotionEvaluator)
375
+ return;
376
+ const tempOrder = this.store.tempOrder;
377
+ if (!tempOrder)
378
+ return;
379
+ this.isApplyingPromotion = true;
380
+ try {
381
+ const result = (0, import_cartPromotion.processCartPromotion)(
382
+ tempOrder.products,
383
+ this.promotionEvaluator
384
+ );
385
+ if (result.giftActions.toRemove.length > 0) {
386
+ const removeSet = new Set(result.giftActions.toRemove.map(String));
387
+ result.products = result.products.filter((p) => {
388
+ const uid = (0, import_cartPromotion.getOrderProductUid)(p);
389
+ return uid ? !removeSet.has(String(uid)) : true;
390
+ });
391
+ }
392
+ for (const reduce of result.giftActions.toReduce) {
393
+ for (const item of reduce.items) {
394
+ const product = result.products.find(
395
+ (p) => (0, import_cartPromotion.getOrderProductUid)(p) === item.uid
396
+ );
397
+ if (product) {
398
+ product.num = item.newQuantity;
399
+ }
400
+ }
401
+ }
402
+ if (result.giftActions.toAdd.length > 0) {
403
+ if (!this.giftSelectResolver) {
404
+ if (result.giftActions.toAdd.some((g) => g.giftOptions.length > 1)) {
405
+ console.warn(
406
+ "[OrderModule] giftSelectResolver not set, skip applying gifts",
407
+ result.giftActions.toAdd
408
+ );
409
+ } else {
410
+ console.warn(
411
+ "[OrderModule] giftSelectResolver not set, skip applying single-option gifts",
412
+ result.giftActions.toAdd
413
+ );
414
+ }
415
+ } else {
416
+ for (const addAction of result.giftActions.toAdd) {
417
+ if (addAction.giftOptions.length === 1) {
418
+ const sameStrategy = result.products.find(
419
+ (p) => {
420
+ var _a, _b;
421
+ return ((_b = (_a = p.metadata) == null ? void 0 : _a._giftInfo) == null ? void 0 : _b.strategyId) === addAction.strategyId;
422
+ }
423
+ );
424
+ if (sameStrategy) {
425
+ sameStrategy.num = (Number(sameStrategy.num) || 0) + (addAction.giftCount || 1);
426
+ continue;
427
+ }
428
+ }
429
+ try {
430
+ const giftProducts = await this.giftSelectResolver({
431
+ strategyId: addAction.strategyId,
432
+ strategyName: addAction.strategyName,
433
+ giftCount: addAction.giftCount,
434
+ giftOptions: addAction.giftOptions,
435
+ sourceProductIds: addAction.sourceProductIds
436
+ });
437
+ if (Array.isArray(giftProducts) && giftProducts.length > 0) {
438
+ for (const gp of giftProducts) {
439
+ if (!gp)
440
+ continue;
441
+ result.products.push(gp);
442
+ }
443
+ }
444
+ } catch (error) {
445
+ console.warn(
446
+ "[OrderModule] giftSelectResolver rejected, skip add gift",
447
+ addAction.strategyId,
448
+ error
449
+ );
450
+ }
451
+ }
452
+ }
453
+ }
454
+ tempOrder.products = result.products;
455
+ this.lastUnfulfilledPromotions = result.unfulfilledPromotions;
456
+ this.lastGiftActions = result.giftActions;
457
+ const payload = {
458
+ unfulfilledPromotions: result.unfulfilledPromotions,
459
+ giftActions: result.giftActions,
460
+ gifts: result.gifts.map((g) => ({
461
+ strategyId: g.strategyId,
462
+ strategyName: g.strategyName,
463
+ giftCount: g.giftCount,
464
+ giftOptions: g.giftOptions
465
+ })),
466
+ products: tempOrder.products
467
+ };
468
+ this.effectsEmit("onPromotionApplied", payload);
469
+ } catch (error) {
470
+ console.error("[OrderModule] applyPromotion failed", error);
471
+ } finally {
472
+ this.isApplyingPromotion = false;
473
+ }
474
+ }
475
+ /** 最近一次 applyPromotion 输出的未满足促销列表 */
476
+ getUnfulfilledPromotions() {
477
+ return this.lastUnfulfilledPromotions;
478
+ }
479
+ /** 最近一次 applyPromotion 输出的赠品操作 diff */
480
+ getLastGiftActions() {
481
+ return this.lastGiftActions;
482
+ }
320
483
  // ─── TempOrder: 初始化入口 ───
321
484
  initTempOrder(params) {
322
485
  if (params.cacheId !== void 0)
@@ -376,6 +539,14 @@ var OrderModule = class extends import_BaseModule.BaseModule {
376
539
  }
377
540
  return tempOrder.request_unique_idempotency_token;
378
541
  }
542
+ buildPaymentSyncIdempotencyToken(tempOrder, payments) {
543
+ const baseToken = this.ensureRequestUniqueIdempotencyToken(tempOrder);
544
+ const paidPaymentsCount = (payments || []).filter((payment) => {
545
+ return String((payment == null ? void 0 : payment.status) || "").toLowerCase() === "paid";
546
+ }).length;
547
+ const paidCount = paidPaymentsCount > 0 ? paidPaymentsCount : payments.length;
548
+ return `${baseToken}_payment_${paidCount}`;
549
+ }
379
550
  hasLocalDatabase() {
380
551
  return !!this.dbManager;
381
552
  }
@@ -1207,6 +1378,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1207
1378
  if (linked) {
1208
1379
  tempOrder.bookings = [...tempOrder.bookings || [], linked.booking];
1209
1380
  }
1381
+ await this.applyPromotion();
1210
1382
  this.applyDiscount();
1211
1383
  await this.recalculateSummary({ createIfMissing: true });
1212
1384
  this.persistTempOrder();
@@ -1337,6 +1509,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1337
1509
  tempOrder.products[productIndex],
1338
1510
  (_g = tempOrder.products[productIndex].metadata) == null ? void 0 : _g.unique_identification_number
1339
1511
  );
1512
+ await this.applyPromotion();
1340
1513
  this.applyDiscount();
1341
1514
  await this.recalculateSummary({ createIfMissing: true });
1342
1515
  this.persistTempOrder();
@@ -1399,6 +1572,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1399
1572
  normalizedProduct.discount_list
1400
1573
  );
1401
1574
  tempOrder.products[productIndex] = normalizedProduct;
1575
+ await this.applyPromotion();
1402
1576
  this.applyDiscount();
1403
1577
  await this.recalculateSummary({ createIfMissing: true });
1404
1578
  this.persistTempOrder();
@@ -1443,6 +1617,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1443
1617
  for (const product of removedProducts) {
1444
1618
  this.removeLinkedBookingsForProduct(tempOrder, product);
1445
1619
  }
1620
+ await this.applyPromotion();
1446
1621
  this.applyDiscount();
1447
1622
  await this.recalculateSummary({ createIfMissing: true });
1448
1623
  this.persistTempOrder();
@@ -1462,6 +1637,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1462
1637
  productsByUid: {}
1463
1638
  };
1464
1639
  }
1640
+ await this.applyPromotion();
1465
1641
  this.applyDiscount();
1466
1642
  await this.recalculateSummary({ createIfMissing: true });
1467
1643
  this.persistTempOrder();
@@ -1493,7 +1669,8 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1493
1669
  ...hasPaymentStatusOverride ? { payment_status: params == null ? void 0 : params.paymentStatus } : {},
1494
1670
  ...hasSmallTicketDataFlagOverride ? { small_ticket_data_flag: params == null ? void 0 : params.smallTicketDataFlag } : {}
1495
1671
  };
1496
- return (params == null ? void 0 : params.enhancePayload) ? params.enhancePayload(nextPayload, ctx) : nextPayload;
1672
+ const enhancedPayload = (params == null ? void 0 : params.enhancePayload) ? params.enhancePayload(nextPayload, ctx) : nextPayload;
1673
+ return enhancedPayload;
1497
1674
  } : void 0;
1498
1675
  const payload = (0, import_utils.buildSubmitPayload)({
1499
1676
  tempOrder,
@@ -1510,6 +1687,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1510
1687
  this.logInfo("submitTempOrder calling sales checkout", {
1511
1688
  orderId: payload.order_id,
1512
1689
  externalSaleNumber: payload.external_sale_number,
1690
+ requestUniqueIdempotencyToken: payload.request_unique_idempotency_token,
1513
1691
  paymentStatus: payload.payment_status,
1514
1692
  paymentsCount: ((_b = payload.payments) == null ? void 0 : _b.length) || 0,
1515
1693
  smallTicketDataFlag: payload.small_ticket_data_flag
@@ -1611,11 +1789,22 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1611
1789
  return { isFullyPaid };
1612
1790
  }
1613
1791
  this.store.syncState = "submitting";
1792
+ const paymentSyncIdempotencyToken = this.buildPaymentSyncIdempotencyToken(
1793
+ tempOrder,
1794
+ mappedPayments
1795
+ );
1614
1796
  const submitResult = await this.submitTempOrder({
1615
1797
  payments: mappedPayments,
1616
1798
  paymentStatus,
1617
1799
  smallTicketDataFlag: params.smallTicketDataFlag,
1618
- channel: params.channel
1800
+ channel: params.channel,
1801
+ enhancePayload: (payload) => {
1802
+ const nextPayload = {
1803
+ ...payload,
1804
+ request_unique_idempotency_token: paymentSyncIdempotencyToken
1805
+ };
1806
+ return nextPayload;
1807
+ }
1619
1808
  });
1620
1809
  return { isFullyPaid, submitResult };
1621
1810
  }
@@ -13,7 +13,106 @@ export declare enum OrderHooks {
13
13
  OnOrderUpdate = "order:onOrderUpdate",
14
14
  OnOrderCancel = "order:onOrderCancel",
15
15
  OnOrderStatusChange = "order:onOrderStatusChange",
16
- OnOrderCustomerChange = "order:onOrderCustomerChange"
16
+ OnOrderCustomerChange = "order:onOrderCustomerChange",
17
+ OnPromotionApplied = "order:onPromotionApplied"
18
+ }
19
+ /**
20
+ * 促销评估器协议(OS 与 PromotionEvaluator 之间的桥接接口)。
21
+ *
22
+ * 与 `pisell_os/src/model/strategy/adapter/promotion/evaluator.ts` 中的
23
+ * `PromotionEvaluator` 实例结构保持兼容。SalesSdkProvider 会从 `appHelper.utils.promotionEvaluator`
24
+ * 读取实例并通过 `setPromotionEvaluator` 注入到 OrderModule。
25
+ *
26
+ * 这里只声明 OrderModule 需要调用的子集,避免反向依赖具体 evaluator 实现。
27
+ */
28
+ export interface OrderPromotionEvaluator {
29
+ evaluateCartWithPricing(input: {
30
+ products: any[];
31
+ channel?: string;
32
+ }): any;
33
+ getProductsApplicableStrategies(input: {
34
+ products: any[];
35
+ channel?: string;
36
+ }, matchVariant?: boolean): any[];
37
+ }
38
+ /** 赠品解析回调 context(OS → SDK 的请求体) */
39
+ export interface OrderGiftSelectContext {
40
+ strategyId: string;
41
+ strategyName: string | Record<string, string>;
42
+ giftCount: number;
43
+ giftOptions: Array<{
44
+ product_id: number;
45
+ product_variant_id: number;
46
+ }>;
47
+ /** 触发此赠品的主商品 metadata.unique_identification_number 列表 */
48
+ sourceProductIds: string[];
49
+ }
50
+ /**
51
+ * 赠品解析回调签名(SDK → OS 的返回值)。
52
+ *
53
+ * - 单选项:SDK 内部自动构造商品,不弹窗
54
+ * - 多选项:SDK 弹窗等用户确认后构造
55
+ * - 用户取消:抛错或返回空数组(OS 视为放弃本次添加)
56
+ *
57
+ * 返回的 OrderProduct 必须是完整的购物车行(含 metadata._giftInfo 标记),OS 会直接 push 入 tempOrder.products。
58
+ */
59
+ export type OrderGiftSelectResolver = (ctx: OrderGiftSelectContext) => Promise<Partial<OrderProduct>[] | null>;
60
+ /** 未满足的促销策略(购物车底部 alert 渲染源) */
61
+ export interface OrderUnfulfilledPromotion {
62
+ strategyId: string;
63
+ strategyName: string | Record<string, string>;
64
+ actionType: string;
65
+ needQuantity: number;
66
+ currentQuantity: number;
67
+ requiredQuantity: number;
68
+ eligibleProducts: Array<{
69
+ product_id: number;
70
+ product_variant_id: number;
71
+ }>;
72
+ strategyMetadata?: any;
73
+ display?: {
74
+ text: string | Record<string, string>;
75
+ type: string;
76
+ };
77
+ }
78
+ /** 上一次 applyPromotion 计算出的赠品操作(仅供调试/读取使用) */
79
+ export interface OrderLastGiftActions {
80
+ toAdd: Array<{
81
+ strategyId: string;
82
+ strategyName: string | Record<string, string>;
83
+ giftCount: number;
84
+ giftOptions: Array<{
85
+ product_id: number;
86
+ product_variant_id: number;
87
+ }>;
88
+ sourceProductIds: string[];
89
+ }>;
90
+ toReduce: Array<{
91
+ strategyId: string;
92
+ items: Array<{
93
+ uid: string;
94
+ currentQuantity: number;
95
+ newQuantity: number;
96
+ }>;
97
+ }>;
98
+ toRemove: string[];
99
+ }
100
+ /** onPromotionApplied 事件 payload */
101
+ export interface OrderPromotionAppliedPayload {
102
+ unfulfilledPromotions: OrderUnfulfilledPromotion[];
103
+ giftActions: OrderLastGiftActions;
104
+ /** evaluator 返回的赠品摘要 */
105
+ gifts: Array<{
106
+ strategyId: string;
107
+ strategyName: string | Record<string, string>;
108
+ giftCount: number;
109
+ giftOptions: Array<{
110
+ product_id: number;
111
+ product_variant_id: number;
112
+ }>;
113
+ }>;
114
+ /** 处理后的商品列表 */
115
+ products: OrderProduct[];
17
116
  }
18
117
  /**
19
118
  * tempOrder 上的下单客户协议字段视图。
@@ -561,6 +660,46 @@ export interface OrderModuleAPI {
561
660
  }) => Promise<Discount[]>;
562
661
  getDiscountList: () => Discount[];
563
662
  applyDiscount: () => void;
663
+ /**
664
+ * 注入促销评估器。
665
+ *
666
+ * SalesSdkProvider 在初始化 bookingTicket 后会自动从 `appHelper.utils.promotionEvaluator`
667
+ * 读取实例并调用本方法注入。host 透明,业务 UI 不感知。
668
+ *
669
+ * @example
670
+ * order.setPromotionEvaluator(appHelper.utils.promotionEvaluator);
671
+ */
672
+ setPromotionEvaluator: (evaluator: OrderPromotionEvaluator | null) => void;
673
+ /**
674
+ * 注入赠品选择 resolver。OS 在 applyPromotion 内部当需要新增赠品时会 await 调用 resolver。
675
+ *
676
+ * 缺省时 OS 会 `console.warn` 并跳过新增(既有赠品的 reduce/remove 仍会执行)。
677
+ *
678
+ * @example
679
+ * order.setGiftSelectResolver((ctx) => giftSelectBridge.request(ctx));
680
+ */
681
+ setGiftSelectResolver: (resolver: OrderGiftSelectResolver | null) => void;
682
+ /**
683
+ * 为商品目录追加促销标签。
684
+ *
685
+ * @example
686
+ * const taggedProducts = order.appendPromotionTags(products);
687
+ */
688
+ appendPromotionTags: <T extends Record<string, any>>(products: T[]) => T[];
689
+ /**
690
+ * 应用购物车促销(评估 → 差异化赠品 → 写回 tempOrder.products → emit onPromotionApplied)。
691
+ *
692
+ * 自动在 addProductToOrder / updateOrderProductQuantity / removeProductFromOrder /
693
+ * clearOrderCartLines / setOrderCustomer 等写路径末尾触发,业务一般无需手动调用。
694
+ *
695
+ * 多选项赠品依赖 giftSelectResolver;如未注入则跳过且 console.warn。
696
+ * 内部以 `_isApplyingPromotion` flag 防递归。
697
+ */
698
+ applyPromotion: () => Promise<void>;
699
+ /** 读取上次 applyPromotion 产出的未满足促销提示 */
700
+ getUnfulfilledPromotions: () => OrderUnfulfilledPromotion[];
701
+ /** 读取上次 applyPromotion 产出的赠品操作 diff(toAdd / toReduce / toRemove) */
702
+ getLastGiftActions: () => OrderLastGiftActions | null;
564
703
  /**
565
704
  * 取消订单
566
705
  * @param params 取消订单参数
@@ -28,6 +28,7 @@ var OrderHooks = /* @__PURE__ */ ((OrderHooks2) => {
28
28
  OrderHooks2["OnOrderCancel"] = "order:onOrderCancel";
29
29
  OrderHooks2["OnOrderStatusChange"] = "order:onOrderStatusChange";
30
30
  OrderHooks2["OnOrderCustomerChange"] = "order:onOrderCustomerChange";
31
+ OrderHooks2["OnPromotionApplied"] = "order:onPromotionApplied";
31
32
  return OrderHooks2;
32
33
  })(OrderHooks || {});
33
34
  // Annotate the CommonJS export names for ESM import in node:
@@ -85,6 +85,17 @@ export declare function resolveManualOverrideLineOriginalPrice(params: {
85
85
  /** 已有行级 original_price(origin 被裁剪时兜底划线价) */
86
86
  lineOriginalPrice?: string | number | null;
87
87
  }, fallbackCompositeOriginal: string): string;
88
+ /**
89
+ * 计算商品行「每单位有效折扣」:discount_list 合计 + 被 Rules 剥离但仍 inPromotion 的差额。
90
+ *
91
+ * Rules.calcDiscount 会把 type=promotion 从 discount_list 去掉,但 metadata._promotion 仍保留;
92
+ * applyProductDiscountPrices 必须回读 _promotion,否则抬价/降价都会在第二步被 source 原价覆盖。
93
+ *
94
+ * @example
95
+ * resolveEffectivePerUnitDiscount({ metadata: { _promotion: { inPromotion: true, originalPrice: 1, finalPrice: 5 } }, discount_list: [] });
96
+ * // => -4
97
+ */
98
+ export declare function resolveEffectivePerUnitDiscount(product: Record<string, any>): number;
88
99
  export declare function createDefaultOrderRulesHooks(): RulesParamsHooks;
89
100
  /**
90
101
  * 通过 session 类商品的开始时间结束时间生成商品的时长