# HDLKiosk 交易安全

> 范围：购物车一致性、Checkout、Payment、Reset、错误与员工处置。

## 1. 安全原则

1. Host/OS 是订单、金额和支付的最终权威。
2. 写命令返回成功不等于业务事实已经可见。
3. timeout/unknown 可能已经生效，不能自动重试危险操作。
4. payment pending/unknown 时不能重付或修改订单；明确的 `session.reset` 是唯一恢复出口。
5. 当前不做跨刷新会话恢复；刷新后的订单与支付处置由 Host/OS 负责。

Operation timeout 只保护不应无限等待的机器阶段，不能限制顾客完成选择、设备支付或跨屏授权的时间。会等待规格、OS 业务弹窗、支付设备或外部事件的 Operation 不声明 `timeoutMs`，由 Host 的完成、取消或终态结果结束；登录认证 UI 同样在 Operation 外独立等待。纯机器调用可以声明超时，例如认证完成后的 `identity.sync`。未声明超时的 Operation 等待期间仍持有冲突域，因此 Host 必须保证正常完成、取消和页面销毁路径最终可终结调用。

## 2. Cart Mutation

所有 add/update/remove/identity/fulfillment 写操作按需要执行：

```text
Host command
→ waitForCartConsistency
→ recalculateSummary
→ validate narrow writer receipt
→ writer receipt 确认目标写入
→ return succeeded / failed / partial / unknown
```

Host 无法证明写后事实时，Operation 不能返回 succeeded。Cart 和 Customer 互斥，避免会员重报价与加购并发。

`waitForCartConsistency` 的成功结果是 Host 的权威一致性屏障。`Projection.cart.consistencyPending` 用于页面禁用和结账前复验，但 React 提交可能晚于屏障 Promise 完成，因此不得在同一写操作调用栈中用该展示态反向推翻已成功的屏障；否则连续加购会产生假 `unknown`。屏障本身失败、Summary 重算失败或明确的写后事实未观察到，仍按 `partialFailure/unknown` 阻断。

## 3. Checkout Prepared Freshness

Prepare 不是某个页面之前或某个 Workflow Resolution 的固定副作用。Checkout Coordinator 把它作为所有支付 dispatch 的前置条件，对当前事实版本执行 `ensurePrepared(currentVersion)`。

事实版本至少包括 session epoch、cart fingerprint、customer/voucher/promotion revision、Summary 应付金额/币种、Requirement 集合和支付能力上下文。状态为：

```text
idle | preparing(version) | prepared(version, token)
     | stale(previousVersion) | failed(version, reason)
```

Prepare 检查：

- cart 非空且不在 consistency pending；
- Activity Contract 汇总出的 Checkout Requirement 已对当前订单适用，且 Host Projection 已证明对应事实满足；当前 HDLKiosk 没有额外的主商品业务 Requirement；
- fulfillment 已写入或明确使用批准的临时策略；
- Summary ready、currency 和 due 合法；
- consistency 与 Summary 重算完成；
- 连续两次 Projection 的 observation/fingerprint 稳定。

成功后生成仅在当前 session 有效的 Prepare Token，记录 epoch、observation、fingerprint、currency、due、有效期和对当前订单实际生效的 `checkoutRequirementIds`。本地 Cart/Customer 写入将状态显式标记为 stale；外部 Voucher/Promotion/Host 写入由 dispatch 前的 fingerprint、Summary 和 Requirement 复验识别。Screen 只展示该状态，不触发或拥有 prepare。

## 4. Checkout Dispatch

所有 Checkout Operation（`checkout.completeNoPayment`、`checkout.payEftpos`、`checkout.requestStaffPaymentApproval`）在 Host dispatch 前执行同一套流程：

1. 调用 `ensurePrepared(currentVersion)`：同版本 token 可复用，缺失、过期或 stale 时对当前事实重新 prepare；
2. Requirement Registry 用最新 Workflow Context 和 Host Projection 重新计算适用要求并验证事实；Checkout Safety 确认 ID 集合与 Prepare Token 一致，再复验订单 fingerprint 和金额；
3. 标记本地 Checkout Safety 为 opening；
4. 调用 Host payment；
5. 根据明确 callback/Projection 归一化结果；
6. pending/unknown/succeeded 后持续冻结危险写；
7. confirmed failed/cancelled 可解冻；下次 dispatch 仍统一经过 `ensurePrepared`，事实未变时允许复用，变化后重新 prepare。

