@pisell/pisellos 2.2.301 → 2.2.303

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 (152) hide show
  1. package/dist/model/strategy/adapter/dataVariant/evaluator.d.ts +18 -2
  2. package/dist/model/strategy/adapter/dataVariant/evaluator.js +287 -101
  3. package/dist/model/strategy/adapter/dataVariant/type.d.ts +25 -0
  4. package/dist/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  5. package/dist/model/strategy/adapter/itemRule/adapter.js +135 -35
  6. package/dist/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  7. package/dist/model/strategy/adapter/itemRule/evaluator.js +25 -1
  8. package/dist/model/strategy/adapter/itemRule/type.d.ts +44 -0
  9. package/dist/model/strategy/adapter/itemRule/type.js +19 -1
  10. package/dist/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +10 -7
  11. package/dist/modules/Discount/index.d.ts +2 -0
  12. package/dist/modules/Discount/index.js +41 -5
  13. package/dist/modules/Discount/types.d.ts +21 -0
  14. package/dist/modules/OpenData/index.d.ts +15 -2
  15. package/dist/modules/OpenData/index.js +307 -19
  16. package/dist/modules/OpenData/types.d.ts +55 -0
  17. package/dist/modules/OpenData/types.js +7 -1
  18. package/dist/modules/OpenData/utils.d.ts +21 -1
  19. package/dist/modules/OpenData/utils.js +138 -0
  20. package/dist/modules/Order/index.d.ts +50 -7
  21. package/dist/modules/Order/index.js +1690 -815
  22. package/dist/modules/Order/salesNotes.d.ts +31 -0
  23. package/dist/modules/Order/salesNotes.js +492 -0
  24. package/dist/modules/Order/types.d.ts +117 -10
  25. package/dist/modules/Order/types.js +6 -1
  26. package/dist/modules/Order/utils/virtualSettlement.d.ts +63 -0
  27. package/dist/modules/Order/utils/virtualSettlement.js +284 -0
  28. package/dist/modules/Order/utils.d.ts +5 -1
  29. package/dist/modules/Order/utils.js +138 -33
  30. package/dist/modules/Payment/walletpass.js +20 -21
  31. package/dist/modules/Product/types.d.ts +23 -0
  32. package/dist/modules/ProductList/index.d.ts +2 -1
  33. package/dist/modules/ProductList/index.js +100 -20
  34. package/dist/modules/ProductList/types.d.ts +10 -0
  35. package/dist/modules/Rules/index.d.ts +5 -0
  36. package/dist/modules/Rules/index.js +30 -14
  37. package/dist/modules/SalesSummary/index.d.ts +1 -0
  38. package/dist/modules/SalesSummary/index.js +4 -3
  39. package/dist/modules/SalesSummary/types.d.ts +1 -0
  40. package/dist/modules/SalesSummary/utils.d.ts +1 -0
  41. package/dist/modules/SalesSummary/utils.js +21 -1
  42. package/dist/plugins/request.d.ts +2 -1
  43. package/dist/plugins/request.js +4 -2
  44. package/dist/server/index.d.ts +21 -0
  45. package/dist/server/index.js +1774 -1285
  46. package/dist/server/modules/order/types.d.ts +50 -0
  47. package/dist/server/modules/order/types.js +2 -0
  48. package/dist/server/utils/small-ticket.d.ts +4 -1
  49. package/dist/server/utils/small-ticket.js +461 -96
  50. package/dist/solution/BaseSales/index.d.ts +26 -3
  51. package/dist/solution/BaseSales/index.js +1492 -873
  52. package/dist/solution/BaseSales/utils/parseSalesResponse.d.ts +2 -0
  53. package/dist/solution/BaseSales/utils/parseSalesResponse.js +1 -1
  54. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -0
  55. package/dist/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +72 -9
  56. package/dist/solution/BookingByStep/index.d.ts +1 -1
  57. package/dist/solution/BookingTicket/index.d.ts +8 -3
  58. package/dist/solution/BookingTicket/index.js +558 -443
  59. package/dist/solution/BookingTicket/utils/addProductDecision.js +2 -1
  60. package/dist/solution/BookingTicket/utils/orderCustomer.js +3 -2
  61. package/dist/solution/RegisterAndLogin/config.d.ts +9 -3
  62. package/dist/solution/RegisterAndLogin/config.js +95 -40
  63. package/dist/solution/RegisterAndLogin/index.js +4 -1
  64. package/dist/solution/Sales/index.d.ts +16 -1
  65. package/dist/solution/Sales/index.js +338 -63
  66. package/dist/solution/Sales/types.d.ts +10 -0
  67. package/dist/solution/ScanOrder/index.d.ts +1 -0
  68. package/dist/solution/ScanOrder/index.js +31 -27
  69. package/dist/solution/ScanOrder/utils.d.ts +1 -0
  70. package/dist/solution/ScanOrder/utils.js +2 -0
  71. package/dist/solution/UnifiedBookingSales/index.d.ts +20 -6
  72. package/dist/solution/UnifiedBookingSales/index.js +874 -527
  73. package/dist/solution/UnifiedBookingSales/types.d.ts +21 -2
  74. package/dist/solution/VenueBooking/index.d.ts +1 -0
  75. package/dist/solution/VenueBooking/index.js +8 -4
  76. package/lib/model/strategy/adapter/dataVariant/evaluator.d.ts +18 -2
  77. package/lib/model/strategy/adapter/dataVariant/evaluator.js +137 -6
  78. package/lib/model/strategy/adapter/dataVariant/type.d.ts +25 -0
  79. package/lib/model/strategy/adapter/itemRule/adapter.d.ts +4 -1
  80. package/lib/model/strategy/adapter/itemRule/adapter.js +73 -4
  81. package/lib/model/strategy/adapter/itemRule/evaluator.d.ts +8 -1
  82. package/lib/model/strategy/adapter/itemRule/evaluator.js +23 -1
  83. package/lib/model/strategy/adapter/itemRule/type.d.ts +44 -0
  84. package/lib/model/strategy/adapter/itemRule/type.js +19 -1
  85. package/lib/model/strategy/adapter/promotion/index.js +1 -46
  86. package/lib/modules/BookingContext/utils/buildCacheItemFromOrderLine.js +4 -0
  87. package/lib/modules/Discount/index.d.ts +2 -0
  88. package/lib/modules/Discount/index.js +34 -2
  89. package/lib/modules/Discount/types.d.ts +21 -0
  90. package/lib/modules/OpenData/index.d.ts +15 -2
  91. package/lib/modules/OpenData/index.js +230 -3
  92. package/lib/modules/OpenData/types.d.ts +55 -0
  93. package/lib/modules/OpenData/types.js +9 -1
  94. package/lib/modules/OpenData/utils.d.ts +21 -1
  95. package/lib/modules/OpenData/utils.js +126 -0
  96. package/lib/modules/Order/index.d.ts +50 -7
  97. package/lib/modules/Order/index.js +704 -64
  98. package/lib/modules/Order/salesNotes.d.ts +31 -0
  99. package/lib/modules/Order/salesNotes.js +382 -0
  100. package/lib/modules/Order/types.d.ts +117 -10
  101. package/lib/modules/Order/types.js +5 -1
  102. package/lib/modules/Order/utils/virtualSettlement.d.ts +63 -0
  103. package/lib/modules/Order/utils/virtualSettlement.js +262 -0
  104. package/lib/modules/Order/utils.d.ts +5 -1
  105. package/lib/modules/Order/utils.js +127 -21
  106. package/lib/modules/Payment/walletpass.js +14 -11
  107. package/lib/modules/Product/types.d.ts +23 -0
  108. package/lib/modules/ProductList/index.d.ts +2 -1
  109. package/lib/modules/ProductList/index.js +47 -2
  110. package/lib/modules/ProductList/types.d.ts +10 -0
  111. package/lib/modules/Rules/index.d.ts +5 -0
  112. package/lib/modules/Rules/index.js +22 -12
  113. package/lib/modules/SalesSummary/index.d.ts +1 -0
  114. package/lib/modules/SalesSummary/index.js +4 -2
  115. package/lib/modules/SalesSummary/types.d.ts +1 -0
  116. package/lib/modules/SalesSummary/utils.d.ts +1 -0
  117. package/lib/modules/SalesSummary/utils.js +17 -1
  118. package/lib/plugins/request.d.ts +2 -1
  119. package/lib/plugins/request.js +3 -2
  120. package/lib/server/index.d.ts +21 -0
  121. package/lib/server/index.js +492 -108
  122. package/lib/server/modules/order/types.d.ts +50 -0
  123. package/lib/server/modules/order/types.js +1 -0
  124. package/lib/server/utils/small-ticket.d.ts +4 -1
  125. package/lib/server/utils/small-ticket.js +427 -72
  126. package/lib/solution/BaseSales/index.d.ts +26 -3
  127. package/lib/solution/BaseSales/index.js +439 -30
  128. package/lib/solution/BaseSales/utils/parseSalesResponse.d.ts +2 -0
  129. package/lib/solution/BaseSales/utils/parseSalesResponse.js +1 -1
  130. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.d.ts +1 -0
  131. package/lib/solution/BaseSales/utils/transformBaseProductToOrderProduct.js +38 -0
  132. package/lib/solution/BookingByStep/index.d.ts +1 -1
  133. package/lib/solution/BookingTicket/index.d.ts +8 -3
  134. package/lib/solution/BookingTicket/index.js +60 -15
  135. package/lib/solution/BookingTicket/utils/addProductDecision.js +1 -0
  136. package/lib/solution/BookingTicket/utils/orderCustomer.js +2 -1
  137. package/lib/solution/RegisterAndLogin/config.d.ts +9 -3
  138. package/lib/solution/RegisterAndLogin/config.js +49 -8
  139. package/lib/solution/RegisterAndLogin/index.js +4 -1
  140. package/lib/solution/Sales/index.d.ts +16 -1
  141. package/lib/solution/Sales/index.js +168 -1
  142. package/lib/solution/Sales/types.d.ts +10 -0
  143. package/lib/solution/ScanOrder/index.d.ts +1 -0
  144. package/lib/solution/ScanOrder/index.js +5 -1
  145. package/lib/solution/ScanOrder/utils.d.ts +1 -0
  146. package/lib/solution/ScanOrder/utils.js +1 -0
  147. package/lib/solution/UnifiedBookingSales/index.d.ts +20 -6
  148. package/lib/solution/UnifiedBookingSales/index.js +169 -16
  149. package/lib/solution/UnifiedBookingSales/types.d.ts +21 -2
  150. package/lib/solution/VenueBooking/index.d.ts +1 -0
  151. package/lib/solution/VenueBooking/index.js +5 -1
  152. package/package.json +1 -1
