@pisell/pisellos 0.10.6 → 0.10.8

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.
Files changed (64) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +12 -10
  3. package/dist/modules/Order/index.d.ts +14 -6
  4. package/dist/modules/Order/index.js +373 -246
  5. package/dist/modules/Order/types.d.ts +26 -3
  6. package/dist/modules/Order/types.js +11 -0
  7. package/dist/modules/Order/utils/discountProductLineIdentity.d.ts +45 -0
  8. package/dist/modules/Order/utils/discountProductLineIdentity.js +227 -0
  9. package/dist/modules/Order/utils/orderCollectionIdentity.d.ts +15 -0
  10. package/dist/modules/Order/utils/orderCollectionIdentity.js +75 -0
  11. package/dist/modules/Rules/index.js +125 -52
  12. package/dist/modules/SalesSummary/utils.js +7 -1
  13. package/dist/server/index.d.ts +6 -0
  14. package/dist/server/index.js +810 -729
  15. package/dist/server/modules/order/index.d.ts +1 -0
  16. package/dist/server/modules/order/index.js +9 -2
  17. package/dist/server/modules/order/types.d.ts +1 -1
  18. package/dist/solution/BaseSales/index.d.ts +15 -5
  19. package/dist/solution/BaseSales/index.js +211 -159
  20. package/dist/solution/BaseSales/utils/cartPromotion.js +26 -3
  21. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +1 -1
  22. package/dist/solution/BaseSales/utils.js +31 -20
  23. package/dist/solution/BookingByStep/index.d.ts +1 -1
  24. package/dist/solution/BookingTicket/index.d.ts +13 -1
  25. package/dist/solution/BookingTicket/index.js +19 -22
  26. package/dist/solution/BookingTicket/types.d.ts +3 -0
  27. package/dist/solution/BookingTicket/utils/cartView.js +44 -32
  28. package/dist/solution/Sales/index.d.ts +1 -0
  29. package/dist/solution/Sales/index.js +16 -2
  30. package/dist/solution/ScanOrder/utils.js +31 -20
  31. package/dist/solution/UnifiedBookingSales/index.d.ts +16 -4
  32. package/dist/solution/UnifiedBookingSales/index.js +113 -48
  33. package/lib/model/strategy/adapter/promotion/index.js +0 -51
  34. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +3 -1
  35. package/lib/modules/Order/index.d.ts +14 -6
  36. package/lib/modules/Order/index.js +192 -52
  37. package/lib/modules/Order/types.d.ts +26 -3
  38. package/lib/modules/Order/utils/discountProductLineIdentity.d.ts +45 -0
  39. package/lib/modules/Order/utils/discountProductLineIdentity.js +170 -0
  40. package/lib/modules/Order/utils/orderCollectionIdentity.d.ts +15 -0
  41. package/lib/modules/Order/utils/orderCollectionIdentity.js +70 -0
  42. package/lib/modules/Rules/index.js +100 -20
  43. package/lib/modules/SalesSummary/utils.js +5 -1
  44. package/lib/server/index.d.ts +6 -0
  45. package/lib/server/index.js +51 -0
  46. package/lib/server/modules/order/index.d.ts +1 -0
  47. package/lib/server/modules/order/index.js +9 -3
  48. package/lib/server/modules/order/types.d.ts +1 -1
  49. package/lib/solution/BaseSales/index.d.ts +15 -5
  50. package/lib/solution/BaseSales/index.js +64 -16
  51. package/lib/solution/BaseSales/utils/cartPromotion.js +26 -2
  52. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +1 -1
  53. package/lib/solution/BaseSales/utils.js +29 -18
  54. package/lib/solution/BookingByStep/index.d.ts +1 -1
  55. package/lib/solution/BookingTicket/index.d.ts +13 -1
  56. package/lib/solution/BookingTicket/index.js +6 -9
  57. package/lib/solution/BookingTicket/types.d.ts +3 -0
  58. package/lib/solution/BookingTicket/utils/cartView.js +43 -30
  59. package/lib/solution/Sales/index.d.ts +1 -0
  60. package/lib/solution/Sales/index.js +15 -3
  61. package/lib/solution/ScanOrder/utils.js +29 -18
  62. package/lib/solution/UnifiedBookingSales/index.d.ts +16 -4
  63. package/lib/solution/UnifiedBookingSales/index.js +80 -26
  64. package/package.json +1 -1
@@ -44,6 +44,7 @@ var import_utils3 = require("../../solution/ScanOrder/utils");
44
44
  var import_manualProductDiscount = require("./utils/manualProductDiscount");
45
45
  var import_bundleDiscountHydration = require("./utils/bundleDiscountHydration");
46
46
  var import_orderCollectionIdentity = require("./utils/orderCollectionIdentity");
47
+ var import_discountProductLineIdentity = require("./utils/discountProductLineIdentity");
47
48
  var import_Discount = require("../Discount");
48
49
  var import_Rules = require("../Rules");
49
50
  var import_types2 = require("../Rules/types");
@@ -65,8 +66,7 @@ function isSameDiscountList(left, right) {
65
66
  );
66
67
  }
67
68
  function isManualProductDiscountProduct(product) {
68
- var _a, _b;
69
- return ((_a = product == null ? void 0 : product.metadata) == null ? void 0 : _a.is_manual_discount) === true || ((_b = product == null ? void 0 : product.metadata) == null ? void 0 : _b.is_manual_discount) === 1 || ((product == null ? void 0 : product.discount_list) || []).some((item) => (item == null ? void 0 : item.type) === "product");
69
+ return (0, import_manualProductDiscount.hasManualProductDiscountFlag)(product == null ? void 0 : product.metadata) || ((product == null ? void 0 : product.discount_list) || []).some((item) => (item == null ? void 0 : item.type) === "product");
70
70
  }
