@pisell/pisellos 0.0.509 → 0.0.511

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.
@@ -31,6 +31,9 @@ export interface ScanOrderOrderProductIdentity {
31
31
  product_id: number | null;
32
32
  product_variant_id: number;
33
33
  identity_key?: string;
34
+ /** 参与合并/匹配;与 remove 通配语义见 isSkuOnlyDeleteIdentity */
35
+ product_option_item?: any[];
36
+ product_bundle?: any[];
34
37
  }
35
38
  export interface ScanOrderOrderProduct extends ScanOrderOrderProductIdentity {
36
39
  order_detail_id: number | null;
@@ -45,8 +48,22 @@ export interface ScanOrderOrderProduct extends ScanOrderOrderProductIdentity {
45
48
  discount_list: any[];
46
49
  product_bundle: any[];
47
50
  metadata: Record<string, any>;
51
+ /** 商品行备注(如顾客对单品的特殊要求) */
52
+ note?: string;
48
53
  _origin?: Record<string, any>;
49
54
  }
55
+ /**
56
+ * 出站 payload 版本的商品行。
57
+ *
58
+ * 与 tempOrder 内部 `ScanOrderOrderProduct` 的字段差异:
59
+ * - `product_option_item[*]`:内部为 `{ product_option_item_id, option_group_id, num, price, ... }`,
60
+ * 出站由 `normalizeSubmitProduct` 重命名为 `{ option_group_item_id, option_group_id, num }`
61
+ * (后端 checkout 协议;丢弃 `price` 等运行时辅助字段)。
62
+ * - `product_bundle[*].option[*]`:同上重命名规则。
63
+ *
64
+ * 运行时(UI 显示、加购合并、指纹、持久化)全部消费内部字段名 `product_option_item_id`,
65
+ * 仅在 SDK 提交边界做一次出站映射,不影响 opaque identity 契约。
66
+ */
50
67
  export interface ScanOrderSubmitProduct extends Omit<ScanOrderOrderProduct, '_origin' | 'identity_key'> {
51
68
  /**
52
69
  * 出站兼容字段:SDK 内部不再消费 payment_price,
@@ -10,6 +10,19 @@ export var ScanOrderHooks = /*#__PURE__*/function (ScanOrderHooks) {
10
10
  return ScanOrderHooks;
11
11
  }({});
12
12
 
13
+ /**
14
+ * 出站 payload 版本的商品行。
15
+ *
16
+ * 与 tempOrder 内部 `ScanOrderOrderProduct` 的字段差异:
17
+ * - `product_option_item[*]`:内部为 `{ product_option_item_id, option_group_id, num, price, ... }`,
18
+ * 出站由 `normalizeSubmitProduct` 重命名为 `{ option_group_item_id, option_group_id, num }`
19
+ * (后端 checkout 协议;丢弃 `price` 等运行时辅助字段)。
20
+ * - `product_bundle[*].option[*]`:同上重命名规则。
21
+ *
22
+ * 运行时(UI 显示、加购合并、指纹、持久化)全部消费内部字段名 `product_option_item_id`,
23
+ * 仅在 SDK 提交边界做一次出站映射,不影响 opaque identity 契约。
24
+ */
25
+
13
26
  /**
14
27
  * ScanOrder 临时订单(runtime-clean 版本)
15
28
  * 只保留运行时字段,不包含任何 __comment 字段
@@ -82,9 +82,24 @@ export declare function buildItemRuleBusinessData(params: {
82
82
  }): ItemRuleBusinessData;
83
83
  export declare function attachItemRuleLimitsToTopLevelProducts<T>(productList: T, limits: QuantityLimitResult[]): T;
84
84
  /**
85
- * 判断两个商品 identity 是否匹配。
86
- * 当双方都没有 identity_key 时回退到 product_id + product_variant_id 比较(向后兼容);
87
- * 一旦有一方携带 identity_key,则必须严格相等。
85
+ * 将选项行 / 套餐行归一成稳定 JSON,用于「同 SKU 是否合并」判断。
86
+ * 数组元素先按主键排序再序列化,避免顺序差异导致误判为不同行。
87
+ */
88
+ export declare function buildProductLineFingerprint(productOptionItem?: unknown, productBundle?: unknown): string;
89
+ /**
90
+ * removeProductFromOrder 仅传 SKU 时的通配 identity(对象上未声明选项键)。
91
+ * 与显式 `product_option_item: []` 区分:后者只匹配「无选项」行。
92
+ */
93
+ export declare function isSkuOnlyDeleteIdentity(x: ScanOrderOrderProductIdentity): boolean;
94
+ /**
95
+ * 判断两个商品 identity 是否匹配(不透明 identity 契约)。
96
+ *
97
+ * 调用约定:始终 `isIdentityMatch(line, callerIdentity)`。
98
+ * - 若 SKU(product_id + product_variant_id)不一致 → 不匹配。
99
+ * - **双方都带** `identity_key` → 严格字符串相等比较(不再猜测合成 key / metadata 桥接)。
100
+ * 这是 opaque identity 契约的标准路径:UI 删改时透传 SDK 回灌的 identity_key。
101
+ * - 否则(即至少一侧未声明 identity_key)→ 进入「SKU 通配 / 显式空选项 / 指纹」回退路径,
102
+ * 忽略线侧 identity_key,按内容语义匹配。这给「行已 opaque、调用方却想按 SKU 通配 / 选项指纹删除」留兼容入口。
88
103
  */
89
104
  export declare function isIdentityMatch(a: ScanOrderOrderProductIdentity, b: ScanOrderOrderProductIdentity): boolean;
90
105
  /**
@@ -12,6 +12,8 @@ function _createForOfIteratorHelper(o, allowArrayLike) { var it = typeof Symbol
12
12
  function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); }
13
13
  function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i]; return arr2; }
14
14
  import dayjs from 'dayjs';
15
+ import { createUuidV4 } from "../../modules/Order/utils";
16
+
15
17
  /**
16
18
  * 构建金额全为 0 的空 summary。
17
19
  * 作为尚未计算金额时的兜底默认值,避免下游因 undefined 报错。
@@ -367,14 +369,75 @@ export function attachItemRuleLimitsToTopLevelProducts(productList, limits) {
367
369
  }
368
370
 
369
371
  /**
370
- * 判断两个商品 identity 是否匹配。
371
- * 当双方都没有 identity_key 时回退到 product_id + product_variant_id 比较(向后兼容);
372
- * 一旦有一方携带 identity_key,则必须严格相等。
372
+ * 将选项行 / 套餐行归一成稳定 JSON,用于「同 SKU 是否合并」判断。
373
+ * 数组元素先按主键排序再序列化,避免顺序差异导致误判为不同行。
374
+ */
375
+ export function buildProductLineFingerprint(productOptionItem, productBundle) {
376
+ var optArr = Array.isArray(productOptionItem) ? productOptionItem : [];
377
+ var normalizedOpts = optArr.map(function (item) {
378
+ var _item$price;
379
+ return {
380
+ product_option_item_id: Number(item === null || item === void 0 ? void 0 : item.product_option_item_id) || 0,
381
+ option_group_id: Number(item === null || item === void 0 ? void 0 : item.option_group_id) || 0,
382
+ num: typeof (item === null || item === void 0 ? void 0 : item.num) === 'number' && !Number.isNaN(item.num) ? Math.max(1, Math.floor(item.num)) : 1,
383
+ price: String((_item$price = item === null || item === void 0 ? void 0 : item.price) !== null && _item$price !== void 0 ? _item$price : '')
384
+ };
385
+ }).sort(function (p, q) {
386
+ if (p.product_option_item_id !== q.product_option_item_id) {
387
+ return p.product_option_item_id - q.product_option_item_id;
388
+ }
389
+ if (p.option_group_id !== q.option_group_id) return p.option_group_id - q.option_group_id;
390
+ if (p.num !== q.num) return p.num - q.num;
391
+ return p.price.localeCompare(q.price);
392
+ });
393
+ var bundleArr = Array.isArray(productBundle) ? productBundle : [];
394
+ var normalizedBundles = bundleArr.map(function (item) {
395
+ var _ref, _item$bundle_id;
396
+ return {
397
+ bundle_id: Number((_ref = (_item$bundle_id = item === null || item === void 0 ? void 0 : item.bundle_id) !== null && _item$bundle_id !== void 0 ? _item$bundle_id : item === null || item === void 0 ? void 0 : item.id) !== null && _ref !== void 0 ? _ref : 0) || 0,
398
+ product_id: Number(item === null || item === void 0 ? void 0 : item.product_id) || 0,
399
+ num: typeof (item === null || item === void 0 ? void 0 : item.num) === 'number' && !Number.isNaN(item.num) ? Math.max(1, Math.floor(item.num)) : 1
400
+ };
401
+ }).sort(function (p, q) {
402
+ if (p.bundle_id !== q.bundle_id) return p.bundle_id - q.bundle_id;
403
+ if (p.product_id !== q.product_id) return p.product_id - q.product_id;
404
+ return p.num - q.num;
405
+ });
406
+ return JSON.stringify([normalizedOpts, normalizedBundles]);
407
+ }
408
+
409
+ /**
410
+ * removeProductFromOrder 仅传 SKU 时的通配 identity(对象上未声明选项键)。
411
+ * 与显式 `product_option_item: []` 区分:后者只匹配「无选项」行。
412
+ */
413
+ export function isSkuOnlyDeleteIdentity(x) {
414
+ if (x.identity_key) return false;
415
+ return !('product_option_item' in x) && !('product_bundle' in x);
416
+ }
417
+ function fingerprintForIdentityWithOptionKeys(x) {
418
+ var row = x;
419
+ var opts = 'product_option_item' in row ? Array.isArray(row.product_option_item) ? row.product_option_item : [] : [];
420
+ var buds = 'product_bundle' in row ? Array.isArray(row.product_bundle) ? row.product_bundle : [] : [];
421
+ return buildProductLineFingerprint(opts, buds);
422
+ }
423
+
424
+ /**
425
+ * 判断两个商品 identity 是否匹配(不透明 identity 契约)。
426
+ *
427
+ * 调用约定:始终 `isIdentityMatch(line, callerIdentity)`。
428
+ * - 若 SKU(product_id + product_variant_id)不一致 → 不匹配。
429
+ * - **双方都带** `identity_key` → 严格字符串相等比较(不再猜测合成 key / metadata 桥接)。
430
+ * 这是 opaque identity 契约的标准路径:UI 删改时透传 SDK 回灌的 identity_key。
431
+ * - 否则(即至少一侧未声明 identity_key)→ 进入「SKU 通配 / 显式空选项 / 指纹」回退路径,
432
+ * 忽略线侧 identity_key,按内容语义匹配。这给「行已 opaque、调用方却想按 SKU 通配 / 选项指纹删除」留兼容入口。
373
433
  */
374
434
  export function isIdentityMatch(a, b) {
375
435
  if (a.product_id !== b.product_id || a.product_variant_id !== b.product_variant_id) return false;
376
- if (!a.identity_key && !b.identity_key) return true;
377
- return a.identity_key === b.identity_key;
436
+ var aHasKey = typeof a.identity_key === 'string' && a.identity_key.length > 0;
437
+ var bHasKey = typeof b.identity_key === 'string' && b.identity_key.length > 0;
438
+ if (aHasKey && bHasKey) return a.identity_key === b.identity_key;
439
+ if (isSkuOnlyDeleteIdentity(a) || isSkuOnlyDeleteIdentity(b)) return true;
440
+ return fingerprintForIdentityWithOptionKeys(a) === fingerprintForIdentityWithOptionKeys(b);
378
441
  }
379
442
 
380
443
  /**
@@ -396,8 +459,11 @@ export function getProductIdentityIndex(products, identity) {
396
459
  export function normalizeOrderProduct(product) {
397
460
  var _product$is_charge_ta;
398
461
  var metadata = _objectSpread({}, product.metadata || {});
399
- if (product.identity_key && !metadata.unique_identification_number) {
400
- metadata.unique_identification_number = product.identity_key;
462
+ // 不透明 identity 契约:每条订单行必须带 identity_key。
463
+ // 调用方未传时由 SDK 自动生成 UUID,后续 update/remove 只做严格比对,避免猜测合成 key。
464
+ var resolvedIdentityKey = product.identity_key && String(product.identity_key).length > 0 ? String(product.identity_key) : createUuidV4();
465
+ if (!metadata.unique_identification_number) {
466
+ metadata.unique_identification_number = resolvedIdentityKey;
401
467
  }
402
468
 
403
469
  // selling_price:券后成交单价;original_price:券前店铺售价。
@@ -414,14 +480,14 @@ export function normalizeOrderProduct(product) {
414
480
  metadata.main_product_selling_price = resolvedSellingPrice;
415
481
  }
416
482
  if (metadata.source_product_price === undefined) {
417
- var _ref, _product$_origin$pric, _product$_origin, _product$_origin2;
418
- metadata.source_product_price = (_ref = (_product$_origin$pric = (_product$_origin = product._origin) === null || _product$_origin === void 0 ? void 0 : _product$_origin.price) !== null && _product$_origin$pric !== void 0 ? _product$_origin$pric : (_product$_origin2 = product._origin) === null || _product$_origin2 === void 0 ? void 0 : _product$_origin2.base_price) !== null && _ref !== void 0 ? _ref : resolvedSellingPrice;
483
+ var _ref2, _product$_origin$pric, _product$_origin, _product$_origin2;
484
+ metadata.source_product_price = (_ref2 = (_product$_origin$pric = (_product$_origin = product._origin) === null || _product$_origin === void 0 ? void 0 : _product$_origin.price) !== null && _product$_origin$pric !== void 0 ? _product$_origin$pric : (_product$_origin2 = product._origin) === null || _product$_origin2 === void 0 ? void 0 : _product$_origin2.base_price) !== null && _ref2 !== void 0 ? _ref2 : resolvedSellingPrice;
419
485
  }
420
486
  var normalizedBundle = (product.product_bundle || []).map(function (item) {
421
- var _ref2, _item$bundle_selling_, _ref3, _ref4, _item$custom_price;
487
+ var _ref3, _item$bundle_selling_, _ref4, _ref5, _item$custom_price;
422
488
  return _objectSpread(_objectSpread({}, item), {}, {
423
- bundle_selling_price: (_ref2 = (_item$bundle_selling_ = item.bundle_selling_price) !== null && _item$bundle_selling_ !== void 0 ? _item$bundle_selling_ : item.price) !== null && _ref2 !== void 0 ? _ref2 : '0.00',
424
- custom_price: (_ref3 = (_ref4 = (_item$custom_price = item.custom_price) !== null && _item$custom_price !== void 0 ? _item$custom_price : item.bundle_selling_price) !== null && _ref4 !== void 0 ? _ref4 : item.price) !== null && _ref3 !== void 0 ? _ref3 : '0.00'
489
+ bundle_selling_price: (_ref3 = (_item$bundle_selling_ = item.bundle_selling_price) !== null && _item$bundle_selling_ !== void 0 ? _item$bundle_selling_ : item.price) !== null && _ref3 !== void 0 ? _ref3 : '0.00',
490
+ custom_price: (_ref4 = (_ref5 = (_item$custom_price = item.custom_price) !== null && _item$custom_price !== void 0 ? _item$custom_price : item.bundle_selling_price) !== null && _ref5 !== void 0 ? _ref5 : item.price) !== null && _ref4 !== void 0 ? _ref4 : '0.00'
425
491
  });
426
492
  });
427
493
  return {
@@ -429,7 +495,7 @@ export function normalizeOrderProduct(product) {
429
495
  product_id: product.product_id,
430
496
  num: getSafeProductNum(product.num),
431
497
  product_variant_id: product.product_variant_id,
432
- identity_key: product.identity_key,
498
+ identity_key: resolvedIdentityKey,
433
499
  product_option_item: product.product_option_item || [],
434
500
  selling_price: resolvedSellingPrice,
435
501
  original_price: resolvedOriginalPrice,
@@ -438,6 +504,7 @@ export function normalizeOrderProduct(product) {
438
504
  discount_list: product.discount_list || [],
439
505
  product_bundle: normalizedBundle,
440
506
  metadata: metadata,
507
+ note: product.note != null ? String(product.note) : '',
441
508
  _origin: product._origin
442
509
  };
443
510
  }
@@ -2,6 +2,7 @@ import { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import { BaseModule } from '../../modules/BaseModule';
3
3
  import { VenueBookingAddLogParams, VenueBookingSlotConfig, VenueDateSummaryItem, VenueSlotSelection, VenueTimeSlotGrid } from './types';
4
4
  import type { ScanOrderOrderProduct, ScanOrderOrderProductIdentity } from '../ScanOrder/types';
5
+ import type { UpdateProductInOrderParams } from '../../modules/Order/types';
5
6
  import type { OpenDataAvailabilityResult } from '../../modules/OpenData';
6
7
  import type { ProductData } from '../../modules/Product/types';
7
8
  import { type CartItemSummary, type PaxInfo, type QuantityCheckResult, type QuantityLimitResult } from '../../model/strategy/adapter/itemRule';
@@ -165,11 +166,7 @@ export declare class VenueBookingImpl extends BaseModule implements Module {
165
166
  getSummary(): Promise<import("./types").ScanOrderSummary>;
166
167
  submitOrder<T = any>(): Promise<T>;
167
168
  addProductToOrder(product: Partial<ScanOrderOrderProduct> & ScanOrderOrderProductIdentity): Promise<ScanOrderOrderProduct[]>;
168
- updateProductInOrder(params: {
169
- product_id: number | null;
170
- product_variant_id: number;
171
- updates: Partial<ScanOrderOrderProduct>;
172
- }): Promise<ScanOrderOrderProduct[]>;
169
+ updateProductInOrder(params: UpdateProductInOrderParams): Promise<ScanOrderOrderProduct[]>;
173
170
  removeProductFromOrder(identity: ScanOrderOrderProductIdentity): Promise<ScanOrderOrderProduct[]>;
174
171
  getProductList(): Promise<ProductData[]>;
175
172
  private loadOpenDataConfig;
@@ -0,0 +1,49 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ // If the importer is in node compatibility mode or this is not an ESM
21
+ // file that has been converted to a CommonJS file using a Babel-
22
+ // compatible transform (i.e. "__esModule" has not been set), then set
23
+ // "default" to the CommonJS "module.exports" for node compatibility.
24
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
25
+ mod
26
+ ));
27
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
+
29
+ // src/model/strategy/adapter/promotion/index.ts
30
+ var promotion_exports = {};
31
+ __export(promotion_exports, {
32
+ BUY_X_GET_Y_FREE_STRATEGY: () => import_examples.BUY_X_GET_Y_FREE_STRATEGY,
33
+ PromotionAdapter: () => import_adapter.PromotionAdapter,
34
+ PromotionEvaluator: () => import_evaluator.PromotionEvaluator,
35
+ X_ITEMS_FOR_Y_PRICE_STRATEGY: () => import_examples.X_ITEMS_FOR_Y_PRICE_STRATEGY,
36
+ default: () => import_adapter2.default
37
+ });
38
+ module.exports = __toCommonJS(promotion_exports);
39
+ var import_evaluator = require("./evaluator");
40
+ var import_adapter = require("./adapter");
41
+ var import_adapter2 = __toESM(require("./adapter"));
42
+ var import_examples = require("./examples");
43
+ // Annotate the CommonJS export names for ESM import in node:
44
+ 0 && (module.exports = {
45
+ BUY_X_GET_Y_FREE_STRATEGY,
46
+ PromotionAdapter,
47
+ PromotionEvaluator,
48
+ X_ITEMS_FOR_Y_PRICE_STRATEGY
49
+ });
@@ -1,6 +1,6 @@
1
1
  import { Module, PisellCore, ModuleOptions } from '../../types';
2
2
  import { BaseModule } from '../BaseModule';
3
- import { OrderModuleAPI, CommitOrderParams, SubmitScanOrderParams, ScanOrderMoreParams, CheckoutOrderParams } from './types';
3
+ import { OrderModuleAPI, CommitOrderParams, SubmitScanOrderParams, ScanOrderMoreParams, CheckoutOrderParams, UpdateProductInOrderParams } from './types';
4
4
  import { CartItem } from '../Cart/types';
5
5
  import { type SubmitPayloadEnhancer } from './utils';
6
6
  import type { ScanOrderOrderProduct, ScanOrderOrderProductIdentity, ScanOrderSummary, ScanOrderTempOrder } from '../../solution/ScanOrder/types';
@@ -71,11 +71,7 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
71
71
  updateTempOrderBuzzer(buzzer: string): string;
72
72
  updateTempOrderContactsInfo(contactsInfo: any[]): any[];
73
73
  addProductToOrder(product: Partial<ScanOrderOrderProduct> & ScanOrderOrderProductIdentity): Promise<ScanOrderOrderProduct[]>;
74
- updateProductInOrder(params: {
75
- product_id: number | null;
76
- product_variant_id: number;
77
- updates: Partial<ScanOrderOrderProduct>;
78
- }): Promise<ScanOrderOrderProduct[]>;
74
+ updateProductInOrder(params: UpdateProductInOrderParams): Promise<ScanOrderOrderProduct[]>;
79
75
  removeProductFromOrder(identity: ScanOrderOrderProductIdentity): Promise<ScanOrderOrderProduct[]>;
80
76
  submitTempOrder<T = any>(params?: {
81
77
  cacheId?: string;
@@ -119,3 +115,4 @@ export declare class OrderModule extends BaseModule implements Module, OrderModu
119
115
  getOrderInfoByRemote(order_id: number): Promise<any>;
120
116
  getLastOrderInfo(): Record<string, any> | undefined;
121
117
  }
118
+ export type { UpdateProductInOrderParams } from './types';
@@ -288,6 +288,29 @@ var OrderModule = class extends import_BaseModule.BaseModule {
288
288
  this.window.localStorage.removeItem(key);
289
289
  return;
290
290
  }
291
+ if (Array.isArray(parsedData.products)) {
292
+ for (const p of parsedData.products) {
293
+ if (!p || typeof p !== "object")
294
+ continue;
295
+ if (!Array.isArray(p.product_option_item)) {
296
+ p.product_option_item = [];
297
+ }
298
+ if (!Array.isArray(p.product_bundle)) {
299
+ p.product_bundle = [];
300
+ }
301
+ const row = p;
302
+ if (typeof row.identity_key !== "string" || row.identity_key.length === 0) {
303
+ const newKey = (0, import_utils.createUuidV4)();
304
+ row.identity_key = newKey;
305
+ if (!row.metadata || typeof row.metadata !== "object") {
306
+ row.metadata = {};
307
+ }
308
+ if (!row.metadata.unique_identification_number) {
309
+ row.metadata.unique_identification_number = newKey;
310
+ }
311
+ }
312
+ }
313
+ }
291
314
  this.store.tempOrder = parsedData;
292
315
  } catch {
293
316
  (_b = (_a = this.window) == null ? void 0 : _a.localStorage) == null ? void 0 : _b.removeItem(key);
@@ -397,21 +420,57 @@ var OrderModule = class extends import_BaseModule.BaseModule {
397
420
  // ─── TempOrder: 商品 CRUD ───
398
421
  async addProductToOrder(product) {
399
422
  const tempOrder = this.ensureTempOrder();
400
- const normalizedProduct = (0, import_utils3.normalizeOrderProduct)(product);
401
- const productIndex = (0, import_utils3.getProductIdentityIndex)(
402
- tempOrder.products,
403
- normalizedProduct
404
- );
405
- if (productIndex === -1) {
406
- tempOrder.products.push(normalizedProduct);
423
+ const hasExplicitIdentityKey = typeof product.identity_key === "string" && product.identity_key.length > 0;
424
+ if (hasExplicitIdentityKey) {
425
+ const normalizedProduct = (0, import_utils3.normalizeOrderProduct)(product);
426
+ const productIndex = (0, import_utils3.getProductIdentityIndex)(
427
+ tempOrder.products,
428
+ normalizedProduct
429
+ );
430
+ if (productIndex === -1) {
431
+ tempOrder.products.push(normalizedProduct);
432
+ } else {
433
+ const targetProduct = tempOrder.products[productIndex];
434
+ tempOrder.products[productIndex] = {
435
+ ...targetProduct,
436
+ ...normalizedProduct,
437
+ num: (0, import_utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num),
438
+ _origin: normalizedProduct._origin || targetProduct._origin
439
+ };
440
+ }
407
441
  } else {
408
- const targetProduct = tempOrder.products[productIndex];
409
- tempOrder.products[productIndex] = {
410
- ...targetProduct,
411
- ...normalizedProduct,
412
- num: (0, import_utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num),
413
- _origin: normalizedProduct._origin || targetProduct._origin
414
- };
442
+ const incomingFingerprint = (0, import_utils3.buildProductLineFingerprint)(
443
+ product.product_option_item,
444
+ product.product_bundle
445
+ );
446
+ const matchedIndex = tempOrder.products.findIndex((item) => {
447
+ if (item.product_id !== product.product_id)
448
+ return false;
449
+ if (item.product_variant_id !== product.product_variant_id)
450
+ return false;
451
+ const existedFingerprint = (0, import_utils3.buildProductLineFingerprint)(
452
+ item.product_option_item,
453
+ item.product_bundle
454
+ );
455
+ return existedFingerprint === incomingFingerprint;
456
+ });
457
+ if (matchedIndex === -1) {
458
+ const normalizedProduct = (0, import_utils3.normalizeOrderProduct)(product);
459
+ tempOrder.products.push(normalizedProduct);
460
+ } else {
461
+ const targetProduct = tempOrder.products[matchedIndex];
462
+ const normalizedProduct = (0, import_utils3.normalizeOrderProduct)({
463
+ ...product,
464
+ identity_key: targetProduct.identity_key
465
+ });
466
+ tempOrder.products[matchedIndex] = {
467
+ ...targetProduct,
468
+ ...normalizedProduct,
469
+ identity_key: targetProduct.identity_key,
470
+ num: (0, import_utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num),
471
+ _origin: normalizedProduct._origin || targetProduct._origin
472
+ };
473
+ }
415
474
  }
416
475
  this.applyDiscount();
417
476
  await this.recalculateSummary({ createIfMissing: true });
@@ -419,12 +478,26 @@ var OrderModule = class extends import_BaseModule.BaseModule {
419
478
  return tempOrder.products;
420
479
  }
421
480
  async updateProductInOrder(params) {
422
- const { product_id, product_variant_id, updates } = params;
481
+ const {
482
+ product_id,
483
+ product_variant_id,
484
+ updates,
485
+ identity_key,
486
+ product_option_item,
487
+ product_bundle
488
+ } = params;
423
489
  const tempOrder = this.ensureTempOrder();
424
- const productIndex = (0, import_utils3.getProductIdentityIndex)(tempOrder.products, {
490
+ const identityLookup = {
425
491
  product_id,
426
492
  product_variant_id
427
- });
493
+ };
494
+ if (identity_key !== void 0)
495
+ identityLookup.identity_key = identity_key;
496
+ if (product_option_item !== void 0)
497
+ identityLookup.product_option_item = product_option_item;
498
+ if (product_bundle !== void 0)
499
+ identityLookup.product_bundle = product_bundle;
500
+ const productIndex = (0, import_utils3.getProductIdentityIndex)(tempOrder.products, identityLookup);
428
501
  if (productIndex === -1) {
429
502
  throw new Error("[Order] 目标商品不存在,无法更新");
430
503
  }
@@ -444,18 +517,9 @@ var OrderModule = class extends import_BaseModule.BaseModule {
444
517
  }
445
518
  async removeProductFromOrder(identity) {
446
519
  const tempOrder = this.ensureTempOrder();
447
- const beforeProducts = tempOrder.products;
448
520
  tempOrder.products = tempOrder.products.filter(
449
521
  (item) => !(0, import_utils3.isIdentityMatch)(item, identity)
450
522
  );
451
- const removedByStrictIdentity = beforeProducts.length - tempOrder.products.length;
452
- if (removedByStrictIdentity === 0 && identity.identity_key) {
453
- tempOrder.products = tempOrder.products.filter((item) => {
454
- const isSameProduct = String(item.product_id) === String(identity.product_id) && String(item.product_variant_id) === String(identity.product_variant_id);
455
- const hasNoIdentityKey = !item.identity_key;
456
- return !(isSameProduct && hasNoIdentityKey);
457
- });
458
- }
459
523
  this.applyDiscount();
460
524
  await this.recalculateSummary({ createIfMissing: true });
461
525
  this.persistTempOrder();
@@ -16,6 +16,15 @@ export interface OrderState {
16
16
  discount: DiscountModule | null;
17
17
  rules: RulesModule | null;
18
18
  }
19
+ /** 更新购物车行:仅传 SKU 时命中同 SKU 第一行;多行同 SKU 须传 identity_key / 选项指纹等 */
20
+ export interface UpdateProductInOrderParams {
21
+ product_id: number | null;
22
+ product_variant_id: number;
23
+ updates: Partial<ScanOrderOrderProduct>;
24
+ identity_key?: string;
25
+ product_option_item?: any[];
26
+ product_bundle?: any[];
27
+ }
19
28
  /**
20
29
  * 订单信息
21
30
  */
@@ -43,6 +52,8 @@ export interface SubmitScanOrderProduct {
43
52
  discount_list: any[];
44
53
  product_bundle: any[];
45
54
  metadata: Record<string, any>;
55
+ /** 商品行备注 */
56
+ note?: string;
46
57
  }
47
58
  export interface SubmitScanOrderSummary {
48
59
  product_quantity: number;
@@ -210,11 +221,7 @@ export interface OrderModuleAPI {
210
221
  updateTempOrderBuzzer: (buzzer: string) => string;
211
222
  updateTempOrderContactsInfo: (contactsInfo: any[]) => any[];
212
223
  addProductToOrder: (product: Partial<ScanOrderOrderProduct> & ScanOrderOrderProductIdentity) => Promise<ScanOrderOrderProduct[]>;
213
- updateProductInOrder: (params: {
214
- product_id: number | null;
215
- product_variant_id: number;
216
- updates: Partial<ScanOrderOrderProduct>;
217
- }) => Promise<ScanOrderOrderProduct[]>;
224
+ updateProductInOrder: (params: UpdateProductInOrderParams) => Promise<ScanOrderOrderProduct[]>;
218
225
  removeProductFromOrder: (identity: ScanOrderOrderProductIdentity) => Promise<ScanOrderOrderProduct[]>;
219
226
  persistTempOrder: () => void;
220
227
  submitTempOrder: <T = any>(params?: {
@@ -81,6 +81,7 @@ export declare function formatV1Product(products: ScanOrderSubmitProduct[]): {
81
81
  product_id: number | null;
82
82
  product_variant_id: number;
83
83
  num: number;
84
+ note: string;
84
85
  rowKey: number | null;
85
86
  session: null;
86
87
  unique: string;
@@ -60,7 +60,14 @@ function createDefaultOrderRulesHooks() {
60
60
  var _a, _b, _c, _d;
61
61
  return {
62
62
  id: product.product_id,
63
- _id: product.identity_key ? `${product.product_id}_${product.product_variant_id}_${product.identity_key}` : `${product.product_id}_${product.product_variant_id}`,
63
+ // Rules 引擎用 _id 作为 processedProductsMap 键;必须与购物车行级 identity 一致,
64
+ // 否则同 SKU 不同小料会在 calcDiscount 重组时互相覆盖。
65
+ // 不透明 identity 契约下,normalizeOrderProduct / restoreTempOrderFromStorage 已保证 identity_key 必存在;
66
+ // 留 fingerprint 分支仅作 Rules 单测里直接传裸 product 时的兜底。
67
+ _id: product.identity_key ? `${product.product_id}_${product.product_variant_id}_${product.identity_key}` : `${product.product_id}_${product.product_variant_id}_${(0, import_utils.buildProductLineFingerprint)(
68
+ product.product_option_item,
69
+ product.product_bundle
70
+ )}`,
64
71
  price: product.selling_price,
65
72
  total: new import_decimal.default(product.selling_price || 0).times(product.num || 1).toNumber(),
66
73
  origin_total: new import_decimal.default(product.original_price || product.selling_price || 0).times(product.num || 1).toNumber(),
@@ -152,6 +159,23 @@ function formatDateTime(date) {
152
159
  function normalizeSubmitPlatform(platform) {
153
160
  return (platform == null ? void 0 : platform.toLowerCase()) === "pc" ? "PC" : "H5";
154
161
  }
162
+ function formatSubmitOptionItems(options) {
163
+ if (!Array.isArray(options))
164
+ return [];
165
+ return options.map((d) => ({
166
+ num: d == null ? void 0 : d.num,
167
+ option_group_item_id: (d == null ? void 0 : d.option_group_item_id) ?? (d == null ? void 0 : d.product_option_item_id),
168
+ option_group_id: d == null ? void 0 : d.option_group_id
169
+ }));
170
+ }
171
+ function formatSubmitBundleItems(bundle) {
172
+ if (!Array.isArray(bundle))
173
+ return [];
174
+ return bundle.map((b) => ({
175
+ ...b,
176
+ option: formatSubmitOptionItems(b == null ? void 0 : b.option)
177
+ }));
178
+ }
155
179
  function normalizeSubmitProduct(product) {
156
180
  const { _origin, identity_key, ...submitProduct } = product;
157
181
  const rawMetadata = submitProduct.metadata || {};
@@ -179,9 +203,9 @@ function normalizeSubmitProduct(product) {
179
203
  return {
180
204
  ...submitProduct,
181
205
  ...bookingUid ? { booking_uid: bookingUid } : {},
182
- product_option_item: submitProduct.product_option_item || [],
206
+ product_option_item: formatSubmitOptionItems(submitProduct.product_option_item),
183
207
  discount_list: submitProduct.discount_list || [],
184
- product_bundle: submitProduct.product_bundle || [],
208
+ product_bundle: formatSubmitBundleItems(submitProduct.product_bundle),
185
209
  metadata: cleanMetadata,
186
210
  // 出站兼容:后端仍消费 payment_price 字段,从 selling_price(券后单价)派生。
187
211
  payment_price: submitProduct.selling_price
@@ -382,6 +406,7 @@ function formatV1Product(products) {
382
406
  product_id: product.product_id,
383
407
  product_variant_id: product.product_variant_id,
384
408
  num: product.num,
409
+ note: String(product.note ?? ""),
385
410
  rowKey: product.product_id,
386
411
  session: null,
387
412
  unique: createUuidV4()
@@ -1,6 +1,7 @@
1
1
  import { Module, ModuleOptions, PisellCore } from '../../types';
2
2
  import { BaseModule } from '../../modules/BaseModule';
3
3
  import { ScanOrderAddLogParams, ScanOrderAvailabilityInfo, ScanOrderOrderProduct, ScanOrderOrderProductIdentity, ScanOrderScanCodeResult } from './types';
4
+ import type { UpdateProductInOrderParams } from '../../modules/Order/types';
4
5
  import type { Discount } from '../../modules/Discount/types';
5
6
  import { type CartItemSummary, type PaxInfo, type QuantityCheckResult, type QuantityLimitResult } from '../../model/strategy/adapter/itemRule';
6
7
  import type { StrategyConfig } from '../../model/strategy/type';
@@ -85,11 +86,12 @@ export declare class ScanOrderImpl extends BaseModule implements Module {
85
86
  private buildSubmitPayloadEnhancer;
86
87
  submitScanOrder<T = any>(): Promise<T>;
87
88
  addProductToOrder(product: Partial<ScanOrderOrderProduct> & ScanOrderOrderProductIdentity): Promise<ScanOrderOrderProduct[]>;
88
- updateProductInOrder(params: {
89
- product_id: number | null;
90
- product_variant_id: number;
91
- updates: Partial<ScanOrderOrderProduct>;
92
- }): Promise<ScanOrderOrderProduct[]>;
89
+ updateProductInOrder(params: UpdateProductInOrderParams): Promise<ScanOrderOrderProduct[]>;
90
+ /**
91
+ * 设置单行商品备注(与整单 `updateTempOrderNote` 区分)。
92
+ * 多行同 SKU 时必须传入与删除/更新一致的 identity(如 `identity_key`)。
93
+ */
94
+ setOrderProductLineNote(identity: ScanOrderOrderProductIdentity, note: string): Promise<ScanOrderOrderProduct[]>;
93
95
  removeProductFromOrder(identity: ScanOrderOrderProductIdentity): Promise<ScanOrderOrderProduct[]>;
94
96
  private loadRuntimeConfigs;
95
97
  private syncItemRuleConfigsFromDineInConfig;
@@ -892,6 +892,36 @@ var _ScanOrderImpl = class extends import_BaseModule.BaseModule {
892
892
  throw error;
893
893
  }
894
894
  }
895
+ /**
896
+ * 设置单行商品备注(与整单 `updateTempOrderNote` 区分)。
897
+ * 多行同 SKU 时必须传入与删除/更新一致的 identity(如 `identity_key`)。
898
+ */
899
+ async setOrderProductLineNote(identity, note) {
900
+ this.logMethodStart("setOrderProductLineNote", {
901
+ product_id: identity.product_id,
902
+ product_variant_id: identity.product_variant_id
903
+ });
904
+ try {
905
+ const params = {
906
+ product_id: identity.product_id,
907
+ product_variant_id: identity.product_variant_id,
908
+ updates: { note: String(note || "") }
909
+ };
910
+ if (identity.identity_key !== void 0)
911
+ params.identity_key = identity.identity_key;
912
+ if (identity.product_option_item !== void 0) {
913
+ params.product_option_item = identity.product_option_item;
914
+ }
915
+ if (identity.product_bundle !== void 0)
916
+ params.product_bundle = identity.product_bundle;
917
+ const products = await this.updateProductInOrder(params);
918
+ this.logMethodSuccess("setOrderProductLineNote", { productCount: products.length });
919
+ return products;
920
+ } catch (error) {
921
+ this.logMethodError("setOrderProductLineNote", error);
922
+ throw error;
923
+ }
924
+ }
895
925
  async removeProductFromOrder(identity) {
896
926
  this.logMethodStart("removeProductFromOrder", {
897
927
  product_id: identity.product_id,