@pisell/pisellos 0.10.6 → 0.10.7

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.
@@ -237,6 +237,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
237
237
  private sanitizeOrderProductForTempOrder;
238
238
  private sanitizeTempOrderProducts;
239
239
  private ensureExtend;
240
+ private repairDuplicateProductLineIdentities;
240
241
  private captureProductRuntime;
241
242
  private createLinkedProductAndBooking;
242
243
  /**
@@ -497,7 +498,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
497
498
  generateIdempotencyToken(): string;
498
499
  syncPaymentsToOrder<T = any>(params: SyncPaymentsToOrderParams): Promise<SyncPaymentsToOrderResult<T>>;
499
500
  createOrder(params: CommitOrderParams['query']): {
500
- type: "virtual" | "appointment_booking";
501
+ type: "appointment_booking" | "virtual";
501
502
  platform: string;
502
503
  sales_channel: string;
503
504
  order_sales_channel: string;
@@ -41,7 +41,7 @@ import { processCartPromotion, appendPromotionTags as _appendPromotionTags, getO
41
41
  import { buildProductLineFingerprint, getProductIdentityIndex, getSafeProductNum, isIdentityMatch, normalizeOrderProduct } from "../../solution/ScanOrder/utils";
42
42
  import { syncManualProductDiscountProductNum } from "./utils/manualProductDiscount";
43
43
  import { expandHydratedProductsForDiscountCollection, restoreHydratedBundleDiscounts } from "./utils/bundleDiscountHydration";
44
- import { getOrderCollectionPersistentId, getOrderCollectionUid, normalizeOrderCollections, setOrderCollectionUid } from "./utils/orderCollectionIdentity";
44
+ import { getOrderCollectionPersistentId, getOrderCollectionUid, normalizeOrderCollections, repairDuplicateProductLineIdentities as repairDuplicateOrderProductLineIdentities, setOrderCollectionUid } from "./utils/orderCollectionIdentity";
45
45
  import { DiscountModule } from "../Discount";
46
46
  import { RulesModule } from "../Rules";
47
47
  import { UnavailableReason } from "../Rules/types";
@@ -890,6 +890,7 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
890
890
  discount_list: normalizeOrderProductDiscountList(syncManualProductDiscountProductNum(mergedProduct.discount_list, mergedProduct.num))
891
891
  });
892
892
  }));
893
+ this.repairDuplicateProductLineIdentities(tempOrder);
893
894
  }
894
895
  if (result !== null && result !== void 0 && result.discountList) {
895
896
  var _this$store$discount15;
@@ -2608,6 +2609,21 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
2608
2609
  }
2609
2610
  return tempOrder._extend;
2610
2611
  }
2612
+ }, {
2613
+ key: "repairDuplicateProductLineIdentities",
2614
+ value: function repairDuplicateProductLineIdentities(tempOrder) {
2615
+ var _tempOrder$_extend7;
2616
+ var result = repairDuplicateOrderProductLineIdentities(tempOrder.products, (_tempOrder$_extend7 = tempOrder._extend) === null || _tempOrder$_extend7 === void 0 ? void 0 : _tempOrder$_extend7.productsByUid);
2617
+ if (!result.repairs.length) return;
2618
+ tempOrder.products = result.products;
2619
+ if (result.productsByUid) {
2620
+ this.ensureExtend(tempOrder).productsByUid = result.productsByUid;
2621
+ }
2622
+ this.logWarning('Repaired duplicate product line identities', {
2623
+ repairCount: result.repairs.length,
2624
+ repairs: result.repairs
2625
+ });
2626
+ }
2611
2627
  }, {
2612
2628
  key: "captureProductRuntime",
2613
2629
  value: function captureProductRuntime(tempOrder, rawProduct, normalizedProduct, existedUid) {
@@ -2880,9 +2896,9 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
2880
2896
  }, {
2881
2897
  key: "applyOrderProductsDepositSummary",
2882
2898
  value: function applyOrderProductsDepositSummary(tempOrder, summary) {
2883
- var _tempOrder$_extend7;
2899
+ var _tempOrder$_extend8;
2884
2900
  var depositSummary = calculateOrderProductsDeposit(tempOrder.products || [], {
2885
- productsByUid: (_tempOrder$_extend7 = tempOrder._extend) === null || _tempOrder$_extend7 === void 0 ? void 0 : _tempOrder$_extend7.productsByUid
2901
+ productsByUid: (_tempOrder$_extend8 = tempOrder._extend) === null || _tempOrder$_extend8 === void 0 ? void 0 : _tempOrder$_extend8.productsByUid
2886
2902
  });
2887
2903
  var manualDepositOverride = this.getManualDepositOverride(tempOrder);
2888
2904
  var currentStructuralFingerprint = manualDepositOverride ? this.buildOrderProductsStructuralFingerprint(tempOrder.products) : '';
@@ -4557,6 +4573,7 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4557
4573
  case 0:
4558
4574
  product_id = params.product_id, product_variant_id = params.product_variant_id, num = params.num, unique_identification_number = params.unique_identification_number, identity_key = params.identity_key, product_sku = params.product_sku, product_bundle = params.product_bundle;
4559
4575
  tempOrder = this.ensureTempOrder();
4576
+ this.repairDuplicateProductLineIdentities(tempOrder);
4560
4577
  identityLookup = {
4561
4578
  product_id: product_id,
4562
4579
  product_variant_id: product_variant_id
@@ -4582,11 +4599,11 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4582
4599
  productIndex = getProductIdentityIndex(tempOrder.products, identityLookup);
4583
4600
  }
4584
4601
  if (!(productIndex === -1)) {
4585
- _context27.next = 12;
4602
+ _context27.next = 13;
4586
4603
  break;
4587
4604
  }
4588
4605
  throw new Error('[Order] 目标商品不存在,无法更新数量');
4589
- case 12:
4606
+ case 13:
4590
4607
  targetProduct = tempOrder.products[productIndex];
4591
4608
  normalizedProduct = normalizeOrderProduct(_objectSpread(_objectSpread({}, targetProduct), {}, {
4592
4609
  num: getSafeProductNum(num),
@@ -4596,22 +4613,22 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4596
4613
  }));
4597
4614
  normalizedProduct.discount_list = normalizeOrderProductDiscountList(normalizedProduct.discount_list);
4598
4615
  tempOrder.products[productIndex] = normalizedProduct;
4599
- _context27.next = 18;
4616
+ _context27.next = 19;
4600
4617
  return this.applyPromotion();
4601
- case 18:
4618
+ case 19:
4602
4619
  this.applyDiscount();
4603
4620
  this.sanitizeTempOrderProducts(tempOrder);
4604
- _context27.next = 22;
4621
+ _context27.next = 23;
4605
4622
  return this.recalculateSummary({
4606
4623
  createIfMissing: true
4607
4624
  });
4608
- case 22:
4625
+ case 23:
4609
4626
  this.persistTempOrder();
4610
4627
  this.logInfo('updateOrderProductQuantity success', {
4611
4628
  params: params
4612
4629
  });
4613
4630
  return _context27.abrupt("return", tempOrder.products);
4614
- case 25:
4631
+ case 26:
4615
4632
  case "end":
4616
4633
  return _context27.stop();
4617
4634
  }
@@ -4704,12 +4721,13 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4704
4721
  while (1) switch (_context30.prev = _context30.next) {
4705
4722
  case 0:
4706
4723
  tempOrder = this.ensureTempOrder();
4724
+ this.repairDuplicateProductLineIdentities(tempOrder);
4707
4725
  if (identities.length) {
4708
- _context30.next = 3;
4726
+ _context30.next = 4;
4709
4727
  break;
4710
4728
  }
4711
4729
  return _context30.abrupt("return", tempOrder.products);
4712
- case 3:
4730
+ case 4:
4713
4731
  productsBeforeRemove = tempOrder.products || [];
4714
4732
  bookingsBeforeRemove = tempOrder.bookings || [];
4715
4733
  matchedProducts = productsBeforeRemove.filter(function (item) {
@@ -4731,7 +4749,7 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4731
4749
  }
4732
4750
  linkedBookingUidsToRemove = new Set();
4733
4751
  _iterator21 = _createForOfIteratorHelper(matchedProducts);
4734
- _context30.prev = 11;
4752
+ _context30.prev = 12;
4735
4753
  _loop3 = /*#__PURE__*/_regeneratorRuntime().mark(function _loop3() {
4736
4754
  var _metadata11, _metadata12, _metadata13, _metadata14;
4737
4755
  var product, productUid, productBookingUid, isAddTimeProduct;
@@ -4766,33 +4784,33 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4766
4784
  }, _loop3);
