@pisell/pisellos 2.1.130 → 2.1.131

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 (36) hide show
  1. package/dist/model/strategy/adapter/promotion/index.js +9 -0
  2. package/dist/modules/Order/index.d.ts +3 -6
  3. package/dist/modules/Order/index.js +119 -41
  4. package/dist/modules/Order/types.d.ts +23 -5
  5. package/dist/modules/Order/types.js +2 -0
  6. package/dist/modules/Order/utils.d.ts +66 -11
  7. package/dist/modules/Order/utils.js +281 -45
  8. package/dist/modules/SalesSummary/utils.js +33 -68
  9. package/dist/modules/ScanOrderLogger/providers/feishu.js +168 -60
  10. package/dist/modules/ScanOrderLogger/types.d.ts +6 -0
  11. package/dist/modules/Summary/utils.js +6 -21
  12. package/dist/solution/ScanOrder/index.d.ts +31 -6
  13. package/dist/solution/ScanOrder/index.js +1062 -498
  14. package/dist/solution/ScanOrder/types.d.ts +52 -2
  15. package/dist/solution/ScanOrder/types.js +16 -1
  16. package/dist/solution/ScanOrder/utils.d.ts +41 -5
  17. package/dist/solution/ScanOrder/utils.js +214 -33
  18. package/dist/solution/VenueBooking/index.d.ts +2 -5
  19. package/dist/solution/VenueBooking/index.js +35 -27
  20. package/lib/modules/Order/index.d.ts +3 -6
  21. package/lib/modules/Order/index.js +109 -30
  22. package/lib/modules/Order/types.d.ts +23 -5
  23. package/lib/modules/Order/utils.d.ts +66 -11
  24. package/lib/modules/Order/utils.js +181 -16
  25. package/lib/modules/SalesSummary/utils.js +13 -47
  26. package/lib/modules/ScanOrderLogger/providers/feishu.js +100 -34
  27. package/lib/modules/ScanOrderLogger/types.d.ts +6 -0
  28. package/lib/modules/Summary/utils.js +4 -18
  29. package/lib/solution/ScanOrder/index.d.ts +31 -6
  30. package/lib/solution/ScanOrder/index.js +315 -14
  31. package/lib/solution/ScanOrder/types.d.ts +52 -2
  32. package/lib/solution/ScanOrder/utils.d.ts +41 -5
  33. package/lib/solution/ScanOrder/utils.js +150 -20
  34. package/lib/solution/VenueBooking/index.d.ts +2 -5
  35. package/lib/solution/VenueBooking/index.js +13 -6
  36. package/package.json +1 -1
@@ -31,26 +31,70 @@ 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;
37
40
  num: number;
38
41
  product_option_item: any[];
39
- /** 券后单品单价(= 订单域实际成交单价)。未应用券时等同 original_price。 */
42
+ /**
43
+ * 券后 **行 composite 单价** = `metadata.main_product_selling_price`
44
+ * + Σ((bundle.bundle_selling_price ?? bundle.price) × (bundle.num ?? 1))
45
+ *
46
+ * 新语义 v2 下 `metadata.main_product_selling_price` 已经**含 option**、含主商品折扣,
47
+ * 因此本字段不再叠加 option。未应用券时等同 `original_price`。
48
+ * Rules 钩子、主商品计税、Summary 统一读 metadata,不以此字段反推。
49
+ */
40
50
  selling_price: string;
41
- /** 券前单品单价(= 店铺售价),不承载后台划线价语义。 */
51
+ /**
52
+ * 券前 **行 composite 单价** = `metadata.main_product_original_price`
53
+ * + Σ(bundle 原价 × num)
54
+ *
55
+ * 新语义 v2 下 `metadata.main_product_original_price` 已经**含 option**、不含折扣。
56
+ * 不承载后台划线价语义。
57
+ */
42
58
  original_price: string;
43
59
  tax_fee: string;
44
60
  is_charge_tax: number;
45
61
  discount_list: any[];
46
62
  product_bundle: any[];
