## MODIFIED Requirements

### Requirement: H1 发起换货单

系统 SHALL 提供 `POST /api/admin/exchanges`（需后台认证 `Auth=true`），用于发起换货单。

请求体 MUST 包含：`old_asset_type`、`old_identifier`、`exchange_reason`，可选 `flow_type`、`remark`。

`flow_type` 未传或为空时，系统 MUST 按 `shipping` 处理，以兼容旧后台调用；传入非空且不属于 `shipping/direct` 时，系统 MUST 返回参数错误。

当 `flow_type=shipping` 时：
- 请求体 MUST NOT 要求 `new_identifier`
- 请求体 MUST NOT 要求 `migrate_data`
- 系统创建成功后 SHALL 返回新建换货单信息（含 `id`、`exchange_no`、`flow_type=shipping`、`status=1`）

当 `flow_type=direct` 时：
- 请求体 MUST 额外包含 `new_identifier`
- 请求体 MAY 包含 `migrate_data`；未传时 MUST 按 `false` 处理
- 系统 MUST 在创建接口内完成新资产校验与换货完成事务
- 创建成功后 SHALL 返回新建换货单信息（含 `id`、`exchange_no`、`flow_type=direct`、`status=4`、`completed_at`）
- 任一步失败时 MUST 回滚整个事务，且 MUST NOT 保留半成品 `direct` 换货单

系统 MUST 校验：
- 旧资产存在且当前用户有权限
- 旧资产当前 `asset_status=2`（已销售）
- 同一资产不存在进行中的 `shipping` 换货单（`status IN (1,2,3)`）
- `direct` 场景下新资产存在
- `direct` 场景下新资产当前用户有权限
- `direct` 场景下新旧资产类型必须一致（卡换卡/设备换设备）
- `direct` 场景下新资产必须 `asset_status=1`（在库）
- `direct` 场景下新旧资产 `shop_id` 必须一致（含二者同为平台库存 `NULL`）
- 若旧资产存在客户绑定，则 `direct` 场景下新资产必须可承接绑定关系
- `direct` 场景下新资产不得存在 `status=1` 的有效客户绑定
- `direct` 场景下新资产不得已被其他 `shipping + status=3` 换货单占用

错误响应 MUST 至少包含：参数错误、资产不存在或无权限、旧资产状态不允许换货、存在进行中换货单、新资产不存在、资产类型不匹配、新资产非在库、新旧资产归属不一致、新资产已被占用、绑定无法承接、钱包冻结余额未处理、迁移失败。

#### Scenario: shipping 正常创建
- **WHEN** 后台以 `flow_type=shipping` 或未传 `flow_type` 发起换货，且旧资产为已销售并无进行中单据
- **THEN** 系统 MUST 创建 `status=1` 的换货单

#### Scenario: direct 创建即完成
- **WHEN** 后台以 `flow_type=direct` 发起换货且新旧资产校验通过
- **THEN** 系统 MUST 在同一事务内创建换货单并完成换货
- **AND** 返回结果 MUST 为 `status=4`

#### Scenario: direct 缺少新资产标识
- **WHEN** 后台以 `flow_type=direct` 发起换货但未传 `new_identifier`
- **THEN** 系统 MUST 拒绝创建并返回参数错误

#### Scenario: direct 迁移标记未传
- **WHEN** 后台以 `flow_type=direct` 发起换货且未传 `migrate_data`
- **THEN** 系统 MUST 按 `migrate_data=false` 创建并完成换货

#### Scenario: direct 完成失败不留半成品单据
- **WHEN** 后台以 `flow_type=direct` 发起换货，但完成事务中的绑定承接或迁移步骤失败
- **THEN** 系统 MUST 回滚整笔事务
- **AND** MUST NOT 查询到本次请求创建的半成品 direct 换货单

#### Scenario: 资产已有进行中 shipping 换货单
- **WHEN** 后台为同一资产重复发起 `shipping` 换货
- **THEN** 系统 MUST 拒绝创建并返回“存在进行中的换货单”

#### Scenario: 旧资产非已销售禁止换货
- **WHEN** 旧资产 `asset_status != 2`
- **THEN** 系统 MUST 拒绝创建并返回资产状态不允许换货

---

### Requirement: H2 换货单列表

系统 SHALL 提供 `GET /api/admin/exchanges`（`Auth=true`），支持分页与条件查询。

查询条件 SHOULD 支持：`status`、`flow_type`、`identifier`（资产标识搜索）、`created_at_start`、`created_at_end`、分页参数。

响应 SHALL 返回列表与分页元数据。
响应项 MUST 返回：旧/新资产标识、`flow_type`、`status`、`shipped_at`、`completed_at`。

#### Scenario: 按流程类型查询 direct 已完成单
- **WHEN** 运营查询 `flow_type=direct` 且 `status=4`
- **THEN** 系统返回所有 direct 已完成换货单并按创建时间倒序

---

### Requirement: H3 换货单详情

系统 SHALL 提供 `GET /api/admin/exchanges/:id`（`Auth=true`）查询换货单详情。

响应 MUST 返回旧/新资产信息、流程类型、收货信息、物流信息、迁移状态信息、`shipped_at`、`completed_at`。

错误响应 MUST 至少包含：换货单不存在或无权限。

#### Scenario: 查询 direct 换货单详情
- **WHEN** 查询一张 `flow_type=direct` 的已完成换货单
- **THEN** 响应 MUST 返回 `flow_type=direct`
- **AND** 收货信息与物流信息可以为空
- **AND** `completed_at` 必须存在

