# PRD：UR#57 活跃退款资产禁止换货

Status: ready-for-agent

---

## Problem Statement

换货创建目前没有检查旧资产是否存在未结束的退款。换货可能在退款审批或资金处理期间改变资产关系，造成资产状态、套餐、钱包和退款处理对象不一致。

七月退款方案又将企微审批状态与实际退款处理状态分离，因此不能只用“审批是否通过”判断退款是否已经结束。

## Solution

创建换货前，根据旧卡或旧设备主键批量查询活跃退款。企微审批中、历史已退回、或企微已通过但退款业务处理尚未成功时拒绝换货；已拒绝、已撤销/删除、已软删除、或退款处理已经成功时放行。

校验进入换货创建 Application 用例并与换货单创建保持事务一致。失败返回统一中文业务错误，前端只展示后端结论，不复制退款状态机。

## User Stories

1. 作为运营人员，我希望退款仍可能影响资产或资金时不能创建换货。
2. 作为运营人员，我希望退款被拒绝、撤销或完成后可以正常换货。
3. 作为运营人员，我希望拦截后已填写的换货资料仍保留，方便更换资产重试。
4. 作为维护人员，我希望审批状态和退款处理状态共同决定活跃性，避免把“审批通过但处理失败”误认为完成。
5. 作为维护人员，我希望校验失败不产生换货单、资产占用或迁移半成品。

## Implementation Decisions

### 活跃退款定义

- 退款 `status=1`（待审批/企微审批中）时拦截。
- 历史退款 `status=4`（已退回）时拦截；新企微流程不再产生该状态，但必须兼容存量数据。
- 退款 `status=2`（审批已通过）且 `processing_status!=2` 时拦截，包括待处理、处理中和处理失败。
- 退款 `status=2` 且 `processing_status=2`（业务处理成功）时放行。
- 退款 `status=3`（已拒绝）或 `status=5`（已撤销/审批已删除）时放行。
- 已软删除退款不参与判断。
- 同一资产只要存在任意一条活跃退款即拒绝；不因另有一条已结束退款而放行。
- 卡使用退款记录的 `iot_card_id`，设备使用 `device_id`；不得依赖可变化的资产展示标识符做关联。

### 换货创建与一致性

- 复用 `POST /api/admin/exchanges`，不新增预检查 API。
- 校验对象是请求中的旧资产；新资产是否可用继续由换货现有规则判断。
- 查询能力接受一组卡 ID 和设备 ID，一次返回存在活跃退款的资产集合；禁止逐资产 N+1。
- 活跃退款校验必须在换货创建事务内完成，并位于任何换货单、资产占用、状态修改、客户绑定或钱包迁移之前。
- 校验失败时整个创建用例不产生数据库副作用，也不调用外部系统。
- 拒绝返回 HTTP 403、`code=1005`、`msg=该资产存在退款申请`、`data=null`；服务端日志和审计记录具体资产与命中原因，客户端不暴露其他人的退款详情。
- 查询失败转换为统一内部错误；不得把数据库错误当作“没有活跃退款”继续创建。
- 这是换货创建的业务不变量。按渐进 DDD 规则，至少将完整“创建换货”用例收口到 Application/Domain；旧 Exchange Service 只能作为过渡门面，不能在事务外追加一次易被绕过的检查。
- 后台、未来批量入口或其他调用创建换货的内部路径必须复用同一用例，不得只在 Handler 校验。

### 与 UR#35 的依赖

- `processing_status` 和 `status=5` 由 UR#35 退款企微终态模型提供；UR#57 不创建另一套退款状态字段。
- UR#57 的活跃退款策略必须引用退款领域/Query 中的正式常量和语义，不硬编码一份与 UR#35 分叉的状态表。
- 推荐先完成 UR#35 的迁移和状态模型，再接入 UR#57；若并行开发，双方先共享同一数据库契约后再编码。
- 企微审批明细、审批人和意见不参与本次判断；本地退款业务状态与处理状态是判定依据。

### 前端

- 前端不增加退款预检查请求，也不自行维护审批/处理状态映射。
- 创建失败时在表单顶部或对应资产处展示“该资产存在退款申请”，并保留所有已填内容。
- 用户替换旧资产后可重新提交；提交期间保持现有防重复交互。
- 成功响应和换货后续流程不变。

## Testing Decisions

- 策略单元测试覆盖状态 1、2、3、4、5，且状态 2 覆盖处理状态 0、1、2、3。
- PostgreSQL 集成测试分别创建卡退款和设备退款，验证软删除、多个历史退款和任一活跃记录命中的组合。
- HTTP 集成测试穿过认证、权限、Handler、Application、Store 和统一错误处理，验证拒绝响应及成功路径。
- 验证拒绝后换货单数量、旧/新资产状态、客户绑定、钱包和套餐均未变化。
- 验证退款查询按一组资产一次完成；批量能力不得随资产数线性增加查询次数。
- 验证数据库查询异常返回脱敏 500，不能降级放行。
- 与 UR#35 联调验证：企微审批中拦截、驳回放行、撤销放行、审批通过但处理失败仍拦截、处理成功放行。
- 前端人工验收验证错误展示、表单保留和更换资产重试。

## Out of Scope

- 不改变退款创建、审批、退款金额或资金处理规则。
- 不新增本地审批动作或前端退款状态预判。
- 不阻止退款完成后的换货。
- 不清理或回填历史退款数据；仅兼容历史已退回状态。
- 不改变新资产选择、换货物流、迁移或完成规则。
- 不通过本需求新增批量换货 API。

## Further Notes

- 推荐实施顺序：UR#35 退款状态模型 → UR#57 活跃退款策略 → 换货创建联调。
- “审批通过”不等于“退款完成”是本需求最重要的验收边界。
