@pisell/pisellos 0.0.510 → 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.
@@ -48,8 +48,22 @@ export interface ScanOrderOrderProduct extends ScanOrderOrderProductIdentity {
48
48
  discount_list: any[];
49
49
  product_bundle: any[];
50
50
  metadata: Record<string, any>;
51
+ /** 商品行备注(如顾客对单品的特殊要求) */
52
+ note?: string;
51
53
  _origin?: Record<string, any>;
52
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
+ */
53
67
  export interface ScanOrderSubmitProduct extends Omit<ScanOrderOrderProduct, '_origin' | 'identity_key'> {
54
68
  /**
55
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 字段
@@ -92,16 +92,14 @@ export declare function buildProductLineFingerprint(productOptionItem?: unknown,
92
92
  */
93
93
  export declare function isSkuOnlyDeleteIdentity(x: ScanOrderOrderProductIdentity): boolean;
94
94
  /**
95
- * 与常见 UI 约定一致:仅当存在**一条** `product_option_item` 时,
96
- * `${product_id}_${option_group_id}_${product_option_item_id}_${num}` 可作为删除用 `identity_key`。
97
- * 多小料行请使用加购时的 `identity_key` / `metadata.unique_identification_number` 或按 `product_option_item` 指纹删除。
98
- */
99
- export declare function buildSyntheticSingleOptionIdentityKey(productId: number | null, productOptionItem: unknown): string | undefined;
100
- /**
101
- * 判断两个商品 identity 是否匹配。
102
- * - 任一方带 `identity_key`:两侧都有则比字符串相等;仅一侧有则比「另一侧有效 key 集合」是否含该 key(见 collectLineIdentityKeyCandidates)。
103
- * - 否则若任一方为「仅 SKU」删除通配(未声明 product_option_item / product_bundle 键),只比 SKU。
104
- * - 否则比较选项 + 套餐指纹(同 SKU 且指纹相同才合并数量)。
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 通配 / 选项指纹删除」留兼容入口。
105
103
  */
106
104
  export declare function isIdentityMatch(a: ScanOrderOrderProductIdentity, b: ScanOrderOrderProductIdentity): boolean;
107
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 报错。
@@ -412,43 +414,6 @@ export function isSkuOnlyDeleteIdentity(x) {
412
414
  if (x.identity_key) return false;
413
415
  return !('product_option_item' in x) && !('product_bundle' in x);
414
416
  }
415
-
416
- /**
417
- * 与常见 UI 约定一致:仅当存在**一条** `product_option_item` 时,
418
- * `${product_id}_${option_group_id}_${product_option_item_id}_${num}` 可作为删除用 `identity_key`。
419
- * 多小料行请使用加购时的 `identity_key` / `metadata.unique_identification_number` 或按 `product_option_item` 指纹删除。
420
- */
421
- export function buildSyntheticSingleOptionIdentityKey(productId, productOptionItem) {
422
- if (productId == null || !Number.isFinite(Number(productId))) return undefined;
423
- var opts = Array.isArray(productOptionItem) ? productOptionItem : [];
424
- if (opts.length !== 1) return undefined;
425
- var o = opts[0];
426
- var gid = Number(o === null || o === void 0 ? void 0 : o.option_group_id) || 0;
427
- var oid = Number(o === null || o === void 0 ? void 0 : o.product_option_item_id) || 0;
428
- var rawNum = o === null || o === void 0 ? void 0 : o.num;
429
- var n = typeof rawNum === 'number' && !Number.isNaN(rawNum) ? Math.max(1, Math.floor(rawNum)) : 1;
430
- return "".concat(productId, "_").concat(gid, "_").concat(oid, "_").concat(n);
431
- }
432
- function collectLineIdentityKeyCandidates(x) {
433
- var _row$metadata;
434
- var out = new Set();
435
- var row = x;
436
- if (typeof row.identity_key === 'string' && row.identity_key.length > 0) {
437
- out.add(row.identity_key);
438
- }
439
- var uid = (_row$metadata = row.metadata) === null || _row$metadata === void 0 ? void 0 : _row$metadata.unique_identification_number;
440
- if (typeof uid === 'string' && uid.length > 0) out.add(uid);
441
- var synthetic = buildSyntheticSingleOptionIdentityKey(row.product_id, row.product_option_item);
442
- if (synthetic) out.add(synthetic);
443
- var opts = 'product_option_item' in row && Array.isArray(row.product_option_item) ? row.product_option_item : [];
444
- var bundles = 'product_bundle' in row && Array.isArray(row.product_bundle) ? row.product_bundle : [];
445
- // 无小料且无套餐时,与常见 UI 约定对齐:`${product_id}_${product_variant_id}_0`
446
- if (opts.length === 0 && bundles.length === 0 && row.product_id != null && Number.isFinite(Number(row.product_id))) {
447
- var vid = Number(row.product_variant_id) || 0;
448
- out.add("".concat(row.product_id, "_").concat(vid, "_0"));
449
- }
450
- return out;
451
- }
452
417
  function fingerprintForIdentityWithOptionKeys(x) {
453
418
  var row = x;
454
419
  var opts = 'product_option_item' in row ? Array.isArray(row.product_option_item) ? row.product_option_item : [] : [];
@@ -457,23 +422,20 @@ function fingerprintForIdentityWithOptionKeys(x) {
457
422
  }
458
423
 
459
424
  /**
460
- * 判断两个商品 identity 是否匹配。
461
- * - 任一方带 `identity_key`:两侧都有则比字符串相等;仅一侧有则比「另一侧有效 key 集合」是否含该 key(见 collectLineIdentityKeyCandidates)。
462
- * - 否则若任一方为「仅 SKU」删除通配(未声明 product_option_item / product_bundle 键),只比 SKU。
463
- * - 否则比较选项 + 套餐指纹(同 SKU 且指纹相同才合并数量)。
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 通配 / 选项指纹删除」留兼容入口。
464
433
  */
465
434
  export function isIdentityMatch(a, b) {
466
435
  if (a.product_id !== b.product_id || a.product_variant_id !== b.product_variant_id) return false;
467
- if (a.identity_key || b.identity_key) {
468
- if (a.identity_key && b.identity_key) return a.identity_key === b.identity_key;
469
- if (a.identity_key && !b.identity_key) {
470
- return collectLineIdentityKeyCandidates(b).has(a.identity_key);
471
- }
472
- if (!a.identity_key && b.identity_key) {
473
- return collectLineIdentityKeyCandidates(a).has(b.identity_key);
474
- }
475
- return false;
476
- }
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;
477
439
  if (isSkuOnlyDeleteIdentity(a) || isSkuOnlyDeleteIdentity(b)) return true;
478
440
  return fingerprintForIdentityWithOptionKeys(a) === fingerprintForIdentityWithOptionKeys(b);
479
441
  }
@@ -497,8 +459,11 @@ export function getProductIdentityIndex(products, identity) {
497
459
  export function normalizeOrderProduct(product) {
498
460
  var _product$is_charge_ta;
499
461
  var metadata = _objectSpread({}, product.metadata || {});
500
- if (product.identity_key && !metadata.unique_identification_number) {
501
- 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;
502
467
  }
503
468
 
504
469
  // selling_price:券后成交单价;original_price:券前店铺售价。
@@ -530,7 +495,7 @@ export function normalizeOrderProduct(product) {
530
495
  product_id: product.product_id,
531
496
  num: getSafeProductNum(product.num),
532
497
  product_variant_id: product.product_variant_id,
533
- identity_key: product.identity_key,
498
+ identity_key: resolvedIdentityKey,
534
499
  product_option_item: product.product_option_item || [],
535
500
  selling_price: resolvedSellingPrice,
536
501
  original_price: resolvedOriginalPrice,
@@ -539,6 +504,7 @@ export function normalizeOrderProduct(product) {
539
504
  discount_list: product.discount_list || [],
540
505
  product_bundle: normalizedBundle,
541
506
  metadata: metadata,
507
+ note: product.note != null ? String(product.note) : '',
542
508
  _origin: product._origin
543
509
  };
544
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';
@@ -298,6 +298,17 @@ var OrderModule = class extends import_BaseModule.BaseModule {
298
298
  if (!Array.isArray(p.product_bundle)) {
299
299
  p.product_bundle = [];
300
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
+ }
301
312
  }
302
313
  }
303
314
  this.store.tempOrder = parsedData;
@@ -409,21 +420,57 @@ var OrderModule = class extends import_BaseModule.BaseModule {
409
420
  // ─── TempOrder: 商品 CRUD ───
410
421
  async addProductToOrder(product) {
411
422
  const tempOrder = this.ensureTempOrder();
412
- const normalizedProduct = (0, import_utils3.normalizeOrderProduct)(product);
413
- const productIndex = (0, import_utils3.getProductIdentityIndex)(
414
- tempOrder.products,
415
- normalizedProduct
416
- );
417
- if (productIndex === -1) {
418
- 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
+ }
419
441
  } else {
420
- const targetProduct = tempOrder.products[productIndex];
421
- tempOrder.products[productIndex] = {
422
- ...targetProduct,
423
- ...normalizedProduct,
424
- num: (0, import_utils3.getSafeProductNum)(targetProduct.num + normalizedProduct.num),
425
- _origin: normalizedProduct._origin || targetProduct._origin
426
- };
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
+ }
427
474
  }
428
475
  this.applyDiscount();
429
476
  await this.recalculateSummary({ createIfMissing: true });
@@ -431,12 +478,26 @@ var OrderModule = class extends import_BaseModule.BaseModule {
431
478
  return tempOrder.products;
432
479
  }
433
480
  async updateProductInOrder(params) {
434
- 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;
435
489
  const tempOrder = this.ensureTempOrder();
436
- const productIndex = (0, import_utils3.getProductIdentityIndex)(tempOrder.products, {
490
+ const identityLookup = {
437
491
  product_id,
438
492
  product_variant_id
439
- });
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);
440
501
  if (productIndex === -1) {
441
502
  throw new Error("[Order] 目标商品不存在,无法更新");
442
503
  }
@@ -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;
@@ -62,6 +62,8 @@ function createDefaultOrderRulesHooks() {
62
62
  id: product.product_id,
63
63
  // Rules 引擎用 _id 作为 processedProductsMap 键;必须与购物车行级 identity 一致,
64
64
  // 否则同 SKU 不同小料会在 calcDiscount 重组时互相覆盖。
65
+ // 不透明 identity 契约下,normalizeOrderProduct / restoreTempOrderFromStorage 已保证 identity_key 必存在;
66
+ // 留 fingerprint 分支仅作 Rules 单测里直接传裸 product 时的兜底。
65
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)(
66
68
  product.product_option_item,
67
69
  product.product_bundle
@@ -157,6 +159,23 @@ function formatDateTime(date) {
157
159
  function normalizeSubmitPlatform(platform) {
158
160
  return (platform == null ? void 0 : platform.toLowerCase()) === "pc" ? "PC" : "H5";
159
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
+ }
160
179
  function normalizeSubmitProduct(product) {
161
180
  const { _origin, identity_key, ...submitProduct } = product;
162
181
  const rawMetadata = submitProduct.metadata || {};
@@ -184,9 +203,9 @@ function normalizeSubmitProduct(product) {
184
203
  return {
185
204
  ...submitProduct,
186
205
  ...bookingUid ? { booking_uid: bookingUid } : {},
187
- product_option_item: submitProduct.product_option_item || [],
206
+ product_option_item: formatSubmitOptionItems(submitProduct.product_option_item),
188
207
  discount_list: submitProduct.discount_list || [],
189
- product_bundle: submitProduct.product_bundle || [],
208
+ product_bundle: formatSubmitBundleItems(submitProduct.product_bundle),
190
209
  metadata: cleanMetadata,
191
210
  // 出站兼容:后端仍消费 payment_price 字段,从 selling_price(券后单价)派生。
192
211
  payment_price: submitProduct.selling_price
@@ -387,6 +406,7 @@ function formatV1Product(products) {
387
406
  product_id: product.product_id,
388
407
  product_variant_id: product.product_variant_id,
389
408
  num: product.num,
409
+ note: String(product.note ?? ""),
390
410
  rowKey: product.product_id,
391
411
  session: null,
392
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,
@@ -48,8 +48,22 @@ export interface ScanOrderOrderProduct extends ScanOrderOrderProductIdentity {
48
48
  discount_list: any[];
49
49
  product_bundle: any[];
50
50
  metadata: Record<string, any>;
51
+ /** 商品行备注(如顾客对单品的特殊要求) */
52
+ note?: string;
51
53
  _origin?: Record<string, any>;
52
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
+ */
53
67
  export interface ScanOrderSubmitProduct extends Omit<ScanOrderOrderProduct, '_origin' | 'identity_key'> {
54
68
  /**
55
69
  * 出站兼容字段:SDK 内部不再消费 payment_price,
@@ -92,16 +92,14 @@ export declare function buildProductLineFingerprint(productOptionItem?: unknown,
92
92
  */
93
93
  export declare function isSkuOnlyDeleteIdentity(x: ScanOrderOrderProductIdentity): boolean;
94
94
  /**
95
- * 与常见 UI 约定一致:仅当存在**一条** `product_option_item` 时,
96
- * `${product_id}_${option_group_id}_${product_option_item_id}_${num}` 可作为删除用 `identity_key`。
97
- * 多小料行请使用加购时的 `identity_key` / `metadata.unique_identification_number` 或按 `product_option_item` 指纹删除。
98
- */
99
- export declare function buildSyntheticSingleOptionIdentityKey(productId: number | null, productOptionItem: unknown): string | undefined;
100
- /**
101
- * 判断两个商品 identity 是否匹配。
102
- * - 任一方带 `identity_key`:两侧都有则比字符串相等;仅一侧有则比「另一侧有效 key 集合」是否含该 key(见 collectLineIdentityKeyCandidates)。
103
- * - 否则若任一方为「仅 SKU」删除通配(未声明 product_option_item / product_bundle 键),只比 SKU。
104
- * - 否则比较选项 + 套餐指纹(同 SKU 且指纹相同才合并数量)。
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 通配 / 选项指纹删除」留兼容入口。
105
103
  */
106
104
  export declare function isIdentityMatch(a: ScanOrderOrderProductIdentity, b: ScanOrderOrderProductIdentity): boolean;
107
105
  /**