---

### Requirement: H4 发货

系统 SHALL 提供 `POST /api/admin/exchanges/:id/ship`（`Auth=true`）。

请求体 MUST 包含：`express_company`、`express_no`、`new_identifier`、`migrate_data`。

系统 MUST 校验：
- 换货单 `flow_type` 必须为 `shipping`
- 当前状态必须为 `2`
- 旧资产当前必须仍为 `asset_status=2`（已销售）
- 新旧资产类型必须一致（卡换卡/设备换设备）
- 新资产必须 `asset_status=1`（在库）
- 新资产当前用户有权限
- 新旧资产 `shop_id` 必须一致（含二者同为平台库存 `NULL`）
- 新资产不得存在 `status=1` 的有效客户绑定
- 新资产不得已被其他 `shipping + status=3` 换货单占用
- 系统 MUST 通过条件更新、行锁或等效机制确保发货成功时新资产仍满足在库且未被占用

成功后 SHALL：
- 更新新资产信息
- 更新物流信息
- 写入 `migrate_data`
- 记录 `shipped_at`
- 将状态改为 `3`
- 将新资产视为被当前换货单占用；在确认完成前不改变新资产 `asset_status`

错误响应 MUST 至少包含：非法状态、流程类型不支持发货、旧资产状态不允许换货、资产类型不匹配、新资产非在库、新旧资产归属不一致、新资产已被占用、资产不存在或无权限。

#### Scenario: direct 单据禁止发货
- **WHEN** `flow_type=direct` 的换货单调用发货接口
- **THEN** 系统 MUST 拒绝并返回流程类型不支持该操作的错误

#### Scenario: 新资产类型不一致
- **WHEN** 旧资产为 `iot_card` 且新资产为 `device`
- **THEN** 系统 MUST 拒绝发货并返回“换货资产类型必须一致”

#### Scenario: 新资产已被其他换货单占用
- **WHEN** 新资产已经作为其他 `shipping + status=3` 换货单的新资产
- **THEN** 系统 MUST 拒绝发货并返回新资产已被占用

---

### Requirement: H5 确认完成

系统 SHALL 提供 `POST /api/admin/exchanges/:id/complete`（`Auth=true`）。

系统 MUST 校验：
- 换货单 `flow_type` 必须为 `shipping`
- 当前状态必须为 `3`

系统 MUST 在**单一数据库事务**中执行完成换货动作。该事务至少包括：
- 校验新资产快照完整且新资产仍满足换货条件
- 校验旧资产仍为 `asset_status=2`（已销售）
- 校验新资产仍为 `asset_status=1`（在库），且未被当前换货单之外的有效记录占用
- 旧资产 `asset_status -> 3`
- 新资产 `asset_status -> 2`
- 若旧资产存在 `PersonalCustomerDevice` 绑定，则绑定切换到新资产资产绑定键
- 若 `migrate_data=true`，执行全量迁移事务（见 `exchange-data-migration` 能力）
- 写入 `completed_at`
- 换货单状态更新为 `4`

成功后 SHALL：
- `migration_completed=true`（若执行迁移）
- 换货单状态更新为 `4`

错误响应 MUST 至少包含：非法状态、流程类型不支持确认完成、旧资产状态不允许换货、新资产状态不允许换货、迁移失败、绑定无法承接、钱包冻结余额未处理、换货单不存在或无权限。

#### Scenario: 需要迁移并完成
- **WHEN** `shipping` 换货单状态为 `3` 且 `migrate_data=true`
- **THEN** 系统 MUST 在同一事务成功后将状态变为 `4` 并记录迁移结果

#### Scenario: 不迁移也必须完成切换
- **WHEN** `shipping` 换货单状态为 `3` 且 `migrate_data=false`
- **THEN** 系统 MUST 仍然在事务内完成旧资产状态切换、新资产状态切换、绑定切换和单据完成

---

### Requirement: H6 取消换货

系统 SHALL 提供 `POST /api/admin/exchanges/:id/cancel`（`Auth=true`）。

系统 MUST 仅允许 `flow_type=shipping` 且 `status IN (1,2)` 时取消，成功后状态更新为 `5`。

系统 MUST 禁止已发货单取消（`status=3`）。
系统 MUST 禁止 `direct` 单据进入取消分支。

#### Scenario: 已发货单取消失败
- **WHEN** `shipping` 换货单状态为 `3` 发起取消
- **THEN** 系统 MUST 返回状态非法错误

#### Scenario: direct 单据取消失败
- **WHEN** `direct` 换货单发起取消
- **THEN** 系统 MUST 返回流程类型不支持该操作的错误

---

### Requirement: H7 旧资产转新

系统 SHALL 提供 `POST /api/admin/exchanges/:id/renew`（`Auth=true`）。

系统 MUST 校验旧资产当前 `asset_status=3`（已换货），并执行：
- `generation + 1`
- `asset_status -> 1`
- 清除累计充值/首充相关状态
- 清除个人客户绑定
- 创建新空钱包

系统 MUST 保留历史数据，不执行历史删除。
系统 MUST NOT 在换货完成阶段修改旧资产 `generation`；`generation` 仅在 `renew` 阶段递增。

错误响应 MUST 至少包含：资产状态不满足转新条件、换货单不存在或无权限。

#### Scenario: 旧资产未处于已换货状态
- **WHEN** 旧资产 `asset_status != 3` 发起转新
- **THEN** 系统 MUST 拒绝并返回“资产当前状态不允许转新”
