# Vending materials

包入口提供 `VendingMachine`、`ConfigureAndRestock` 和 `VendingStaffPage`，以及各组件 Props、`Vending*` 服务/数据类型。三个页面分别导出，补货不初始化销售模块。

```tsx
import {
  VendingMachine,
  ConfigureAndRestock,
  VendingStaffPage,
} from '@pisell/private-materials';
```

## 销售入口

`VendingMachine` 组合 Idle → Home → BigSale kiosk → 出货 → 结果。通过 `context` 接收租户、业务、门店、设备和语言，通过 `brand`、`subtitle`、`machine` 接收宿主已解析的展示信息。销售内容依赖现有 BigSale 的 Engine/应用运行环境，与直接使用 BigSale 相同。

宿主销售模块的 `otherParams.platform` 与 `otherParams.channel` 使用 `vending`，页面仍复用 `template="kiosk"`。共享自助场景判断支持 kiosk/vending，保留商品布局、客户验证、预约选择及零元订单完成方式；实际商品、定价与订单渠道保持 `vending`。需配套使用支持 vending 云端结账的 PisellOS，支付弹窗的现金显示由宿主配置控制。

Home 的 Redeem Socks 入口打开独立 `RedeemPage`，展示扫码指引、脉冲图标和可编辑码框；Back 返回 Home，手动输入后点击 Find Sale 或按 Enter 与扫码共用处理链路。注入 `scanSource` 后，Idle 欢迎页、Home／Redeem 接收设备事件的 `data`（只去除首尾空白），保存一次性请求并在后台激活 BigSale，原页面显示方块 loading Toast，结果确定后再决定是否进入销售页面。入口页不查接口，SalesSdk 在本轮 bootstrap 成功且上一单清理及重算完成后，复用购买页原有客户／券／商品扫码处理；不增加历史订单或预订查询。Idle 的点击开始仍进入 Home。

- `scanSource.subscribe(listener, { priority })`：宿主通过现有扫码焦点栈订阅并返回释放函数，不重复发布原生事件。出货/结果、员工页面不接入口扫码；`scanEnabled=false` 用于员工验证，帮助弹窗也会暂停监听。
- 入口交接使用 `scanScope=product-voucher-collection`。手环／券码根据本次 `batch-search(resolve_ticket_order=1)` 返回的券决定是否进入兑换列表；wallet 识别并绑定客户后，等待客户 `prepare/config` 券资产加载完成。手环／券码没有有效商品券时保留扫码前页面，居中提示并恢复扫码；wallet 无有效商品券时仍进入普通购买页。已返回的券若均未到兑换时间或均已过期，显示对应原因；无详细原因时使用通用提示。空购物车的 `product_not_applicable` 不代表券失效，不据此排除候选券。进入兑换列表后展示当前销售会话内全部有效 `good_pass`，并按本机商品范围筛选可选商品，不仅限于本次扫描结果。
- 分流只在本次扫码完全结束后执行；中途优惠回调不提前揭开页面。需要配套支持 `product-voucher-collection` / `redemption` 结果的 pisellos。旧运行时缺少结果或券加载失败时，保留原页面并显示方块 Toast 错误提示，不误判为无券。
- 交接期间首码优先，后续扫码不重复执行，也不替换持续显示的 loading Toast。处理完成、失败或页面离开时关闭 loading Toast；失败后结束本次请求，恢复扫码，不保留原码重试，也不自动重放。执行中暂停闲置返回。无有效券、处理失败或取消后停用后台销售会话，复用 Kiosk 的清单及客户清理；下一次扫码仍需等待清理及重算完成。
- BigSale 可选 `scanRequest: { id, code }`、`onScanRequestAccepted(id)`、`onScanRequestSettled(id, result)` 仅透传给 SalesSdk。已接收 ID 在当前挂载会话内只执行一次；初始化失败可在接收前回报 `initialization_failed` / `order_reset_failed`，售卖机以方块 Toast 提示后结束本次请求，下一次扫码使用新的 ID。未传参数的调用方沿用原监听及业务逻辑。
- 扫码值只留在当前页面内存，不写 URL、存储或日志。返回首页、页面失活或业务作用域变化清除待处理请求，旧回调通过请求 ID 与会话校验隔离。`scanSource.log` 仅接收来源页、请求 ID、作用域、结果及耗时。