@@ -1,8 +1,12 @@
1
- import type { OpenDataConfig } from '../../modules/OpenData';
2
- import type { ProductListLoadProductsParams } from '../../modules/ProductList';
1
+ import type { InvoiceLayoutConfig, InvoiceTemplateSettings, OpenDataConfig } from '../../modules/OpenData';
2
+ import type { ProductListLoadProductsParams, ProductListStoreQueryParams } from '../../modules/ProductList';
3
+ import type { ProductData } from '../../modules/Product/types';
3
4
  import type { LoadScheduleAvailableDateParams } from '../../modules/Schedule/types';
4
5
  import type { AvailabilityDateRange, AvailabilityErrorCode, AvailabilityId, AvailabilityOccupancy, AvailabilityProduct, AvailabilityProjection, AvailabilityQuery, AvailabilityRequirementSelection, AvailabilityResource, AvailabilitySelectionValidationResult, DateAvailabilityQueryResult, GetProductTimeRangesRequest, ProductAvailabilityQueryResult, ProductTimeRange, ProductTimeRangeGroup, ResourceAvailabilityGroup, ResourceAvailabilityRangeGroup, TimeAvailabilityGroup, TimeAvailabilityItem } from '../../modules/ResourcePlanner';
5
6
  import type { BookingTicketState } from '../BookingTicket/types';