63
+ /**
64
+ * 行级扩展 metadata。价格相关的权威字段:
65
+ *
66
+ * - `source_product_price`:主商品/variant 基础价(已应用报价单),**不含 option、不含折扣**。
67
+ * 是派生 `main_product_*` 的唯一源。variant 分支优先读 `metadata.origin.variant[vid].price`。
68
+ * - `main_product_original_price` = `source_product_price + Σ(option.price × option.num)`,
69
+ * **含 option、不含折扣**。
70
+ * - `main_product_selling_price` = `main_product_original_price − 主商品券 per-unit amount`,
71
+ * **含 option、含主商品折扣**。Rules 钩子 / Summary / 计税均以此为主商品权威源。
72
+ * - `price_schema_version`(当前 = 2):schema 版本 sentinel,用于跨端协商价格口径、
73
+ * 区分 v1 旧缓存以便 `normalizeOrderProduct` 触发迁移。
74
+ */
47
75
  metadata: Record<string, any>;
76
+ /** 商品行备注(如顾客对单品的特殊要求) */
77
+ note?: string;
48
78
  _origin?: Record<string, any>;
49
79
  }
80
+ /**
81
+ * 出站 payload 版本的商品行。
82
+ *
83
+ * 与 tempOrder 内部 `ScanOrderOrderProduct` 的字段差异:
84
+ * - `product_option_item[*]`:内部为 `{ product_option_item_id, option_group_id, num, price, ... }`,
85
+ * 出站由 `normalizeSubmitProduct` 重命名为 `{ option_group_item_id, option_group_id, num }`
86
+ * (后端 checkout 协议;丢弃 `price` 等运行时辅助字段)。
87
+ * - `product_bundle[*].option[*]`:同上重命名规则。
88
+ *
89
+ * 运行时(UI 显示、加购合并、指纹、持久化)全部消费内部字段名 `product_option_item_id`,
90
+ * 仅在 SDK 提交边界做一次出站映射,不影响 opaque identity 契约。
91
+ */
50
92
  export interface ScanOrderSubmitProduct extends Omit<ScanOrderOrderProduct, '_origin' | 'identity_key'> {
51
93
  /**
52
94
  * 出站兼容字段:SDK 内部不再消费 payment_price,
53
95
  * 仅在提交后端时由 selling_price 派生,保持原有后端契约。
96
+ * 新语义 v2 下 selling_price 是 composite(含 option、含 bundle、含主商品折扣),
97
+ * 因此 payment_price 同样是 composite。
54
98
  */
55
99
  payment_price: string;
56
100
  }
@@ -235,3 +279,9 @@ export interface ScanOrderLoggerRuntimeConfig {
235
279
  }
236
280
  export interface ScanOrderAddLogParams extends ScanOrderLogInput {
237
281
  }
