@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,
@@ -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
  /**
@@ -33,6 +33,7 @@ __export(utils_exports, {
33
33
  attachItemRuleLimitsToTopLevelProducts: () => attachItemRuleLimitsToTopLevelProducts,
34
34
  buildItemRuleBusinessData: () => buildItemRuleBusinessData,
35
35
  buildProductKey: () => buildProductKey,
36
+ buildProductLineFingerprint: () => buildProductLineFingerprint,
36
37
  buildQuantityLimitIndex: () => buildQuantityLimitIndex,
37
38
  collectLinkProductIdsFromReservationRules: () => collectLinkProductIdsFromReservationRules,
38
39
  computeResourceIsFull: () => computeResourceIsFull,
@@ -44,6 +45,7 @@ __export(utils_exports, {
44
45
  getTopLevelVariantId: () => getTopLevelVariantId,
45
46
  hasCustomCapacityProduct: () => hasCustomCapacityProduct,
46
47
  isIdentityMatch: () => isIdentityMatch,
48
+ isSkuOnlyDeleteIdentity: () => isSkuOnlyDeleteIdentity,
47
49
  normalizeEnabledItemRuleIds: () => normalizeEnabledItemRuleIds,
48
50
  normalizeItemRuleStrategies: () => normalizeItemRuleStrategies,
49
51
  normalizeOrderProduct: () => normalizeOrderProduct,
@@ -59,6 +61,7 @@ __export(utils_exports, {
59
61
  });
60
62
  module.exports = __toCommonJS(utils_exports);
61
63
  var import_dayjs = __toESM(require("dayjs"));
64
+ var import_utils = require("../../modules/Order/utils");
62
65
  function createEmptySummary() {
63
66
  return {
64
67
  product_quantity: 0,
@@ -339,12 +342,58 @@ function attachItemRuleLimitsToTopLevelProducts(productList, limits) {
339
342
  }
340
343
  return productList;
341
344
  }
345
+ function buildProductLineFingerprint(productOptionItem, productBundle) {
346
+ const optArr = Array.isArray(productOptionItem) ? productOptionItem : [];
347
+ const normalizedOpts = optArr.map((item) => ({
348
+ product_option_item_id: Number(item == null ? void 0 : item.product_option_item_id) || 0,
349
+ option_group_id: Number(item == null ? void 0 : item.option_group_id) || 0,
350
+ num: typeof (item == null ? void 0 : item.num) === "number" && !Number.isNaN(item.num) ? Math.max(1, Math.floor(item.num)) : 1,
351
+ price: String((item == null ? void 0 : item.price) ?? "")
352
+ })).sort((p, q) => {
353
+ if (p.product_option_item_id !== q.product_option_item_id) {
354
+ return p.product_option_item_id - q.product_option_item_id;
355
+ }
356
+ if (p.option_group_id !== q.option_group_id)
357
+ return p.option_group_id - q.option_group_id;
358
+ if (p.num !== q.num)
359
+ return p.num - q.num;
360
+ return p.price.localeCompare(q.price);
361
+ });
362
+ const bundleArr = Array.isArray(productBundle) ? productBundle : [];
363
+ const normalizedBundles = bundleArr.map((item) => ({
364
+ bundle_id: Number((item == null ? void 0 : item.bundle_id) ?? (item == null ? void 0 : item.id) ?? 0) || 0,
365
+ product_id: Number(item == null ? void 0 : item.product_id) || 0,
366
+ num: typeof (item == null ? void 0 : item.num) === "number" && !Number.isNaN(item.num) ? Math.max(1, Math.floor(item.num)) : 1
367
+ })).sort((p, q) => {
368
+ if (p.bundle_id !== q.bundle_id)
369
+ return p.bundle_id - q.bundle_id;
370
+ if (p.product_id !== q.product_id)
371
+ return p.product_id - q.product_id;
372
+ return p.num - q.num;
373
+ });
374
+ return JSON.stringify([normalizedOpts, normalizedBundles]);
375
+ }
376
+ function isSkuOnlyDeleteIdentity(x) {
377
+ if (x.identity_key)
378
+ return false;
379
+ return !("product_option_item" in x) && !("product_bundle" in x);
380
+ }
381
+ function fingerprintForIdentityWithOptionKeys(x) {
382
+ const row = x;
383
+ const opts = "product_option_item" in row ? Array.isArray(row.product_option_item) ? row.product_option_item : [] : [];
384
+ const buds = "product_bundle" in row ? Array.isArray(row.product_bundle) ? row.product_bundle : [] : [];
385
+ return buildProductLineFingerprint(opts, buds);
386
+ }
342
387
  function isIdentityMatch(a, b) {
343
388
  if (a.product_id !== b.product_id || a.product_variant_id !== b.product_variant_id)
344
389
  return false;
345
- if (!a.identity_key && !b.identity_key)
390
+ const aHasKey = typeof a.identity_key === "string" && a.identity_key.length > 0;
391
+ const bHasKey = typeof b.identity_key === "string" && b.identity_key.length > 0;
392
+ if (aHasKey && bHasKey)
393
+ return a.identity_key === b.identity_key;
394
+ if (isSkuOnlyDeleteIdentity(a) || isSkuOnlyDeleteIdentity(b))
346
395
  return true;
347
- return a.identity_key === b.identity_key;
396
+ return fingerprintForIdentityWithOptionKeys(a) === fingerprintForIdentityWithOptionKeys(b);
348
397
  }
349
398
  function getProductIdentityIndex(products, identity) {
350
399
  return products.findIndex((item) => isIdentityMatch(item, identity));
@@ -352,8 +401,9 @@ function getProductIdentityIndex(products, identity) {
352
401
  function normalizeOrderProduct(product) {
353
402
  var _a, _b;
354
403
  const metadata = { ...product.metadata || {} };
355
- if (product.identity_key && !metadata.unique_identification_number) {
356
- metadata.unique_identification_number = product.identity_key;
404
+ const resolvedIdentityKey = product.identity_key && String(product.identity_key).length > 0 ? String(product.identity_key) : (0, import_utils.createUuidV4)();
405
+ if (!metadata.unique_identification_number) {
406
+ metadata.unique_identification_number = resolvedIdentityKey;
357
407
  }
358
408
  const resolvedSellingPrice = product.selling_price || "0.00";
359
409
  const resolvedOriginalPrice = product.original_price || resolvedSellingPrice;
@@ -376,7 +426,7 @@ function normalizeOrderProduct(product) {
376
426
  product_id: product.product_id,
377
427
  num: getSafeProductNum(product.num),
378
428
  product_variant_id: product.product_variant_id,
379
- identity_key: product.identity_key,
429
+ identity_key: resolvedIdentityKey,
380
430
  product_option_item: product.product_option_item || [],
381
431
  selling_price: resolvedSellingPrice,
382
432
  original_price: resolvedOriginalPrice,
@@ -385,6 +435,7 @@ function normalizeOrderProduct(product) {
385
435
  discount_list: product.discount_list || [],
386
436
  product_bundle: normalizedBundle,
387
437
  metadata,
438
+ note: product.note != null ? String(product.note) : "",
388
439
  _origin: product._origin
389
440
  };
390
441
  }
@@ -505,6 +556,7 @@ function pickFirstCustomCapacityDimensionId(products) {
505
556
  attachItemRuleLimitsToTopLevelProducts,
506
557
  buildItemRuleBusinessData,
507
558
  buildProductKey,
559
+ buildProductLineFingerprint,
508
560
  buildQuantityLimitIndex,
509
561
  collectLinkProductIdsFromReservationRules,
510
562
  computeResourceIsFull,
@@ -516,6 +568,7 @@ function pickFirstCustomCapacityDimensionId(products) {
516
568
  getTopLevelVariantId,
517
569
  hasCustomCapacityProduct,
518
570
  isIdentityMatch,
571
+ isSkuOnlyDeleteIdentity,
519
572
  normalizeEnabledItemRuleIds,
520
573
  normalizeItemRuleStrategies,
521
574
  normalizeOrderProduct,
@@ -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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "private": false,
3
3
  "name": "@pisell/pisellos",
4
- "version": "0.0.509",
4
+ "version": "0.0.511",
5
5
  "description": "一个可扩展的前端模块化SDK框架,支持插件系统",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",