4767
4785
  });
4768
4786
  _iterator21.s();
4769
- case 14:
4787
+ case 15:
4770
4788
  if ((_step21 = _iterator21.n()).done) {
4771
- _context30.next = 20;
4789
+ _context30.next = 21;
4772
4790
  break;
4773
4791
  }
4774
- return _context30.delegateYield(_loop3(), "t0", 16);
4775
- case 16:
4792
+ return _context30.delegateYield(_loop3(), "t0", 17);
4793
+ case 17:
4776
4794
  if (!_context30.t0) {
4777
- _context30.next = 18;
4795
+ _context30.next = 19;
4778
4796
  break;
4779
4797
  }
4780
- return _context30.abrupt("continue", 18);
4781
- case 18:
4782
- _context30.next = 14;
4798
+ return _context30.abrupt("continue", 19);
4799
+ case 19:
4800
+ _context30.next = 15;
4783
4801
  break;
4784
- case 20:
4785
- _context30.next = 25;
4802
+ case 21:
4803
+ _context30.next = 26;
4786
4804
  break;
4787
- case 22:
4788
- _context30.prev = 22;
4789
- _context30.t1 = _context30["catch"](11);
4805
+ case 23:
4806
+ _context30.prev = 23;
4807
+ _context30.t1 = _context30["catch"](12);
4790
4808
  _iterator21.e(_context30.t1);
4791
- case 25:
4792
- _context30.prev = 25;
4809
+ case 26:
4810
+ _context30.prev = 26;
4793
4811
  _iterator21.f();
4794
- return _context30.finish(25);
4795
- case 28:
4812
+ return _context30.finish(26);
4813
+ case 29:
4796
4814
  if (linkedBookingUidsToRemove.size > 0) {
4797
4815
  // 删除主预约商品时,同步移除挂在同一 booking_uid 下的加时商品,避免残留孤儿行。
4798
4816
  tempOrder.products = (tempOrder.products || []).filter(function (line) {
@@ -4802,18 +4820,18 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
4802
4820
  return !(isAddTimeProduct && lineBookingUid !== undefined && lineBookingUid !== null && linkedBookingUidsToRemove.has(String(lineBookingUid)));
4803
4821
  });
4804
4822
  }
4805
- _context30.next = 31;
4823
+ _context30.next = 32;
4806
4824
  return this.finalizeAfterProductsMutation(tempOrder);
4807
- case 31:
4825
+ case 32:
4808
4826
  this.logInfo('removeProductsFromOrder success', {
4809
4827
  identities: identities
4810
4828
  });
4811
4829
  return _context30.abrupt("return", tempOrder.products);
4812
- case 33:
4830
+ case 34:
4813
4831
  case "end":
4814
4832
  return _context30.stop();
4815
4833
  }
4816
- }, _callee27, this, [[11, 22, 25, 28]]);
4834
+ }, _callee27, this, [[12, 23, 26, 29]]);
4817
4835
  }));