零元订单通过 `checkout.completeNoPayment` 二次复验 `due=0`，再调用 OS SalesSDK 的正式提交契约；不能因为 `due=0` 本地伪造 Payment Completed。提交后结果不明确时与支付 unknown 一样硬阻断，避免重复下单。

## 5. Payment 状态

| 状态      | 顾客可执行动作                                    |
| --------- | ------------------------------------------------- |
| idle      | 可继续编辑；支付前自动 ensure prepare             |
| pending   | 等待 Host，不可重付/reset                         |
| succeeded | 立即清空 Host 工作区并展示成功态；10 秒后结束会话 |
| failed    | 返回当前流程或支付选择；重试时重新 ensure         |
| unknown   | 阻塞页，只允许员工核对                            |

“We are checking your order” 表示支付或副作用最终结果无法确认，不是普通 loading。页面必须展示 trace reference，不能提供再次支付按钮。

硬阻断只保留给 P0 副作用：支付可能已派发/扣款、支付最终化无法确认，或 reset 可能部分完成。购物车、会员、履约、定制和 checkout 派发前的短暂不稳定停留当前页、显示 reference 并允许重试，不再将整个 Workflow 转为 blocked。

## 6. 会话边界与未来扩展

当前 Session、Coordinator Epoch、Prepare Token 和 Checkout Safety 都是页面生命周期内状态。刷新页面不会恢复原 Workflow 节点，也不会用浏览器存储推断支付结果。

Host/OS 必须承担跨刷新后的订单识别、支付查询和人工处置入口。在其能力明确前，HDLKiosk 不实现猜测式恢复。

Kernel 保留 `WorkflowLifecycle` 和 Actor 初始状态注入接口。以后需要跨刷新能力时，由 Kernel 外的独立模块：

1. 读取 OS 当前订单和支付事实；
2. 读取必要且最小的本地证据；
3. 形成 Entry、恢复节点或 blocked 的初始化决策；
4. 把初始状态交给 Actor。

该模块可以被添加或移除，而不改变 Workflow Compiler、Operation Coordinator 和 Screen。

## 7. Reset 与返回 Entry

普通 Reset 只有在 Payment 非 pending/unknown 时执行。支付明确成功后则由
`session.finalizeCompletedOrder` 立即执行相同的 Host 清理屏障，但保留 Payment Completed
会话和结果快照：

```text
clearAndReset
→ Host writer receipt 确认 products / bookings 为空
→ wait consistency
→ 保留成功页订单快照
→ Payment Completed
```

`sourceData` 是当前 render 的展示数据，可能比 OS writer receipt 晚一个 commit；清理操作不在同一调用栈中用展示数据反向否定已经确认的空订单。

完成后清理失败不改变已经确认的支付事实；返回 Entry 时必须再次执行验证式 reset，仍失败才停留并要求员工处理。

当前 HDLKiosk 不拥有 idle 计时或警告 UI。Payment Completed 的 10 秒倒计时和手动返回调用 Session Controller 提供的 `completeOrder(source)`；故障恢复调用 `recover(source)`。Runtime 对并发执行互斥，订单已完成清理时释放当前 Session，否则执行验证式 reset。不得直接 dispose Actor、释放入口锁或清空页面状态来冒充订单已清理。

## 8. 错误模型

错误统一包含：

- code、source、recovery；
- safeMessageKey；
- traceId；
- 可选 cause，仅用于本地诊断。

Screen 通过统一 Error Presentation 映射为安全文案。不要把底层异常、原始订单、条码、会员或支付数据展示给顾客或写入遥测。

## 9. Telemetry

正式 Host 需要接入当前 no-op telemetry Port，至少记录：

- session、entry 和 workflow；
- device connection、拒绝、去重和背压；
- Operation duration/result；
- consistency 和 Summary 收敛；
- checkout prepare/open/result、payment unknown；
- reset/return-to-entry/staff intervention。

只记录枚举、计数、耗时和 trace，不记录 PII、原始条码、支付参数或完整业务对象。

## 10. 员工处置待办

生产前为以下状态定义入口、鉴权、审计和 SOP：

- payment unknown；
- Cash waiting；
- OS 主商品加购的最终结果；
- reset failure；
- scale fault；
- OS/网络不可用。

员工操作不能破坏 payment freeze，也不能在没有权威结果时把订单标为成功。
