# PiPayment integration

`@pisell/private-materials` 只保留 EFTPOS 宿主 UI 与交互适配。Headless SDK 的唯一
生产实现是独立包 `@pisell/pi-payment-sdk`，本包不再保留 SDK 副本。

- `host.ts`：从 `appHelper.utils.getPiPaymentSDK()` 获取宿主管理的 SDK。
- `paymentUI/`：承载统一的 `PiPayment` 分发入口和宿主 SDK 解析能力。
- `paymentUI/eftpos/`：承载 EFTPOS 组件、页面 schema、message resolver 与领域结果适配。

宿主应在一个登录生命周期内返回同一 SDK 实例，退出登录、切换租户或环境时
销毁并清空实例。UI 每次打开支付时都重新调用 getter，不跨登录缓存。

```ts
let piPaymentSDK: PiPaymentSDK | undefined;

appHelper.utils.getPiPaymentSDK = () => {
  if (!piPaymentSDK) {
    piPaymentSDK = new PiPaymentSDK({ environment, getToken, logger });
  }
  return piPaymentSDK;
};
```

## 公开入口

包入口导出 `PiPayment` 及其 UI 契约，以及
`resolvePiPaymentSDK` / `PiPaymentAppHelperUtils`。SDK 契约请直接从
`@pisell/pi-payment-sdk` 导入。

EFTPOS UI 的宿主事件统一为扁平的 `PiPaymentChangeEvent`。业务调用方直接读取
`paymentNumber`、`transactionNumber` 及当前事件的金额或错误字段；完整 SDK
result/error、页面、Session 和 attempt 生命周期不会暴露。不再提供 `event.params`、
`event.snapshot` 或历史 Checkout 命名字段。

每次支付 attempt 创建后，UI 会向宿主发送一次 `processing`；超时打印请求使用
`printOnTimeout`，随后按主动关闭语义发送 `closed` 并释放当前 Session。
Provider 返回的普通小票由 UI 直接派发 `print_string` DeviceTask，不再通过
`printRequested` 交给业务调用方处理。

SCI Provider 的成功小票只读取统一 `payment.receipt`，不回退
`provider_response` 中的厂商旧字段。`receipt` 按 `type` 区分 `merchant`、
`customer`、`signature` 和 `unknown`；当前成功打印忽略 `signature`，其余按
`merchant`、`customer`、`unknown` 的顺序打印。空数组表示没有可用小票。

缺少 getter、getter 返回空值或 SDK 已销毁时，UI 会拒绝启动支付并记录诊断。
`app.getPlugin('piPayment')` 和 `app.piPayment` 不再是支持的解析方式。

## EFTPOS UI 边界

`PiPayment` 通过 imperative ref 保留现有 `open(options)` / `close()` 接入方式。
宿主必须通过 `type={PiPaymentType.Eftpos}` 显式选择 UI 流程；当前只实现 `eftpos`，
未来 online 等支付 UI 将作为新的 `PiPaymentType` 判别分支接入。
内部 `usePiPaymentEftposFlow` 只负责组合展示状态，具体职责分为：

- `flowUtils`：无 React 依赖的首屏、Reader、失败参数和 attempt 参数决策。
- `usePaymentSessionController`：Session 生命周期、支付命令、action lock、任务去重和销毁。
- `useSessionEventHandler`：SDK 事件到页面状态和宿主事件的唯一映射入口。
- `useNetworkFlow`：浏览器网络监听、显式网络状态优先级和断网页恢复。

跨 hook、SDK 回调和公开动作使用稳定函数引用；动态 options、state、context 和 logger
通过当前 render 或 ref 读取，避免重渲染触发重复 Session、Reader 查询或事件监听。

UI 保持以下业务行为：固定金额跳过金额页、单 Reader 自动选择、失败重试、签名和
取消 action、打印/打印超时、人工确认、成功延迟通知、断网恢复以及关闭/卸载销毁。

Adyen POS 的签名属于交易成功后的本地人工复核，不复用普通 `signature` action：

- `processing` 且终端要求签名时继续轮询，并禁用 Abort。
- SDK 收到成功终态后释放请求资源，通过 Provider 中立的 `postSuccessReview` effect
  暂缓 UI 向宿主发送 `success`。
- 接受签名后发送 `success`；拒绝签名需要二次确认，且确认后仍发送 `success`。拒绝
  不会调用后端 action，也不会自动退款。

Checkout 只把实体 `EFTPOS_ADYENPOS` 映射到 PiPayment 的 `adyenpos` Provider。
Adyen Alipay/WeChat Scan 不属于本接入，仍沿用原流程；共享灰度开关关闭时 Adyen POS
也继续回退旧 EFTPOS 流程。

## 文案边界

- SDK 只输出 `PaymentDisplayText`：稳定 message key 或标注来源的动态 literal。
- UI 通过唯一 resolver 依次应用宿主 resolver、宿主 messages、默认 Provider/UI 文案和 fallback。
- 组件和页面不得按 Provider 判断文案，技术诊断信息不得直接展示。
- 定制文案通过 `PiPayment` 的 `messages` 或 `messageResolver` 注入，无需修改 SDK。

## 目录约束

- `PiPayment` 入口测试位于 `paymentUI/__tests__/`，EFTPOS 测试位于
  `paymentUI/eftpos/__tests__/`。