71
71
  var ORDER_PAYMENT_STATUS_PAID = "paid";
72
72
  var ORDER_PAYMENT_STATUS_PENDING = "payment_pending";
@@ -136,7 +136,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
136
136
  this.draftPersistEnabled = true;
137
137
  }
138
138
  collectHydratedEditDiscounts(products) {
139
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
139
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l;
140
140
  const discountMap = /* @__PURE__ */ new Map();
141
141
  for (const product of (0, import_bundleDiscountHydration.expandHydratedProductsForDiscountCollection)(products)) {
142
142
  for (const item of (product == null ? void 0 : product.discount_list) || []) {
@@ -151,16 +151,18 @@ var OrderModule = class extends import_BaseModule.BaseModule {
151
151
  const key = Number.isFinite(id) ? id : String(resourceId);
152
152
  const title = ((_b = item == null ? void 0 : item.discount) == null ? void 0 : _b.title) ?? {};
153
153
  const amount = Number((item == null ? void 0 : item.amount) || 0);
154
+ const discountCardType = (_c = item == null ? void 0 : item.discount) == null ? void 0 : _c.discount_card_type;
155
+ const parValue = isDiscountCard ? discountCardType === "fixed_amount" ? ((_d = item == null ? void 0 : item.discount) == null ? void 0 : _d.fixed_amount) ?? (item == null ? void 0 : item.amount) : (_e = item == null ? void 0 : item.discount) == null ? void 0 : _e.percent : "0";
154
156
  const detail = {
155
157
  amount: String((item == null ? void 0 : item.amount) ?? "0"),
156
158
  type,
157
159
  resource_id: key,
158
160
  title,
159
- original_amount: String(((_c = item == null ? void 0 : item.discount) == null ? void 0 : _c.original_amount) ?? (item == null ? void 0 : item.amount) ?? "0"),
160
- num: Number(((_d = item == null ? void 0 : item.metadata) == null ? void 0 : _d.num) ?? (product == null ? void 0 : product.num) ?? (product == null ? void 0 : product.product_quantity) ?? 1) || 1,
161
+ original_amount: String(((_f = item == null ? void 0 : item.discount) == null ? void 0 : _f.original_amount) ?? (item == null ? void 0 : item.amount) ?? "0"),
162
+ num: Number(((_g = item == null ? void 0 : item.metadata) == null ? void 0 : _g.num) ?? (product == null ? void 0 : product.num) ?? (product == null ? void 0 : product.product_quantity) ?? 1) || 1,
161
163
  metadata: item == null ? void 0 : item.metadata,
162
164
  discount: item == null ? void 0 : item.discount,
163
- _num: Number(((_e = item == null ? void 0 : item.metadata) == null ? void 0 : _e.num) ?? (product == null ? void 0 : product.num) ?? (product == null ? void 0 : product.product_quantity) ?? 1) || 1
165
+ _num: Number(((_h = item == null ? void 0 : item.metadata) == null ? void 0 : _h.num) ?? (product == null ? void 0 : product.num) ?? (product == null ? void 0 : product.product_quantity) ?? 1) || 1
164
166
  };
165
167
  const existed = discountMap.get(key);
166
168
  if (existed) {
@@ -169,7 +171,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
169
171
  if (!isDiscountCard) {
170
172
  existed.balance = String(Number(existed.balance || 0) + amount);
171
173
  }
172
- const productIds = Array.isArray((_f = existed.limited_relation_product_data) == null ? void 0 : _f.product_ids) ? existed.limited_relation_product_data.product_ids : [];
174
+ const productIds = Array.isArray((_i = existed.limited_relation_product_data) == null ? void 0 : _i.product_ids) ? existed.limited_relation_product_data.product_ids : [];
173
175
  if ((product == null ? void 0 : product.product_id) !== void 0 && (product == null ? void 0 : product.product_id) !== null && !productIds.includes(product.product_id)) {
174
176
  existed.limited_relation_product_data = {
175
177
  ...existed.limited_relation_product_data || {},
@@ -192,9 +194,9 @@ var OrderModule = class extends import_BaseModule.BaseModule {
192
194
  encoded: "",
193
195
  code: "",
194
196
  tag: type,
195
- product_id: ((_g = item == null ? void 0 : item.discount) == null ? void 0 : _g.discount_product_id) ?? (product == null ? void 0 : product.product_id) ?? 0,
197
+ product_id: ((_j = item == null ? void 0 : item.discount) == null ? void 0 : _j.discount_product_id) ?? (product == null ? void 0 : product.product_id) ?? 0,
196
198
  relation_type: "",
197
- par_value: String(((_h = item == null ? void 0 : item.discount) == null ? void 0 : _h.original_amount) ?? (item == null ? void 0 : item.amount) ?? "0"),
199
+ par_value: String(parValue ?? "0"),
198
200
  used_par_value: String((item == null ? void 0 : item.amount) ?? "0"),
199
201
  limit_status: "enable",
200
202
  limited_relation_product_data: {
@@ -224,8 +226,8 @@ var OrderModule = class extends import_BaseModule.BaseModule {
224
226
  discount: {
225
227
  ...(item == null ? void 0 : item.discount) || {},
226
228
  resource_id: key,
227
- discount_product_id: ((_i = item == null ? void 0 : item.discount) == null ? void 0 : _i.discount_product_id) ?? (product == null ? void 0 : product.product_id) ?? 0,
228
- discount_calculation_mode: ((_j = item == null ? void 0 : item.discount) == null ? void 0 : _j.discount_calculation_mode) ?? "item_level"
229
+ discount_product_id: ((_k = item == null ? void 0 : item.discount) == null ? void 0 : _k.discount_product_id) ?? (product == null ? void 0 : product.product_id) ?? 0,
230
+ discount_calculation_mode: ((_l = item == null ? void 0 : item.discount) == null ? void 0 : _l.discount_calculation_mode) ?? "item_level"
229
231
  },
230
232
  applicableProductDetails: [detail],
231
233
  appliedProductDetails: [detail]
@@ -277,7 +279,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
277
279
  const isPersistedBundleProduct = isPersistedProduct || Boolean(
278
280
  (bundle == null ? void 0 : bundle.order_detail_id) || (bundle == null ? void 0 : bundle.orderDetailId) || (bundle == null ? void 0 : bundle.booking_id)
279
281
  );
280
- const bundleQty = new import_decimal.default(Number(qty) || 1).times(Number((bundle == null ? void 0 : bundle.num) ?? (bundle == null ? void 0 : bundle.quantity) ?? 1) || 1).toNumber();
282
+ const bundleQty = Number(qty) || 1;
281
283
  for (const pd of (bundle == null ? void 0 : bundle.discount_list) || []) {
282
284
  addProductDiscountAmount(pd, bundleQty, isPersistedBundleProduct);
283
285
  }
@@ -694,6 +696,8 @@ var OrderModule = class extends import_BaseModule.BaseModule {
694
696
  };
695
697
  })
696
698
  );
699
+ (0, import_discountProductLineIdentity.ensureUniqueProductLineUids)(tempOrder);
700
+ (0, import_discountProductLineIdentity.consolidateDiscountFreeProductLines)(tempOrder);
697
701
  }
698
702
  if (result == null ? void 0 : result.discountList) {
699
703
  OrderModule.populateSavedAmounts(
@@ -916,6 +920,52 @@ var OrderModule = class extends import_BaseModule.BaseModule {
916
920
  cacheKey: ["tempOrder"]
917
921
  });
918
922
  }
923
+ /**
924
+ * 终止当前模块实例的 sessionStorage 订单恢复会话。
925
+ *
926
+ * 支付跳转等终态场景需要同时删除当前 cacheId 的 tempOrder 快照,并关闭
927
+ * 本实例后续异步任务的再次写入;新模块实例初始化时会按 otherParams 重新启用。
928
+ */
929
+ clearSessionStoragePersistedTempOrder() {
930
+ var _a;
931
+ this.sessionStorageRestoreRecord = null;
932
+ if (!this.sessionStoragePersistEnabled)
933
+ return;
934
+ this.sessionStoragePersistEnabled = false;
935
+ if (!this.cacheId || !this.fatherModule || !((_a = this.window) == null ? void 0 : _a.sessionStorage)) {
936
+ return;
937
+ }
938
+ const sessionStorage = this.window.sessionStorage;
939
+ const rawCacheData = sessionStorage.getItem(this.fatherModule);
940
+ if (!rawCacheData)
941
+ return;
942
+ let parsedCacheData;
943
+ try {
944
+ parsedCacheData = JSON.parse(rawCacheData);
945
+ } catch {
946
+ return;
947
+ }
948
+ if (!parsedCacheData || typeof parsedCacheData !== "object" || Array.isArray(parsedCacheData)) {
949
+ return;
950
+ }
951
+ const cacheRoot = parsedCacheData;
952
+ const cacheBucket = cacheRoot[this.cacheId];
953
+ if (!cacheBucket || typeof cacheBucket !== "object" || Array.isArray(cacheBucket)) {
954
+ return;
955
+ }
956
+ const moduleBucket = cacheBucket[this.name];
957
+ if (!moduleBucket || typeof moduleBucket !== "object" || Array.isArray(moduleBucket)) {
958
+ return;
959
+ }
960
+ delete moduleBucket.tempOrder;
961
+ if (Object.keys(moduleBucket).length === 0) {
962
+ delete cacheBucket[this.name];
963
+ }
964
+ if (Object.keys(cacheBucket).length === 0) {
965
+ delete cacheRoot[this.cacheId];
966
+ }
967
+ sessionStorage.setItem(this.fatherModule, JSON.stringify(cacheRoot));
968
+ }
919
969
  consumeSessionStorageRestore() {
920
970
  const record = this.sessionStorageRestoreRecord;
921
971
  this.sessionStorageRestoreRecord = null;
@@ -1921,6 +1971,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1921
1971
  tempOrder.products = (tempOrder.products || []).map(
1922
1972
  (product) => this.sanitizeOrderProductForTempOrder(product)
1923
1973
  );
1974
+ (0, import_discountProductLineIdentity.ensureUniqueProductLineUids)(tempOrder);
1924
1975
  }
1925
1976
  ensureExtend(tempOrder) {
1926
1977
  if (!tempOrder._extend || typeof tempOrder._extend !== "object") {
@@ -1931,6 +1982,23 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1931
1982
  }
1932
1983
  return tempOrder._extend;
1933
1984
  }
1985
+ repairDuplicateProductLineIdentities(tempOrder) {
1986
+ var _a;
1987
+ const result = (0, import_orderCollectionIdentity.repairDuplicateProductLineIdentities)(
1988
+ tempOrder.products,
1989
+ (_a = tempOrder._extend) == null ? void 0 : _a.productsByUid
1990
+ );
1991
+ if (!result.repairs.length)
1992
+ return;
1993
+ tempOrder.products = result.products;
1994
+ if (result.productsByUid) {
1995
+ this.ensureExtend(tempOrder).productsByUid = result.productsByUid;
1996
+ }
1997
+ this.logWarning("Repaired duplicate product line identities", {
1998
+ repairCount: result.repairs.length,
1999
+ repairs: result.repairs
2000
+ });
2001
+ }
1934
2002
  captureProductRuntime(tempOrder, rawProduct, normalizedProduct, existedUid) {
1935
2003
  var _a;
1936
2004
  const uid = existedUid || this.getProductUid(normalizedProduct);
@@ -2576,6 +2644,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
2576
2644
  delete tempOrder._extend.customerSnapshot;
2577
2645
  this.store.availableWalletIds = [];
2578
2646
  this.clearCustomerBoundDiscounts(tempOrder);
2647
+ this.applyDiscount();
2579
2648
  this.persistTempOrder();
2580
2649
  this.saveDraftInBackground();
2581
2650
  this.core.effects.emit(import_types.OrderHooks.OnOrderCustomerChange, null);
@@ -2940,8 +3009,8 @@ var OrderModule = class extends import_BaseModule.BaseModule {
2940
3009
  * @example
2941
3010
  * await this.appendProductLineToTempOrder(tempOrder, { product_id: 1, num: 1 }, booking);
2942
3011
  */
2943
- async appendProductLineToTempOrder(tempOrder, product, booking) {
2944
- var _a, _b;
3012
+ async appendProductLineToTempOrder(tempOrder, product, booking, options) {
3013
+ var _a, _b, _c, _d, _e, _f, _g;
2945
3014
  const linked = booking ? this.createLinkedProductAndBooking({ product, booking }) : null;
2946
3015
  const productToAdd = (linked == null ? void 0 : linked.product) || product;
2947
3016
  const incomingFingerprint = (0, import_utils3.buildProductLineFingerprint)(
@@ -2952,7 +3021,12 @@ var OrderModule = class extends import_BaseModule.BaseModule {
2952
3021
  productToAdd,
2953
3022
  (0, import_utils3.normalizeOrderProduct)(productToAdd)
2954
3023
  );
2955
- const incomingBookingUid = productToAdd.booking_uid ?? ((_a = productToAdd.metadata) == null ? void 0 : _a.booking_uid);
3024
+ const isWeighingProduct = Number(((_a = normalizedIncoming.product_sku) == null ? void 0 : _a.open_sold_weight) || 0) === 1;
3025
+ const netWeight = Number((_b = normalizedIncoming.product_sku) == null ? void 0 : _b.net_weight);
3026
+ if (isWeighingProduct && (!Number.isFinite(netWeight) || netWeight <= 0)) {
3027
+ throw new Error("[Order] 称重商品净重必须大于 0");
3028
+ }
3029
+ const incomingBookingUid = productToAdd.booking_uid ?? ((_c = productToAdd.metadata) == null ? void 0 : _c.booking_uid);
2956
3030
  if (incomingBookingUid !== void 0 && incomingBookingUid !== null && String(incomingBookingUid) !== "") {
2957
3031
  normalizedIncoming.booking_uid = String(incomingBookingUid);
2958
3032
  normalizedIncoming.metadata = {
@@ -2967,14 +3041,18 @@ var OrderModule = class extends import_BaseModule.BaseModule {
2967
3041
  normalizedIncoming
2968
3042
  );
2969
3043
  const hasIncomingProductNote = this.hasOrderProductLineNote(normalizedIncoming);
3044
+ const hasIncomingManualProductDiscount = isManualProductDiscountProduct(normalizedIncoming);
2970
3045
  const hasIncomingBookingUid = this.hasLinkedBookingUid(productToAdd) || this.hasLinkedBookingUid(normalizedIncoming);
2971
- const shouldForceNewLine = !!booking || hasIncomingProductNote || hasIncomingBookingUid;
3046
+ const isIncomingCustomItem = ((_e = (_d = productToAdd.metadata) == null ? void 0 : _d.origin) == null ? void 0 : _e.isCustomItem) === true || ((_f = productToAdd._origin) == null ? void 0 : _f.isCustomItem) === true;
3047
+ const shouldForceNewLine = !!booking || hasIncomingProductNote || hasIncomingManualProductDiscount || hasIncomingBookingUid || isIncomingCustomItem || isWeighingProduct;
2972
3048
  const matchedIndex = hasIncomingGoodPass || shouldForceNewLine ? -1 : tempOrder.products.findIndex((item) => {
2973
3049
  var _a2;
2974
3050
  if (this.hasGoodPassDiscount(item))
2975
3051
  return false;
2976
3052
  if (this.hasOrderProductLineNote(item))
2977
3053
  return false;
3054
+ if (isManualProductDiscountProduct(item))
3055
+ return false;
2978
3056
  if (item.order_detail_id !== void 0 && item.order_detail_id !== null)
2979
3057
  return false;
2980
3058
  if ((_a2 = item.metadata) == null ? void 0 : _a2.is_edit_for_runtime)
@@ -3017,10 +3095,19 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3017
3095
  booking_uid: linked.bookingUid
3018
3096
  };
3019
3097
  }
3020
- tempOrder.products = [...tempOrder.products, normalizedIncoming];
3098
+ const insertAtIndex = options == null ? void 0 : options.insertAtIndex;
3099
+ if (Number.isInteger(insertAtIndex) && Number(insertAtIndex) >= 0 && Number(insertAtIndex) < tempOrder.products.length) {
3100
+ tempOrder.products = [
3101
+ ...tempOrder.products.slice(0, insertAtIndex),
3102
+ normalizedIncoming,
3103
+ ...tempOrder.products.slice(insertAtIndex)
3104
+ ];
3105
+ } else {
3106
+ tempOrder.products = [...tempOrder.products, normalizedIncoming];
3107
+ }
3021
3108
  } else {
3022
3109
  const targetProduct = matchedProduct;
3023
- const targetUid = (_b = targetProduct.metadata) == null ? void 0 : _b.unique_identification_number;
3110
+ const targetUid = (_g = targetProduct.metadata) == null ? void 0 : _g.unique_identification_number;
3024
3111
  const normalizedProduct = (0, import_utils.mergeOrderProductDisplayFields)(
3025
3112
  targetProduct,
3026
3113
  (0, import_utils3.normalizeOrderProduct)({
@@ -3035,17 +3122,10 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3035
3122
  normalizedProduct.discount_list
3036
3123
  );
3037
3124
  const nextNum = (0, import_utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num);
3038
- const targetIsManualProductDiscount = isManualProductDiscountProduct(targetProduct);
3039
- const nextDiscountList = targetIsManualProductDiscount ? (0, import_utils.normalizeOrderProductDiscountList)(
3040
- (0, import_manualProductDiscount.syncManualProductDiscountProductNum)(
3041
- targetProduct.discount_list,
3042
- nextNum
3043
- )
3044
- ) : normalizedProduct.discount_list;
3045
3125
  tempOrder.products[matchedIndex] = {
3046
3126
  ...targetProduct,
3047
3127
  ...normalizedProduct,
3048
- discount_list: nextDiscountList,
3128
+ discount_list: normalizedProduct.discount_list,
3049
3129
  metadata: {
3050
3130
  ...targetProduct.metadata || {},
3051
3131
  ...normalizedProduct.metadata || {},
@@ -3056,16 +3136,6 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3056
3136
  ...normalizedProduct.product_sku || { option: [] },
3057
3137
  option: (0, import_utils.getProductSkuOptions)(normalizedProduct)
3058
3138
  },
3059
- ...targetIsManualProductDiscount ? {
3060
- selling_price: targetProduct.selling_price,
3061
- original_price: targetProduct.original_price,
3062
- payment_price: targetProduct.payment_price,
3063
- metadata: {
3064
- ...normalizedProduct.metadata || {},
3065
- ...targetProduct.metadata || {},
3066
- unique_identification_number: targetUid
3067
- }
3068
- } : {},
3069
3139
  note: targetProduct.note,
3070
3140
  order_detail_id: targetProduct.order_detail_id,
3071
3141
  num: nextNum
@@ -3076,6 +3146,14 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3076
3146
  tempOrder.products[matchedIndex],
3077
3147
  targetUid
3078
3148
  );
3149
+ if (matchedIndex !== tempOrder.products.length - 1) {
3150
+ const mergedProduct = tempOrder.products[matchedIndex];
3151
+ tempOrder.products = [
3152
+ ...tempOrder.products.slice(0, matchedIndex),
3153
+ ...tempOrder.products.slice(matchedIndex + 1),
3154
+ mergedProduct
3155
+ ];
3156
+ }
3079
3157
  }
3080
3158
  if (linked) {
3081
3159
  tempOrder.bookings = [...tempOrder.bookings || [], linked.booking];
@@ -3088,8 +3166,15 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3088
3166
  * await order.addProductToOrder({ product_id: 1, num: 3 }, { start_time: '10:00' });
3089
3167
  * // → tempOrder.products 新增 3 行,每行 num=1,bookings 3 条
3090
3168
  */
3091
- async addProductToOrder(product, booking) {
3169
+ async addProductToOrder(product, booking, options) {
3092
3170
  const tempOrder = this.ensureTempOrder();
3171
+ const insertAfterProductUid = options == null ? void 0 : options.insertAfterProductUid;
3172
+ const anchorIndex = booking && insertAfterProductUid ? tempOrder.products.findIndex((line) => {
3173
+ var _a;
3174
+ const uid = ((_a = line.metadata) == null ? void 0 : _a.unique_identification_number) || line.unique_identification_number;
3175
+ return uid !== void 0 && uid !== null && String(uid) === String(insertAfterProductUid);
3176
+ }) : -1;
3177
+ let insertAtIndex = anchorIndex >= 0 ? anchorIndex + 1 : void 0;
3093
3178
  if (booking) {
3094
3179
  const productRecord = product;
3095
3180
  const splitCount = (0, import_utils3.getSafeProductNum)(
@@ -3103,14 +3188,22 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3103
3188
  await this.appendProductLineToTempOrder(
3104
3189
  tempOrder,
3105
3190
  singleLine,
3106
- (0, import_lodash_es.cloneDeep)(booking)
3191
+ (0, import_lodash_es.cloneDeep)(booking),
3192
+ { insertAtIndex }
3107
3193
  );
3194
+ if (insertAtIndex !== void 0)
3195
+ insertAtIndex += 1;
3108
3196
  }
3109
3197
  await this.finalizeProductOrderMutation(tempOrder);
3110
3198
  return tempOrder.products;
3111
3199
  }
3112
3200
  }
3113
- await this.appendProductLineToTempOrder(tempOrder, product, booking);
3201
+ await this.appendProductLineToTempOrder(
3202
+ tempOrder,
3203
+ product,
3204
+ booking,
3205
+ { insertAtIndex }
3206
+ );
3114
3207
  await this.finalizeProductOrderMutation(tempOrder);
3115
3208
  this.logInfo("addProductToOrder success", {
3116
3209
  product_id: product.product_id,
@@ -3484,6 +3577,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3484
3577
  product_bundle
3485
3578
  } = params;
3486
3579
  const tempOrder = this.ensureTempOrder();
3580
+ this.repairDuplicateProductLineIdentities(tempOrder);
3487
3581
  const identityLookup = {
3488
3582
  product_id,
3489
3583
  product_variant_id
@@ -3591,14 +3685,22 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3591
3685
  * @example
3592
3686
  * await order.removeProductsFromOrder([identityA, identityB]);
3593
3687
  */
3594
- async removeProductsFromOrder(identities) {
3688
+ async removeProductsFromOrder(identities, options) {
3595
3689
  var _a, _b, _c, _d;
3596
3690
  const tempOrder = this.ensureTempOrder();
3691
+ this.repairDuplicateProductLineIdentities(tempOrder);
3597
3692
  if (!identities.length) {
3598
3693
  return tempOrder.products;
3599
3694
  }
3600
3695
  const productsBeforeRemove = tempOrder.products || [];
3601
3696
  const bookingsBeforeRemove = tempOrder.bookings || [];
3697
+ const preserveDisplayPosition = options == null ? void 0 : options.preserveDisplayPosition;
3698
+ const anchorIndex = preserveDisplayPosition ? productsBeforeRemove.findIndex((item) => {
3699
+ var _a2;
3700
+ const uid = ((_a2 = item.metadata) == null ? void 0 : _a2.unique_identification_number) || item.unique_identification_number;
3701
+ return uid !== void 0 && uid !== null && String(uid) === String(preserveDisplayPosition.anchorProductUid);
3702
+ }) : -1;
3703
+ const productsAfterAnchor = anchorIndex >= 0 ? productsBeforeRemove.slice(anchorIndex + 1) : [];
3602
3704
  const matchedProducts = productsBeforeRemove.filter((item) => {
3603
3705
  return identities.some((identity) => (0, import_utils3.isIdentityMatch)(item, identity));
3604
3706
  });
@@ -3639,14 +3741,37 @@ var OrderModule = class extends import_BaseModule.BaseModule {
3639
3741
  return !(isAddTimeProduct && lineBookingUid !== void 0 && lineBookingUid !== null && linkedBookingUidsToRemove.has(String(lineBookingUid)));
3640
3742
  });
3641
3743
  }
3744
+ if (preserveDisplayPosition && anchorIndex >= 0) {
3745
+ const remainingProductIndex = tempOrder.products.findIndex((item) => {
3746
+ var _a2;
3747
+ const uid = ((_a2 = item.metadata) == null ? void 0 : _a2.unique_identification_number) || item.unique_identification_number;
3748
+ return uid !== void 0 && uid !== null && String(uid) === String(preserveDisplayPosition.remainingProductUid);
3749
+ });
3750
+ if (remainingProductIndex >= 0) {
3751
+ const remainingProduct = tempOrder.products[remainingProductIndex];
3752
+ const nextProduct = productsAfterAnchor.find(
3753
+ (item) => tempOrder.products.includes(item)
3754
+ );
3755
+ const productsWithoutRemaining = [
3756
+ ...tempOrder.products.slice(0, remainingProductIndex),
3757
+ ...tempOrder.products.slice(remainingProductIndex + 1)
3758
+ ];
3759
+ const nextProductIndex = nextProduct ? productsWithoutRemaining.indexOf(nextProduct) : -1;
3760
+ tempOrder.products = nextProductIndex >= 0 ? [
3761
+ ...productsWithoutRemaining.slice(0, nextProductIndex),
3762
+ remainingProduct,
3763
+ ...productsWithoutRemaining.slice(nextProductIndex)
3764
+ ] : [...productsWithoutRemaining, remainingProduct];
3765
+ }
3766
+ }
3642
3767
  await this.finalizeAfterProductsMutation(tempOrder);
3643
3768
  this.logInfo("removeProductsFromOrder success", {
3644
3769
  identities
3645
3770
  });
3646
3771
  return tempOrder.products;
3647
3772
  }
3648
- async removeProductFromOrder(identity) {
3649
- return this.removeProductsFromOrder([identity]);
3773
+ async removeProductFromOrder(identity, options) {
3774
+ return this.removeProductsFromOrder([identity], options);
3650
3775
  }
3651
3776
  /**
3652
3777
  * 仅清空购物车行(products / bookings),不重置整单。
@@ -4300,24 +4425,39 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4300
4425
  * 例如数组字段、商品行唯一值、价格 metadata、summary 派生状态和 _extend。
4301
4426
  */
4302
4427
  async hydrateTempOrderFromRecord(record, options) {
4303
- var _a, _b, _c, _d, _e;
4428
+ var _a, _b, _c, _d, _e, _f;
4304
4429
  const sourceRaw = record && typeof record === "object" && record.data && record.order_id == null ? record.data : record || {};
4430
+ const duplicateProductIdentityRepair = (0, import_orderCollectionIdentity.repairDuplicateProductLineIdentities)(
4431
+ Array.isArray(sourceRaw.products) ? sourceRaw.products : [],
4432
+ (_a = sourceRaw._extend) == null ? void 0 : _a.productsByUid
4433
+ );
4434
+ const duplicateIdentityReadySource = duplicateProductIdentityRepair.repairs.length ? {
4435
+ ...sourceRaw,
4436
+ products: duplicateProductIdentityRepair.products,
4437
+ ...duplicateProductIdentityRepair.productsByUid ? {
4438
+ _extend: {
4439
+ ...sourceRaw._extend || {},
4440
+ productsByUid: duplicateProductIdentityRepair.productsByUid
4441
+ }
4442
+ } : {}
4443
+ } : sourceRaw;
4305
4444
  const identityReadySource = this.seedAnonymousBookingUidsFromProducts(
4306
- sourceRaw
4445
+ duplicateIdentityReadySource
4307
4446
  );
4308
4447
  const normalizedCollections = (0, import_orderCollectionIdentity.normalizeOrderCollections)(identityReadySource);
4309
4448
  const raw = normalizedCollections.order;
4310
- const hasIdentityChanges = Object.values(normalizedCollections.stats).some(
4449
+ const hasIdentityChanges = duplicateProductIdentityRepair.repairs.length > 0 || Object.values(normalizedCollections.stats).some(
4311
4450
  (stats) => stats.backfilledUidCount > 0 || stats.collapsedDuplicateCount > 0 || stats.uidConflictCount > 0 || stats.anonymousRowCount > 0
4312
4451
  );
4313
4452
  if (hasIdentityChanges) {
4314
4453
  const identityLogMetadata = {
4315
4454
  collections: normalizedCollections.stats,
4316
- uidRemapCount: normalizedCollections.uidRemaps.length
4455
+ uidRemapCount: normalizedCollections.uidRemaps.length,
4456
+ duplicateProductUidRepairCount: duplicateProductIdentityRepair.repairs.length
4317
4457
  };
4318
4458
  const hasIdentityWarnings = Object.values(normalizedCollections.stats).some(
4319
4459
  (stats) => stats.uidConflictCount > 0 || stats.anonymousRowCount > 0
4320
- );
4460
+ ) || duplicateProductIdentityRepair.repairs.length > 0;
4321
4461
  if (hasIdentityWarnings) {
4322
4462
  this.logWarning(
4323
4463
  "Normalized hydrated order collection identities",
@@ -4348,10 +4488,10 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4348
4488
  nextTempOrder.discount_list = restoredSessionDiscounts;
4349
4489
  this.store.summary = (0, import_utils.createEmptySummary)();
4350
4490
  this.store.lastOrderInfo = void 0;
4351
- await ((_a = this.store.discount) == null ? void 0 : _a.setOriginalDiscountList(
4491
+ await ((_b = this.store.discount) == null ? void 0 : _b.setOriginalDiscountList(
4352
4492
  (0, import_lodash_es.cloneDeep)(restoredSessionDiscounts)
4353
4493
  ));
4354
- await ((_b = this.store.discount) == null ? void 0 : _b.setDiscountList(
4494
+ await ((_c = this.store.discount) == null ? void 0 : _c.setDiscountList(
4355
4495
  (0, import_lodash_es.cloneDeep)(restoredSessionDiscounts)
4356
4496
  ));
4357
4497
  if (options == null ? void 0 : options.recalcOnHydrate) {
@@ -4394,7 +4534,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4394
4534
  this.store.syncState = "local";
4395
4535
  }
4396
4536
  const hydratedEditDiscounts = this.collectHydratedEditDiscounts(nextTempOrder.products);
4397
- const currentDiscounts = typeof ((_c = this.store.discount) == null ? void 0 : _c.getDiscountList) === "function" ? this.store.discount.getDiscountList() || [] : [];
4537
+ const currentDiscounts = typeof ((_d = this.store.discount) == null ? void 0 : _d.getDiscountList) === "function" ? this.store.discount.getDiscountList() || [] : [];
4398
4538
  const currentScanDiscounts = currentDiscounts.filter((discount) => discount == null ? void 0 : discount.isScan);
4399
4539
  const hydratedDiscountIds = new Set(
4400
4540
  hydratedEditDiscounts.map((discount) => discount == null ? void 0 : discount.id)
@@ -4412,8 +4552,8 @@ var OrderModule = class extends import_BaseModule.BaseModule {
4412
4552
  nextTempOrder.products,
4413
4553
  nextDiscounts
4414
4554
  );
4415
- await ((_d = this.store.discount) == null ? void 0 : _d.setOriginalDiscountList(nextDiscounts));
4416
- await ((_e = this.store.discount) == null ? void 0 : _e.setDiscountList(nextDiscounts));
4555
+ await ((_e = this.store.discount) == null ? void 0 : _e.setOriginalDiscountList(nextDiscounts));
4556
+ await ((_f = this.store.discount) == null ? void 0 : _f.setDiscountList(nextDiscounts));
4417
4557
  }
4418
4558
  if (options == null ? void 0 : options.recalcOnHydrate) {
4419
4559
  await this.recalculateSummary({ createIfMissing: false });
@@ -321,6 +321,24 @@ export interface ReplaceTempOrderOptions {
321
321
  saveDraft?: boolean;
322
322
  }
323
323
  export type AddProductBookingInput = Record<string, any>;
324
+ /**
325
+ * 添加商品行时的顺序控制。
326
+ * 默认追加到 products 尾部;预约数量同步可插入到既有预约组的最新行之后,
327
+ * 避免步进器修改数量改变该组展示位置。
328
+ */
329
+ export interface AddProductToOrderOptions {
330
+ insertAfterProductUid?: string;
331
+ }
332
+ /**
333
+ * 删除商品后的可选位置保持策略。
334
+ * 预约合并行减量时仍删除最新真实行,再把剩余代表行移动到被删除代表行原位置。
335
+ */
336
+ export interface RemoveProductsFromOrderOptions {
337
+ preserveDisplayPosition?: {
338
+ remainingProductUid: string;
339
+ anchorProductUid: string;
340
+ };
341
+ }
324
342
  /** 更新购物车行:仅传 SKU 时命中同 SKU 第一行;多行同 SKU 须传 unique_identification_number / 选项指纹等 */
325
343
  export interface UpdateOrderProductParams {
326
344
  product_id: number | null;
@@ -726,17 +744,22 @@ export interface OrderModuleAPI {
726
744
  updateTempOrderCustomer: (customer: UpdateTempOrderCustomerInput) => OrderSnapshot['customer'];
727
745
  updateTempOrderBuzzer: (buzzer: string) => string;
728
746
  updateTempOrderContactsInfo: (contactsInfo: Record<string, any> | null) => Record<string, any> | null;
729
- addProductToOrder: (product: Partial<OrderProduct> & OrderProductIdentity, booking?: AddProductBookingInput) => Promise<OrderProduct[]>;
747
+ addProductToOrder: (product: Partial<OrderProduct> & OrderProductIdentity, booking?: AddProductBookingInput, options?: AddProductToOrderOptions) => Promise<OrderProduct[]>;
730
748
  updateOrderProduct: (params: UpdateOrderProductParams) => Promise<OrderProduct[]>;
731
749
  /** 批量更新购物车行,末尾仅触发一次促销重算与 summary 刷新 */
732
750
  updateOrderProducts: (paramsList: UpdateOrderProductParams[]) => Promise<OrderProduct[]>;
733
751
  updateOrderProductQuantity: (params: UpdateOrderProductQuantityParams) => Promise<OrderProduct[]>;
734
752
  updateOrderBooking: (params: UpdateOrderBookingParams) => any[];
735
- removeProductFromOrder: (identity: OrderProductIdentity) => Promise<OrderProduct[]>;
753
+ removeProductFromOrder: (identity: OrderProductIdentity, options?: RemoveProductsFromOrderOptions) => Promise<OrderProduct[]>;
736
754
  /** 批量删除购物车行,末尾仅触发一次促销重算与 summary 刷新 */
737
- removeProductsFromOrder: (identities: OrderProductIdentity[]) => Promise<OrderProduct[]>;
755
+ removeProductsFromOrder: (identities: OrderProductIdentity[], options?: RemoveProductsFromOrderOptions) => Promise<OrderProduct[]>;
738
756
  /** 在显式开启时保存当前 tempOrder 的 sessionStorage 会话快照。 */
739
757
  persistTempOrder: () => void;
758
+ /**
759
+ * 删除当前 cacheId 的 tempOrder 会话快照,并阻止当前模块实例再次写入。
760
+ * 新模块实例初始化时会按 otherParams 重新启用持久化。
761
+ */
762
+ clearSessionStoragePersistedTempOrder: () => void;
740
763
  /** 取出一次待恢复的 sessionStorage tempOrder 快照。 */
741
764
  consumeSessionStorageRestore: () => Record<string, any> | null;
742
765
  /**
@@ -0,0 +1,45 @@
1
+ import type { OrderTempOrder } from '../types';
2
+ /**
3
+ * 商品券拆行的身份与回并工具。
4
+ *
5
+ * 背景:
6
+ * Rules.calcDiscount 应用 good_pass 时,会把一个数量行拆成若干「用券行」和
7
+ * 「普通余量行」。Rules 返回的拆分行可能复制同一份
8
+ * metadata.unique_identification_number,进而造成:
9
+ * - 按 UID 删除时同时命中多行;
10
+ * - UI 把不同券行视为同一个商品;
11
+ * - 后续商品更新无法准确定位目标行。
12
+ *
13
+ * 处理原则:
14
+ * 1. 拆分后每一行必须拥有独立 UID;
15
+ * 2. 原始拆分来源仅记录在 tempOrder._extend.productsByUid 的运行态索引中,
16
+ * 不写入商品 metadata,不污染提交协议;
17
+ * 3. 券被移除后,只回并被本工具确认来自同一折扣拆分源的行;
18
+ * 4. 预约、赠品、促销、手动折扣、备注、称重、holder 和已落库行均为硬边界。
19
+ *
20
+ * 两个导出函数都会原地更新 tempOrder。调用顺序必须是:
21
+ * ensureUniqueProductLineUids -> consolidateDiscountFreeProductLines。
22
+ */
23
+ /** productsByUid 运行态记录中用于关联同一次折扣拆分的字段。 */
24
+ export declare const DISCOUNT_SPLIT_ORIGIN_UID_KEY = "discountSplitOriginUid";
25
+ /**
26
+ * 修复 Rules 折扣拆行复制 UID 的问题,并为拆分行记录共同来源。
27
+ *
28
+ * 第一条商品保留原 UID,后续重复行生成新 UUID;对应的 productsByUid.runtime
29
+ * 会复制原行 runtime(包括 origin),以保证删除、编辑及展示字段回读仍可工作。
30
+ *
31
+ * 没有 UID 的裸商品不会被补写 UID。这是为了兼容 Rules/mock 的历史协议形状;
32
+ * 正常 Order 写路径会在 normalize 阶段提前创建 UID。
33
+ */
34
+ export declare function ensureUniqueProductLineUids(tempOrder: OrderTempOrder): void;
35
+ /**
36
+ * 把券移除后已经恢复原价的同源折扣拆分行重新合并。
37
+ *
38
+ * 此函数刻意不执行通用的「同 SKU 合并」:没有 discountSplitOriginUid 的普通行
39
+ * 可能是业务 hook、导入订单或其它流程主动拆开的,不能在折扣重算时擅自合并。
40
+ *
41
+ * payment_price、tax_fee、surcharge_fee 属于 summary 计算层的派生值。券行与普通行
42
+ * 在回并前可能暂时不同,因此它们不参与 mergeKey;原始价格、SKU/option/bundle
43
+ * 结构以及税务/商品类型配置仍参与比较,防止不同商品语义被误合并。
44
+ */
45
+ export declare function consolidateDiscountFreeProductLines(tempOrder: OrderTempOrder): void;