@pisell/private-materials 6.12.370 → 6.12.371

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.
@@ -124,6 +124,8 @@ Home 的 Redeem Socks 入口打开独立 `RedeemPage`,展示扫码指引、扫
124
124
 
125
125
  Sale 与 Redeem 共用同一购物车和实时券列表,券不按来源预留,也不注册 `productVoucherGroups` 或向商品附加 `productVoucherGroupKey`。兑换分组仅用于展示,已选和剩余数量按商品行实际 `good_pass` 抵扣记录统计;Apply、Unapply、登录资产变化后重新计算。Unapply 本身保留普通购物车行为;若用户随后在兑换页明确新增商品,可通过同一 `setDiscount` 操作重新启用对应券。兑换页的 Also buy something? 入口暂时隐藏;主按钮直接结算同一订单。
126
126
 
127
+ 零售兑换页按券的 `product_id` 合并展示,同一商品 ID 的数字和字符串形式归入同组,不按持有人、扩展字段或适用规则拆组;缺少商品 ID 的券按实例单独展示。组内可兑商品取各券适用范围的并集,每个商品的新增数量按尚未实际抵扣且适用于该商品的券数限制;本机无适用商品且无关联已选商品的券不计入额度。加购不预填持有人,实际抵扣仍保留各张券的实例 ID 和原有规则。此分组逻辑仅用于 vendingMachine,不改变普通销售、优惠钱包或底层抵扣规则。
128
+
127
129
  零价商品可在兑换页按普通免费商品加入购物车并正常结算、出货,不要求分配商品券。页面从实际零价且无折扣记录的订单行读取数量,支持加减和键盘修改;免费数量计入已选商品,不占用券额度,继续受实时库存限制。存在免费选择时,汇总分别展示已选商品数和剩余商品券数;多个券组匹配同一免费商品时共享数量,汇总只计一次。有价商品仍按实际 `good_pass` 分配统计,`pisell_os` 的抵扣规则保持原样。
128
130
 
129
131
  售货机购物车通过 `saleDetailModuleConfig.cartItems.renderProductLineItemStatus` 在 A12 右侧展示兑换印章:仅商品行实际分配了 `good_pass` 时替代价格、编辑和数量区域,保留原删除按钮及确认流程;Unapply 后恢复正常展示。印章由内联 SVG 绘制圆框、勾选和多语言文字,颜色跟随卡片主题色 `--pc-a12-accent`,保留倾斜样式,不依赖图片文件;以透明背景、不降低不透明度的水印形式相对右侧区域居中绝对定位,不撑高卡片、不拦截点击,删除按钮保持在上层。文字跟随页面语言,未提供的语言回退英文。此配置仅由 VendingSale 启用,普通 Kiosk、H5、POS 及 A12 的其它调用方保持原样;不改变订单金额或券分配逻辑。
@@ -4,7 +4,7 @@ import { useSalesInventory } from "../../plus/salesSdk/inventory/context.js";
4
4
  import { resolveLocalizedText } from "../../plus/salesSdk/utils/resolveLocalizedText.js";
5
5
  import { pickIdentity } from "../../plus/salesSdk/utils/identity.js";
6
6
  import { resolveEffectiveSaleDetailAmountDue } from "../../plus/saleDetail/utils.js";
7
- import { resolveVoucherInitialHolderIds, sanitizeVoucherAddProduct } from "../../plus/saleDetail/components/PromotionWalletPanels/voucherProductAdd.js";
7
+ import { sanitizeVoucherAddProduct } from "../../plus/saleDetail/components/PromotionWalletPanels/voucherProductAdd.js";
8
8
  import { useVendingLocale } from "./locales.js";
9
9
  import { Brand, CustomerViewport, Icon as Icon$1 } from "./CustomerScreens.js";
10
10
  import { VendingActionBar } from "./VendingBottomBar.js";
@@ -12,7 +12,7 @@ import { buildVendingCatalog, buildVendingSkuProduct } from "./catalogModel.js";
12
12
  import VoucherSkuList from "./VoucherSkuList.js";
13
13
  import useVendingCartInteraction from "./useVendingCartInteraction.js";
14
14
  import { buildVoucherSkuChoices, isFreeVoucherProduct, isSimpleVoucherProduct, usesInlineVoucherSkus } from "./voucherSkuSelection.js";
15
- import { appliedProductVoucherIds, buildRedemptionGroups, isFreeRedemptionItem, redemptionRows } from "./voucherRedemption.js";
15
+ import { buildRedemptionGroups, getRemainingProductVouchers, isFreeRedemptionItem, redemptionRows } from "./voucherRedemption.js";
16
16
  import React, { useEffect, useRef } from "react";
17
17
  import { useToast } from "@pisell/materials";
18
18
  import { useMemoizedFn } from "ahooks";
@@ -55,14 +55,14 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
55
55
  var _ticket$getDiscountLi, _ticket$getTempOrder3, _context$cart$tempOrd;
