@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
@@ -0,0 +1,170 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+
19
+ // src/modules/Order/utils/discountProductLineIdentity.ts
20
+ var discountProductLineIdentity_exports = {};
21
+ __export(discountProductLineIdentity_exports, {
22
+ DISCOUNT_SPLIT_ORIGIN_UID_KEY: () => DISCOUNT_SPLIT_ORIGIN_UID_KEY,
23
+ consolidateDiscountFreeProductLines: () => consolidateDiscountFreeProductLines,
24
+ ensureUniqueProductLineUids: () => ensureUniqueProductLineUids
25
+ });
26
+ module.exports = __toCommonJS(discountProductLineIdentity_exports);
27
+ var import_lodash_es = require("lodash-es");
28
+ var import_utils = require("../../../solution/ScanOrder/utils");
29
+ var import_orderCollectionIdentity = require("./orderCollectionIdentity");
30
+ var DISCOUNT_SPLIT_ORIGIN_UID_KEY = "discountSplitOriginUid";
31
+ function getProductLineUid(product) {
32
+ var _a;
33
+ if (!product || typeof product !== "object")
34
+ return null;
35
+ const uid = ((_a = product.metadata) == null ? void 0 : _a.unique_identification_number) ?? product.unique_identification_number;
36
+ if (uid === void 0 || uid === null || uid === "")
37
+ return null;
38
+ return String(uid);
39
+ }
40
+ function getProductSkuOptions(product) {
41
+ var _a;
42
+ return Array.isArray((_a = product.product_sku) == null ? void 0 : _a.option) ? product.product_sku.option : [];
43
+ }
44
+ function hasProductLineNote(product) {
45
+ return typeof product.note === "string" && product.note.trim().length > 0;
46
+ }
47
+ function isManualProductDiscountLine(product) {
48
+ var _a, _b;
49
+ return ((_a = product.metadata) == null ? void 0 : _a.is_manual_discount) === true || ((_b = product.metadata) == null ? void 0 : _b.is_manual_discount) === 1 || (product.discount_list || []).some((item) => (item == null ? void 0 : item.type) === "product");
50
+ }
51
+ function ensureProductsByUid(tempOrder) {
52
+ if (!tempOrder._extend || typeof tempOrder._extend !== "object") {
53
+ tempOrder._extend = { productsByUid: {} };
54
+ }
55
+ if (!tempOrder._extend.productsByUid) {
56
+ tempOrder._extend.productsByUid = {};
57
+ }
58
+ return tempOrder._extend.productsByUid;
59
+ }
60
+ function ensureUniqueProductLineUids(tempOrder) {
61
+ const usedUids = /* @__PURE__ */ new Set();
62
+ const productsByUid = ensureProductsByUid(tempOrder);
63
+ tempOrder.products = (tempOrder.products || []).map((product) => {
64
+ const sourceUid = getProductLineUid(product);
65
+ if (!sourceUid)
66
+ return product;
67
+ if (!usedUids.has(sourceUid)) {
68
+ usedUids.add(sourceUid);
69
+ return product;
70
+ }
71
+ let splitUid = (0, import_orderCollectionIdentity.createUuidV4)();
72
+ while (usedUids.has(splitUid)) {
73
+ splitUid = (0, import_orderCollectionIdentity.createUuidV4)();
74
+ }
75
+ product.metadata = {
76
+ ...product.metadata || {},
77
+ unique_identification_number: splitUid
78
+ };
79
+ const sourceRuntime = productsByUid[sourceUid] || {};
80
+ const discountSplitOriginUid = String(
81
+ sourceRuntime[DISCOUNT_SPLIT_ORIGIN_UID_KEY] || sourceUid
82
+ );
83
+ productsByUid[sourceUid] = {
84
+ ...sourceRuntime,
85
+ [DISCOUNT_SPLIT_ORIGIN_UID_KEY]: discountSplitOriginUid
86
+ };
87
+ productsByUid[splitUid] = {
88
+ ...(0, import_lodash_es.cloneDeep)(sourceRuntime),
89
+ [DISCOUNT_SPLIT_ORIGIN_UID_KEY]: discountSplitOriginUid
90
+ };
91
+ usedUids.add(splitUid);
92
+ return product;
93
+ });
94
+ }
95
+ function consolidateDiscountFreeProductLines(tempOrder) {
96
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
97
+ const consolidatedProducts = [];
98
+ const productIndexByKey = /* @__PURE__ */ new Map();
99
+ for (const product of tempOrder.products || []) {
100
+ const productBundle = Array.isArray(product.product_bundle) ? product.product_bundle : [];
101
+ const hasDiscount = (product.discount_list || []).length > 0 || productBundle.some((bundle) => ((bundle == null ? void 0 : bundle.discount_list) || []).length > 0);
102
+ const bookingUid = product.booking_uid ?? ((_a = product.metadata) == null ? void 0 : _a.booking_uid);
103
+ const bookingId = product.booking_id ?? ((_b = product.metadata) == null ? void 0 : _b.booking_id);
104
+ const orderDetailId = product.order_detail_id ?? product.orderDetailId;
105
+ const holderId = product.holder_id ?? ((_c = product.metadata) == null ? void 0 : _c.holder_id);
106
+ const hasMergeBoundary = hasDiscount || Boolean(((_d = product.metadata) == null ? void 0 : _d._giftInfo) || product._giftInfo) || Boolean((_e = product.metadata) == null ? void 0 : _e._promotion) || isManualProductDiscountLine(product) || hasProductLineNote(product) || Number(((_f = product.product_sku) == null ? void 0 : _f.open_sold_weight) || 0) === 1 || ((_g = product.metadata) == null ? void 0 : _g.is_edit_for_runtime) === true || bookingId !== void 0 && bookingId !== null && bookingId !== 0 && bookingId !== "0" || bookingUid !== void 0 && bookingUid !== null && String(bookingUid) !== "" || holderId !== void 0 && holderId !== null && String(holderId) !== "" || orderDetailId !== void 0 && orderDetailId !== null && orderDetailId !== "";
107
+ if (hasMergeBoundary) {
108
+ consolidatedProducts.push(product);
109
+ continue;
110
+ }
111
+ const productUid = getProductLineUid(product);
112
+ const discountSplitOriginUid = productUid ? (_j = (_i = (_h = tempOrder._extend) == null ? void 0 : _h.productsByUid) == null ? void 0 : _i[productUid]) == null ? void 0 : _j[DISCOUNT_SPLIT_ORIGIN_UID_KEY] : void 0;
113
+ if (!discountSplitOriginUid) {
114
+ consolidatedProducts.push(product);
115
+ continue;
116
+ }
117
+ const fingerprint = (0, import_utils.buildProductLineFingerprint)(
118
+ getProductSkuOptions(product),
119
+ productBundle
120
+ );
121
+ const priceSignature = JSON.stringify({
122
+ selling_price: String(product.selling_price ?? ""),
123
+ original_price: String(product.original_price ?? ""),
124
+ custom_price: String(product.custom_price ?? ""),
125
+ is_charge_tax: Number(product.is_charge_tax ?? 0),
126
+ gift_card: Number(product.gift_card ?? 0),
127
+ bundle_edit: Number(product.bundle_edit ?? 0),
128
+ price_schema_version: ((_k = product.metadata) == null ? void 0 : _k.price_schema_version) ?? "",
129
+ source_product_price: String(((_l = product.metadata) == null ? void 0 : _l.source_product_price) ?? ""),
130
+ main_product_selling_price: String(
131
+ ((_m = product.metadata) == null ? void 0 : _m.main_product_selling_price) ?? ""
132
+ ),
133
+ main_product_original_price: String(
134
+ ((_n = product.metadata) == null ? void 0 : _n.main_product_original_price) ?? ""
135
+ ),
136
+ bundle: productBundle.map((bundle) => ({
137
+ bundle_id: (bundle == null ? void 0 : bundle.bundle_id) ?? (bundle == null ? void 0 : bundle.id) ?? "",
138
+ product_id: (bundle == null ? void 0 : bundle.bundle_product_id) ?? (bundle == null ? void 0 : bundle._bundle_product_id) ?? (bundle == null ? void 0 : bundle.product_id) ?? "",
139
+ product_variant_id: (bundle == null ? void 0 : bundle.bundle_variant_id) ?? (bundle == null ? void 0 : bundle.variant_id) ?? (bundle == null ? void 0 : bundle.product_variant_id) ?? 0,
140
+ price: String((bundle == null ? void 0 : bundle.price) ?? ""),
141
+ original_price: String((bundle == null ? void 0 : bundle.original_price) ?? ""),
142
+ bundle_selling_price: String((bundle == null ? void 0 : bundle.bundle_selling_price) ?? "")
143
+ }))
144
+ });
145
+ const mergeKey = [
146
+ discountSplitOriginUid,
147
+ product.product_id,
148
+ product.product_variant_id ?? 0,
149
+ fingerprint,
150
+ priceSignature
151
+ ].join("#");
152
+ const matchedIndex = productIndexByKey.get(mergeKey);
153
+ if (matchedIndex === void 0) {
154
+ productIndexByKey.set(mergeKey, consolidatedProducts.length);
155
+ consolidatedProducts.push(product);
156
+ continue;
157
+ }
158
+ const matchedProduct = consolidatedProducts[matchedIndex];
159
+ matchedProduct.num = (0, import_utils.getSafeProductNum)(
160
+ Number(matchedProduct.num || 0) + Number(product.num || 0)
161
+ );
162
+ }
163
+ tempOrder.products = consolidatedProducts;
164
+ }
165
+ // Annotate the CommonJS export names for ESM import in node:
166
+ 0 && (module.exports = {
167
+ DISCOUNT_SPLIT_ORIGIN_UID_KEY,
168
+ consolidateDiscountFreeProductLines,
169
+ ensureUniqueProductLineUids
170
+ });
@@ -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
  });