282
+ /** ScanOrder.scanCode 对外的轻量结果(不含 discountList,UI 在 resolve 后调 getDiscountList) */
283
+ export interface ScanOrderScanCodeResult {
284
+ isAvailable: boolean;
285
+ type?: 'server' | string;
286
+ unavailableReason?: 'time_limit' | string;
287
+ }
@@ -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 字段
@@ -17,4 +30,6 @@ export var ScanOrderHooks = /*#__PURE__*/function (ScanOrderHooks) {
17
30
 
18
31
  /** `resource_capacity[i].capacity_list[j]` */
19
32
 
20
- /** `/order/resource/occupy-detail` 单条 `occupy_details[i]` */
33
+ /** `/order/resource/occupy-detail` 单条 `occupy_details[i]` */
34
+
35
+ /** ScanOrder.scanCode 对外的轻量结果(不含 discountList,UI 在 resolve 后调 getDiscountList) */
@@ -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
  /**
@@ -93,10 +108,26 @@ export declare function isIdentityMatch(a: ScanOrderOrderProductIdentity, b: Sca
93
108
  */
94
109
  export declare function getProductIdentityIndex(products: ScanOrderOrderProduct[], identity: ScanOrderOrderProductIdentity): number;
95
110
  /**
96
- * 对外部传入的商品对象做归一化:
111
+ * 对外部传入的商品对象做归一化(v2 composite 语义):
97
112
  * - 补全可选字段默认值(未传则使用兜底值,避免后续计算时因 undefined 导致异常)
98
113
  * - 对 num 调用 getSafeProductNum 做安全处理
99
114
  * - 保留 _origin 供后续业务流程(如促销规则)使用
115
+ *
116
+ * 价格字段语义(metadata 权威源 + composite 派生):
117
+ * - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),**不含 option**、
118
+ * **不含折扣**。是推导 main_product_* 的起点。variant 分支优先读 `metadata.origin.variant[vid].price`。
119
+ * - `metadata.main_product_original_price`:`source + Σ(option.price × option.num)`,**含 option**、
120
+ * **不含折扣**。
121
+ * - `metadata.main_product_selling_price`:`main_product_original_price - 主商品券 per-unit amount`,
122
+ * **含 option**、**含折扣**。
123
+ * - 行级 `selling_price` = `main_product_selling_price + Σ(bundle_selling_price × num)`。
124
+ * - 行级 `original_price` = `main_product_original_price + Σ(bundle 原价 × num)`。
125
+ *
126
+ * 迁移与幂等:
127
+ * - `metadata.price_schema_version === 2` → 已新语义归一化,保留 main_product_* 原值(保留折扣)。
128
+ * - 其它情况(v1 / 缺字段 / 无 metadata)→ 按"main_product_selling_price 曾是 main-only"的旧约定
129
+ * 反推 legacyDiscount,再以新 source + options 基准重算。最终统一打上 `price_schema_version: 2`。
130
+ * - 因此多次 normalize 不会重复叠加 option/bundle。
100
131
  */
101
132
  export declare function normalizeOrderProduct(product: Partial<ScanOrderOrderProduct> & ScanOrderOrderProductIdentity): ScanOrderOrderProduct;
102
133
  /**
@@ -112,7 +143,7 @@ export declare function hasCustomCapacityProduct(products: ProductData[]): boole
112
143
  /**
113
144
  * 根据预约规则商品的 resource.type 计算桌台是否已被"占满"。
114
145
  * - single:只要有 `lastOrderId` 即视为占用
115
- * - multiple:当前时间落在 `capacity_list[i]` 的 `[start_at, end_at]`(inclusive)内的 pax 之和 > 总容量
146
+ * - multiple:当前时间落在 `capacity_list[i]` 的 `[start_at, end_at]`(inclusive)内的 pax 之和 >= 总容量
116
147
  * - 其他('capacity' / undefined):返回 false,不施加限制
117
148
  */
118
149
  export declare function computeResourceIsFull(params: {
@@ -129,3 +160,8 @@ export declare function pickFirstCustomCapacityPaxBounds(products: ProductData[]
129
160
  min?: number;
130
161
  max?: number;
131
162
  } | undefined;
163
+ /**
164
+ * 第一个 `capacity.type === 'custom'` 的商品,取其 `custom[0].id`(提交 booking metadata.capacity 维度 id)。
165
+ * 无匹配时返回 `undefined`,调用方应回退为 `0`。
166
+ */
167
+ export declare function pickFirstCustomCapacityDimensionId(products: ProductData[]): string | number | undefined;
@@ -12,6 +12,9 @@ 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 Decimal from 'decimal.js';
16
+ import { composeLinePrice, createUuidV4, sumOptionUnitPrice } from "../../modules/Order/utils";
17
+
15
18
  /**
16
19
  * 构建金额全为 0 的空 summary。
17
20
  * 作为尚未计算金额时的兜底默认值,避免下游因 undefined 报错。
@@ -367,14 +370,75 @@ export function attachItemRuleLimitsToTopLevelProducts(productList, limits) {
367
370
  }
368
371
 
369
372
  /**
370
- * 判断两个商品 identity 是否匹配。
371
- * 当双方都没有 identity_key 时回退到 product_id + product_variant_id 比较(向后兼容);
372
- * 一旦有一方携带 identity_key,则必须严格相等。
373
+ * 将选项行 / 套餐行归一成稳定 JSON,用于「同 SKU 是否合并」判断。
374
+ * 数组元素先按主键排序再序列化,避免顺序差异导致误判为不同行。
375
+ */
376
+ export function buildProductLineFingerprint(productOptionItem, productBundle) {
377
+ var optArr = Array.isArray(productOptionItem) ? productOptionItem : [];
378
+ var normalizedOpts = optArr.map(function (item) {
379
+ var _item$price;
380
+ return {
381
+ product_option_item_id: Number(item === null || item === void 0 ? void 0 : item.product_option_item_id) || 0,
382
+ option_group_id: Number(item === null || item === void 0 ? void 0 : item.option_group_id) || 0,
383
+ num: typeof (item === null || item === void 0 ? void 0 : item.num) === 'number' && !Number.isNaN(item.num) ? Math.max(1, Math.floor(item.num)) : 1,
384
+ price: String((_item$price = item === null || item === void 0 ? void 0 : item.price) !== null && _item$price !== void 0 ? _item$price : '')
385
+ };
386
+ }).sort(function (p, q) {
387
+ if (p.product_option_item_id !== q.product_option_item_id) {
388
+ return p.product_option_item_id - q.product_option_item_id;
389
+ }
390
+ if (p.option_group_id !== q.option_group_id) return p.option_group_id - q.option_group_id;
391
+ if (p.num !== q.num) return p.num - q.num;
392
+ return p.price.localeCompare(q.price);
393
+ });
394
+ var bundleArr = Array.isArray(productBundle) ? productBundle : [];
395
+ var normalizedBundles = bundleArr.map(function (item) {
396
+ var _ref, _item$bundle_id;
397
+ return {
398
+ 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,
399
+ product_id: Number(item === null || item === void 0 ? void 0 : item.product_id) || 0,
400
+ num: typeof (item === null || item === void 0 ? void 0 : item.num) === 'number' && !Number.isNaN(item.num) ? Math.max(1, Math.floor(item.num)) : 1
401
+ };
402
+ }).sort(function (p, q) {
403
+ if (p.bundle_id !== q.bundle_id) return p.bundle_id - q.bundle_id;
404
+ if (p.product_id !== q.product_id) return p.product_id - q.product_id;
405
+ return p.num - q.num;
406
+ });
407
+ return JSON.stringify([normalizedOpts, normalizedBundles]);
408
+ }
409
+
410
+ /**
411
+ * removeProductFromOrder 仅传 SKU 时的通配 identity(对象上未声明选项键)。
412
+ * 与显式 `product_option_item: []` 区分:后者只匹配「无选项」行。
413
+ */
414
+ export function isSkuOnlyDeleteIdentity(x) {
415
+ if (x.identity_key) return false;
416
+ return !('product_option_item' in x) && !('product_bundle' in x);
417
+ }
418
+ function fingerprintForIdentityWithOptionKeys(x) {
419
+ var row = x;
420
+ var opts = 'product_option_item' in row ? Array.isArray(row.product_option_item) ? row.product_option_item : [] : [];
421
+ var buds = 'product_bundle' in row ? Array.isArray(row.product_bundle) ? row.product_bundle : [] : [];
422
+ return buildProductLineFingerprint(opts, buds);
423
+ }
424
+
425
+ /**
426
+ * 判断两个商品 identity 是否匹配(不透明 identity 契约)。
427
+ *
428
+ * 调用约定:始终 `isIdentityMatch(line, callerIdentity)`。
429
+ * - 若 SKU(product_id + product_variant_id)不一致 → 不匹配。
430
+ * - **双方都带** `identity_key` → 严格字符串相等比较(不再猜测合成 key / metadata 桥接)。
431
+ * 这是 opaque identity 契约的标准路径:UI 删改时透传 SDK 回灌的 identity_key。
432
+ * - 否则(即至少一侧未声明 identity_key)→ 进入「SKU 通配 / 显式空选项 / 指纹」回退路径,
433
+ * 忽略线侧 identity_key,按内容语义匹配。这给「行已 opaque、调用方却想按 SKU 通配 / 选项指纹删除」留兼容入口。
373
434
  */
374
435
  export function isIdentityMatch(a, b) {
375
436
  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;
437
+ var aHasKey = typeof a.identity_key === 'string' && a.identity_key.length > 0;
438
+ var bHasKey = typeof b.identity_key === 'string' && b.identity_key.length > 0;
439
+ if (aHasKey && bHasKey) return a.identity_key === b.identity_key;
440
+ if (isSkuOnlyDeleteIdentity(a) || isSkuOnlyDeleteIdentity(b)) return true;
441
+ return fingerprintForIdentityWithOptionKeys(a) === fingerprintForIdentityWithOptionKeys(b);
378
442
  }
379
443
 
380
444
  /**
@@ -388,34 +452,35 @@ export function getProductIdentityIndex(products, identity) {
388
452
  }
389
453
 
390
454
  /**
391
- * 对外部传入的商品对象做归一化:
455
+ * 对外部传入的商品对象做归一化(v2 composite 语义):
392
456
  * - 补全可选字段默认值(未传则使用兜底值,避免后续计算时因 undefined 导致异常)
393
457
  * - 对 num 调用 getSafeProductNum 做安全处理
394
458
  * - 保留 _origin 供后续业务流程(如促销规则)使用
459
+ *
460
+ * 价格字段语义(metadata 权威源 + composite 派生):
461
+ * - `metadata.source_product_price`:主商品/variant 基础价(已应用报价单),**不含 option**、
462
+ * **不含折扣**。是推导 main_product_* 的起点。variant 分支优先读 `metadata.origin.variant[vid].price`。
463
+ * - `metadata.main_product_original_price`:`source + Σ(option.price × option.num)`,**含 option**、
464
+ * **不含折扣**。
465
+ * - `metadata.main_product_selling_price`:`main_product_original_price - 主商品券 per-unit amount`,
466
+ * **含 option**、**含折扣**。
467
+ * - 行级 `selling_price` = `main_product_selling_price + Σ(bundle_selling_price × num)`。
468
+ * - 行级 `original_price` = `main_product_original_price + Σ(bundle 原价 × num)`。
469
+ *
470
+ * 迁移与幂等:
471
+ * - `metadata.price_schema_version === 2` → 已新语义归一化,保留 main_product_* 原值(保留折扣)。
472
+ * - 其它情况(v1 / 缺字段 / 无 metadata)→ 按"main_product_selling_price 曾是 main-only"的旧约定
473
+ * 反推 legacyDiscount,再以新 source + options 基准重算。最终统一打上 `price_schema_version: 2`。
474
+ * - 因此多次 normalize 不会重复叠加 option/bundle。
395
475
  */
396
476
  export function normalizeOrderProduct(product) {
397
- var _product$is_charge_ta;
477
+ var _metadata$origin, _variantList$find, _product$is_charge_ta;
398
478
  var metadata = _objectSpread({}, product.metadata || {});
399
- if (product.identity_key && !metadata.unique_identification_number) {
400
- metadata.unique_identification_number = product.identity_key;
401
- }
402
-
403
- // selling_price:券后成交单价;original_price:券前店铺售价。
404
- // 初次加购时两者相等(入口层应保证 caller 传的 original_price == selling_price);
405
- // 券应用后由 Rules 引擎只改动 selling_price,保留 original_price 以便还原 / 展示划线对比。
406
- // 注意:这里 caller 若显式传入了 original_price(例如 Rules 回写或单测场景),要尊重该值,
407
- // 让券后状态能正确通过再次 normalize。
408
- var resolvedSellingPrice = product.selling_price || '0.00';
409
- var resolvedOriginalPrice = product.original_price || resolvedSellingPrice;
410
- if (metadata.main_product_original_price === undefined) {
411
- metadata.main_product_original_price = resolvedOriginalPrice;
412
- }
413
- if (metadata.main_product_selling_price === undefined) {
414
- metadata.main_product_selling_price = resolvedSellingPrice;
415
- }
416
- 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;
479
+ // 不透明 identity 契约:每条订单行必须带 identity_key。
480
+ // 调用方未传时由 SDK 自动生成 UUID,后续 update/remove 只做严格比对,避免猜测合成 key。
481
+ var resolvedIdentityKey = product.identity_key && String(product.identity_key).length > 0 ? String(product.identity_key) : createUuidV4();
482
+ if (!metadata.unique_identification_number) {
483
+ metadata.unique_identification_number = resolvedIdentityKey;
419
484
  }
420
485
  var normalizedBundle = (product.product_bundle || []).map(function (item) {
421
486
  var _ref2, _item$bundle_selling_, _ref3, _ref4, _item$custom_price;
@@ -424,20 +489,104 @@ export function normalizeOrderProduct(product) {
424
489
  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'
425
490
  });
426
491
  });
492
+ var normalizedOptions = product.product_option_item || [];
493
+ var optionSum = sumOptionUnitPrice(normalizedOptions);
494
+
495
+ // 1) 解析 source_product_price。
496
+ // 优先级:
497
+ // 1) metadata.source_product_price(v2 权威)
498
+ // 2) variantPrice(命中 variant_id,从 metadata.origin.variant[vid].price 读)
499
+ // 3) _origin.price / _origin.base_price(后端语义的基础价)
500
+ // 4) v1 兼容:无 v2 标记但存在 metadata.main_product_original_price(v1 main-only,即旧 source)
501
+ // 5) 入参 original_price(v1 addProduct 约定:original_price 为 pre-discount 基础价)
502
+ // 6) 入参 selling_price(最末兜底,新添加路径 caller 只给 selling_price)
503
+ var isV2 = metadata.price_schema_version === 2 || metadata.price_schema_version === '2';
504
+ var variantId = Number(product.product_variant_id || 0);
505
+ var variantList = variantId ? metadata === null || metadata === void 0 || (_metadata$origin = metadata.origin) === null || _metadata$origin === void 0 ? void 0 : _metadata$origin.variant : null;
506
+ var variantPrice = Array.isArray(variantList) ? (_variantList$find = variantList.find(function (v) {
507
+ return Number(v === null || v === void 0 ? void 0 : v.id) === variantId;
508
+ })) === null || _variantList$find === void 0 ? void 0 : _variantList$find.price : undefined;
509
+ var resolvedSource = function (_product$_origin, _product$_origin2, _ref5, _product$original_pri) {
510
+ if (metadata.source_product_price !== undefined) {
511
+ return String(metadata.source_product_price);
512
+ }
513
+ if (variantPrice !== undefined && variantPrice !== null) {
514
+ return String(variantPrice);
515
+ }
516
+ var originPrice = (_product$_origin = product._origin) === null || _product$_origin === void 0 ? void 0 : _product$_origin.price;
517
+ if (originPrice !== undefined && originPrice !== null) {
518
+ return String(originPrice);
519
+ }
520
+ var originBasePrice = (_product$_origin2 = product._origin) === null || _product$_origin2 === void 0 ? void 0 : _product$_origin2.base_price;
521
+ if (originBasePrice !== undefined && originBasePrice !== null) {
522
+ return String(originBasePrice);
523
+ }
524
+ if (!isV2 && metadata.main_product_original_price !== undefined) {
525
+ return String(metadata.main_product_original_price);
526
+ }
527
+ return (_ref5 = (_product$original_pri = product.original_price) !== null && _product$original_pri !== void 0 ? _product$original_pri : product.selling_price) !== null && _ref5 !== void 0 ? _ref5 : '0.00';
528
+ }();
529
+
530
+ // 2) 派生 main_product_original_price(含 option、不含折扣)
531
+ var mainOriginalDec = new Decimal(Number(resolvedSource) || 0).plus(optionSum);
532
+ var mainOriginalStr = mainOriginalDec.toDecimalPlaces(2).toFixed(2);
533
+
534
+ // 3) 派生 main_product_selling_price(含 option、含主商品折扣)
535
+ // - v2 数据:直接沿用 metadata.main_product_selling_price(保留折扣)
536
+ // - v1 metadata:main_product_selling_price 旧语义是 main-only(≈ source-level 折后价),
537
+ // 反推 legacyDiscount = main_original(旧) - main_selling(旧),再用新 main_original - legacyDiscount
538
+ // - v1 addProduct 入参:top-level selling_price < original_price 表示主商品折扣;
539
+ // 用差额作为 legacyDiscount 映射到新 main_selling
540
+ // - 其它:视为无折扣,main_selling = main_original
541
+ var mainSellingDec;
542
+ if (isV2 && metadata.main_product_selling_price != null) {
543
+ mainSellingDec = new Decimal(Number(metadata.main_product_selling_price) || 0);
544
+ } else if (metadata.main_product_selling_price != null && metadata.main_product_original_price != null) {
545
+ var legacyOriginal = new Decimal(Number(metadata.main_product_original_price) || 0);
546
+ var legacySelling = new Decimal(Number(metadata.main_product_selling_price) || 0);
547
+ var legacyDiscount = legacyOriginal.minus(legacySelling);
548
+ mainSellingDec = mainOriginalDec.minus(legacyDiscount);
549
+ } else if (product.original_price != null && product.selling_price != null && new Decimal(Number(product.original_price) || 0).greaterThan(new Decimal(Number(product.selling_price) || 0))) {
550
+ var topOriginal = new Decimal(Number(product.original_price) || 0);
551
+ var topSelling = new Decimal(Number(product.selling_price) || 0);
552
+ var _legacyDiscount = topOriginal.minus(topSelling);
553
+ mainSellingDec = mainOriginalDec.minus(_legacyDiscount);
554
+ } else {
555
+ mainSellingDec = mainOriginalDec;
556
+ }
557
+ var mainSellingStr = mainSellingDec.toDecimalPlaces(2).toFixed(2);
558
+
559
+ // 4) 落盘 metadata:三字段 + schema 版本 sentinel
560
+ metadata.source_product_price = resolvedSource;
561
+ metadata.main_product_original_price = mainOriginalStr;
562
+ metadata.main_product_selling_price = mainSellingStr;
563
+ metadata.price_schema_version = 2;
564
+
565
+ // 5) 合成行级 composite(main 已含 option,本步只叠 bundle)
566
+ var composedSellingPrice = composeLinePrice({
567
+ mainPrice: mainSellingStr,
568
+ bundle: normalizedBundle
569
+ });
570
+ var composedOriginalPrice = composeLinePrice({
571
+ mainPrice: mainOriginalStr,
572
+ bundle: normalizedBundle,
573
+ useOriginalBundle: true
574
+ });
427
575
  return {
428
576
  order_detail_id: product.order_detail_id || null,
429
577
  product_id: product.product_id,
430
578
  num: getSafeProductNum(product.num),
431
579
  product_variant_id: product.product_variant_id,
432
- identity_key: product.identity_key,
433
- product_option_item: product.product_option_item || [],
434
- selling_price: resolvedSellingPrice,
435
- original_price: resolvedOriginalPrice,
580
+ identity_key: resolvedIdentityKey,
581
+ product_option_item: normalizedOptions,
582
+ selling_price: composedSellingPrice,
583
+ original_price: composedOriginalPrice,
436
584
  tax_fee: product.tax_fee || '0.00',
437
585
  is_charge_tax: (_product$is_charge_ta = product.is_charge_tax) !== null && _product$is_charge_ta !== void 0 ? _product$is_charge_ta : 0,
438
586
  discount_list: product.discount_list || [],
439
587
  product_bundle: normalizedBundle,
440
588
  metadata: metadata,
589
+ note: product.note != null ? String(product.note) : '',
441
590
  _origin: product._origin
442
591
  };
443
592
  }
@@ -512,7 +661,7 @@ export function hasCustomCapacityProduct(products) {
512
661
  /**
513
662
  * 根据预约规则商品的 resource.type 计算桌台是否已被"占满"。
514
663
  * - single:只要有 `lastOrderId` 即视为占用
515
- * - multiple:当前时间落在 `capacity_list[i]` 的 `[start_at, end_at]`(inclusive)内的 pax 之和 > 总容量
664
+ * - multiple:当前时间落在 `capacity_list[i]` 的 `[start_at, end_at]`(inclusive)内的 pax 之和 >= 总容量
516
665
  * - 其他('capacity' / undefined):返回 false,不施加限制
517
666
  */
518
667
  export function computeResourceIsFull(params) {
@@ -544,7 +693,7 @@ export function computeResourceIsFull(params) {
544
693
  } finally {
545
694
  _iterator8.f();
546
695
  }
547
- return occupied > totalCapacity;
696
+ return occupied >= totalCapacity;
548
697
  }
549
698
 
550
699
  /**
@@ -580,4 +729,36 @@ export function pickFirstCustomCapacityPaxBounds(products) {
580
729
  _iterator9.f();
581
730
  }
582
731
  return undefined;
732
+ }
733
+
734
+ /**
735
+ * 第一个 `capacity.type === 'custom'` 的商品,取其 `custom[0].id`(提交 booking metadata.capacity 维度 id)。
736
+ * 无匹配时返回 `undefined`,调用方应回退为 `0`。
737
+ */
738
+ export function pickFirstCustomCapacityDimensionId(products) {
739
+ var _iterator10 = _createForOfIteratorHelper(products),
740
+ _step10;
741
+ try {
742
+ for (_iterator10.s(); !(_step10 = _iterator10.n()).done;) {
743
+ var p = _step10.value;
744
+ var cap = p === null || p === void 0 ? void 0 : p.capacity;
745
+ if (!cap || cap.type !== 'custom') continue;
746
+ if (!Array.isArray(cap.custom) || cap.custom.length === 0) continue;
747
+ var row = cap.custom[0];
748
+ if (!row || _typeof(row) !== 'object') continue;
749
+ var id = row.id;
750
+ if (id === null || id === undefined || id === '') continue;
751
+ if (typeof id === 'number' && Number.isFinite(id)) return id;
752
+ if (typeof id === 'string') {
753
+ var trimmed = id.trim();
754
+ if (!trimmed) continue;
755
+ return trimmed;
756
+ }
757
+ }
758
+ } catch (err) {
759
+ _iterator10.e(err);
760
+ } finally {
761
+ _iterator10.f();
762
+ }
763
+ return undefined;
583
764
  }
@@ -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;