- `sales`：宿主初始化的 `osModuleName`、`otherParams`、就绪/失败状态和 `retry`。可选 `inventory` 是宿主创建的实例级库存控制器，`checkoutHooks` 提供付款前的业务预检；`otherParams.catalogScopeKey` 隔离基础目录。已注册销售模块由宿主应用所有，物料不会在作用域重挂载时销毁它，宿主负责更新参数并清理上一实例订单。
- `fulfillment`：宿主提供是否出货判断、串行队列、作用域有效性检查和暂停/恢复屏保的方法。队列接口为 `getSnapshot` / `subscribe` / `cancel`，一次物理调用只处理一件商品，同一订单的会话防重复由宿主维护。
- `blockNavigation`：可选的路由阻止适配，返回解除阻止函数；POS 接入时提供，用于避免闲置跳转覆盖支付后结果。
- `onRequestSettings` / `onRequestStaff`：打开宿主员工验证弹窗。物料不校验 PIN，也不变更登录账号或跳转 Settings。
- `isActive` / `isVisible`：分别表示宿主页面激活和销售区域可见；宿主切到员工页时保留销售组件挂载。`idleRequest` 递增时返回 Idle。

宿主应提供稳定的服务回调引用。租户、业务、门店或设备变化会重置组件会话并取消旧队列订阅；单纯切换语言保留当前页面状态。出货全部成功后 30 秒返回 Idle，失败由用户主动返回；出货和结果页底部始终显示联系工作人员文案。

单件结果可附带 `message`、`code` 和 `uncertain`。宿主无法确认物理出货结果时返回 `success: false, uncertain: true`；队列仍以 `failed` 标识已处理的非成功项，展示时单独标记为“需要核对”，不计入明确未出货数量、不启动成功倒计时。`message` 展示宿主提供的本地化原因。硬件调用、结果分类与是否重试由宿主管理，物料不调用原生插件。

队列快照可附带 `inventorySync`（`syncing / synced / review`），独立于物理出货结果。宿主汇总扣库存后可更新此状态；待核对时结果页提示工作人员处理，但不会改变成功件数或重新启动 30 秒倒计时。物料不发起扣库存或自动补偿。

## 库存扩展

- BigSale 只透传 `inventory` / `checkoutHooks` 并报告列表进入。SalesSdk 的 `useCatalogInventory` 管理进入/重试/取消；详情在基础数据就绪后调用同一库存控制器，缓存命中及购物车编辑同样执行。
- 库存与商品价格缓存分开。共享 `InventoryAvailability` 和 `inventoryProductView` 统一驱动卡片/详情/规格；兼容旧渲染器的 stock 字段只存在于临时视图，不写回 OS。视图状态用非序列化 Symbol 标记，不进入销售 API。未知展示 `—`，失败保留最近成功值并提供重试。
- 购物车在 Order 的通用 `onBeforeCartCommit` 入口校验原始规则算出的草稿，不维护另一套订单合并规则；库存异常时允许减少/删除，禁止增加。未注入扩展的业务沿用既有库存与销售规则。
- 普通与 0 元结账都经过 SalesSdk `prepareCheckout`。本次准备凭据携带不可变 `checkoutSnapshot` 并校验提交前购物车是否变化；正常支付完成或确认完成的 0 元订单再通过 `onPaidCheckout` 交给宿主。库存请求与设备规则不写入 Kiosk 模板。
- 设备端购物车与支付页共用 `resolveKioskPresentation` 的 `self-service` 外观。Kiosk/Vending 待付金额大于 0 时进入 Kiosk 自身的 `payment` 页面，由 `Kiosk/payment` 负责控制，不经过 SalesSdk `openCheckout`、宿主 PaymentModalPlugin 或 PaymentModal。H5、PC 和 POS 继续使用既有支付入口；设备页的外观不再跨 SDK/宿主透传。
- 独立页复用公共 `PaymentMethodRuntime`、pending 支付生命周期和 BaseSales 订单 API，直接使用当前购物车/钱包。支付方式按实际 `channel` 和宿主终端偏好筛选，Vending 排除标准及自定义现金方式，也不开钱箱。只有一种支付方式时仍需点击；付款结果待确认时锁定操作，已收款后的失败只重试订单同步。Kiosk 的自定义支付继续等候主屏授权结果。
- 付款页面期间锁住销售扫码并暂停屏保，返回购物车保留原订单与券；只有明确足额支付并同步成功后，才冻结订单及原 checkoutSnapshot，再进入现有成功/出货交接。0 元仍使用原有直接确认流程，不进入支付方式页。
- 隐藏零元金额和显示 Confirm 由 Vending 平台决定，不依赖 `renderCatalog` 是否存在；自定义列表本身不改变其它 Kiosk/H5/PC 的金额展示规则。
- `checkoutSnapshot.lines[].inventoryKey` 是结账时冻结的本次购买 SKU。宿主把它转换为 `DispenseItem.sku`，无需读取已清空的购物车或“最近一次结账”状态。快照仅供内存交接，未注入扩展的支付回调保持原顺序。
- 物料只订阅宿主队列；订单防重、映射准备、逐件分配及插件控制由宿主承担。硬件参数不由 BigSale 或此组件解释。