56
56
  return buildRedemptionGroups((ticket === null || ticket === void 0 || (_ticket$getDiscountLi = ticket.getDiscountList) === null || _ticket$getDiscountLi === void 0 ? void 0 : _ticket$getDiscountLi.call(ticket)) || context.cart.discountList, products, (ticket === null || ticket === void 0 || (_ticket$getTempOrder3 = ticket.getTempOrder) === null || _ticket$getTempOrder3 === void 0 || (_ticket$getTempOrder3 = _ticket$getTempOrder3.call(ticket)) === null || _ticket$getTempOrder3 === void 0 ? void 0 : _ticket$getTempOrder3.products) || ((_context$cart$tempOrd = context.cart.tempOrder) === null || _context$cart$tempOrd === void 0 ? void 0 : _context$cart$tempOrd.products) || []);
57
57
  });
58
- const remaining = useMemoizedFn((key) => {
59
- var _getLiveGroups$find;
60
- return ((_getLiveGroups$find = getLiveGroups().find((group) => group.key === key)) === null || _getLiveGroups$find === void 0 ? void 0 : _getLiveGroups$find.remaining) || 0;
58
+ const remaining = useMemoizedFn((key, product) => {
59
+ const group = getLiveGroups().find((entry) => entry.key === key);
60
+ return group ? getRemainingProductVouchers(group, product).length : 0;
61
61
  });
62
62
  const restoreUnappliedVouchers = useMemoizedFn(function() {
63
- var _ref = _asyncToGenerator(function* (key, targetSelected, isCurrent) {
63
+ var _ref = _asyncToGenerator(function* (key, product, targetSelected, isCurrent) {
64
64
  let group = getLiveGroups().find((item) => item.key === key);
65
- const manuallyUnapplied = (group === null || group === void 0 ? void 0 : group.vouchers.filter((voucher) => voucher.source.isManualSelect)) || [];
65
+ const manuallyUnapplied = group ? getRemainingProductVouchers(group, product).filter((voucher) => voucher.source.isManualSelect) : [];
66
66
  for (const voucher of manuallyUnapplied) {
67
67
  if (!isCurrent() || !group || group.selected >= targetSelected) return;
68
68
  yield context.cart.setDiscount({
@@ -72,7 +72,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
72
72
  group = getLiveGroups().find((item) => item.key === key);
73
73
  }
74
74
  });
75
- return function(_x, _x2, _x3) {
75
+ return function(_x, _x2, _x3, _x4) {
76
76
  return _ref.apply(this, arguments);
77
77
  };
78
78
  }());
@@ -85,7 +85,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
85
85
  var _variant$id;
86
86
  const id = (_variant$id = variant.id) !== null && _variant$id !== void 0 ? _variant$id : variant.product_variant_id;
87
87
  const stock = inventory.selectionAvailability(product, id, items);
88
- const limit = freeSelection || isFreeVoucherProduct(product, id) ? Infinity : remaining(group.key);
88
+ const limit = freeSelection || isFreeVoucherProduct(product, id) ? Infinity : remaining(group.key, product);
89
89
  return stock.availability.status === "ready" ? Math.min(stock.availableQuantity, limit) : 0;
90
90
  }));
91
91
  });
@@ -99,17 +99,17 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
99
99
  quantity,
100
100
  debounceMs: 0,
101
101
  notShowToast: true,
102
- getQuantityLimit: free ? void 0 : () => remaining(group.key),
103
- initialHolderIds: group.vouchers[0] ? resolveVoucherInitialHolderIds(group.vouchers[0]) : []
102
+ getQuantityLimit: free ? void 0 : () => remaining(group.key, product),
103
+ initialHolderIds: []
104
104
  });
105
105
  if (result.status === "added" && isCurrent() && !free) {
106
106
  const after = getLiveGroups().find((item) => item.key === group.key);
107
107
  const freeAdded = Math.max(0, ((after === null || after === void 0 ? void 0 : after.freeQuantity) || 0) - ((before === null || before === void 0 ? void 0 : before.freeQuantity) || 0));
108
- yield restoreUnappliedVouchers(group.key, ((before === null || before === void 0 ? void 0 : before.selected) || 0) + Math.max(0, quantity - freeAdded), isCurrent);
108
+ yield restoreUnappliedVouchers(group.key, product, ((before === null || before === void 0 ? void 0 : before.selected) || 0) + Math.max(0, quantity - freeAdded), isCurrent);
109
109
  }
110
110
  return result;
111
111
  });
112
- return function(_x4, _x5, _x6, _x7) {
112
+ return function(_x5, _x6, _x7, _x8) {
113
113
  return _ref3.apply(this, arguments);
114
114
  };
115
115
  }());
@@ -118,7 +118,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
118
118
  if (increaseLimit(group, product) < 1) throw new Error(t("collectLimit"));
119
119
  return addVoucherProduct(group, product, 1, isCurrent);
120
120
  });
121
- return function(_x8) {
121
+ return function(_x9) {
122
122
  return _ref4.apply(this, arguments);
123
123
  };
124
124
  }());
@@ -152,16 +152,14 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
152
152
  const row = (_buildVendingCatalog$2 = buildVendingCatalog([product], [], localized)[0]) === null || _buildVendingCatalog$2 === void 0 ? void 0 : _buildVendingCatalog$2.rows[0];
153
153
  const currentItems = ((_ref5 = (_ticket$getTempOrder6 = ticket === null || ticket === void 0 || (_ticket$getTempOrder7 = ticket.getTempOrder) === null || _ticket$getTempOrder7 === void 0 ? void 0 : _ticket$getTempOrder7.call(ticket)) !== null && _ticket$getTempOrder6 !== void 0 ? _ticket$getTempOrder6 : context.cart.tempOrder) === null || _ref5 === void 0 ? void 0 : _ref5.products) || [];
