Files
junhong_cmp_fiber/.scratch/ur57-block-exchange-active-refund/PRD.md
2026-07-21 15:26:07 +09:00

90 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PRDUR#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 活跃退款策略 → 换货创建联调。
- “审批通过”不等于“退款完成”是本需求最重要的验收边界。