7
+ import type { AllergyRequirement } from '../../model/strategy/adapter/itemRule';
8
+ import type { SalesNoteDraftInput } from '../../modules/Order/types';
9
+ export type { ProductListStoreQueryParams } from '../../modules/ProductList';
6
10
  export type { AvailabilityAbsoluteRange, AvailabilityAssignment, AvailabilityCell, AvailabilityDateRange, AvailabilityDisplayRange, AvailabilityErrorCode, AvailabilityId, AvailabilityMode, AvailabilityOccupancy, AvailabilityProduct, AvailabilityProductKind, AvailabilityProjection, AvailabilityProjectionInput, AvailabilityProjectionMeta, AvailabilityQuery, AvailabilityQueryResult, AvailabilityRequirementGroup, AvailabilityRequirementGroupResult, AvailabilityRequirementSelection, AvailabilityResource, AvailabilityResourceOption, AvailabilityResourceType, AvailabilityResourceWindow, AvailabilityScheduleMode, AvailabilityScheduleRef, AvailabilityScheduleRule, AvailabilitySelectionValidationParams, AvailabilitySelectionValidationResult, AvailabilitySessionRange, AvailabilityStatus, DateAvailabilityGroup, DateAvailabilityItem, DateAvailabilityQuery, DateAvailabilityQueryResult, GetProductTimeRangesRequest, ProductAvailabilityGroup, ProductAvailabilityItem, ProductAvailabilityQuery, ProductAvailabilityQueryResult, ProductTimeRange, ProductTimeRangeGroup, QueryAvailabilityBaseRequest, ResourceAvailabilityGroup, ResourceAvailabilityQuery, ResourceAvailabilityQueryResult, ResourceAvailabilityRangeGroup, TimeAvailabilityGroup, TimeAvailabilityItem, TimeAvailabilityQuery, TimeAvailabilityQueryResult, } from '../../modules/ResourcePlanner';
7
11
  export interface UnifiedBookingSalesState extends BookingTicketState {
8
12
  }