154
154
  if (!(row === null || row === void 0 ? void 0 : row.enabled) || inventoryFailed || inventoryRefreshing || (inventory === null || inventory === void 0 ? void 0 : inventory.selectionAvailability(product, 0, currentItems).availability.status) !== "ready") return;
155
- const fresh = group.vouchers.filter((voucher) => freshIds.has(String(voucher.id)));
156
- const allocated = new Set(group.items.flatMap(appliedProductVoucherIds));
157
- const unused = fresh.filter((voucher) => !voucher.source.isManualSelect && !allocated.has(String(voucher.id)));
155
+ const unused = getRemainingProductVouchers(group, product).filter((voucher) => freshIds.has(String(voucher.id)) && !voucher.source.isManualSelect);
158
156
  const quantity = Math.min(unused.length, increaseLimit(group, product, 0), isFreeVoucherProduct(product, 0) ? Math.max(0, group.quota - group.freeQuantity) : Infinity);
159
157
  if (quantity > 0) {
160
158
  const result = yield addVoucherProduct(group, buildVendingSkuProduct(row), quantity, isCurrent);
161
159
  if (isCurrent() && result.status !== "added" && result.status !== "cancelled") throw new Error(t("catalogUpdateFailed"));
162
160
  }
163
161
  });
164
- return function(_x9) {
162
+ return function(_x10) {
165
163
  return _ref6.apply(this, arguments);
166
164
  };
167
165
  }());
@@ -213,10 +211,10 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
213
211
  const result = yield addVoucherProduct(liveGroup, buildVendingSkuProduct(liveRow), delta, isCurrent);
214
212
  if (isCurrent() && result.status !== "added" && result.status !== "cancelled") throw new Error(t("catalogUpdateFailed"));
215
213
  } else {
216
- var _getLiveGroups$find2;
217
- const selectedBefore = ((_getLiveGroups$find2 = getLiveGroups().find((entry) => entry.key === group.key)) === null || _getLiveGroups$find2 === void 0 ? void 0 : _getLiveGroups$find2.selected) || 0;
214
+ var _getLiveGroups$find;
215
+ const selectedBefore = ((_getLiveGroups$find = getLiveGroups().find((entry) => entry.key === group.key)) === null || _getLiveGroups$find === void 0 ? void 0 : _getLiveGroups$find.selected) || 0;
218
216
  yield context.cart.updateItemQuantity(item, Number(item.num) + delta);
219
- if (isCurrent() && !free) yield restoreUnappliedVouchers(group.key, selectedBefore + delta, isCurrent);
217
+ if (isCurrent() && !free) yield restoreUnappliedVouchers(group.key, product, selectedBefore + delta, isCurrent);
220
218
  }
221
219
  return;
222
220
  }
@@ -233,7 +231,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
233
231
  yield context.cart.waitForCartConsistency();
234
232
  }
235
233
  });
236
- return function(_x10) {
234
+ return function(_x11) {
237
235
  return _ref9.apply(this, arguments);
238
236
  };
239
237
  }());
@@ -340,7 +338,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
340
338
  product,
341
339
  items: (order === null || order === void 0 ? void 0 : order.products) || [],
342
340
  selectedItems: group.items,
343
- remaining: group.remaining,
341
+ remaining: getRemainingProductVouchers(group, product).length,
344
342
  inventory,
345
343
  localize: localized
346
344
  }),
@@ -2,33 +2,15 @@ import { resolveLocalizedText } from "../../plus/salesSdk/utils/resolveLocalized
2
2
  import { enrichCartItem } from "../../plus/salesSdk/utils/enrich.js";
3
3
  import { buildVoucherViews } from "../../plus/saleDetail/components/PromotionWalletPanels/adapters.js";
4
4
  //#region src/components/vendingMachine/voucherRedemption.ts
5
- /** Stable rule identity; array ordering in the response is not a different voucher type. */
5
+ /** Normalize selection details without depending on response ordering. */
6
6
  function canonical(value) {
7
7
  if (Array.isArray(value)) return value.map(canonical).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
8
8
  if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
9
9
  return value;
10
10
  }
11
11
  function voucherGroupKey(voucher) {
12
- var _voucher$metadata, _voucher$holder;
13
- const source = voucher.limited_relation_product_data || {};
14
- const relation = {
15
- type: source.type,
16
- filter: source.filter,
17
- product_ids: source.product_ids,
18
- exclude_product_ids: source.exclude_product_ids,
19
- product_collection_id: source.product_collection_id,
20
- package_sub_item_usage_rules: source.package_sub_item_usage_rules
21
- };
22
- return JSON.stringify(canonical({
23
- productId: voucher.product_id,
24
- relation,
25
- holder: (_voucher$metadata = voucher.metadata) === null || _voucher$metadata === void 0 ? void 0 : _voucher$metadata.holder,
26
- holderId: (_voucher$holder = voucher.holder) === null || _voucher$holder === void 0 ? void 0 : _voucher$holder.holder_id,
27
- rules: (voucher.extension_data || []).map((field) => ({
28
- key: field.field_key,
29
- value: field.value
30
- }))
31
- }));
12
+ const productId = voucher.product_id;
13
+ return JSON.stringify(productId == null || String(productId) === "" ? ["voucher", String(voucher.id)] : ["product", String(productId)]);
32
14
  }