- Headless SDK 不导入 UI；UI 只通过独立包的公开类型和 Session 事件交互。
- UI 不允许深层导入 SDK 的 schema、locales 或 Provider 实现。
- provider 原始 response 和 action payload 在 SDK 内保持不透明，页面只渲染中立 schema。


### 支付日志

每次 `open` 创建独立的 `PiPaymentLogger`，通过 `paymentLoggerRef` 交给各 hook 使用。
日志公共字段位于 `metadata` 顶层：`salesId`、`paymentNumber`（宿主支付标识）、
`deviceNumber`、`provider`、`sessionId`、`transactionNumber`（后端交易号）、
`attemptIndex` 和同次 open 内递增的 `sequence`。未产生的标识为 `null`。
Session 创建后补充 `sessionId`；新 attempt 开始时清除旧交易号，仅在 SDK 的
`attemptCreated` 事件中绑定新交易号。关闭期间保留上下文，完成清理后释放当前实例。
各处日志不重复填写公共标识，由实例统一补齐；原始 payload/params 中的字段照常保留。

SDK 回调的完整事件保存在 `metadata.payload`，包括 `data`、`payment`、`attempt`、
`result`、厂商返回、错误和小票等事件实际携带的字段；不再用字段数量或少量摘要替代。
这里记录的是 SDK 对外事件，不能补回 SDK 内部未传出的 HTTP `response.data`。
SDK 调用日志保留业务参数（付款码 `authCode` 仍不写入日志），宿主通知保留实际发出的事件。

打印和查询任务在派发前调用 `snapshot()`，固定当时的上下文及宿主日志端口；重试、
关闭或重新 open 后的异步结果仍归属于原任务。任务派发完成只表示 `dispatch` 已返回，
不代表设备打印完成或查询已有支付结果。

UI 日志入口将 `metadata` 原样传给宿主 `logger.addLog`，序列化、错误对象处理和存储
由宿主负责，不修改原始 payload。该入口同时收集本次 open 的内存 timeline：
每条包含序号、时间和事件名，支付/退款/action 命令与最终结果保留详情，其他事件仅记摘要。
飞书上报把公共信息放在头部，交易号和 attemptIndex 随条目记录。时间线副本支持
Error、BigInt 和循环引用的序列化，并排除认证 Header、Token、付款码。

`reportToFeishu()` 在关闭、成功自动关闭、卸载或打开失败时取出并清空缓冲，异步发送一次。
`snapshot()` 与原实例共享 timeline 和上报标记，关闭后的迟到日志仅写宿主，不补报。
上报函数独立实现在 PiPaymentUI 的 `sendWarningLog.ts` 中。在该文件的 `piPaymentWebhook.test/prod` 填写
支付专用机器人完整 webhook URL（含独立 key），未配置时跳过发送，不使用普通告警群。
本实现不扩展 SDK HTTP 日志，不持久化、不自动重试，也不增加分片发送。


## Adyen Scan 专用付款弹窗

`adyenwechat` / `adyenalipay` 且 `mode: 'pay'` 时，由 EFTPOS 的
`renderPage` 渲染 `adyenScan/AdyenScanContent`。所有渠道共用一个 ref、
`usePiPaymentEftposFlow` 状态源和主 `CompensatedPisellContainer`；入口根据
`isAdyenScanPayment(options)` 调整标题、关闭行为和样式类，通用容器不感知支付渠道。
设备选择、金额编辑、扫码、结果及网络页面仍沿用原 flow page 阶段。

扫码样式仅位于 `.pi-payment-adyen-scan-container` 下，弹窗以支付页为基准
固定为 646px（小屏最高为视口高度减 48px），设备列表单独滚动。
金额编辑复用原 `AmountPage`；退款及其他 Provider 使用原页面。

公共 controller 统一创建、清理 Session，管理打开占用、回调代次与延迟启动任务。
`adyenScan/useAdyenScanController`、`useAdyenScanEvents` 在同一状态源上组合
公共命令和事件处理器，只扩展扫码换码、重试和成功完成时机；网络监听与副屏同步
各保留一个入口。`useAdyenScanPresentation` 负责二维码展示状态，
`useAdyenScanActions` 负责容器关闭与页面按钮共用的二次确认。

- 二维码使用 SDK schema 的 `expiresAt`；小于 30 秒显示倒计时，过期后灰化并清理副屏二维码。
- 有二维码时，Cancel 和右上角关闭共用二次确认；生成阶段无二维码时可直接关闭。
- 即将失效时 Regenerate 需要确认，过期后直接发起。确认期间收到支付结果会使确认失效；
  仅时间到期不会自动执行确认。
- 明确失败且允许重试时复用 Session；进行中或 unresolved 时重建本地 Session，
  复用最终金额和手续费，生成新的交易号及幂等标识，保持弹窗及宿主支付关联。
  此操作不会取消后端旧交易，旧交易继续由既有后台任务查询。
- Complete 和 1.5 秒自动完成共用幂等动作，只发布一次 success，不追加 closed。
- 专用文案支持现有 messages/messageResolver，新增键前缀为 `ui.adyenScan.`，
  键名见 `adyenScan/locales.ts`；取消标题与说明继续使用现有 provider 消息键。