## 独立补货与员工页面

`ConfigureAndRestock` 接收 `context`、`brand`、`machine` 和必填 `service`，`service.load` 返回分组货道及商品选项，`service.submit` 接收变更前后快照和可为空的员工身份。组件不内置 mock 或接口地址，真实库存查询和后端映射由宿主实现。

- `RestockData.groupType` 可为 `board`；`channel.row` 是分组 key，`groupName` 可选，`code` 展示原始货道编号。补货列表不再占用黑色控制板侧列；有多个分组时使用紧凑标题区分。跨板相同货道号需要不同的 `channel.id`。
- 页面固定为全部货道的行内编辑列表，不展示视图切换和 All/Low/Changed 筛选。标题右侧保留“全部填满”，按 service 提供的每行 `capacity` 更新所有可操作、已选商品行的草稿；相同 `spaceId` 的编辑及重置同步。
- `quantity: null` 表示库存未知，显示 `—`；`issue` 展示配置或库存异常原因。非启用行、存在异常的行不允许编辑或填满。商品选项的 `disabled` 防止选择停用货品。
- `inventoryItemId` 与销售 `productId` 分开保留，库存适配无需提供销售商品 ID。`spaceId`、`stockId`、`boardDeviceId`、`laneId` 随变更快照传给 service。
- 可选 `service.testChannel(context, channel, signal)` 返回 `DispenseResult`，每行测试按钮使用原始控制板和货道绑定，不依赖商品或库存是否可编辑。未接入该方法或硬件编号无效时禁用测试。测试会驱动一次出货，不自动改库存；页面提示按测试后的实物填写数量，并在测试期间阻止编辑、提交和离开。宿主需等待真实任务结果，已受理的硬件命令不能因页面卸载而被当作取消。
- 点击底部“核对补货”先冻结本次变更，Review 展示货道、商品/SKU、原数量、目标数量及增减量，确认后才调用 `service.submit`。按商品分别汇总增加与减少件数；换商品拆成旧商品全量移出、新商品移入，共享 `spaceId` 只累计一次。返回编辑保留草稿；提交失败沿用原有草稿和错误处理。
- `canSubmit: false` 会禁用提交并显示当前编辑不能保存的提示。宿主写接口未接入时应明确返回此标志，同时让 `submit` 拒绝请求，避免草稿被当作真实保存；省略则保留原有提交行为。
- 异步写入 service 的 `submit` 可返回 `{ status: 'accepted', data }`：接口业务成功后，页面直接使用宿主提供的受理基线、清空草稿并显示 PisellToast，不主动重新加载。新绑定的未知 Stock ID 及 `submissionId` 由宿主负责，组件只在下次提交的 `before` 中原样传回，不把本地显示值写入销售库存。
- `submit` 也可返回 `inventorySync` 状态，`load` 返回同一待同步/待核对状态及已禁用的受影响货道。结果不明确时清除可重提草稿、禁用受影响货位并提供只读刷新，不误报保存成功。明确拒绝应抛出带展示原因的 Error，页面保留草稿。返回 `synced` 或未返回状态的同步/演示 service 使用提交值更新页面，不追加查询。
- 员工首页优先统计 `availableQuantity`；旧适配省略该字段时沿用 `quantity`。可用数量未知显示 `—`，共享货位不重复累计可售库存。

`staff`、`onBack`、`onSignOut` 按接入入口选传：独立 Procode 直接展示补货，不传 `onBack` 就不显示返回按钮。宿主通过 `blockNavigation` 保留路由离开时的修改提示；页面刷新/关闭及内部返回也有草稿保护。

`VendingStaffPage` 接收已经验证的 `staff`、同一 `service` 和进入补货/退出回调，仅负责设备管理页展示。

## 资源和语言

三个独立页面通过现有 Ant Design `ConfigProvider` 的 token 继承宿主主题，主色、交互状态、浅底和边框映射为组件根节点的 `--vm-*` 变量。导出的 `useVendingThemeTokens` 让宿主设备设置继承同一份物料主题，避免主项目与物料的 Ant Design 实例不同而读到默认蓝色；宿主可将返回值传入自身 `ConfigProvider`。员工选择与 PIN 验证弹窗沿用宿主组件的默认配色，不注入售货机主题。帮助、草稿确认及保存提示通过 hooks 保留主题上下文。销售容器只为本实例注入 `--pisell-kiosk-*` 展示变量，未注入的 BigSale 沿用原样式；成功/失败/警告色、黄绿色辅助色和中性色保留。