33
15
  function isRedeemableVoucher(voucher) {
34
16
  var _source$config;
@@ -64,13 +46,21 @@ function isFreeRedemptionItem(item) {
64
46
  const price = item.selling_price;
65
47
  return price != null && String(price).trim() !== "" && Number(price) === 0 && !((_item$discount_list = item.discount_list) === null || _item$discount_list === void 0 ? void 0 : _item$discount_list.length);
66
48
  }
49
+ /** A merged group can contain instances with different eligible products. */
50
+ function getRemainingProductVouchers(group, product) {
51
+ const used = new Set(group.items.flatMap(appliedProductVoucherIds));
52
+ return group.vouchers.filter((voucher) => !used.has(String(voucher.id)) && voucherAppliesToProduct(voucher.source, product));
53
+ }
67
54
  function buildRedemptionGroups(current, products, items = []) {
68
55
  const groups = /* @__PURE__ */ new Map();
69
56
  const seen = /* @__PURE__ */ new Set();
57
+ const allocatedIds = new Set(items.flatMap(appliedProductVoucherIds));
58
+ const freeItems = items.filter(isFreeRedemptionItem);
70
59
  for (const voucher of buildVoucherViews(current).filter(isRedeemableVoucher)) {
71
60
  if (seen.has(String(voucher.id))) continue;
72
61
  seen.add(String(voucher.id));
73
62
  const source = voucher.source;
63
+ if (!products.some((product) => voucherAppliesToProduct(source, product)) && !allocatedIds.has(String(voucher.id)) && !freeItems.some((item) => voucherAppliesToProduct(source, { id: item.product_id }))) continue;
74
64
  const key = voucherGroupKey(source);
75
65
  let group = groups.get(key);
76
66
  if (!group) {
@@ -84,7 +74,7 @@ function buildRedemptionGroups(current, products, items = []) {
84
74
  freeQuantity: 0,
85
75
  remaining: 0,
86
76
  items: [],
87
- products: products.filter((product) => voucherAppliesToProduct(source, product))
77
+ products: []
88
78
  };
89
79
  groups.set(key, group);
90
80
  }
@@ -92,14 +82,15 @@ function buildRedemptionGroups(current, products, items = []) {
92
82
  group.vouchers.push(voucher);
93
83
  group.quota += 1;
94
84
  }
95
- const freeItems = new Set(items.filter(isFreeRedemptionItem));
85
+ const freeItemSet = new Set(freeItems);
96
86
  for (const group of groups.values()) {
87
+ group.products = products.filter((product) => group.vouchers.some((voucher) => voucherAppliesToProduct(voucher.source, product)));
97
88
  const ids = new Set(group.discountIds.map(String));
98
89
  const used = /* @__PURE__ */ new Set();
99
90
  group.items = items.filter((item) => {
100
91
  const allocated = appliedProductVoucherIds(item).filter((id) => ids.has(id));
101
92
  allocated.forEach((id) => used.add(id));
102
- if (freeItems.has(item) && group.vouchers.some((voucher) => voucherAppliesToProduct(voucher.source, { id: item.product_id }))) {
93
+ if (freeItemSet.has(item) && group.vouchers.some((voucher) => voucherAppliesToProduct(voucher.source, { id: item.product_id }))) {
103
94
  group.freeQuantity += Number(item.num || 0);
104
95
  return true;
105
96
  }
@@ -134,4 +125,4 @@ function redemptionRows(items, displayContext) {
134
125
  return [...rows.values()];
135
126
  }
136
127
  //#endregion
137
- export { appliedProductVoucherIds, buildRedemptionGroups, isFreeRedemptionItem, mergeRedemptionVouchers, redemptionRows };
128
+ export { appliedProductVoucherIds, buildRedemptionGroups, getRemainingProductVouchers, isFreeRedemptionItem, mergeRedemptionVouchers, redemptionRows };
@@ -37,7 +37,7 @@ declare const flowModuleRegistry: {
37
37
  Component: _$react.FC<FlowModuleProps<string>>;
38
38
  initializeWhileInactive: true;
39
39
  hardRequires: ("productData" | "booking.selectedCandidate")[];
40
- refreshOn: ("quantity" | "productData" | "skuValue" | "booking.customerId" | "booking.duration" | "booking.selectedCandidate")[];
40
+ refreshOn: ("productData" | "skuValue" | "quantity" | "booking.customerId" | "booking.duration" | "booking.selectedCandidate")[];
41
41
  isApplicable: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => boolean;
42
42
  canInitialize: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => boolean;
43
43
  refresh: (context: SelectionFlowContextValue<string>, module: CompiledSelectionFlowModuleConfig<string>) => Promise<void>;
@@ -55,7 +55,7 @@ declare const flowModuleRegistry: {
55
55
  isSkippable: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => any;
56
56
  provides: "booking.capacity"[];
57
57
  hardRequires: "productData"[];
58
- refreshOn: ("quantity" | "productData" | "skuValue")[];
58
+ refreshOn: ("productData" | "skuValue" | "quantity")[];
59
59
  isApplicable: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => boolean;
60
60
  canInitialize: () => true;
61
61
  refresh: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>, module: _$_pisell_materials_src0.CompiledSelectionFlowModuleConfig<string>) => void;
@@ -124,6 +124,8 @@ Home 的 Redeem Socks 入口打开独立 `RedeemPage`,展示扫码指引、扫
124
124
 
125
125
  Sale 与 Redeem 共用同一购物车和实时券列表,券不按来源预留,也不注册 `productVoucherGroups` 或向商品附加 `productVoucherGroupKey`。兑换分组仅用于展示,已选和剩余数量按商品行实际 `good_pass` 抵扣记录统计;Apply、Unapply、登录资产变化后重新计算。Unapply 本身保留普通购物车行为;若用户随后在兑换页明确新增商品,可通过同一 `setDiscount` 操作重新启用对应券。兑换页的 Also buy something? 入口暂时隐藏;主按钮直接结算同一订单。
126
126
 
127
+ 零售兑换页按券的 `product_id` 合并展示,同一商品 ID 的数字和字符串形式归入同组,不按持有人、扩展字段或适用规则拆组;缺少商品 ID 的券按实例单独展示。组内可兑商品取各券适用范围的并集,每个商品的新增数量按尚未实际抵扣且适用于该商品的券数限制;本机无适用商品且无关联已选商品的券不计入额度。加购不预填持有人,实际抵扣仍保留各张券的实例 ID 和原有规则。此分组逻辑仅用于 vendingMachine,不改变普通销售、优惠钱包或底层抵扣规则。
128
+
127
129
  零价商品可在兑换页按普通免费商品加入购物车并正常结算、出货,不要求分配商品券。页面从实际零价且无折扣记录的订单行读取数量,支持加减和键盘修改;免费数量计入已选商品,不占用券额度,继续受实时库存限制。存在免费选择时,汇总分别展示已选商品数和剩余商品券数;多个券组匹配同一免费商品时共享数量,汇总只计一次。有价商品仍按实际 `good_pass` 分配统计,`pisell_os` 的抵扣规则保持原样。
128
130
 
129
131
  售货机购物车通过 `saleDetailModuleConfig.cartItems.renderProductLineItemStatus` 在 A12 右侧展示兑换印章:仅商品行实际分配了 `good_pass` 时替代价格、编辑和数量区域,保留原删除按钮及确认流程;Unapply 后恢复正常展示。印章由内联 SVG 绘制圆框、勾选和多语言文字,颜色跟随卡片主题色 `--pc-a12-accent`,保留倾斜样式,不依赖图片文件;以透明背景、不降低不透明度的水印形式相对右侧区域居中绝对定位,不撑高卡片、不拦截点击,删除按钮保持在上层。文字跟随页面语言,未提供的语言回退英文。此配置仅由 VendingSale 启用,普通 Kiosk、H5、POS 及 A12 的其它调用方保持原样;不改变订单金额或券分配逻辑。
@@ -57,14 +57,14 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
57
57
  var _ticket$getDiscountLi, _ticket$getTempOrder3, _context$cart$tempOrd;
58
58
  return require_voucherRedemption.buildRedemptionGroups((ticket === null || ticket === void 0 || (_ticket$getDiscountLi = ticket.getDiscountList) === null || _ticket$getDiscountLi === void 0 ? void 0 : _ticket$getDiscountLi.call(ticket)) || context.cart.discountList, products, (ticket === null || ticket === void 0 || (_ticket$getTempOrder3 = ticket.getTempOrder) === null || _ticket$getTempOrder3 === void 0 || (_ticket$getTempOrder3 = _ticket$getTempOrder3.call(ticket)) === null || _ticket$getTempOrder3 === void 0 ? void 0 : _ticket$getTempOrder3.products) || ((_context$cart$tempOrd = context.cart.tempOrder) === null || _context$cart$tempOrd === void 0 ? void 0 : _context$cart$tempOrd.products) || []);
59
59
  });
60
- const remaining = (0, ahooks.useMemoizedFn)((key) => {
61
- var _getLiveGroups$find;
62
- return ((_getLiveGroups$find = getLiveGroups().find((group) => group.key === key)) === null || _getLiveGroups$find === void 0 ? void 0 : _getLiveGroups$find.remaining) || 0;
60
+ const remaining = (0, ahooks.useMemoizedFn)((key, product) => {
61
+ const group = getLiveGroups().find((entry) => entry.key === key);
62
+ return group ? require_voucherRedemption.getRemainingProductVouchers(group, product).length : 0;
63
63
  });
64
64
  const restoreUnappliedVouchers = (0, ahooks.useMemoizedFn)(function() {
65
- var _ref = require_asyncToGenerator._asyncToGenerator(function* (key, targetSelected, isCurrent) {
65
+ var _ref = require_asyncToGenerator._asyncToGenerator(function* (key, product, targetSelected, isCurrent) {
66
66
  let group = getLiveGroups().find((item) => item.key === key);
67
- const manuallyUnapplied = (group === null || group === void 0 ? void 0 : group.vouchers.filter((voucher) => voucher.source.isManualSelect)) || [];
67
+ const manuallyUnapplied = group ? require_voucherRedemption.getRemainingProductVouchers(group, product).filter((voucher) => voucher.source.isManualSelect) : [];
68
68
  for (const voucher of manuallyUnapplied) {
69
69
  if (!isCurrent() || !group || group.selected >= targetSelected) return;
70
70
  yield context.cart.setDiscount({
@@ -74,7 +74,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
74
74
  group = getLiveGroups().find((item) => item.key === key);
75
75
  }
76
76
  });
77
- return function(_x, _x2, _x3) {
77
+ return function(_x, _x2, _x3, _x4) {
78
78
  return _ref.apply(this, arguments);
79
79
  };
80
80
  }());
@@ -87,7 +87,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
87
87
  var _variant$id;
88
88
  const id = (_variant$id = variant.id) !== null && _variant$id !== void 0 ? _variant$id : variant.product_variant_id;
89
89
  const stock = inventory.selectionAvailability(product, id, items);
90
- const limit = freeSelection || require_voucherSkuSelection.isFreeVoucherProduct(product, id) ? Infinity : remaining(group.key);
90
+ const limit = freeSelection || require_voucherSkuSelection.isFreeVoucherProduct(product, id) ? Infinity : remaining(group.key, product);
91
91
  return stock.availability.status === "ready" ? Math.min(stock.availableQuantity, limit) : 0;
92
92
  }));
93
93
  });
@@ -101,17 +101,17 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
101
101
  quantity,
102
102
  debounceMs: 0,
103
103
  notShowToast: true,
104
- getQuantityLimit: free ? void 0 : () => remaining(group.key),
105
- initialHolderIds: group.vouchers[0] ? require_voucherProductAdd.resolveVoucherInitialHolderIds(group.vouchers[0]) : []
104
+ getQuantityLimit: free ? void 0 : () => remaining(group.key, product),
105
+ initialHolderIds: []
106
106
  });
107
107
  if (result.status === "added" && isCurrent() && !free) {
108
108
  const after = getLiveGroups().find((item) => item.key === group.key);
109
109
  const freeAdded = Math.max(0, ((after === null || after === void 0 ? void 0 : after.freeQuantity) || 0) - ((before === null || before === void 0 ? void 0 : before.freeQuantity) || 0));
110
- yield restoreUnappliedVouchers(group.key, ((before === null || before === void 0 ? void 0 : before.selected) || 0) + Math.max(0, quantity - freeAdded), isCurrent);
110
+ yield restoreUnappliedVouchers(group.key, product, ((before === null || before === void 0 ? void 0 : before.selected) || 0) + Math.max(0, quantity - freeAdded), isCurrent);
111
111
  }
112
112
  return result;
113
113
  });
114
- return function(_x4, _x5, _x6, _x7) {
114
+ return function(_x5, _x6, _x7, _x8) {
115
115
  return _ref3.apply(this, arguments);
116
116
  };
117
117
  }());
@@ -120,7 +120,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
120
120
  if (increaseLimit(group, product) < 1) throw new Error(t("collectLimit"));
121
121
  return addVoucherProduct(group, product, 1, isCurrent);
122
122
  });
123
- return function(_x8) {
123
+ return function(_x9) {
124
124
  return _ref4.apply(this, arguments);
125
125
  };
126
126
  }());