@@ -243,7 +243,7 @@ var RulesModule = class extends import_BaseModule.BaseModule {
243
243
  const discountType = selectedDiscount.tag || selectedDiscount.type;
244
244
  return selectedDiscount.isEditMode === true && selectedDiscount.isSelected === true && ["good_pass", "discount_card", "product_discount_card"].includes(discountType);
245
245
  };
246
- const resolveReapplyEditModeDiscountPercent = (originProduct, selectedDiscount) => {
246
+ const resolveReapplyBookingDiscountPercent = (originProduct, selectedDiscount) => {
247
247
  var _a2;
248
248
  if (!shouldReapplyBookingDiscount(originProduct, selectedDiscount))
249
249
  return void 0;
@@ -318,6 +318,9 @@ var RulesModule = class extends import_BaseModule.BaseModule {
318
318
  bundleIndex,
319
319
  // 虚拟商品属性
320
320
  price: Number(bundleItem.price || 0),
321
+ // 新增商品运行态通常带 _bundle_product_id;订单 hydrate 后只有
322
+ // bundle_product_id。Change time 必须兼容两种结构,否则 bundle
323
+ // 子项无法命中折扣卡的 limited_relation_product_data.product_ids。
321
324
  id: bundleItem.bundle_product_id ?? bundleItem._bundle_product_id ?? bundleItem.product_id,
322
325
  _id: `${product._id}_bundle_${bundleIndex}`,
323
326
  parentId: product._id,
@@ -779,13 +782,6 @@ var RulesModule = class extends import_BaseModule.BaseModule {
779
782
  if (isPersistedProduct && hasExistingWalletDiscount) {
780
783
  if (flatItem.type === "main") {
781
784
  processedProductsMap.set(product._id, [originProduct]);
782
- } else {
783
- processedFlatItemsMap.set(flatItem._id, [
784
- {
785
- ...flatItem,
786
- processed: true
787
- }
788
- ]);
789
785
  }
790
786
  return;
791
787
  }
@@ -862,15 +858,46 @@ var RulesModule = class extends import_BaseModule.BaseModule {
862
858
  }
863
859
  return false;
864
860
  });
865
- const explicitlySelectedDiscountCard = (options == null ? void 0 : options.isSelected) === true && options.discountId !== void 0 ? applicableDiscounts.find((discount) => {
861
+ const explicitlySelectedDiscount = (options == null ? void 0 : options.isSelected) === true && options.discountId !== void 0 ? applicableDiscounts.find((discount) => {
866
862
  const discountType = discount.tag || discount.type;
867
- return String(discount.id) === String(options.discountId) && ["discount_card", "product_discount_card"].includes(discountType);
863
+ return String(discount.id) === String(options.discountId) && ["good_pass", "discount_card", "product_discount_card"].includes(
864
+ discountType
865
+ );
868
866
  }) : void 0;
867
+ const explicitlySelectedDiscountCard = explicitlySelectedDiscount && (explicitlySelectedDiscount.tag || explicitlySelectedDiscount.type) !== "good_pass" ? explicitlySelectedDiscount : void 0;
868
+ const appliedDiscountCardIds = new Set(
869
+ (product.discount_list || []).filter(
870
+ (item) => ["discount_card", "product_discount_card"].includes(
871
+ (item == null ? void 0 : item.tag) || (item == null ? void 0 : item.type)
872
+ )
873
+ ).map(
874
+ (item) => {
875
+ var _a3;
876
+ return ((_a3 = item == null ? void 0 : item.discount) == null ? void 0 : _a3.resource_id) ?? (item == null ? void 0 : item.resource_id) ?? (item == null ? void 0 : item.id);
877
+ }
878
+ ).filter((id) => id !== void 0 && id !== null).map(String)
879
+ );
880
+ const appliedDiscountCard = (options == null ? void 0 : options.scan) ? void 0 : applicableDiscounts.find((discount) => {
881
+ const discountType = discount.tag || discount.type;
882
+ return discount.isSelected === true && ["discount_card", "product_discount_card"].includes(discountType) && appliedDiscountCardIds.has(String(discount.id));
883
+ });
869
884
  const scannedSelectedDiscountCard = applicableDiscounts.find(
870
885
  (n) => n.isScan && n.isSelected && (n.tag || n.type) !== "good_pass"
871
886
  );
872
- const selectedDiscountCard = explicitlySelectedDiscountCard || scannedSelectedDiscountCard;
873
- const selectedDiscount = selectedDiscountCard || applicableDiscounts[0];
887
+ const selectedDiscountCard = explicitlySelectedDiscountCard || appliedDiscountCard || scannedSelectedDiscountCard;
888
+ const selectedDiscount = explicitlySelectedDiscount || selectedDiscountCard || applicableDiscounts[0];
889
+ const prioritizedApplicableDiscounts = explicitlySelectedDiscount && (explicitlySelectedDiscount.tag || explicitlySelectedDiscount.type) === "good_pass" ? [
890
+ explicitlySelectedDiscount,
891
+ ...applicableDiscounts.filter(
892
+ (discount) => {
893
+ const discountType = discount.tag || discount.type;
894
+ return discountType === "good_pass" && String(discount.id) !== String(explicitlySelectedDiscount.id);
895
+ }
896
+ ),
897
+ ...applicableDiscounts.filter(
898
+ (discount) => (discount.tag || discount.type) !== "good_pass"
899
+ )
900
+ ] : applicableDiscounts;
874
901
  let isManualDiscount = false;
875
902
  let isItemRewardProductDiscount = false;
876
903
  if (flatItem.type === "main") {
@@ -1049,21 +1076,65 @@ var RulesModule = class extends import_BaseModule.BaseModule {
1049
1076
  const arr = [];
1050
1077
  if (flatItem.type === "main") {
1051
1078
  if (splitCount < totalQuantity && isNeedSplit) {
1079
+ const remainingQuantity = totalQuantity - splitCount;
1080
+ const applicableNonGoodPassById = new Map(
1081
+ applicableDiscounts.filter((discount) => (discount.tag || discount.type) !== "good_pass").map((discount) => [String(discount.id), discount])
1082
+ );
1083
+ const remainderDiscountList = (product.discount_list || []).filter((discount) => {
1084
+ var _a3;
1085
+ const discountType = discount.tag || discount.type;
1086
+ if (discountType === "promotion")
1087
+ return true;
1088
+ if (discountType === "good_pass")
1089
+ return false;
1090
+ const resourceId = ((_a3 = discount.discount) == null ? void 0 : _a3.resource_id) ?? discount.id;
1091
+ return resourceId !== void 0 && resourceId !== null && applicableNonGoodPassById.has(String(resourceId));
1092
+ }).map((discount) => {
1093
+ var _a3;
1094
+ if ((discount.tag || discount.type) === "promotion")
1095
+ return discount;
1096
+ const resourceId = ((_a3 = discount.discount) == null ? void 0 : _a3.resource_id) ?? discount.id;
1097
+ const matchedDiscount = applicableNonGoodPassById.get(String(resourceId));
1098
+ if (matchedDiscount) {
1099
+ usedDiscounts.set(matchedDiscount.id, true);
1100
+ const appliedProducts = appliedDiscountProducts.get(matchedDiscount.id) || [];
1101
+ appliedProducts.push({
1102
+ ...discount,
1103
+ _num: remainingQuantity
1104
+ });
1105
+ appliedDiscountProducts.set(matchedDiscount.id, appliedProducts);
1106
+ }
1107
+ return {
1108
+ ...discount,
1109
+ // _num 是 Rules 运行态的行数量;Order 写回 tempOrder 时会裁剪该字段。
1110
+ _num: remainingQuantity
1111
+ };
1112
+ });
1113
+ const hasRetainedWalletDiscount = remainderDiscountList.some(
1114
+ (discount) => !["promotion", "good_pass"].includes(discount.tag || discount.type)
1115
+ );
1052
1116
  let total = product.origin_total ?? product.total;
1053
1117
  if ((product.discount_list || []).some((item) => item.type === "promotion")) {
1054
1118
  total = product.total ?? product.origin_total;
1055
1119
  }
1056
1120
  arr.push(
1057
1121
  this.hooks.setProduct(originProduct, {
1058
- discount_list: this.filterDiscountListByType(product.discount_list, "promotion"),
1059
- quantity: totalQuantity - splitCount,
1122
+ // 商品券只替换被拆出的 splitCount 件。余量行原本仍有效的折扣卡
1123
+ // 不能随 good_pass 拆分一起清除;已取消或已失效的折扣不会进入此列表。
1124
+ discount_list: remainderDiscountList,
1125
+ quantity: remainingQuantity,
1060
1126
  _id: product._id.split("___")[0],
1061
- total
1127
+ total,
1128
+ // 保留余量行当前的折后主价;Order.applyProductDiscountPrices 随后会基于
1129
+ // source_product_price + discount_list 再归一化,避免出现重复折扣。
1130
+ ...hasRetainedWalletDiscount ? {
1131
+ main_product_selling_price: product.main_product_selling_price ?? product.price
1132
+ } : {}
1062
1133
  })
1063
1134
  );
1064
1135
  }
1065
1136
  for (let i = 0; i < splitCount; i++) {
1066
- const selectedDiscount2 = selectedDiscountCard || applicableDiscounts[i];
1137
+ const selectedDiscount2 = isNeedSplit ? prioritizedApplicableDiscounts[i] : selectedDiscountCard || applicableDiscounts[i];
1067
1138
  usedDiscounts.set(selectedDiscount2.id, true);
1068
1139
  if ((selectedDiscount2.tag || selectedDiscount2.type) === "good_pass") {
1069
1140
  const currentCount = usedProductIdCounts.get(selectedDiscount2.product_id) || 0;
@@ -1091,8 +1162,8 @@ var RulesModule = class extends import_BaseModule.BaseModule {
1091
1162
  amount = productAllocation.discountAmount;
1092
1163
  productDiscountDifference = productAllocation.difference;
1093
1164
  mainProductSellingPrice = Math.max(new import_decimal.default(product.price).minus(amount).toNumber(), 0);
1094
- } else if (resolveReapplyEditModeDiscountPercent(originProduct, selectedDiscount2) !== void 0) {
1095
- const percent = resolveReapplyEditModeDiscountPercent(originProduct, selectedDiscount2);
1165
+ } else if (resolveReapplyBookingDiscountPercent(originProduct, selectedDiscount2) !== void 0) {
1166
+ const percent = resolveReapplyBookingDiscountPercent(originProduct, selectedDiscount2);
1096
1167
  reapplyDiscountPercent = new import_decimal.default(percent).toFixed(2);
1097
1168
  mainProductSellingPrice = new import_decimal.default(product.price || 0).mul(new import_decimal.default(100).minus(percent)).div(100).toDecimalPlaces(2).toNumber();
1098
1169
  amount = new import_decimal.default(product.price).minus(mainProductSellingPrice).toNumber();
@@ -1190,7 +1261,7 @@ var RulesModule = class extends import_BaseModule.BaseModule {
1190
1261
  const discountNum = splitCount;
1191
1262
  const normalNum = totalQuantity - discountNum;
1192
1263
  for (let i = 0; i < discountNum; i++) {
1193
- const selectedDiscount2 = applicableDiscounts[i];
1264
+ const selectedDiscount2 = prioritizedApplicableDiscounts[i];
1194
1265
  usedDiscounts.set(selectedDiscount2.id, true);
1195
1266
  if ((selectedDiscount2.tag || selectedDiscount2.type) === "good_pass") {
1196
1267
  const currentCount = usedProductIdCounts.get(selectedDiscount2.product_id) || 0;
@@ -1267,10 +1338,19 @@ var RulesModule = class extends import_BaseModule.BaseModule {
1267
1338
  let targetProductTotal;
1268
1339
  let fixedAmountPerItem;
1269
1340
  let productDiscountDifference;
1341
+ let reapplyDiscountPercent;
1270
1342
  if (isOrderLevel && productAllocation) {
1271
1343
  fixedAmountPerItem = productAllocation.discountAmount;
1272
1344
  productDiscountDifference = productAllocation.difference;
1273
1345
  targetProductTotal = Math.max(new import_decimal.default(productOriginTotal).minus(fixedAmountPerItem).toNumber(), 0);
1346
+ } else if (resolveReapplyBookingDiscountPercent(originProduct, selectedDiscount2) !== void 0) {
1347
+ const percent = resolveReapplyBookingDiscountPercent(
1348
+ originProduct,
1349
+ selectedDiscount2
1350
+ );
1351
+ reapplyDiscountPercent = new import_decimal.default(percent).toFixed(2);
1352
+ targetProductTotal = new import_decimal.default(productOriginTotal).mul(new import_decimal.default(100).minus(percent)).div(100).toDecimalPlaces(2).toNumber();
1353
+ fixedAmountPerItem = new import_decimal.default(productOriginTotal).minus(targetProductTotal).toNumber();
1274
1354
  } else {
1275
1355
  targetProductTotal = (0, import_utils.getDiscountAmount)(
1276
1356
  selectedDiscount2,
@@ -1292,7 +1372,7 @@ var RulesModule = class extends import_BaseModule.BaseModule {
1292
1372
  title: selectedDiscount2.format_title,
1293
1373
  original_amount: product.original_price,
1294
1374
  product_id: product.id || product.product_id,
1295
- percent: selectedDiscount2.par_value,
1375
+ percent: reapplyDiscountPercent ?? selectedDiscount2.par_value,
1296
1376
  discount_product_id: selectedDiscount2.product_id
1297
1377
  },
1298
1378
  metadata: {
@@ -99,7 +99,11 @@ function getUnitOriginalTotal(product) {
99
99
  function isGeneratedVoucherProduct(product) {
100
100
  const record = product;
101
101
  const metadata = (record == null ? void 0 : record.metadata) || {};
102
- return (record == null ? void 0 : record.extension_type) === "product_voucher" || metadata.parent_order_detail_id !== void 0 || metadata.gift_card_type !== void 0;
102
+ const isGeneratedChild = metadata.parent_order_detail_id !== void 0 || metadata.gift_card_type !== void 0;
103
+ if (isGeneratedChild)
104
+ return true;
105
+ const isPersisted = (record == null ? void 0 : record.order_detail_id) !== void 0 && (record == null ? void 0 : record.order_detail_id) !== null;
106
+ return (record == null ? void 0 : record.extension_type) === "product_voucher" && !isPersisted;
103
107
  }
104
108
  function buildSurchargeServiceItems(products) {
105
109
  return products.map((product) => {
@@ -164,6 +164,12 @@ declare class Server {
164
164
  * 不影响当前界面展示,适用于切回前台、定时刷新等场景
165
165
  */
166
166
  refreshOrdersInBackground(): Promise<void>;
167
+ /**
168
+ * 编辑正式订单时强制查询 Server 最新快照并覆盖本地缓存。
169
+ * 商品成员增删由 OrderModule 在 SQLite/Store 更新完成后广播,
170
+ * UI 可复用 order:onRemoteProductMembershipChanged 处理冲突。
171
+ */
172
+ checkRemoteOrderProductMembership(orderId: string | number): Promise<void>;
167
173
  /**
168
174
  * 清空所有server模块的IndexedDB缓存
169
175
  * @returns Promise<void>
@@ -1789,6 +1789,57 @@ var Server = class {
1789
1789
  });
1790
1790
  }
1791
1791
  }
1792
+ /**
1793
+ * 编辑正式订单时强制查询 Server 最新快照并覆盖本地缓存。
1794
+ * 商品成员增删由 OrderModule 在 SQLite/Store 更新完成后广播,
1795
+ * UI 可复用 order:onRemoteProductMembershipChanged 处理冲突。
1796
+ */
1797
+ async checkRemoteOrderProductMembership(orderId) {
1798
+ const normalizedOrderId = String(orderId ?? "").trim();
1799
+ if (!normalizedOrderId || normalizedOrderId.startsWith("local_order")) {
1800
+ this.logInfo("checkRemoteOrderProductMembership: 跳过非正式订单", {
1801
+ orderId
1802
+ });
1803
+ return;
1804
+ }
1805
+ if (!this.order) {
1806
+ const error = new Error("Order 模块未注册");
1807
+ this.logWarning("checkRemoteOrderProductMembership: Order 模块未注册", {
1808
+ orderId: normalizedOrderId
1809
+ });
1810
+ throw error;
1811
+ }
1812
+ this.logInfo("checkRemoteOrderProductMembership 开始", {
1813
+ orderId: normalizedOrderId
1814
+ });
1815
+ const startTime = Date.now();
1816
+ try {
1817
+ const freshOrders = await this.order.fetchOrdersByHttp([
1818
+ normalizedOrderId
1819
+ ]);
1820
+ const freshOrder = freshOrders.find(
1821
+ (order) => String(order == null ? void 0 : order.order_id) === normalizedOrderId
1822
+ );
1823
+ if (!freshOrder) {
1824
+ throw new Error("Server 未返回目标订单最新快照");
1825
+ }
1826
+ await this.order.upsertOrdersFromRemote(
1827
+ [freshOrder],
1828
+ "manual.editOpenRemoteCheck"
1829
+ );
1830
+ this.logInfo("checkRemoteOrderProductMembership 完成", {
1831
+ orderId: normalizedOrderId,
1832
+ duration: `${Date.now() - startTime}ms`
1833
+ });
1834
+ } catch (error) {
1835
+ this.logError("checkRemoteOrderProductMembership 失败", {
1836
+ orderId: normalizedOrderId,
1837
+ duration: `${Date.now() - startTime}ms`,
1838
+ error: error instanceof Error ? error.message : String(error)
1839
+ });
1840
+ throw error;
1841
+ }
1842
+ }
1792
1843
  /**
1793
1844
  * 清空所有server模块的IndexedDB缓存
1794
1845
  * @returns Promise<void>
@@ -235,6 +235,7 @@ export declare class OrderModule extends BaseModule implements Module {
235
235
  */
236
236
  private lookupOrderIndexByIdentityKeys;
237
237
  private isRemoteEchoMergeSource;
238
+ private isRemoteProductMembershipChangeSource;
238
239
  private getProductMembershipKey;
239
240
  private doProductsHaveStableMembershipIdentity;
240
241
  /**
@@ -920,10 +920,13 @@ var OrderModule = class extends import_BaseModule.BaseModule {
920
920
  */
921
921
  async upsertOrdersFromRemote(freshOrders, source = "upsertOrdersFromRemote") {
922
922
  if (!(freshOrders == null ? void 0 : freshOrders.length)) {
923
- this.logInfo("upsertOrdersFromRemote-订单列表为空", {});
923
+ this.logInfo("upsertOrdersFromRemote-订单列表为空", { source });
924
924
  return;
925
925
  }
926
- this.logInfo("upsertOrdersFromRemote-开始", { count: freshOrders.length, source });
926
+ this.logInfo("upsertOrdersFromRemote-开始", {
927
+ count: freshOrders.length,
928
+ source
929
+ });
927
930
  await this.mergeOrdersToStore(freshOrders, source);
928
931
  }
929
932
  async markOrderSyncedByExternalSaleNumber(params) {
@@ -1685,6 +1688,9 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1685
1688
  isRemoteEchoMergeSource(source) {
1686
1689
  return source === "pubsub.bodyUpsert" || source === "pubsub.httpRefresh";
1687
1690
  }
1691
+ isRemoteProductMembershipChangeSource(source) {
1692
+ return this.isRemoteEchoMergeSource(source) || source === "manual.editOpenRemoteCheck";
1693
+ }
1688
1694
  getProductMembershipKey(product) {
1689
1695
  const persistentId = (0, import_orderCollectionIdentity.getOrderCollectionPersistentId)("product", product);
1690
1696
  if (persistentId)
@@ -1717,7 +1723,7 @@ var OrderModule = class extends import_BaseModule.BaseModule {
1717
1723
  * 对比合并前后的商品成员集合。商品字段修改与数组重排不会产生 diff。
1718
1724
  */
1719
1725
  getRemoteProductMembershipChange(existingOrder, mergedOrder, source) {
1720
- if (!this.isRemoteEchoMergeSource(source))
1726
+ if (!this.isRemoteProductMembershipChangeSource(source))
1721
1727
  return null;
1722
1728
  const previousProducts = Array.isArray(existingOrder.products) ? existingOrder.products : [];
1723
1729
  const currentProducts = Array.isArray(mergedOrder.products) ? mergedOrder.products : [];
@@ -568,7 +568,7 @@ export interface OrderChangedPayload {
568
568
  /** 触发来源,便于订阅者区分 pubsub / 远端覆盖 / 清空等场景 */
569
569
  source?: string;
570
570
  }
571
- export type RemoteProductMembershipChangeSource = 'pubsub.bodyUpsert' | 'pubsub.httpRefresh';
571
+ export type RemoteProductMembershipChangeSource = 'pubsub.bodyUpsert' | 'pubsub.httpRefresh' | 'manual.editOpenRemoteCheck';
572
572
  /**
573
573
  * 远端订单快照相对本地订单产生商品成员增删时的广播载荷。
574
574
  * 只表达商品行成员关系变化,不包含数量、价格、备注等字段级修改。