4818
4836
  function removeProductsFromOrder(_x34) {
4819
4837
  return _removeProductsFromOrder.apply(this, arguments);
@@ -5911,26 +5929,35 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
5911
5929
  key: "hydrateTempOrderFromRecord",
5912
5930
  value: (function () {
5913
5931
  var _hydrateTempOrderFromRecord = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee44(record, options) {
5914
- var _this$store$discount22;
5915
- var sourceRaw, identityReadySource, normalizedCollections, raw, hasIdentityChanges, identityLogMetadata, hasIdentityWarnings, isSessionStorageHydrate, nextTempOrder, _this$store$discount20, _this$store$discount21, restoredSessionDiscounts, nextExtend, sourceLastOrderInfo, isDraftOrder, syntheticIdentityOptions, rawSummary, summarySurcharges, hadSyntheticDraftOrderId, hydratedEditDiscounts, currentDiscounts, currentScanDiscounts, hydratedDiscountIds, retainedScanDiscounts, nextDiscounts, shouldSkipEmptyDiscountSync, _this$store$discount23, _this$store$discount24;
5932
+ var _sourceRaw$_extend, _this$store$discount22;
5933
+ var sourceRaw, duplicateProductIdentityRepair, duplicateIdentityReadySource, identityReadySource, normalizedCollections, raw, hasIdentityChanges, identityLogMetadata, hasIdentityWarnings, isSessionStorageHydrate, nextTempOrder, _this$store$discount20, _this$store$discount21, restoredSessionDiscounts, nextExtend, sourceLastOrderInfo, isDraftOrder, syntheticIdentityOptions, rawSummary, summarySurcharges, hadSyntheticDraftOrderId, hydratedEditDiscounts, currentDiscounts, currentScanDiscounts, hydratedDiscountIds, retainedScanDiscounts, nextDiscounts, shouldSkipEmptyDiscountSync, _this$store$discount23, _this$store$discount24;
5916
5934
  return _regeneratorRuntime().wrap(function _callee44$(_context47) {
5917
5935
  while (1) switch (_context47.prev = _context47.next) {
5918
5936
  case 0:
5919
5937
  sourceRaw = record && _typeof(record) === 'object' && record.data && record.order_id == null ? record.data : record || {};
5920
- identityReadySource = this.seedAnonymousBookingUidsFromProducts(sourceRaw);
5938
+ duplicateProductIdentityRepair = repairDuplicateOrderProductLineIdentities(Array.isArray(sourceRaw.products) ? sourceRaw.products : [], (_sourceRaw$_extend = sourceRaw._extend) === null || _sourceRaw$_extend === void 0 ? void 0 : _sourceRaw$_extend.productsByUid);
5939
+ duplicateIdentityReadySource = duplicateProductIdentityRepair.repairs.length ? _objectSpread(_objectSpread({}, sourceRaw), {}, {
5940
+ products: duplicateProductIdentityRepair.products
5941
+ }, duplicateProductIdentityRepair.productsByUid ? {
5942
+ _extend: _objectSpread(_objectSpread({}, sourceRaw._extend || {}), {}, {
5943
+ productsByUid: duplicateProductIdentityRepair.productsByUid
5944
+ })
5945
+ } : {}) : sourceRaw;
5946
+ identityReadySource = this.seedAnonymousBookingUidsFromProducts(duplicateIdentityReadySource);
5921
5947
  normalizedCollections = normalizeOrderCollections(identityReadySource);
5922
5948
  raw = normalizedCollections.order;
5923
- hasIdentityChanges = Object.values(normalizedCollections.stats).some(function (stats) {
5949
+ hasIdentityChanges = duplicateProductIdentityRepair.repairs.length > 0 || Object.values(normalizedCollections.stats).some(function (stats) {
5924
5950
  return stats.backfilledUidCount > 0 || stats.collapsedDuplicateCount > 0 || stats.uidConflictCount > 0 || stats.anonymousRowCount > 0;
5925
5951
  });
5926
5952
  if (hasIdentityChanges) {
5927
5953
  identityLogMetadata = {
5928
5954
  collections: normalizedCollections.stats,
5929
- uidRemapCount: normalizedCollections.uidRemaps.length
5955
+ uidRemapCount: normalizedCollections.uidRemaps.length,
5956
+ duplicateProductUidRepairCount: duplicateProductIdentityRepair.repairs.length
5930
5957
  };
5931
5958
  hasIdentityWarnings = Object.values(normalizedCollections.stats).some(function (stats) {
5932
5959
  return stats.uidConflictCount > 0 || stats.anonymousRowCount > 0;
5933
- });
5960
+ }) || duplicateProductIdentityRepair.repairs.length > 0;
5934
5961
  if (hasIdentityWarnings) {
5935
5962
  this.logWarning('Normalized hydrated order collection identities', identityLogMetadata);
5936
5963
  } else {
@@ -5943,7 +5970,7 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
5943
5970
  });
5944
5971
  this.store.tempOrder = nextTempOrder;
5945
5972
  if (!isSessionStorageHydrate) {
5946
- _context47.next = 27;
5973
+ _context47.next = 29;
5947
5974
  break;
5948
5975
  }
5949
5976
  // 会话快照是未提交购物车,不应获得编辑订单的 saved/edit 标记与服务端基线。
@@ -5959,24 +5986,24 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
5959
5986
  nextTempOrder.discount_list = restoredSessionDiscounts;
5960
5987
  this.store.summary = createEmptySummary();
5961
5988
  this.store.lastOrderInfo = undefined;
5962
- _context47.next = 20;
5963
- return (_this$store$discount20 = this.store.discount) === null || _this$store$discount20 === void 0 ? void 0 : _this$store$discount20.setOriginalDiscountList(cloneDeep(restoredSessionDiscounts));
5964
- case 20:
5965
5989
  _context47.next = 22;
5966
- return (_this$store$discount21 = this.store.discount) === null || _this$store$discount21 === void 0 ? void 0 : _this$store$discount21.setDiscountList(cloneDeep(restoredSessionDiscounts));
5990
+ return (_this$store$discount20 = this.store.discount) === null || _this$store$discount20 === void 0 ? void 0 : _this$store$discount20.setOriginalDiscountList(cloneDeep(restoredSessionDiscounts));
5967
5991
  case 22:
5992
+ _context47.next = 24;
5993
+ return (_this$store$discount21 = this.store.discount) === null || _this$store$discount21 === void 0 ? void 0 : _this$store$discount21.setDiscountList(cloneDeep(restoredSessionDiscounts));
5994
+ case 24:
5968
5995
  if (!(options !== null && options !== void 0 && options.recalcOnHydrate)) {
5969
- _context47.next = 25;
5996
+ _context47.next = 27;
5970
5997
  break;
5971
5998
  }
5972
- _context47.next = 25;
5999
+ _context47.next = 27;
5973
6000
  return this.recalculateSummary({
5974
6001
  createIfMissing: false
5975
6002
  });
5976
- case 25:
6003
+ case 27:
5977
6004
  this.persistTempOrder();
5978
6005
  return _context47.abrupt("return", nextTempOrder);
5979
- case 27:
6006
+ case 29:
5980
6007
  // Server list compatibility may synthesize order_id from external_sale_number; tempOrder must not.
5981
6008
  sourceLastOrderInfo = sourceRaw.lastOrderInfo || sourceRaw;
5982
6009
  isDraftOrder = Number(nextTempOrder.is_draft_order || 0) === 1;
@@ -6021,28 +6048,28 @@ export var OrderModule = /*#__PURE__*/function (_BaseModule) {
6021
6048
  nextDiscounts = [].concat(_toConsumableArray(hydratedEditDiscounts), _toConsumableArray(retainedScanDiscounts));
6022
6049
  shouldSkipEmptyDiscountSync = nextDiscounts.length === 0 && currentDiscounts.length === 0;
6023
6050
  if (shouldSkipEmptyDiscountSync) {
6024
- _context47.next = 50;
6051
+ _context47.next = 52;
6025
6052
  break;
6026
6053
  }
6027
6054
  OrderModule.populateSavedAmounts(nextTempOrder.products, nextDiscounts);
6028
- _context47.next = 48;
6029
- return (_this$store$discount23 = this.store.discount) === null || _this$store$discount23 === void 0 ? void 0 : _this$store$discount23.setOriginalDiscountList(nextDiscounts);
6030
- case 48:
6031
6055
  _context47.next = 50;
6032
- return (_this$store$discount24 = this.store.discount) === null || _this$store$discount24 === void 0 ? void 0 : _this$store$discount24.setDiscountList(nextDiscounts);
6056
+ return (_this$store$discount23 = this.store.discount) === null || _this$store$discount23 === void 0 ? void 0 : _this$store$discount23.setOriginalDiscountList(nextDiscounts);
6033
6057
  case 50:
6058
+ _context47.next = 52;
6059
+ return (_this$store$discount24 = this.store.discount) === null || _this$store$discount24 === void 0 ? void 0 : _this$store$discount24.setDiscountList(nextDiscounts);
6060
+ case 52:
6034
6061
  if (!(options !== null && options !== void 0 && options.recalcOnHydrate)) {
6035
- _context47.next = 53;
6062
+ _context47.next = 55;
6036
6063
  break;
6037
6064
  }
6038
- _context47.next = 53;
6065
+ _context47.next = 55;
6039
6066
  return this.recalculateSummary({
6040
6067
  createIfMissing: false
6041
6068
  });
6042
- case 53:
6069
+ case 55:
6043
6070
  this.persistTempOrder();
6044
6071
  return _context47.abrupt("return", nextTempOrder);
6045
- case 55:
6072
+ case 57:
6046
6073
  case "end":
6047
6074
  return _context47.stop();
6048
6075
  }
@@ -24,6 +24,16 @@ export interface NormalizeOrderCollectionsResult<T> {
24
24
  stats: Record<OrderCollectionKind, OrderCollectionIdentityStats>;
25
25
  uidRemaps: OrderCollectionUidRemap[];
26
26
  }
27
+ export interface ProductLineIdentityRepair {
28
+ index: number;
29
+ from: string;
30
+ to: string;
31
+ }
32
+ export interface RepairDuplicateProductLineIdentitiesResult<T> {
33
+ products: T[];
34
+ productsByUid?: Record<string, Record<string, any>>;
35
+ repairs: ProductLineIdentityRepair[];
36
+ }
27
37
  export declare function createUuidV4(): string;
28
38
  export declare function isBlankOrderCollectionIdentity(value: unknown): boolean;
29
39
  export declare function getOrderCollectionPersistentId(kind: OrderCollectionKind, item: unknown): string | null;
@@ -31,6 +41,11 @@ export declare function getOrderCollectionUid(kind: OrderCollectionKind, item: u
31
41
  export declare function buildOrderCollectionFallbackUid(kind: OrderCollectionKind, persistentId: string | number): string;
32
42
  export declare function getOrderCollectionReferenceUid(kind: OrderCollectionKind, item: unknown): string | null;
33
43
  export declare function setOrderCollectionUid<T>(kind: OrderCollectionKind, item: T, uid: string): T;
44
+ /**
45
+ * Rules 会把同一商品按商品券拆成多条真实订单行。拆行结果会继承原商品 UID,
46
+ * 因此必须在通用 collection normalization 之前为后续行派生稳定身份,避免被折叠。
47
+ */
48
+ export declare function repairDuplicateProductLineIdentities<T extends Record<string, any>>(products: T[] | null | undefined, productsByUid?: Record<string, Record<string, any>> | null): RepairDuplicateProductLineIdentitiesResult<T>;
34
49
  /**
35
50
  * 持久化行 ID 是服务端事实主键;只有至少一侧尚未持久化时,才允许用 runtime UID 对齐。
36
51
  */
@@ -74,6 +74,81 @@ export function setOrderCollectionUid(kind, item, uid) {
74
74
  delete record[uidField];
75
75
  return record;
76
76
  }
77
+ function getGoodPassResourceId(product) {
78
+ var _ref, _ref2, _goodPass$discount$re, _goodPass$discount;
79
+ var goodPass = (product.discount_list || []).find(function (item) {
80
+ var _item$discount, _item$discount2;
81
+ var type = (item === null || item === void 0 ? void 0 : item.type) || (item === null || item === void 0 ? void 0 : item.tag) || (item === null || item === void 0 || (_item$discount = item.discount) === null || _item$discount === void 0 ? void 0 : _item$discount.type) || (item === null || item === void 0 || (_item$discount2 = item.discount) === null || _item$discount2 === void 0 ? void 0 : _item$discount2.tag);
82
+ return type === 'good_pass';
83
+ });
84
+ if (!goodPass) return null;
85
+ var resourceId = (_ref = (_ref2 = (_goodPass$discount$re = (_goodPass$discount = goodPass.discount) === null || _goodPass$discount === void 0 ? void 0 : _goodPass$discount.resource_id) !== null && _goodPass$discount$re !== void 0 ? _goodPass$discount$re : goodPass.resource_id) !== null && _ref2 !== void 0 ? _ref2 : goodPass.discount_id) !== null && _ref !== void 0 ? _ref : goodPass.id;
86
+ return isBlankOrderCollectionIdentity(resourceId) ? null : String(resourceId);
87
+ }
88
+ function buildSplitProductUid(originalUid, product, duplicateOrdinal) {
89
+ var encodedOriginalUid = encodeURIComponent(originalUid);
90
+ var resourceId = getGoodPassResourceId(product);
91
+ if (resourceId) {
92
+ return "os-split:product:".concat(encodedOriginalUid, ":good-pass:").concat(encodeURIComponent(resourceId), ":").concat(duplicateOrdinal);
93
+ }
94
+ return "os-split:product:".concat(encodedOriginalUid, ":duplicate:").concat(duplicateOrdinal);
95
+ }
96
+
97
+ /**
98
+ * Rules 会把同一商品按商品券拆成多条真实订单行。拆行结果会继承原商品 UID,
99
+ * 因此必须在通用 collection normalization 之前为后续行派生稳定身份,避免被折叠。
100
+ */
101
+ export function repairDuplicateProductLineIdentities(products, productsByUid) {
102
+ var repairedProducts = cloneDeep(products || []);
103
+ var repairedProductsByUid = productsByUid ? cloneDeep(productsByUid) : undefined;
104
+ var occupiedUids = new Set(repairedProducts.map(function (product) {
105
+ return getOrderCollectionUid('product', product);
106
+ }).filter(function (uid) {
107
+ return Boolean(uid);
108
+ }));
109
+ Object.keys(repairedProductsByUid || {}).forEach(function (uid) {
110
+ return occupiedUids.add(uid);
111
+ });
112
+ var occurrences = new Map();
113
+ var repairs = [];
114
+ repairedProducts.forEach(function (product, index) {
115
+ var originalUid = getOrderCollectionUid('product', product);
116
+ if (!originalUid) return;
117
+ var occurrence = (occurrences.get(originalUid) || 0) + 1;
118
+ occurrences.set(originalUid, occurrence);
119
+ if (occurrence === 1) return;
120
+ var duplicateOrdinal = occurrence - 1;
121
+ var baseCandidate = buildSplitProductUid(originalUid, product, duplicateOrdinal);
122
+ var nextUid = baseCandidate;
123
+ var conflictOrdinal = 1;
124
+ while (occupiedUids.has(nextUid)) {
125
+ nextUid = "".concat(baseCandidate, ":conflict:").concat(conflictOrdinal);
126
+ conflictOrdinal += 1;
127
+ }
128
+ var repairedProduct = setOrderCollectionUid('product', product, nextUid);
129
+ repairedProduct.identity_key = nextUid;
130
+ repairedProducts[index] = repairedProduct;
131
+ occupiedUids.add(nextUid);
132
+ repairs.push({
133
+ index: index,
134
+ from: originalUid,
135
+ to: nextUid
136
+ });
137
+ var runtimeSource = repairedProductsByUid === null || repairedProductsByUid === void 0 ? void 0 : repairedProductsByUid[originalUid];
138
+ if (runtimeSource && repairedProductsByUid) {
139
+ repairedProductsByUid[nextUid] = _objectSpread(_objectSpread({}, cloneDeep(runtimeSource)), runtimeSource.rowKey !== undefined ? {
140
+ rowKey: nextUid
141
+ } : {});
142
+ }
143
+ });
144
+ return _objectSpread(_objectSpread({
145
+ products: repairedProducts
146
+ }, repairedProductsByUid ? {
147
+ productsByUid: repairedProductsByUid
148
+ } : {}), {}, {
149
+ repairs: repairs
150
+ });
151
+ }
77
152
 
78
153
  /**
79
154
  * 持久化行 ID 是服务端事实主键;只有至少一侧尚未持久化时,才允许用 runtime UID 对齐。
@@ -326,7 +326,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
326
326
  date: string;
327
327
  status: string;
328
328
  week: string;
329
- weekNum: 0 | 1 | 2 | 3 | 4 | 5 | 6;
329
+ weekNum: 0 | 5 | 2 | 1 | 6 | 4 | 3;
330
330
  }[]>;
331
331
  submitTimeSlot(timeSlots: TimeSliceItem): void;
332
332
  private getScheduleDataByIds;
@@ -447,7 +447,19 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
447
447
  * 加车两阶段主决策(规格弹窗 / 资源编辑 / 直接加车)。
448
448
  * 与 ticketBooking `handleSelectProduct` + `handleBooking4Service` 等价。
449
449
  */
450
- decideAddProduct(item: any, options?: Partial<AddProductDecideContext>): import("./utils/addProductDecision").AddProductDecision;
450
+ decideAddProduct(item: any, options?: Partial<AddProductDecideContext>): {
451
+ action: "reloadCatalog";
452
+ } | {
453
+ action: "add";
454
+ cacheItem: any;
455
+ } | {
456
+ action: "requiresDetail";
457
+ payload: AddProductRequiresDetailPayload;
458
+ } | {
459
+ action: "requiresBookingEdit";
460
+ cacheItem: any;
461
+ payload: import("./utils/addProductDecision").AddProductRequiresBookingEditPayload;
462
+ };
451
463
  /** 规格弹窗 callback 后的第二段决策。 */
452
464
  decideAfterDetail(cacheItem: any, options?: Partial<AddProductDecideContext>): import("./utils/addProductDecision").AddProductDecision;
453
465
  /**
@@ -402,35 +402,35 @@ export var BookingTicketImpl = /*#__PURE__*/function (_BaseSalesImpl) {
402
402
  value: function () {
403
403
  var _configureIdGeneratorFromOpenData = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee5() {
404
404
  var _openDataConfig, _openDataConfig2, _openDataConfig3, _openDataConfig4, _openDataConfig5, _openDataConfig6;
405
- var openDataConfig, idGenerator, receiptSequenceLength, receiptSequenceStart, prefix, shopOrderPrefix, resetReceiptSequenceDaily, operatingDayBoundary, deviceId, businessCode;
405
+ var idGenerator, openDataConfig, receiptSequenceLength, receiptSequenceStart, prefix, shopOrderPrefix, resetReceiptSequenceDaily, operatingDayBoundary, deviceId, businessCode;
406
406
  return _regeneratorRuntime().wrap(function _callee5$(_context5) {
407
407
  while (1) switch (_context5.prev = _context5.next) {
408
408
  case 0:
409
409
  // 业务码切换或配置失败时不能复用上一次初始化的短号。
410
410
  this.setPaymentNumberDevicePrefix(null);
411
- openDataConfig = null;
412
- _context5.prev = 2;
413
- _context5.next = 5;
414
- return this.loadOpenDataConfig();
415
- case 5:
416
- openDataConfig = _context5.sent;
417
- _context5.next = 12;
418
- break;
419
- case 8:
420
- _context5.prev = 8;
421
- _context5.t0 = _context5["catch"](2);
422
- console.warn('[BookingTicket] OpenData 配置加载失败,跳过 idGenerator 配置', _context5.t0);
423
- return _context5.abrupt("return");
424
- case 12:
425
- // if (!openDataConfig) return;
426
411
  idGenerator = this.getIdGeneratorPlugin();
427
412
  if (idGenerator !== null && idGenerator !== void 0 && idGenerator.configure) {
428
- _context5.next = 16;
413
+ _context5.next = 5;
429
414
  break;
430
415
  }
431
416
  console.warn('[BookingTicket] idGenerator 插件不可用,跳过配置');
432
417
  return _context5.abrupt("return");
418
+ case 5:
419
+ openDataConfig = null;
420
+ _context5.prev = 6;
421
+ _context5.next = 9;
422
+ return this.loadOpenDataConfig();
423
+ case 9:
424
+ openDataConfig = _context5.sent;
425
+ _context5.next = 16;
426
+ break;
427
+ case 12:
428
+ _context5.prev = 12;
429
+ _context5.t0 = _context5["catch"](6);
430
+ console.warn('[BookingTicket] OpenData 配置加载失败,跳过 idGenerator 配置', _context5.t0);
431
+ return _context5.abrupt("return");
433
432
  case 16:
433
+ // if (!openDataConfig) return;
434
434
  receiptSequenceLength = this.normalizePositiveInteger((_openDataConfig = openDataConfig) === null || _openDataConfig === void 0 ? void 0 : _openDataConfig['sale.short_number_digits'], 5);
435
435
  receiptSequenceStart = this.normalizePositiveInteger((_openDataConfig2 = openDataConfig) === null || _openDataConfig2 === void 0 ? void 0 : _openDataConfig2['sale.short_number_start_number'], 1);
436
436
  prefix = this.normalizeIdPrefix((_openDataConfig3 = openDataConfig) === null || _openDataConfig3 === void 0 ? void 0 : _openDataConfig3['sale.short_number_prefix'], '');
@@ -477,7 +477,7 @@ export var BookingTicketImpl = /*#__PURE__*/function (_BaseSalesImpl) {
477
477
  case "end":
478
478
  return _context5.stop();
479
479
  }
480
- }, _callee5, this, [[2, 8], [30, 35]]);
480
+ }, _callee5, this, [[6, 12], [30, 35]]);
481
481
  }));
482
482
  function configureIdGeneratorFromOpenData() {
483
483
  return _configureIdGeneratorFromOpenData.apply(this, arguments);
@@ -63,8 +63,19 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
63
63
  private openDataTarget;
64
64
  private loadOpenDataInFlight;
65
65
  private loadOpenDataInFlightTarget;
66
+ protected getSubmitOrderSalesChannel(): string;
66
67
  protected isSessionStoragePersistEnabled(): boolean;
67
68
  protected shouldUseSessionStorageForSubModule(moduleName?: string): boolean;
69
+ /**
70
+ * 读取与当前 cacheId 同分桶的 Solution 会话状态。
71
+ * 该状态独立于 otherParams,适合页面皮肤保存短生命周期 UI 状态。
72
+ */
73
+ getSessionStateValue<T = unknown>(key: string): T | undefined;
74
+ /**
75
+ * 写入与当前 cacheId 同分桶的 Solution 会话状态。
76
+ * value 为 undefined 时删除该 key;存储失败不阻断业务流程。
77
+ */
78
+ setSessionStateValue(key: string, value: unknown): void;
68
79
  /**
69
80
  * Kiosk 不能把已收款订单留在本地等待重试:默认直接等待云端 checkout。
70
81
  * 兼容仍需要 WebPOS 待同步模式的入口可显式传 checkoutSyncTaskEnabled: true。
@@ -40,6 +40,9 @@ import { BookingTicket } from "../BookingTicket";
40
40
  export * from "./types";
41
41
  var OPEN_DATA_SECTION_CODES = ['sale', 'reservation', 'fulfillment', 'menu', 'workflow'];
42
42
  var OPEN_DATA_CACHE_TTL = 5 * 60 * 1000;
43
+ function isRecord(value) {
44
+ return !!value && _typeof(value) === 'object' && !Array.isArray(value);
45
+ }
43
46
  function pickLocalizedText(value) {
44
47
  if (!value) return undefined;
45
48
  if (typeof value === 'string') return value;
@@ -520,10 +523,17 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
520
523
  return _this;
521
524
  }
522
525
  _createClass(UnifiedBookingSalesImpl, [{
526
+ key: "getSubmitOrderSalesChannel",
527
+ value: function getSubmitOrderSalesChannel() {
528
+ var _this$otherParams$cha, _this$otherParams;
529
+ var channel = String((_this$otherParams$cha = (_this$otherParams = this.otherParams) === null || _this$otherParams === void 0 ? void 0 : _this$otherParams.channel) !== null && _this$otherParams$cha !== void 0 ? _this$otherParams$cha : '').trim();
530
+ return channel || _get(_getPrototypeOf(UnifiedBookingSalesImpl.prototype), "getSubmitOrderSalesChannel", this).call(this);
531
+ }
532
+ }, {
523
533
  key: "isSessionStoragePersistEnabled",
524
534
  value: function isSessionStoragePersistEnabled() {
525
- var _this$otherParams;
526
- return Boolean(this.cacheId && ((_this$otherParams = this.otherParams) === null || _this$otherParams === void 0 ? void 0 : _this$otherParams.enableSessionStoragePersist) === true);
535
+ var _this$otherParams2;
536
+ return Boolean(this.cacheId && ((_this$otherParams2 = this.otherParams) === null || _this$otherParams2 === void 0 ? void 0 : _this$otherParams2.enableSessionStoragePersist) === true);
527
537
  }
528
538
  }, {
529
539
  key: "shouldUseSessionStorageForSubModule",
@@ -531,6 +541,49 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
531
541
  return this.isSessionStoragePersistEnabled() && moduleName === 'order';
532
542
  }
533
543
 
544
+ /**
545
+ * 读取与当前 cacheId 同分桶的 Solution 会话状态。
546
+ * 该状态独立于 otherParams,适合页面皮肤保存短生命周期 UI 状态。
547
+ */
548
+ }, {
549
+ key: "getSessionStateValue",
550
+ value: function getSessionStateValue(key) {
551
+ if (!key || !this.isSessionStoragePersistEnabled()) return undefined;
552
+ var cacheData = this.readSessionCacheData();
553
+ var solutionCache = cacheData === null || cacheData === void 0 ? void 0 : cacheData[this.name];
554
+ if (!isRecord(solutionCache) || !isRecord(solutionCache.sessionState)) {
555
+ return undefined;
556
+ }
557
+ return solutionCache.sessionState[key];
558
+ }
559
+
560
+ /**
561
+ * 写入与当前 cacheId 同分桶的 Solution 会话状态。
562
+ * value 为 undefined 时删除该 key;存储失败不阻断业务流程。
563
+ */
564
+ }, {
565
+ key: "setSessionStateValue",
566
+ value: function setSessionStateValue(key, value) {
567
+ if (!key || !this.isSessionStoragePersistEnabled()) return;
568
+ var cacheData = this.readSessionCacheData();
569
+ var solutionCache = cacheData === null || cacheData === void 0 ? void 0 : cacheData[this.name];
570
+ var currentSessionState = isRecord(solutionCache) && isRecord(solutionCache.sessionState) ? solutionCache.sessionState : {};
571
+ var nextSessionState = _objectSpread({}, currentSessionState);
572
+ if (value === undefined) {
573
+ delete nextSessionState[key];
574
+ } else {
575
+ nextSessionState[key] = value;
576
+ }
577
+ this.checkSaveCache({
578
+ cacheId: this.cacheId,
579
+ fatherModule: this.name,
580
+ store: {
581
+ sessionState: nextSessionState
582
+ },
583
+ cacheKey: ['sessionState']
584
+ });
585
+ }
586
+
534
587
  /**
535
588
  * Kiosk 不能把已收款订单留在本地等待重试:默认直接等待云端 checkout。
536
589
  * 兼容仍需要 WebPOS 待同步模式的入口可显式传 checkoutSyncTaskEnabled: true。
@@ -538,8 +591,8 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
538
591
  }, {
539
592
  key: "shouldUseCheckoutSyncTask",
540
593
  value: function shouldUseCheckoutSyncTask() {
541
- var _this$otherParams2;
542
- var configured = (_this$otherParams2 = this.otherParams) === null || _this$otherParams2 === void 0 ? void 0 : _this$otherParams2.checkoutSyncTaskEnabled;
594
+ var _this$otherParams3;
595
+ var configured = (_this$otherParams3 = this.otherParams) === null || _this$otherParams3 === void 0 ? void 0 : _this$otherParams3.checkoutSyncTaskEnabled;
543
596
  return typeof configured === 'boolean' ? configured : false;
544
597
  }
545
598
 
@@ -697,8 +750,8 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
697
750
  }, {
698
751
  key: "isAuthenticatedCustomerUserPlatform",
699
752
  value: function isAuthenticatedCustomerUserPlatform() {
700
- var _this$otherParams3;
701
- var platform = String(((_this$otherParams3 = this.otherParams) === null || _this$otherParams3 === void 0 ? void 0 : _this$otherParams3.platform) || '').toLowerCase();
753
+ var _this$otherParams4;
754
+ var platform = String(((_this$otherParams4 = this.otherParams) === null || _this$otherParams4 === void 0 ? void 0 : _this$otherParams4.platform) || '').toLowerCase();
702
755
  return platform === 'pc' || platform === 'h5';
703
756
  }
704
757
  }, {
@@ -877,13 +930,13 @@ export var UnifiedBookingSalesImpl = /*#__PURE__*/function (_BookingTicket) {
877
930
  key: "getOpenData",
878
931
  value: function () {
879
932
  var _getOpenData = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee6() {
880
- var _this$otherParams4, _this$otherParams5, _this$store$openData;
933
+ var _this$otherParams5, _this$otherParams6, _this$store$openData;
881
934
  var businessCode, channel, target, cachedData;
882
935
  return _regeneratorRuntime().wrap(function _callee6$(_context6) {
883
936
  while (1) switch (_context6.prev = _context6.next) {
884
937
  case 0:
885
- businessCode = String(((_this$otherParams4 = this.otherParams) === null || _this$otherParams4 === void 0 ? void 0 : _this$otherParams4.businessCode) || '').trim();
886
- channel = String(((_this$otherParams5 = this.otherParams) === null || _this$otherParams5 === void 0 ? void 0 : _this$otherParams5.channel) || '').trim();
938
+ businessCode = String(((_this$otherParams5 = this.otherParams) === null || _this$otherParams5 === void 0 ? void 0 : _this$otherParams5.businessCode) || '').trim();
939
+ channel = String(((_this$otherParams6 = this.otherParams) === null || _this$otherParams6 === void 0 ? void 0 : _this$otherParams6.channel) || '').trim();
887
940
  if (!(!businessCode || !channel)) {
888
941
  _context6.next = 4;
889
942
  break;
@@ -1,51 +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
- ITEM_REWARD_STRATEGY: () => import_examples.ITEM_REWARD_STRATEGY,
34
- PromotionAdapter: () => import_adapter.PromotionAdapter,
35
- PromotionEvaluator: () => import_evaluator.PromotionEvaluator,
36
- X_ITEMS_FOR_Y_PRICE_STRATEGY: () => import_examples.X_ITEMS_FOR_Y_PRICE_STRATEGY,
37
- default: () => import_adapter2.default
38
- });
39
- module.exports = __toCommonJS(promotion_exports);
40
- var import_evaluator = require("./evaluator");
41
- var import_adapter = require("./adapter");
42
- var import_adapter2 = __toESM(require("./adapter"));
43
- var import_examples = require("./examples");
44
- // Annotate the CommonJS export names for ESM import in node:
45
- 0 && (module.exports = {
46
- BUY_X_GET_Y_FREE_STRATEGY,
47
- ITEM_REWARD_STRATEGY,
48
- PromotionAdapter,
49
- PromotionEvaluator,
50
- X_ITEMS_FOR_Y_PRICE_STRATEGY
51
- });
@@ -237,6 +237,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
237
237
  private sanitizeOrderProductForTempOrder;
238
238
  private sanitizeTempOrderProducts;
239
239
  private ensureExtend;
240
+ private repairDuplicateProductLineIdentities;
240
241
  private captureProductRuntime;
241
242
  private createLinkedProductAndBooking;
242
243
  /**
@@ -497,7 +498,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
497
498
  generateIdempotencyToken(): string;
498
499
  syncPaymentsToOrder<T = any>(params: SyncPaymentsToOrderParams): Promise<SyncPaymentsToOrderResult<T>>;
499
500
  createOrder(params: CommitOrderParams['query']): {
500
- type: "virtual" | "appointment_booking";
501
+ type: "appointment_booking" | "virtual";
501
502
  platform: string;
502
503
  sales_channel: string;
503
504
  order_sales_channel: string;
@@ -694,6 +694,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
694
694
  };
695
695
  })
696
696
  );
697
+ this.repairDuplicateProductLineIdentities(tempOrder);
697
698
  }
698
699
  if (result == null ? void 0 : result.discountList) {
699
700
  OrderModule.populateSavedAmounts(
@@ -1931,6 +1932,23 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1931
1932
  }
1932
1933
  return tempOrder._extend;
1933
1934
  }
1935
+ repairDuplicateProductLineIdentities(tempOrder) {
1936
+ var _a;
1937
+ const result = (0, import_orderCollectionIdentity.repairDuplicateProductLineIdentities)(
1938
+ tempOrder.products,
1939
+ (_a = tempOrder._extend) == null ? void 0 : _a.productsByUid
1940
+ );
1941
+ if (!result.repairs.length)
1942
+ return;
1943
+ tempOrder.products = result.products;
1944
+ if (result.productsByUid) {
1945
+ this.ensureExtend(tempOrder).productsByUid = result.productsByUid;
1946
+ }
1947
+ this.logWarning("Repaired duplicate product line identities", {
1948
+ repairCount: result.repairs.length,
1949
+ repairs: result.repairs
1950
+ });
1951
+ }
1934
1952
  captureProductRuntime(tempOrder, rawProduct, normalizedProduct, existedUid) {
1935
1953
  var _a;
1936
1954
  const uid = existedUid || this.getProductUid(normalizedProduct);
@@ -3484,6 +3502,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3484
3502
  product_bundle
3485
3503
  } = params;
3486
3504
  const tempOrder = this.ensureTempOrder();
3505
+ this.repairDuplicateProductLineIdentities(tempOrder);
3487
3506
  const identityLookup = {
3488
3507
  product_id,
3489
3508
  product_variant_id
@@ -3594,6 +3613,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3594
3613
  async removeProductsFromOrder(identities) {
3595
3614
  var _a, _b, _c, _d;
3596
3615
  const tempOrder = this.ensureTempOrder();
3616
+ this.repairDuplicateProductLineIdentities(tempOrder);
3597
3617
  if (!identities.length) {
3598
3618
  return tempOrder.products;
3599
3619
  }
@@ -4300,24 +4320,39 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4300
4320
  * 例如数组字段、商品行唯一值、价格 metadata、summary 派生状态和 _extend。
4301
4321
  */
4302
4322
  async hydrateTempOrderFromRecord(record, options) {
4303
- var _a, _b, _c, _d, _e;
4323
+ var _a, _b, _c, _d, _e, _f;
4304
4324
  const sourceRaw = record && typeof record === "object" && record.data && record.order_id == null ? record.data : record || {};
4325
+ const duplicateProductIdentityRepair = (0, import_orderCollectionIdentity.repairDuplicateProductLineIdentities)(
4326
+ Array.isArray(sourceRaw.products) ? sourceRaw.products : [],
4327
+ (_a = sourceRaw._extend) == null ? void 0 : _a.productsByUid
4328
+ );
4329
+ const duplicateIdentityReadySource = duplicateProductIdentityRepair.repairs.length ? {
4330
+ ...sourceRaw,
4331
+ products: duplicateProductIdentityRepair.products,
4332
+ ...duplicateProductIdentityRepair.productsByUid ? {
4333
+ _extend: {
4334
+ ...sourceRaw._extend || {},
4335
+ productsByUid: duplicateProductIdentityRepair.productsByUid
4336
+ }
4337
+ } : {}
4338
+ } : sourceRaw;
4305
4339
  const identityReadySource = this.seedAnonymousBookingUidsFromProducts(
4306
- sourceRaw
4340
+ duplicateIdentityReadySource
4307
4341
  );
4308
4342
  const normalizedCollections = (0, import_orderCollectionIdentity.normalizeOrderCollections)(identityReadySource);
4309
4343
  const raw = normalizedCollections.order;
4310
- const hasIdentityChanges = Object.values(normalizedCollections.stats).some(
4344
+ const hasIdentityChanges = duplicateProductIdentityRepair.repairs.length > 0 || Object.values(normalizedCollections.stats).some(
4311
4345
  (stats) => stats.backfilledUidCount > 0 || stats.collapsedDuplicateCount > 0 || stats.uidConflictCount > 0 || stats.anonymousRowCount > 0
4312
4346
  );
4313
4347
  if (hasIdentityChanges) {
4314
4348
  const identityLogMetadata = {
4315
4349
  collections: normalizedCollections.stats,
4316
- uidRemapCount: normalizedCollections.uidRemaps.length
4350
+ uidRemapCount: normalizedCollections.uidRemaps.length,
4351
+ duplicateProductUidRepairCount: duplicateProductIdentityRepair.repairs.length
4317
4352
  };
4318
4353
  const hasIdentityWarnings = Object.values(normalizedCollections.stats).some(
4319
4354
  (stats) => stats.uidConflictCount > 0 || stats.anonymousRowCount > 0
4320
- );
4355
+ ) || duplicateProductIdentityRepair.repairs.length > 0;
4321
4356
  if (hasIdentityWarnings) {
4322
4357
  this.logWarning(
4323
4358
  "Normalized hydrated order collection identities",
@@ -4348,10 +4383,10 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4348
4383
  nextTempOrder.discount_list = restoredSessionDiscounts;
4349
4384
  this.store.summary = (0, import_utils.createEmptySummary)();
4350
4385
  this.store.lastOrderInfo = void 0;
4351
- await ((_a = this.store.discount) == null ? void 0 : _a.setOriginalDiscountList(
4386
+ await ((_b = this.store.discount) == null ? void 0 : _b.setOriginalDiscountList(
4352
4387
  (0, import_lodash_es.cloneDeep)(restoredSessionDiscounts)
4353
4388
  ));
4354
- await ((_b = this.store.discount) == null ? void 0 : _b.setDiscountList(
4389
+ await ((_c = this.store.discount) == null ? void 0 : _c.setDiscountList(
4355
4390
  (0, import_lodash_es.cloneDeep)(restoredSessionDiscounts)
4356
4391
  ));
4357
4392
  if (options == null ? void 0 : options.recalcOnHydrate) {
@@ -4394,7 +4429,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4394
4429
  this.store.syncState = "local";
4395
4430
  }
4396
4431
  const hydratedEditDiscounts = this.collectHydratedEditDiscounts(nextTempOrder.products);
4397
- const currentDiscounts = typeof ((_c = this.store.discount) == null ? void 0 : _c.getDiscountList) === "function" ? this.store.discount.getDiscountList() || [] : [];
4432
+ const currentDiscounts = typeof ((_d = this.store.discount) == null ? void 0 : _d.getDiscountList) === "function" ? this.store.discount.getDiscountList() || [] : [];
4398
4433
  const currentScanDiscounts = currentDiscounts.filter((discount) => discount == null ? void 0 : discount.isScan);
4399
4434
  const hydratedDiscountIds = new Set(
4400
4435
  hydratedEditDiscounts.map((discount) => discount == null ? void 0 : discount.id)
@@ -4412,8 +4447,8 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4412
4447
  nextTempOrder.products,
4413
4448
  nextDiscounts
4414
4449
  );
4415
- await ((_d = this.store.discount) == null ? void 0 : _d.setOriginalDiscountList(nextDiscounts));
4416
- await ((_e = this.store.discount) == null ? void 0 : _e.setDiscountList(nextDiscounts));
4450
+ await ((_e = this.store.discount) == null ? void 0 : _e.setOriginalDiscountList(nextDiscounts));
4451
+ await ((_f = this.store.discount) == null ? void 0 : _f.setDiscountList(nextDiscounts));
4417
4452
  }
4418
4453
  if (options == null ? void 0 : options.recalcOnHydrate) {
4419
4454
  await this.recalculateSummary({ createIfMissing: false });
@@ -24,6 +24,16 @@ export interface NormalizeOrderCollectionsResult<T> {
24
24
  stats: Record<OrderCollectionKind, OrderCollectionIdentityStats>;
25
25
  uidRemaps: OrderCollectionUidRemap[];
26
26
  }
27
+ export interface ProductLineIdentityRepair {
28
+ index: number;
29
+ from: string;
30
+ to: string;
31
+ }
32
+ export interface RepairDuplicateProductLineIdentitiesResult<T> {
33
+ products: T[];
34
+ productsByUid?: Record<string, Record<string, any>>;
35
+ repairs: ProductLineIdentityRepair[];
36
+ }
27
37
  export declare function createUuidV4(): string;
28
38
  export declare function isBlankOrderCollectionIdentity(value: unknown): boolean;
29
39
  export declare function getOrderCollectionPersistentId(kind: OrderCollectionKind, item: unknown): string | null;
@@ -31,6 +41,11 @@ export declare function getOrderCollectionUid(kind: OrderCollectionKind, item: u
31
41
  export declare function buildOrderCollectionFallbackUid(kind: OrderCollectionKind, persistentId: string | number): string;
32
42
  export declare function getOrderCollectionReferenceUid(kind: OrderCollectionKind, item: unknown): string | null;
33
43
  export declare function setOrderCollectionUid<T>(kind: OrderCollectionKind, item: T, uid: string): T;
44
+ /**
45
+ * Rules 会把同一商品按商品券拆成多条真实订单行。拆行结果会继承原商品 UID,
46
+ * 因此必须在通用 collection normalization 之前为后续行派生稳定身份,避免被折叠。
47
+ */
48
+ export declare function repairDuplicateProductLineIdentities<T extends Record<string, any>>(products: T[] | null | undefined, productsByUid?: Record<string, Record<string, any>> | null): RepairDuplicateProductLineIdentitiesResult<T>;
34
49
  /**
35
50
  * 持久化行 ID 是服务端事实主键;只有至少一侧尚未持久化时,才允许用 runtime UID 对齐。
36
51
  */
@@ -30,6 +30,7 @@ __export(orderCollectionIdentity_exports, {
30
30
  mergeOrderCollectionItems: () => mergeOrderCollectionItems,
31
31
  normalizeOrderCollection: () => normalizeOrderCollection,
32
32
  normalizeOrderCollections: () => normalizeOrderCollections,
33
+ repairDuplicateProductLineIdentities: () => repairDuplicateProductLineIdentities,
33
34
  setOrderCollectionUid: () => setOrderCollectionUid
34
35
  });
35
36
  module.exports = __toCommonJS(orderCollectionIdentity_exports);
@@ -105,6 +106,74 @@ function setOrderCollectionUid(kind, item, uid) {
105
106
  delete record[uidField];
106
107
  return record;
107
108
  }
109
+ function getGoodPassResourceId(product) {
110
+ var _a;
111
+ const goodPass = (product.discount_list || []).find((item) => {
112
+ var _a2, _b;
113
+ const type = (item == null ? void 0 : item.type) || (item == null ? void 0 : item.tag) || ((_a2 = item == null ? void 0 : item.discount) == null ? void 0 : _a2.type) || ((_b = item == null ? void 0 : item.discount) == null ? void 0 : _b.tag);
114
+ return type === "good_pass";
115
+ });
116
+ if (!goodPass)
117
+ return null;
118
+ const resourceId = ((_a = goodPass.discount) == null ? void 0 : _a.resource_id) ?? goodPass.resource_id ?? goodPass.discount_id ?? goodPass.id;
119
+ return isBlankOrderCollectionIdentity(resourceId) ? null : String(resourceId);
120
+ }
121
+ function buildSplitProductUid(originalUid, product, duplicateOrdinal) {
122
+ const encodedOriginalUid = encodeURIComponent(originalUid);
123
+ const resourceId = getGoodPassResourceId(product);
124
+ if (resourceId) {
125
+ return `os-split:product:${encodedOriginalUid}:good-pass:${encodeURIComponent(resourceId)}:${duplicateOrdinal}`;
126
+ }
127
+ return `os-split:product:${encodedOriginalUid}:duplicate:${duplicateOrdinal}`;
128
+ }
129
+ function repairDuplicateProductLineIdentities(products, productsByUid) {
130
+ const repairedProducts = (0, import_lodash_es.cloneDeep)(products || []);
131
+ const repairedProductsByUid = productsByUid ? (0, import_lodash_es.cloneDeep)(productsByUid) : void 0;
132
+ const occupiedUids = new Set(
133
+ repairedProducts.map((product) => getOrderCollectionUid("product", product)).filter((uid) => Boolean(uid))
134
+ );
135
+ Object.keys(repairedProductsByUid || {}).forEach((uid) => occupiedUids.add(uid));
136
+ const occurrences = /* @__PURE__ */ new Map();
137
+ const repairs = [];
138
+ repairedProducts.forEach((product, index) => {
139
+ const originalUid = getOrderCollectionUid("product", product);
140
+ if (!originalUid)
141
+ return;
142
+ const occurrence = (occurrences.get(originalUid) || 0) + 1;
143
+ occurrences.set(originalUid, occurrence);
144
+ if (occurrence === 1)
145
+ return;
146
+ const duplicateOrdinal = occurrence - 1;
147
+ const baseCandidate = buildSplitProductUid(
148
+ originalUid,
149
+ product,
150
+ duplicateOrdinal
151
+ );
152
+ let nextUid = baseCandidate;
153
+ let conflictOrdinal = 1;
154
+ while (occupiedUids.has(nextUid)) {
155
+ nextUid = `${baseCandidate}:conflict:${conflictOrdinal}`;
156
+ conflictOrdinal += 1;
157
+ }
158
+ const repairedProduct = setOrderCollectionUid("product", product, nextUid);
159
+ repairedProduct.identity_key = nextUid;
160
+ repairedProducts[index] = repairedProduct;
161
+ occupiedUids.add(nextUid);
162
+ repairs.push({ index, from: originalUid, to: nextUid });
163
+ const runtimeSource = repairedProductsByUid == null ? void 0 : repairedProductsByUid[originalUid];
164
+ if (runtimeSource && repairedProductsByUid) {
165
+ repairedProductsByUid[nextUid] = {
166
+ ...(0, import_lodash_es.cloneDeep)(runtimeSource),
167
+ ...runtimeSource.rowKey !== void 0 ? { rowKey: nextUid } : {}
168
+ };
169
+ }
170
+ });
171
+ return {
172
+ products: repairedProducts,
173
+ ...repairedProductsByUid ? { productsByUid: repairedProductsByUid } : {},
174
+ repairs
175
+ };
176
+ }
108
177
  function isSameOrderCollectionItem(kind, left, right) {
109
178
  const leftPersistentId = getOrderCollectionPersistentId(kind, left);
110
179
  const rightPersistentId = getOrderCollectionPersistentId(kind, right);
@@ -414,5 +483,6 @@ function normalizeOrderCollections(sourceOrder, options) {
414
483
  mergeOrderCollectionItems,
415
484
  normalizeOrderCollection,
416
485
  normalizeOrderCollections,
486
+ repairDuplicateProductLineIdentities,
417
487
  setOrderCollectionUid
418
488
  });
@@ -326,7 +326,7 @@ export declare class BookingByStepImpl extends BaseModule implements Module {
326
326
  date: string;
327
327
  status: string;
328
328
  week: string;
329
- weekNum: 0 | 1 | 2 | 3 | 4 | 5 | 6;
329
+ weekNum: 0 | 5 | 2 | 1 | 6 | 4 | 3;
330
330
  }[]>;
331
331
  submitTimeSlot(timeSlots: TimeSliceItem): void;
332
332
  private getScheduleDataByIds;
@@ -447,7 +447,19 @@ export declare class BookingTicketImpl extends BaseSalesImpl implements Module {
447
447
  * 加车两阶段主决策(规格弹窗 / 资源编辑 / 直接加车)。
448
448
  * 与 ticketBooking `handleSelectProduct` + `handleBooking4Service` 等价。
449
449
  */
450
- decideAddProduct(item: any, options?: Partial<AddProductDecideContext>): import("./utils/addProductDecision").AddProductDecision;
450
+ decideAddProduct(item: any, options?: Partial<AddProductDecideContext>): {
451
+ action: "reloadCatalog";
452
+ } | {
453
+ action: "add";
454
+ cacheItem: any;
455
+ } | {
456
+ action: "requiresDetail";
457
+ payload: AddProductRequiresDetailPayload;
458
+ } | {
459
+ action: "requiresBookingEdit";
460
+ cacheItem: any;
461
+ payload: import("./utils/addProductDecision").AddProductRequiresBookingEditPayload;
462
+ };
451
463
  /** 规格弹窗 callback 后的第二段决策。 */
452
464
  decideAfterDetail(cacheItem: any, options?: Partial<AddProductDecideContext>): import("./utils/addProductDecision").AddProductDecision;
453
465
  /**
@@ -242,6 +242,11 @@ var BookingTicketImpl = class extends import_BaseSales.BaseSalesImpl {
242
242
  }
243
243
  async configureIdGeneratorFromOpenData() {
244
244
  this.setPaymentNumberDevicePrefix(null);
245
+ const idGenerator = this.getIdGeneratorPlugin();
246
+ if (!(idGenerator == null ? void 0 : idGenerator.configure)) {
247
+ console.warn("[BookingTicket] idGenerator 插件不可用,跳过配置");
248
+ return;
249
+ }
245
250
  let openDataConfig = null;
246
251
  try {
247
252
  openDataConfig = await this.loadOpenDataConfig();
@@ -249,11 +254,6 @@ var BookingTicketImpl = class extends import_BaseSales.BaseSalesImpl {
249
254
  console.warn("[BookingTicket] OpenData 配置加载失败,跳过 idGenerator 配置", error);
250
255
  return;
251
256
  }
252
- const idGenerator = this.getIdGeneratorPlugin();
253
- if (!(idGenerator == null ? void 0 : idGenerator.configure)) {
254
- console.warn("[BookingTicket] idGenerator 插件不可用,跳过配置");
255
- return;
256
- }
257
257
  const receiptSequenceLength = this.normalizePositiveInteger(
258
258
  openDataConfig == null ? void 0 : openDataConfig["sale.short_number_digits"],
259
259
  5
@@ -63,8 +63,19 @@ export declare class UnifiedBookingSalesImpl extends BookingTicket {
63
63
  private openDataTarget;
64
64
  private loadOpenDataInFlight;
65
65
  private loadOpenDataInFlightTarget;
66
+ protected getSubmitOrderSalesChannel(): string;
66
67
  protected isSessionStoragePersistEnabled(): boolean;
67
68
  protected shouldUseSessionStorageForSubModule(moduleName?: string): boolean;
69
+ /**
70
+ * 读取与当前 cacheId 同分桶的 Solution 会话状态。
71
+ * 该状态独立于 otherParams,适合页面皮肤保存短生命周期 UI 状态。
72
+ */
73
+ getSessionStateValue<T = unknown>(key: string): T | undefined;
74
+ /**
75
+ * 写入与当前 cacheId 同分桶的 Solution 会话状态。
76
+ * value 为 undefined 时删除该 key;存储失败不阻断业务流程。
77
+ */
78
+ setSessionStateValue(key: string, value: unknown): void;
68
79
  /**
69
80
  * Kiosk 不能把已收款订单留在本地等待重试:默认直接等待云端 checkout。
70
81
  * 兼容仍需要 WebPOS 待同步模式的入口可显式传 checkoutSyncTaskEnabled: true。
@@ -49,6 +49,9 @@ var import_BookingTicket = require("../BookingTicket");
49
49
  __reExport(UnifiedBookingSales_exports, require("./types"), module.exports);
50
50
  var OPEN_DATA_SECTION_CODES = ["sale", "reservation", "fulfillment", "menu", "workflow"];
51
51
  var OPEN_DATA_CACHE_TTL = 5 * 60 * 1e3;
52
+ function isRecord(value) {
53
+ return !!value && typeof value === "object" && !Array.isArray(value);
54
+ }
52
55
  function pickLocalizedText(value) {
53
56
  if (!value)
54
57
  return void 0;
@@ -469,6 +472,11 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
469
472
  this.loadOpenDataInFlight = null;
470
473
  this.loadOpenDataInFlightTarget = null;
471
474
  }
475
+ getSubmitOrderSalesChannel() {
476
+ var _a;
477
+ const channel = String(((_a = this.otherParams) == null ? void 0 : _a.channel) ?? "").trim();
478
+ return channel || super.getSubmitOrderSalesChannel();
479
+ }
472
480
  isSessionStoragePersistEnabled() {
473
481
  var _a;
474
482
  return Boolean(
@@ -478,6 +486,43 @@ var UnifiedBookingSalesImpl = class extends import_BookingTicket.BookingTicket {
478
486
  shouldUseSessionStorageForSubModule(moduleName) {
479
487
  return this.isSessionStoragePersistEnabled() && moduleName === "order";
480
488
  }
489
+ /**
490
+ * 读取与当前 cacheId 同分桶的 Solution 会话状态。
491
+ * 该状态独立于 otherParams,适合页面皮肤保存短生命周期 UI 状态。
492
+ */
493
+ getSessionStateValue(key) {
494
+ if (!key || !this.isSessionStoragePersistEnabled())
495
+ return void 0;
496
+ const cacheData = this.readSessionCacheData();
497
+ const solutionCache = cacheData == null ? void 0 : cacheData[this.name];
498
+ if (!isRecord(solutionCache) || !isRecord(solutionCache.sessionState)) {
499
+ return void 0;
500
+ }
501
+ return solutionCache.sessionState[key];
502
+ }
503
+ /**
504
+ * 写入与当前 cacheId 同分桶的 Solution 会话状态。
505
+ * value 为 undefined 时删除该 key;存储失败不阻断业务流程。
506
+ */
507
+ setSessionStateValue(key, value) {
508
+ if (!key || !this.isSessionStoragePersistEnabled())
509
+ return;
510
+ const cacheData = this.readSessionCacheData();
511
+ const solutionCache = cacheData == null ? void 0 : cacheData[this.name];
512
+ const currentSessionState = isRecord(solutionCache) && isRecord(solutionCache.sessionState) ? solutionCache.sessionState : {};
513
+ const nextSessionState = { ...currentSessionState };
514
+ if (value === void 0) {
515
+ delete nextSessionState[key];
516
+ } else {
517
+ nextSessionState[key] = value;
518
+ }
519
+ this.checkSaveCache({
520
+ cacheId: this.cacheId,
521
+ fatherModule: this.name,
522
+ store: { sessionState: nextSessionState },
523
+ cacheKey: ["sessionState"]
524
+ });
525
+ }
481
526
  /**
482
527
  * Kiosk 不能把已收款订单留在本地等待重试:默认直接等待云端 checkout。
483
528
  * 兼容仍需要 WebPOS 待同步模式的入口可显式传 checkoutSyncTaskEnabled: true。
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "0.10.6",
4
+ "version": "0.10.7",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",