@@ -154,16 +154,14 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
154
154
  const row = (_buildVendingCatalog$2 = require_catalogModel.buildVendingCatalog([product], [], localized)[0]) === null || _buildVendingCatalog$2 === void 0 ? void 0 : _buildVendingCatalog$2.rows[0];
155
155
  const currentItems = ((_ref5 = (_ticket$getTempOrder6 = ticket === null || ticket === void 0 || (_ticket$getTempOrder7 = ticket.getTempOrder) === null || _ticket$getTempOrder7 === void 0 ? void 0 : _ticket$getTempOrder7.call(ticket)) !== null && _ticket$getTempOrder6 !== void 0 ? _ticket$getTempOrder6 : context.cart.tempOrder) === null || _ref5 === void 0 ? void 0 : _ref5.products) || [];
156
156
  if (!(row === null || row === void 0 ? void 0 : row.enabled) || inventoryFailed || inventoryRefreshing || (inventory === null || inventory === void 0 ? void 0 : inventory.selectionAvailability(product, 0, currentItems).availability.status) !== "ready") return;
157
- const fresh = group.vouchers.filter((voucher) => freshIds.has(String(voucher.id)));
158
- const allocated = new Set(group.items.flatMap(require_voucherRedemption.appliedProductVoucherIds));
159
- const unused = fresh.filter((voucher) => !voucher.source.isManualSelect && !allocated.has(String(voucher.id)));
157
+ const unused = require_voucherRedemption.getRemainingProductVouchers(group, product).filter((voucher) => freshIds.has(String(voucher.id)) && !voucher.source.isManualSelect);
160
158
  const quantity = Math.min(unused.length, increaseLimit(group, product, 0), require_voucherSkuSelection.isFreeVoucherProduct(product, 0) ? Math.max(0, group.quota - group.freeQuantity) : Infinity);