@@ -24,6 +28,12 @@ export declare enum UnifiedBookingSalesHooks {
24
28
  export interface UnifiedBookingSalesOtherParams extends Record<string, any> {
25
29
  enableSessionStoragePersist?: boolean;
26
30
  }
31
+ export type UnifiedBookingSalesAllergyRequirement = AllergyRequirement;
32
+ export interface UnifiedBookingSalesResolveProductAllergyRequirementParams {
33
+ productId: number | string;
34
+ productVariantId?: number | string;
35
+ channel?: string;
36
+ }
27
37
  export interface UnifiedBookingSalesLoadOpenDataParams {
28
38
  businessCode: string;
29
39
  channel: string;
@@ -151,6 +161,8 @@ export interface ContextualCommitAvailabilitySelectionParams {
151
161
  requirementSelections: AvailabilityRequirementSelection[];
152
162
  partySize: number;
153
163
  bookingCount?: number;
164
+ /** 新增商品行时与商品、预约在同一次 Order mutation 内写入的 Sales Notes。 */
165
+ salesNotes?: SalesNoteDraftInput[];
154
166
  /** 编辑预约时,允许本次显式报价覆盖已落库订单行的历史价格。 */
155
167
  allowBookingReprice?: boolean;
156
168
  /** 编辑预约重报价后,允许订单模块重新应用已选编辑态折扣。 */
@@ -205,9 +217,16 @@ export interface UnifiedBookingSalesDiscountResult {
205
217
  }
206
218
  export interface UnifiedBookingSalesApi {
207
219
  loadOpenData: (params: UnifiedBookingSalesLoadOpenDataParams) => Promise<OpenDataConfig>;
220
+ /** Returns the latest OpenData payload cached by this solution instance. */
208
221
  getOpenData: () => Promise<OpenDataConfig | null>;
222
+ /** Returns the normalized Documents Invoice layout for the active target. */
223
+ getInvoiceLayoutConfig: () => Promise<InvoiceLayoutConfig>;
224
+ /** Returns the Native Invoice template settings for the active target. */
225
+ getInvoiceTemplateSettings: () => Promise<InvoiceTemplateSettings>;
226
+ /** Date-first calendar availability backed by ScheduleModule. */
209
227
  loadScheduleAvailableDate: (params: LoadScheduleAvailableDateParams) => Promise<unknown>;
210
228
  loadProductsByScheduleDate: (params: UnifiedBookingSalesLoadProductsByScheduleDateParams) => Promise<unknown[]>;
229
+ queryProducts: (params?: ProductListStoreQueryParams) => Promise<ProductData[]>;
211
230
  stageRetailProducts: (params: UnifiedBookingSalesStageRetailProductsParams) => Promise<Record<string, any> | null>;
212
231
  addRetailProduct: (params: UnifiedBookingSalesAddRetailProductParams) => Promise<Record<string, any>[]>;
213
232
  prepareAvailabilityProjection: (params: UnifiedBookingSalesPrepareAvailabilityProjectionParams) => Promise<AvailabilityContextRef>;
@@ -15,6 +15,7 @@ interface VenueBookingItemRuleRuntimeConfig {
15
15
  pax?: Partial<PaxInfo>;
16
16
  historicalItems?: CartItemSummary[];
17
17
  serviceType?: string;
18
+ channel?: string;
18
19
  submissionIndex?: number;
19
20
  custom?: Record<string, any>;
20
21
  }
@@ -3140,7 +3140,11 @@ export var VenueBookingImpl = /*#__PURE__*/function (_BaseSalesImpl) {
3140
3140
  }, {
3141
3141
  key: "getItemRuleRuntimeConfig",
3142
3142
  value: function getItemRuleRuntimeConfig() {
3143
- return this.itemRuleRuntimeConfig || {};
3143
+ var _ref6, _runtimeConfig$channe, _runtimeConfig$custom, _this$otherParams21;
3144
+ var runtimeConfig = this.itemRuleRuntimeConfig || {};
3145
+ return _objectSpread(_objectSpread({}, runtimeConfig), {}, {
3146
+ channel: (_ref6 = (_runtimeConfig$channe = runtimeConfig.channel) !== null && _runtimeConfig$channe !== void 0 ? _runtimeConfig$channe : (_runtimeConfig$custom = runtimeConfig.custom) === null || _runtimeConfig$custom === void 0 ? void 0 : _runtimeConfig$custom.channel) !== null && _ref6 !== void 0 ? _ref6 : (_this$otherParams21 = this.otherParams) === null || _this$otherParams21 === void 0 ? void 0 : _this$otherParams21.channel
3147
+ });
3144
3148
  }
3145
3149
  }, {
3146
3150
  key: "ensureItemRuleConfigsLoaded",
@@ -3806,14 +3810,14 @@ export var VenueBookingImpl = /*#__PURE__*/function (_BaseSalesImpl) {
3806
3810
  key: "setOtherParams",
3807
3811
  value: function () {
3808
3812
  var _setOtherParams = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime().mark(function _callee46(params) {
3809
- var _ref7,
3810
- _ref7$cover,
3813
+ var _ref8,
3814
+ _ref8$cover,
3811
3815
  cover,
3812
3816
  _args46 = arguments;
3813
3817
  return _regeneratorRuntime().wrap(function _callee46$(_context46) {
3814
3818
  while (1) switch (_context46.prev = _context46.next) {
3815
3819
  case 0:
3816
- _ref7 = _args46.length > 1 && _args46[1] !== undefined ? _args46[1] : {}, _ref7$cover = _ref7.cover, cover = _ref7$cover === void 0 ? false : _ref7$cover;
3820
+ _ref8 = _args46.length > 1 && _args46[1] !== undefined ? _args46[1] : {}, _ref8$cover = _ref8.cover, cover = _ref8$cover === void 0 ? false : _ref8$cover;
3817
3821
  if (cover) {
3818
3822
  this.otherParams = params;
3819
3823
  } else {
@@ -3,7 +3,7 @@ import type { ConditionScope, DataVariantBusinessData, DataVariantMenu, DataVari
3
3
  /**
4
4
  * Data Variant 评估器。
5
5
  *
6
- * 当前阶段只提供策略模型能力和示例验证,不接入 ProductList 业务链路。
6
+ * OS Server 商品路由开启时由 ProductsModule 调用;路由关闭时由 ProductList 在远端响应后调用。
7
7
  */
8
8
  export declare class DataVariantEvaluator {
9
9
  private adapter;
@@ -17,7 +17,7 @@ export declare class DataVariantEvaluator {
17
17
  /**
18
18
  * 批量解析商品 Data Variant。
19
19
  *
20
- * 先执行全局预计算,再只针对商品自身 data_variants 引用到的规则做商品级判断。
20
+ * 优先使用商品 metadata.data_variant_ids;缺少权威命中结果时才执行本地策略判断。
21
21
  */
22
22
  resolveProducts(businessData: DataVariantBusinessData): ResolvedDataVariantResult;
23
23
  /**
@@ -34,8 +34,24 @@ export declare class DataVariantEvaluator {
34
34
  * 这里不依赖餐牌,只读取策略配置和调用方传入的 scheduleList。
35
35
  */
36
36
  buildScheduleTimePoints(strategyConfigs: StrategyConfig<DataVariantRuleMetadata>[], scheduleList: NonNullable<DataVariantBusinessData['scheduleList']>): string[];
37
+ /**
38
+ * 判断商品(含套餐子商品)是否仍需要本地执行策略条件。
39
+ * metadata.data_variant_ids 只要是数组,即使为空,也表示接口已经给出权威命中结果。
40
+ */
41
+ private requiresLocalStrategyEvaluation;
42
+ /**
43
+ * 将接口返回的高 -> 低优先级 Variant ID 转成数值优先级。
44
+ * 返回 null 表示接口没有提供该字段;空 Map 表示已计算但没有任何命中。
45
+ */
46
+ private getAuthoritativeVariantPriorities;
47
+ private normalizeDataVariantId;
37
48
  private buildGlobalEffectiveRuleMap;
38
49
  private resolveProduct;
50
+ /**
51
+ * 多货币 Data Variant 只生成候选列表,不参与商品字段覆盖。
52
+ * rule_id=0 是无条件默认价;同币种命中的条件价优先于默认价。
53
+ */
54
+ private resolveSettlementOptions;
39
55
  /**
40
56
  * 解析套餐子商品上的 Data Variant 覆盖。
41
57
  *
@@ -10,7 +10,7 @@ var _adapter = require("./adapter");
10
10
  /**
11
11
  * Data Variant 评估器。
12
12
  *
13
- * 当前阶段只提供策略模型能力和示例验证,不接入 ProductList 业务链路。
13
+ * OS Server 商品路由开启时由 ProductsModule 调用;路由关闭时由 ProductList 在远端响应后调用。
14
14
  */
15
15
  class DataVariantEvaluator {
16
16
  adapter;
@@ -32,16 +32,22 @@ class DataVariantEvaluator {
32
32
  /**
33
33
  * 批量解析商品 Data Variant。
34
34
  *
35
- * 先执行全局预计算,再只针对商品自身 data_variants 引用到的规则做商品级判断。
35
+ * 优先使用商品 metadata.data_variant_ids;缺少权威命中结果时才执行本地策略判断。
36
36
  */
37
37
  resolveProducts(businessData) {
38
38
  const strategyConfigs = businessData.strategyConfigs ? this.normalizeStrategyConfigs(businessData.strategyConfigs) : this.strategyConfigs;
39
39
  const menuScopeMap = this.buildMenuScopeMap(businessData.menuList || []);
40
40
  const scheduleTimePoints = this.buildScheduleTimePoints(strategyConfigs, businessData.scheduleList || []);
41
+ // metadata.data_variant_ids 存在时,接口已经完成规则计算;只有仍有商品缺少
42
+ // 该权威结果时才初始化本地策略引擎,避免 OS/接口与客户端重复评估。
43
+ const shouldEvaluateStrategies = businessData.products.some(product => this.requiresLocalStrategyEvaluation(product));
41
44
  const {
42
45
  globalEffectiveRuleMap,
43
46
  rawResults
44
- } = this.buildGlobalEffectiveRuleMap(strategyConfigs, businessData, menuScopeMap);
47
+ } = shouldEvaluateStrategies ? this.buildGlobalEffectiveRuleMap(strategyConfigs, businessData, menuScopeMap) : {
48
+ globalEffectiveRuleMap: new Map(),
49
+ rawResults: []
50
+ };
45
51
  const products = businessData.products.map(product => {
46
52
  const resolvedProduct = this.resolveProduct(product, businessData, globalEffectiveRuleMap, menuScopeMap);
47
53
  // 套餐子商品也可能携带 data_variants,需在同一策略上下文内继续评估并写回 bundle_item。
@@ -124,6 +130,46 @@ class DataVariantEvaluator {
124
130
  const matchedSchedules = scheduleList.filter(schedule => scheduleIdSet.has(Number(schedule.id)));
125
131
  return this.extractTimePointsFromSchedules(matchedSchedules);
126
132
  }
133
+
134
+ /**
135
+ * 判断商品(含套餐子商品)是否仍需要本地执行策略条件。
136
+ * metadata.data_variant_ids 只要是数组,即使为空,也表示接口已经给出权威命中结果。
137
+ */
138
+ requiresLocalStrategyEvaluation(product) {
139
+ const dataVariants = this.getProductDataVariants(product);
140
+ const hasConditionalVariant = dataVariants.some(variant => Number(variant.data_variant_rule_id) !== 0);
141
+ if (hasConditionalVariant && this.getAuthoritativeVariantPriorities(product) === null) {
142
+ return true;
143
+ }
144
+ const bundleGroups = Array.isArray(product.bundle_group) ? product.bundle_group : [];
145
+ return bundleGroups.some(group => {
146
+ const bundleItems = Array.isArray(group?.bundle_item) ? group.bundle_item : [];
147
+ return bundleItems.some(item => this.requiresLocalStrategyEvaluation(this.toBundleItemDataVariantProduct(item)));
148
+ });
149
+ }
150
+
151
+ /**
152
+ * 将接口返回的高 -> 低优先级 Variant ID 转成数值优先级。
153
+ * 返回 null 表示接口没有提供该字段;空 Map 表示已计算但没有任何命中。
154
+ */
155
+ getAuthoritativeVariantPriorities(product) {
156
+ const rawIds = product?.metadata?.data_variant_ids;
157
+ if (!Array.isArray(rawIds)) return null;
158
+ const orderedIds = [];
159
+ const seen = new Set();
160
+ for (const rawId of rawIds) {
161
+ const id = this.normalizeDataVariantId(rawId);
162
+ if (id === null || seen.has(id)) continue;
163
+ seen.add(id);
164
+ orderedIds.push(id);
165
+ }
166
+ return new Map(orderedIds.map((id, index) => [id, orderedIds.length - index]));
167
+ }
168
+ normalizeDataVariantId(value) {
169
+ if (value === undefined || value === null) return null;
170
+ const normalized = String(value).trim();
171
+ return normalized || null;
172
+ }
127
173
  buildGlobalEffectiveRuleMap(strategyConfigs, businessData, menuScopeMap) {
128
174
  const globalEffectiveRuleMap = new Map();
129
175
  const rawResults = [];
@@ -155,7 +201,22 @@ class DataVariantEvaluator {
155
201
  const dataVariants = this.getProductDataVariants(product);
156
202
  if (!dataVariants.length) return product;
157
203
  const matchedVariants = [];
204
+ const authoritativeVariantPriorities = this.getAuthoritativeVariantPriorities(product);
205
+ const settlementVariants = dataVariants.filter(variant => variant.business_type === 'multi_currency_settlement' && variant.status === 'enable');
158
206
  for (const variant of dataVariants) {
207
+ if (variant.business_type === 'multi_currency_settlement') continue;
208
+ if (authoritativeVariantPriorities !== null) {
209
+ const variantId = this.normalizeDataVariantId(variant.id);
210
+ const priority = variantId === null ? undefined : authoritativeVariantPriorities.get(variantId);
211
+ if (priority === undefined) continue;
212
+ matchedVariants.push({
213
+ ruleId: String(variant.data_variant_rule_id),
214
+ priority,
215
+ data: variant.data || {},
216
+ variant
217
+ });
218
+ continue;
219
+ }
159
220
  const rule = globalEffectiveRuleMap.get(String(variant.data_variant_rule_id));
160
221
  if (!rule) continue;
161
222
  if (!this.evaluateProductRules(rule, product, businessData, menuScopeMap)) {
@@ -168,8 +229,76 @@ class DataVariantEvaluator {
168
229
  variant
169
230
  });
170
231
  }
171
- if (!matchedVariants.length) return product;
172
- return this.mergeMatchedVariants(product, matchedVariants);
232
+ const resolvedProduct = matchedVariants.length ? this.mergeMatchedVariants(product, matchedVariants) : product;
233
+ const availableSettlementOptions = this.resolveSettlementOptions(settlementVariants, product, businessData, globalEffectiveRuleMap, menuScopeMap, authoritativeVariantPriorities);
234
+ if (!settlementVariants.length) return resolvedProduct;
235
+ return {
236
+ ...resolvedProduct,
237
+ available_settlement_options: availableSettlementOptions
238
+ };
239
+ }
240
+
241
+ /**
242
+ * 多货币 Data Variant 只生成候选列表,不参与商品字段覆盖。
243
+ * rule_id=0 是无条件默认价;同币种命中的条件价优先于默认价。
244
+ */
245
+ resolveSettlementOptions(variants, product, businessData, globalEffectiveRuleMap, menuScopeMap, authoritativeVariantPriorities) {
246
+ if (!variants.length) return [];
247
+ const candidates = variants.flatMap(variant => {
248
+ const settlement = variant.data?.multi_currency_settlement;
249
+ if (!settlement || typeof settlement !== 'object') return [];
250
+ const ruleId = String(variant.data_variant_rule_id);
251
+ const isDefault = Number(variant.data_variant_rule_id) === 0;
252
+ const rule = isDefault ? undefined : globalEffectiveRuleMap.get(ruleId);
253
+ const variantId = this.normalizeDataVariantId(variant.id);
254
+ const authoritativePriority = variantId === null ? undefined : authoritativeVariantPriorities?.get(variantId);
255
+ let available;
256
+ let priority;
257
+ if (isDefault) {
258
+ // rule 0 是无条件默认候选,不依赖接口命中 ID 或本地策略引擎。
259
+ available = true;
260
+ priority = Number.NEGATIVE_INFINITY;
261
+ } else if (authoritativeVariantPriorities !== null) {
262
+ // 数组存在(包括 [])即表示接口已经算过规则,不再在客户端二次判断。
263
+ available = authoritativePriority !== undefined;
264
+ priority = authoritativePriority ?? rule?.priority ?? Number.NEGATIVE_INFINITY;
265
+ } else {
266
+ available = Boolean(rule && this.evaluateProductRules(rule, product, businessData, menuScopeMap));
267
+ priority = rule?.priority ?? Number.NEGATIVE_INFINITY;
268
+ }
269
+ const settlementType = String(settlement.settlement_type || '');
270
+ const currencyProductId = this.toNumber(settlement.currency_product_id);
271
+ const settlementKey = settlementType === 'virtual_currency' ? `virtual_currency:${currencyProductId || ''}` : settlementType || `variant:${variant.id ?? ruleId}`;
272
+ return [{
273
+ key: settlementKey,
274
+ priority,
275
+ isDefault,
276
+ option: {
277
+ ...settlement,
278
+ settlement_type: settlementType,
279
+ ...(currencyProductId ? {
280
+ currency_product_id: currencyProductId
281
+ } : {}),
282
+ price: String(variant.data?.price ?? '0'),
283
+ data_variant_id: variant.id,
284
+ data_variant_rule_id: variant.data_variant_rule_id,
285
+ available,
286
+ disabled_reason: available ? '' : String(settlement.disabled_reason || 'Not available for this purchase.')
287
+ }
288
+ }];
289
+ });
290
+ const grouped = new Map();
291
+ for (const candidate of candidates) {
292
+ const group = grouped.get(candidate.key) || [];
293
+ group.push(candidate);
294
+ grouped.set(candidate.key, group);
295
+ }
296
+ return Array.from(grouped.values()).map(group => {
297
+ const matchedConditional = group.filter(candidate => !candidate.isDefault && candidate.option.available).sort((a, b) => b.priority - a.priority)[0];
298
+ const defaultCandidate = group.find(candidate => candidate.isDefault);
299
+ const unavailableCandidate = [...group].filter(candidate => !candidate.option.available).sort((a, b) => b.priority - a.priority)[0];
300
+ return (matchedConditional || defaultCandidate || unavailableCandidate).option;
301
+ });
173
302
  }
174
303
 
175
304
  /**
@@ -222,7 +351,8 @@ class DataVariantEvaluator {
222
351
  product_variant_id: variantId,
223
352
  variant_id: variantId,
224
353
  collection: item.collection,
225
- data_variants: item.data_variants
354
+ data_variants: item.data_variants,
355
+ metadata: item.metadata
226
356
  };
227
357
  }
228
358
 
@@ -453,6 +583,7 @@ class DataVariantEvaluator {
453
583
  }
454
584
  result._dataVariant = {
455
585
  appliedRuleIds,
586
+ appliedVariantIds: Array.from(new Set([...sortedVariants].reverse().map(matched => this.normalizeDataVariantId(matched.variant.id)).filter(id => id !== null))),
456
587
  appliedFields: Array.from(appliedFields),
457
588
  original
458
589
  };
@@ -35,6 +35,10 @@ export interface DataVariantItem {
35
35
  module_data_id?: number;
36
36
  /** 关联策略模型 metadata.id / data_variant_rule.id */
37
37
  data_variant_rule_id: number | string;
38
+ /** 业务类型;多货币结算候选不会走普通字段覆盖 */
39
+ business_type?: 'multi_currency_settlement' | string;
40
+ /** 数据状态;多货币结算仅处理 enable 记录 */
41
+ status?: 'enable' | 'disable' | string;
38
42
  /** 命中策略后需要覆盖到商品上的字段 */
39
43
  data: Record<string, any>;
40
44
  created_at?: string | null;
@@ -53,11 +57,30 @@ export interface DataVariantProduct {
53
57
  id?: number | string;
54
58
  [key: string]: any;
55
59
  }>;
60
+ /**
61
+ * 商品接口已经完成规则计算时返回的命中结果。
62
+ * 数组按 Data Variant 优先级从高到低排列;空数组表示已计算但无命中。
63
+ */
64
+ metadata?: {
65
+ data_variant_ids?: Array<number | string>;
66
+ [key: string]: any;
67
+ };
56
68
  /** 后端 /product/query with dataVariants 返回的智能定价变体列表 */
57
69
  data_variants?: DataVariantItem[];
70
+ /** 当前上下文解析后的可选结算货币,不覆盖商品原始多货币配置 */
71
+ available_settlement_options?: AvailableSettlementOption[];
58
72
  _dataVariant?: DataVariantAppliedInfo;
59
73
  [key: string]: any;
60
74
  }
75
+ export interface AvailableSettlementOption extends Record<string, any> {
76
+ settlement_type: 'legal_currency' | 'virtual_currency' | string;
77
+ currency_product_id?: number;
78
+ price: string;
79
+ data_variant_id?: number;
80
+ data_variant_rule_id: number | string;
81
+ available: boolean;
82
+ disabled_reason?: string;
83
+ }
61
84
  /**
62
85
  * 餐牌商品配置项。
63
86
  */
@@ -131,6 +154,8 @@ export interface MatchedDataVariant {
131
154
  */
132
155
  export interface DataVariantAppliedInfo {
133
156
  appliedRuleIds: string[];
157
+ /** 本地策略实际应用的 Data Variant ID,按优先级从高到低排列。 */
158
+ appliedVariantIds?: Array<number | string>;
134
159
  appliedFields: string[];
135
160
  original: DataVariantProduct;
136
161
  }
@@ -1,6 +1,6 @@
1
1
  import type { EvaluationResult, RuntimeContext, ActionEffect } from '../../type';
2
2
  import type { BusinessAdapter } from '../type';
3
- import { type ItemRuleBusinessData, type QuantityCheckResult, type PrefillItemResult, type QuantityLimitResult } from './type';
3
+ import { type ItemRuleBusinessData, type QuantityCheckResult, type PrefillItemResult, type QuantityLimitResult, type AllergyRequirement } from './type';
4
4
  /**
5
5
  * ItemRule 适配器
6
6
  *
@@ -14,7 +14,10 @@ export declare class ItemRuleAdapter implements BusinessAdapter {
14
14
  transformResult(result: EvaluationResult, businessData?: ItemRuleBusinessData): {
15
15
  quantityChecks: QuantityCheckResult[];
16
16
  prefillItems: PrefillItemResult[];
17
+ allergyRequirements: AllergyRequirement[];
17
18
  };
19
+ private processAllergyCheck;
20
+ private normalizeAllergyDataSourceItems;
18
21
  formatConfig(result: EvaluationResult, businessData?: ItemRuleBusinessData): {
19
22
  result: EvaluationResult;
20
23
  businessData?: ItemRuleBusinessData;
@@ -19,14 +19,17 @@ class ItemRuleAdapter {
19
19
  pax,
20
20
  cartItems,
21
21
  serviceType,
22
+ channel,
22
23
  submissionIndex,
23
24
  custom
24
25
  } = businessData;
26
+ const resolvedChannel = String(channel ?? custom?.channel ?? '').trim();
25
27
  return {
26
28
  entities: {
27
29
  cartItems,
28
30
  historicalItems: businessData.historicalItems || [],
29
- pax
31
+ pax,
32
+ currentProduct: businessData.currentProduct
30
33
  },
31
34
  attributes: {
32
35
  serviceType: serviceType || '',
@@ -36,7 +39,8 @@ class ItemRuleAdapter {
36
39
  submissionIndex: submissionIndex ?? 0,
37
40
  cartItemCount: cartItems.length,
38
41
  cartTotalQuantity: cartItems.reduce((sum, item) => sum + item.quantity, 0),
39
- ...custom
42
+ ...custom,
43
+ channel: resolvedChannel
40
44
  },
41
45
  metadata: {
42
46
  timestamp: Date.now()
@@ -46,10 +50,12 @@ class ItemRuleAdapter {
46
50
  transformResult(result, businessData) {
47
51
  const quantityChecks = [];
48
52
  const prefillItems = [];
53
+ const allergyRequirements = [];
49
54
  if (!result.applicable || !businessData) {
50
55
  return {
51
56
  quantityChecks,
52
- prefillItems
57
+ prefillItems,
58
+ allergyRequirements
53
59
  };
54
60
  }
55
61
  for (const action of result.matchedActions) {
@@ -61,13 +67,76 @@ class ItemRuleAdapter {
61
67
  } else if (action.type === _type.ITEM_RULE_ACTION_TYPES.PREFILL_CART) {
62
68
  const items = this.processPrefillCart(action, businessData);
63
69
  prefillItems.push(...items);
70
+ } else if (action.type === _type.ITEM_RULE_ACTION_TYPES.ALLERGY_CHECK) {
71
+ const requirement = this.processAllergyCheck(action, result, businessData);
72
+ if (requirement) allergyRequirements.push(requirement);
64
73
  }
65
74
  }
66
75
  return {
67
76
  quantityChecks,
68
- prefillItems
77
+ prefillItems,
78
+ allergyRequirements
69
79
  };
70
80
  }
81
+
82
+ // ============================================
83
+ // Allergy Check 处理
84
+ // ============================================
85
+
86
+ processAllergyCheck(action, result, businessData) {
87
+ const config = action.config;
88
+ const currentProduct = businessData.currentProduct;
89
+ if (!config || config.targetType !== 'product' || config.promptScope !== 'once_per_order_product' || !Array.isArray(config.targets) || !currentProduct) {
90
+ return null;
91
+ }
92
+ const currentProductId = Number(currentProduct.product_id);
93
+ const currentVariantId = Number(currentProduct.product_variant_id ?? 0);
94
+ if (!Number.isFinite(currentProductId) || currentProductId <= 0) return null;
95
+ const productTargets = config.targets.filter(item => Number(item.product_id) === currentProductId);
96
+ const target = productTargets.find(item => currentVariantId > 0 && Number(item.product_variant_id ?? 0) === currentVariantId) ?? productTargets.find(item => Number(item.product_variant_id ?? 0) <= 0);
97
+ if (!target || target.dataSource?.type !== 'inline') return null;
98
+ const items = this.normalizeAllergyDataSourceItems(target.dataSource.items);
99
+ if (!items.length) return null;
100
+ return {
101
+ strategyId: result.config.metadata.id,
102
+ ruleId: action.id,
103
+ priority: action.priority ?? 0,
104
+ targetType: 'product',
105
+ product_id: currentProductId,
106
+ ...(Number(target.product_variant_id ?? 0) > 0 ? {
107
+ product_variant_id: Number(target.product_variant_id)
108
+ } : {}),
109
+ dataSource: {
110
+ type: 'inline',
111
+ items
112
+ },
113
+ promptScope: config.promptScope,
114
+ allowNote: config.allowNote === true
115
+ };
116
+ }
117
+ normalizeAllergyDataSourceItems(items) {
118
+ if (!Array.isArray(items)) return [];
119
+ const uniqueIds = new Set();
120
+ const normalized = [];
121
+ for (const item of items) {
122
+ const id = String(item?.id ?? '').trim();
123
+ const rawLabel = item?.label;
124
+ const label = typeof rawLabel === 'string' ? rawLabel.trim() : Object.entries(rawLabel || {}).reduce((labels, [locale, value]) => {
125
+ if (typeof value === 'string' && value.trim().length > 0) {
126
+ labels[locale] = value.trim();
127
+ }
128
+ return labels;
129
+ }, {});
130
+ const hasLabel = typeof label === 'string' ? label.length > 0 : Object.keys(label).length > 0;
131
+ if (!id || !hasLabel || uniqueIds.has(id)) continue;
132
+ uniqueIds.add(id);
133
+ normalized.push({
134
+ id,
135
+ label
136
+ });
137
+ }
138
+ return normalized;
139
+ }
71
140
  formatConfig(result, businessData) {
72
141
  return {
73
142
  result,
@@ -1,5 +1,5 @@
1
1
  import type { StrategyConfig } from '../../type';
2
- import { type ItemRuleBusinessData, type ItemRuleEvaluationResult, type QuantityCheckResult, type QuantityLimitResult, type PrefillItemResult } from './type';
2
+ import { type ItemRuleBusinessData, type ItemRuleEvaluationResult, type QuantityCheckResult, type QuantityLimitResult, type PrefillItemResult, type AllergyRequirement } from './type';
3
3
  /**
4
4
  * ItemRule 评估器
5
5
  *
@@ -21,6 +21,13 @@ export declare class ItemRuleEvaluator {
21
21
  * 完整评估:数量校验 + 预填购物车
22
22
  */
23
23
  evaluate(businessData: ItemRuleBusinessData): ItemRuleEvaluationResult;
24
+ /**
25
+ * 获取当前商品优先级最高的过敏门禁要求。
26
+ *
27
+ * 不同商品或不同 Action 的数据源不会合并;相同优先级按
28
+ * strategyId + ruleId 稳定排序,保证多次评估结果一致。
29
+ */
30
+ getAllergyRequirement(businessData: ItemRuleBusinessData): AllergyRequirement | null;
24
31
  /**
25
32
  * 获取当前配置中各商品的数量限制
26
33
  *
@@ -43,6 +43,7 @@ class ItemRuleEvaluator {
43
43
  const configs = businessData.strategyConfigs ? businessData.strategyConfigs.filter(c => c.metadata.type === 'item_rule') : this.strategyConfigs;
44
44
  const allQuantityChecks = [];
45
45
  const allPrefillItems = [];
46
+ const allAllergyRequirements = [];
46
47
  const rawResults = [];
47
48
  const context = this.adapter.prepareContext(businessData);
48
49
  for (const config of configs) {
@@ -50,21 +51,42 @@ class ItemRuleEvaluator {
50
51
  rawResults.push(evalResult);
51
52
  const {
52
53
  quantityChecks,
53
- prefillItems
54
+ prefillItems,
55
+ allergyRequirements
54
56
  } = this.adapter.transformResult(evalResult, businessData);
55
57
  allQuantityChecks.push(...quantityChecks);
56
58
  allPrefillItems.push(...prefillItems);
59
+ allAllergyRequirements.push(...allergyRequirements);
57
60
  }
58
61
  const failedChecks = allQuantityChecks.filter(c => !c.passed);
59
62
  return {
60
63
  quantityChecks: allQuantityChecks,
61
64
  prefillItems: allPrefillItems,
65
+ allergyRequirements: allAllergyRequirements,
62
66
  allPassed: failedChecks.length === 0,
63
67
  failedChecks,
64
68
  rawResults
65
69
  };
66
70
  }
67
71
 
72
+ /**
73
+ * 获取当前商品优先级最高的过敏门禁要求。
74
+ *
75
+ * 不同商品或不同 Action 的数据源不会合并;相同优先级按
76
+ * strategyId + ruleId 稳定排序,保证多次评估结果一致。
77
+ */
78
+ getAllergyRequirement(businessData) {
79
+ const requirements = this.evaluate(businessData).allergyRequirements;
80
+ if (!requirements.length) return null;
81
+ return [...requirements].sort((left, right) => {
82
+ const priorityDiff = right.priority - left.priority;
83
+ if (priorityDiff !== 0) return priorityDiff;
84
+ const strategyDiff = left.strategyId.localeCompare(right.strategyId);
85
+ if (strategyDiff !== 0) return strategyDiff;
86
+ return left.ruleId.localeCompare(right.ruleId);
87
+ })[0];
88
+ }
89
+
68
90
  /**
69
91
  * 获取当前配置中各商品的数量限制
70
92
  *