组件导入自身的 LESS，随包携带 DM Sans、Bricolage Grotesque 字体以及字体/图标许可证。保持原有设备容器自适应布局，无固定画布宽度。`context.locale` 通过现有物料 `LocaleOverrideProvider` 传递；宿主弹窗可复用 `getVendingText`、`localizeVendingText`，显式传入宿主语言。

`CustomerViewport` 直接填满宿主容器，不再按 768×1024 计算整页缩放。顾客页面、商品列表和 Kiosk 购物车统一使用 CSS 像素；窄容器通过容器查询调整间距、换行和商品列数。购买列表与购物车的浮动底栏共用 `kioskSale/selfServiceBar.less` 尺寸规则，列表滚动区直接预留底栏高度和底部间距。宿主设备预览器自身的缩放仍由宿主管理。

当前库存与出货扩展已完成静态审查、语法与引用检查，并通过本地源码浏览器联调验证库存刷新与拦截、普通现金结账、Web 串行模拟出货、补货草稿及失败重试。兑换扫码入口已通过相关单元测试；未运行物料 build 或发布配套产物。原开发产物加载问题、0 元及其他支付异常、原生真机验收仍待验证。

兑换页的 Select 对纯组合规格商品打开底部 SKU 抽屉（`CompensatedPisellContainer`）：高度随内容撑开，达到屏幕可用高度后仅列表滚动，标题与确认/取消按钮保持可见。复用 `catalogModel` 的实际组合列表与 `NumericStepper`，本次数量先保存在本地草稿，取消不改购物车。确认时按券组剩余额度及本机库存检查，通过原 `addProductWithFlow` 顺序提交明确的 SKU 和数量，沿用普通购物车用券规则；已成功的行从草稿移除，后续行失败不会造成重复添加。其它需要选项、套餐或称重输入的商品继续使用原详情流程。抽屉期间暂停扫码和闲置退出，离开当前订单或页面时丢弃草稿。

Sale 与 Redeem 共用同一购物车和实时券列表，券不按来源预留，也不注册 `productVoucherGroups` 或向商品附加 `productVoucherGroupKey`。兑换分组仅用于展示，已选和剩余数量按商品行实际 `good_pass` 抵扣记录统计；Apply、Unapply、登录资产变化后重新计算。Unapply 本身保留普通购物车行为；若用户随后在兑换页明确新增商品，可通过同一 `setDiscount` 操作重新启用对应券。兑换页的 Also buy something? 进入购买列表并保留当前订单、客户和券；Next 进入同一购物车核对页。

售货机购物车通过 `saleDetailModuleConfig.cartItems.renderProductLineItemStatus` 在 A12 右侧展示兑换印章：仅商品行实际分配了 `good_pass` 时替代价格、编辑和数量区域，保留原删除按钮及确认流程；Unapply 后恢复正常展示。印章由内联 SVG 绘制圆框、勾选和多语言文字，颜色跟随卡片主题色 `--pc-a12-accent`，保留倾斜样式，不依赖图片文件；以透明背景、不降低不透明度的水印形式相对右侧区域居中绝对定位，不撑高卡片、不拦截点击，删除按钮保持在上层。文字跟随页面语言，未提供的语言回退英文。此配置仅由 VendingSale 启用，普通 Kiosk、H5、POS 及 A12 的其它调用方保持原样；不改变订单金额或券分配逻辑。

兑换列表隐藏本机没有匹配商品且没有已抵扣购物车项的券组，也不把它们计入可选数量；目录变化后仍保留已有抵扣项供用户减少或移除。`product-voucher` 扫码范围仅在兑换列表可见时启用，进入购物车后恢复普通销售扫码范围，返回列表再恢复商品券扫描。

Find Your Sale 支持手动输入订单号或码，点击 Find Sale 或按 Enter 后复用入口扫码处理链路；空白不提交，处理中禁止重复提交，结果提示与页面路由和扫码一致。扫码仍会自动填入并提交。

购买、Find Your Sale 和商品券选择页统一通过 `VendingActionBar` 渲染悬浮底栏，共用 Kiosk 的尺寸、离底距离和窄屏规则；各页滚动内容按同一变量预留底栏空间。购买底栏的 Promotion 位于购物车与 Next 之间，复用 Kiosk 现有优惠面板及其中的扫码/手动输入功能。待支付为 0 时隐藏 To pay；售卖机使用的设备端自定义目录购物车同时隐藏金额明细，将 Checkout 改为 Confirm，仍调用原结账动作。普通 Kiosk、H5 和 PC 的默认展示不变。Choose Your Items 不提供 Find Sale 或返回兑换列表入口，普通购买扫码不改变当前页面。兑换展示已选数量/额度，Also buy something? 为居中、按内容宽度展示的无边框按钮；查找页保留表单提交动作。