161
159
  if (quantity > 0) {
162
160
  const result = yield addVoucherProduct(group, require_catalogModel.buildVendingSkuProduct(row), quantity, isCurrent);
163
161
  if (isCurrent() && result.status !== "added" && result.status !== "cancelled") throw new Error(t("catalogUpdateFailed"));
164
162
  }
165
163
  });
166
- return function(_x9) {
164
+ return function(_x10) {
167
165
  return _ref6.apply(this, arguments);
168
166
  };
169
167
  }());
@@ -215,10 +213,10 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
215
213
  const result = yield addVoucherProduct(liveGroup, require_catalogModel.buildVendingSkuProduct(liveRow), delta, isCurrent);
216
214
  if (isCurrent() && result.status !== "added" && result.status !== "cancelled") throw new Error(t("catalogUpdateFailed"));
217
215
  } else {
218
- var _getLiveGroups$find2;
219
- const selectedBefore = ((_getLiveGroups$find2 = getLiveGroups().find((entry) => entry.key === group.key)) === null || _getLiveGroups$find2 === void 0 ? void 0 : _getLiveGroups$find2.selected) || 0;
216
+ var _getLiveGroups$find;
217
+ const selectedBefore = ((_getLiveGroups$find = getLiveGroups().find((entry) => entry.key === group.key)) === null || _getLiveGroups$find === void 0 ? void 0 : _getLiveGroups$find.selected) || 0;
220
218
  yield context.cart.updateItemQuantity(item, Number(item.num) + delta);
221
- if (isCurrent() && !free) yield restoreUnappliedVouchers(group.key, selectedBefore + delta, isCurrent);
219
+ if (isCurrent() && !free) yield restoreUnappliedVouchers(group.key, product, selectedBefore + delta, isCurrent);
222
220
  }
223
221
  return;
224
222
  }
@@ -235,7 +233,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
235
233
  yield context.cart.waitForCartConsistency();
236
234
  }
237
235
  });
238
- return function(_x10) {
236
+ return function(_x11) {
239
237
  return _ref9.apply(this, arguments);
240
238
  };
241
239
  }());
@@ -342,7 +340,7 @@ function VoucherCollectPage({ brand, context, products, loading, isActive, inven
342
340
  product,
343
341
  items: (order === null || order === void 0 ? void 0 : order.products) || [],
344
342
  selectedItems: group.items,
345
- remaining: group.remaining,
343
+ remaining: require_voucherRedemption.getRemainingProductVouchers(group, product).length,
346
344
  inventory,
347
345
  localize: localized
348
346
  }),
@@ -2,33 +2,15 @@ const require_resolveLocalizedText = require("../../plus/salesSdk/utils/resolveL
2
2
  const require_enrich = require("../../plus/salesSdk/utils/enrich.js");
3
3
  const require_adapters = require("../../plus/saleDetail/components/PromotionWalletPanels/adapters.js");
4
4
  //#region src/components/vendingMachine/voucherRedemption.ts
5
- /** Stable rule identity; array ordering in the response is not a different voucher type. */
5
+ /** Normalize selection details without depending on response ordering. */
6
6
  function canonical(value) {
7
7
  if (Array.isArray(value)) return value.map(canonical).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
8
8
  if (value && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((key) => [key, canonical(value[key])]));
9
9
  return value;
10
10
  }
11
11
  function voucherGroupKey(voucher) {
12
- var _voucher$metadata, _voucher$holder;
13
- const source = voucher.limited_relation_product_data || {};
14
- const relation = {
15
- type: source.type,
16
- filter: source.filter,
17
- product_ids: source.product_ids,
18
- exclude_product_ids: source.exclude_product_ids,
19
- product_collection_id: source.product_collection_id,
20
- package_sub_item_usage_rules: source.package_sub_item_usage_rules
21
- };
22
- return JSON.stringify(canonical({
23
- productId: voucher.product_id,
24
- relation,
25
- holder: (_voucher$metadata = voucher.metadata) === null || _voucher$metadata === void 0 ? void 0 : _voucher$metadata.holder,
26
- holderId: (_voucher$holder = voucher.holder) === null || _voucher$holder === void 0 ? void 0 : _voucher$holder.holder_id,
27
- rules: (voucher.extension_data || []).map((field) => ({
28
- key: field.field_key,
29
- value: field.value
30
- }))
31
- }));
12
+ const productId = voucher.product_id;
13
+ return JSON.stringify(productId == null || String(productId) === "" ? ["voucher", String(voucher.id)] : ["product", String(productId)]);
32
14
  }
33
15
  function isRedeemableVoucher(voucher) {
34
16
  var _source$config;
@@ -64,13 +46,21 @@ function isFreeRedemptionItem(item) {
64
46
  const price = item.selling_price;
65
47
  return price != null && String(price).trim() !== "" && Number(price) === 0 && !((_item$discount_list = item.discount_list) === null || _item$discount_list === void 0 ? void 0 : _item$discount_list.length);
66
48
  }
49
+ /** A merged group can contain instances with different eligible products. */
50
+ function getRemainingProductVouchers(group, product) {
51
+ const used = new Set(group.items.flatMap(appliedProductVoucherIds));
52
+ return group.vouchers.filter((voucher) => !used.has(String(voucher.id)) && voucherAppliesToProduct(voucher.source, product));
53
+ }
67
54
  function buildRedemptionGroups(current, products, items = []) {
68
55
  const groups = /* @__PURE__ */ new Map();
69
56
  const seen = /* @__PURE__ */ new Set();
57
+ const allocatedIds = new Set(items.flatMap(appliedProductVoucherIds));
58
+ const freeItems = items.filter(isFreeRedemptionItem);
70
59
  for (const voucher of require_adapters.buildVoucherViews(current).filter(isRedeemableVoucher)) {
71
60
  if (seen.has(String(voucher.id))) continue;
72
61
  seen.add(String(voucher.id));
73
62
  const source = voucher.source;
63
+ if (!products.some((product) => voucherAppliesToProduct(source, product)) && !allocatedIds.has(String(voucher.id)) && !freeItems.some((item) => voucherAppliesToProduct(source, { id: item.product_id }))) continue;
74
64
  const key = voucherGroupKey(source);
75
65
  let group = groups.get(key);
76
66
  if (!group) {
@@ -84,7 +74,7 @@ function buildRedemptionGroups(current, products, items = []) {
84
74
  freeQuantity: 0,
85
75
  remaining: 0,
86
76
  items: [],
87
- products: products.filter((product) => voucherAppliesToProduct(source, product))
77
+ products: []
88
78
  };
89
79
  groups.set(key, group);
90
80
  }
@@ -92,14 +82,15 @@ function buildRedemptionGroups(current, products, items = []) {
92
82
  group.vouchers.push(voucher);
93
83
  group.quota += 1;
94
84
  }
95
- const freeItems = new Set(items.filter(isFreeRedemptionItem));
85
+ const freeItemSet = new Set(freeItems);
96
86
  for (const group of groups.values()) {
87
+ group.products = products.filter((product) => group.vouchers.some((voucher) => voucherAppliesToProduct(voucher.source, product)));
97
88
  const ids = new Set(group.discountIds.map(String));
98
89
  const used = /* @__PURE__ */ new Set();
99
90
  group.items = items.filter((item) => {
100
91
  const allocated = appliedProductVoucherIds(item).filter((id) => ids.has(id));
101
92
  allocated.forEach((id) => used.add(id));
102
- if (freeItems.has(item) && group.vouchers.some((voucher) => voucherAppliesToProduct(voucher.source, { id: item.product_id }))) {
93
+ if (freeItemSet.has(item) && group.vouchers.some((voucher) => voucherAppliesToProduct(voucher.source, { id: item.product_id }))) {
103
94
  group.freeQuantity += Number(item.num || 0);
104
95
  return true;
105
96
  }
@@ -136,6 +127,7 @@ function redemptionRows(items, displayContext) {
136
127
  //#endregion
137
128
  exports.appliedProductVoucherIds = appliedProductVoucherIds;
138
129
  exports.buildRedemptionGroups = buildRedemptionGroups;
130
+ exports.getRemainingProductVouchers = getRemainingProductVouchers;
139
131
  exports.isFreeRedemptionItem = isFreeRedemptionItem;
140
132
  exports.mergeRedemptionVouchers = mergeRedemptionVouchers;
141
133
  exports.redemptionRows = redemptionRows;
@@ -37,7 +37,7 @@ declare const flowModuleRegistry: {
37
37
  Component: _$react.FC<FlowModuleProps<string>>;
38
38
  initializeWhileInactive: true;
39
39
  hardRequires: ("productData" | "booking.selectedCandidate")[];
40
- refreshOn: ("quantity" | "productData" | "skuValue" | "booking.customerId" | "booking.duration" | "booking.selectedCandidate")[];
40
+ refreshOn: ("productData" | "skuValue" | "quantity" | "booking.customerId" | "booking.duration" | "booking.selectedCandidate")[];
41
41
  isApplicable: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => boolean;
42
42
  canInitialize: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => boolean;
43
43
  refresh: (context: SelectionFlowContextValue<string>, module: CompiledSelectionFlowModuleConfig<string>) => Promise<void>;
@@ -55,7 +55,7 @@ declare const flowModuleRegistry: {
55
55
  isSkippable: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => any;
56
56
  provides: "booking.capacity"[];
57
57
  hardRequires: "productData"[];
58
- refreshOn: ("quantity" | "productData" | "skuValue")[];
58
+ refreshOn: ("productData" | "skuValue" | "quantity")[];
59
59
  isApplicable: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>) => boolean;
60
60
  canInitialize: () => true;
61
61
  refresh: (context: _$_pisell_materials_src0.SelectionFlowContextValue<SelectionFlowDraft, SelectionFlowDraftUpdate, SelectionDraftField, string, SelectionFlowServices, unknown>, module: _$_pisell_materials_src0.CompiledSelectionFlowModuleConfig<string>) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pisell/private-materials",
3
- "version": "6.12.370",
3
+ "version": "6.12.371",
4
4
  "main": "./lib/index.js",
5
5
  "module": "./es/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -74,10 +74,10 @@
74
74
  "styled-components": "^6.0.0-rc.3",
75
75
  "use-sync-external-store": "^1.6.0",
76
76
  "@pisell/pi-payment-sdk": "0.0.4",
77
- "@pisell/date-picker": "3.0.12",
77
+ "@pisell/utils": "3.0.7",
78
78
  "@pisell/materials": "6.12.88",
79
79
  "@pisell/icon": "0.0.11",
80
- "@pisell/utils": "3.0.7"
80
+ "@pisell/date-picker": "3.0.12"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "react": "^18.0.0",