fix: 退回报销支持留痕删除(09-04-reimbursement-state-gap-finance-view)

这个提交包含在:
2026-09-10 16:38:37 +08:00
父节点 e5685f18b2
当前提交 73fcd5a65a
共修改 27 个文件,包含 696 行新增和 94 行删除
@@ -1,5 +1,33 @@
# 报销状态空隙与财务处理视角:设计
## 2026-09-10 追加阶段:退回删除最小持久化方案(已批准本地实施)
用户在官方资料调研后确认实施:修改重提为主,删除为次;无需填写删除理由。易快报公开接口明确草稿/已驳回可删除,但未证明其物理存储或删除后发票释放方式;本项目留痕设计是自己的安全合同,不冒充行业统一实现。
票据释放前将当时仍关联的 PDF 文件引用、摘要保留为本单附件,通过报销原有材料权限读取;使用不可变存储对象,不读取票据重新绑定后的内容。删除审计记录票据与明细引用。历史撤回早已释放、没有关联证据的票据不猜测回填;本轮保留删除时实际仍存在的证据及原有流程记录,不宣称补全过去缺失的提交快照。
本阶段仅处理用户确认的退回删除与重新建单;下文 R1/R2 为旧交付合同,不重开其范围。执行副本基于已发布 e5685f1,独立于发票与付款副本。
### 核查结果
- 当前 DELETE /reimbursements/:id 只允许 draft,调用 reimbursementForm.delete;WorkflowInstance.reimbursement_form 为 onDelete:Cascade,其 tasks/history 继续级联。因此“退回/撤回到 draft 且有流程历史”同样不能继续物理删除,更不能只把 rejected 加入白名单。
- 模型只有 draft/reviewing/approved/paid/rejected/cancelled,无 deleted_at/deleted_by。cancelled 只表达状态,不能区分主动删除及其他取消,也不能单独保证所有列表/统计的删除语义。不借改写 description 或挪用付款字段保存删除标记。
### 推荐合同
1. 新增 nullable deleted_at 与 deleted_by(删除者引用)及相应索引;既有记录保持 null,不回填业务数据,不新增状态枚举。删除时记录原状态、删除者/时间和释放的票据引用至既有审计,状态落为 cancelled,前端依据删除标记显示“已删除”。这是明确的删除语义,不将 cancelled 本身冒充删除判据。
2. 仅 owner/submitter 且具备 expense.delete_own 可删 draft/rejected;只读全范围、管理员通配均不替代参与关系。仍在活动审批、有非取消付款占用或已付款的异常数据拒绝。重复删除先授权再幂等返回。
3. 未进入过 workflow 的普通 draft 保持现有删除路径;曾提交的 draft 以及 rejected 使用留痕删除。已有单据、明细、附件、workflow tasks/history 保留;正常列表/搜索/统计/导出排除已删除单据,旧深链仅向原有权人员呈现只读历史与已删除提示,不新增查看他人记录的权限。
4. 删除事务先锁单再锁票,记录关联引用后仅释放当前仍绑定本单的有效票;不改归属、不覆盖别单关联、不删除文件。旧单取消态及显式删除校验共同阻断提交/修改/绑定/审批/付款,发票能否用于新单仍执行原归属/去重/状态门禁。
5. 发票释放后历史材料读取从本次审计保存的引用与原单授权出发,不能因为票已换绑就授权旧单访问新单;只保存必要引用,不把完整票面原始 payload 写进审计。具体查询与材料快照边界在实现前以测试锁定,不把仅保留文件声称为历史材料关联已保留。
### 验证与回滚
- 本地先复现 rejected 拒绝和有历史 draft 的级联风险;随后覆盖双方参与人、非参与人、重复删除、活动流程/付款拒绝、旧深链只读、各列表旁路、删除与提交/绑定并发、释放票据重新建单。
- 隔离 PostgreSQL 与真实认证/浏览器验证“退回→删除→历史仍在→票据重用→新单提交”;公网不自动删除真实退回单,不将上次发布授权沿用到本迁移。
- 迁移仅增 nullable 字段;旧应用不理解删除标记,回滚后可能重新显示已删除历史。不得声称只换旧镜像行为无变化;首次产生删除记录后优先前向修复,必要回退需兼容删除过滤,不移除审计或猜测恢复已释放票据。
- 用户已批准本地迁移/跨层实施;生产迁移、提交推送及部署仍另批。本阶段不修改原 R1/R2 权限授予,不扩经理六个 view_all。
## 1. 设计边界
本任务包含两个独立变更单元:
@@ -1,5 +1,43 @@
# 报销状态空隙与财务处理视角:实施计划
## 2026-09-10 退回删除接续
- 用户要求继续剩余任务,先退回删除、后付款体验;沿既有 task,不新增管理任务。独立副本 `artifacts/isolated-worktrees/20260910-rejected-delete` 基线 e5685f1,官方 current 已对齐,无新分支,未混入其他工作树未提交内容。
- 已核对最新 PRD 追加范围及付款 task 综合方案。现有 DELETE 会级联删除 workflow 历史;draft 本身不是可物理删除的充分条件。当前模型没有专用删除标记,不能只放开 rejected。
- design 顶部补充 deleted_at/deleted_by 最小迁移、审计与材料引用、参与人授权、查询排除、锁顺序及旧应用回滚风险;行业调研后用户确认实施,本轮已编写迁移并仅在一次性合成数据库执行,未运行生产写入。
- [x] 当前实现与历史级联风险核查。
- [x] 用户在行业调研后确认本地实施,生产操作与提交仍另批。
- [x] 实施服务端删除/材料历史/查询边界和双端入口,补充针对性单测及真实隔离场景;没有声称新增测试先在旧代码上完整跑红。
- [x] 完成真实隔离流程、并发/权限反例与前端回归,保留独立副本作为只读复核入口。
- [ ] 独立只读复核(当前 inline,仅主会话自审,不冒充独立 reviewer)。
- [ ] 通过后再按授权提交发布;付款体验在独立副本与原 task 承接,不一并提交。
### 实现与证据
- 运行时:仅 draft/rejected + 删除权限 + owner/submitter 可删;拒绝活动流程/待办、非取消付款占用、paid_at、异常关联票据。重复留痕删除先授权,再幂等返回。
- 无流程历史的 draft 原样物理删除;历史 draft/rejected 保留 form/items/workflow/attachments,写 cancelled + deleted_at/deleted_by,并保留票据 PDF 引用/摘要;删除审计与释放同事务。
- 查询:共享报销可读条件、财务台账、原始 SQL 统计、门户计数及项目汇总排除删除标记。详情、材料按既有权限只读,进度不再要求继续处理。
- 并发补口:附件上传落库时重取单锁校验状态;项目关联使用条件 updateMany,防止读到可编辑状态后越过删除继续写入。
- 双端详情/列表允许退回删除,确认不填理由;手机删除位于“更多”,删除成功提示使用“报销单”而非“草稿”。历史详情及打印有删除标识。
- Prisma validate、相关 node --check、前端 typecheck/build 已通过。
- 后端最终 focused:22 文件、279/279,通过记录在 artifacts/qa-flow/20260910-rejected-delete/unit-final.log。运行目录为本副本 server;命令为 PAYMENT_ORDER_PYTHON_BIN=<既有 BIFF8 虚拟环境解释器> npx vitest run tests/unit/routes/reimbursements*.test.js tests/unit/routes/reimbursement.submit-side-effects.test.js tests/unit/routes/paymentBatches.reimbursement.test.js tests/unit/services/reimbursementVisibility.test.js tests/unit/services/financeLedger.test.js tests/unit/routes/exports*.test.js tests/unit/routes/search.test.js tests/unit/routes/portal.test.js tests/unit/routes/projects.test.js。
- 页面回归:本地构建产物 preview,system-action-confirmation.spec.ts --grep '报销',6/6;ui-preview.log。与真实隔离 HTTP/browser 分开计,不把 mock 页面回归当真实服务验收。
- 隔离:RUN_ISOLATED_DB_TEST=1 RUN_RETAINED_DELETE_TEST=1 RUN_BROWSER_TEST=1 node scripts/qa/test-invoice-first-http.cjs;真实 PostgreSQL、密码登录/JWT、HTTP、桌面/手机;合成票据,OCR 为手动调度真实 worker + mock provider。涵盖退回/驳回/撤回→删除→原历史/凭证可读→票据重用→新单提交;删除与 submit/bind/delete 锁屏障;审计异常全事务回滚。
- 增补原始统计/台账排除的最终复跑记录:final-isolated.log,PASS_REAL_DB_HTTP,退出 0;临时容器、合成文件和浏览器已清理。不以之前只输出 PASS 但退出 143 的管道运行替代完整退出结果。
- UI strict/self-test、错误解释 strict、process contract baseline、Trellis governance、git diff --check 通过;文档 registry 为 errors=0、warnings=1(既有 22 项未注册文件),不扩修。
### 调试与证据边界
- 首轮 BIFF8 回归 1 项失败为系统 python 缺 xlwt,使用既有专用解释器后 76 项付款相关测试通过;未修改付款产品逻辑。
- 隔离 fixture 最初复用了同一文件摘要,被真实重复发票门禁正确拒绝;改为每个场景独立合成 PDF,不通过假摘要/放宽去重绕过测试。
- 一轮新手机“更多”定位与旧构建产物不一致而超时;重建后真实双端通过。原日志留存,不把测试器问题写成业务通过。
- Vite dev 冷启动首项停在登录控件加载,终止该 mock 运行后改用构建产物 preview,6 项重跑通过;没有要求真实用户代测,也没有修改生产登录。
- 本轮保留删除时实际存在的流程、明细和凭证,不回填历史已释放票据、不补造过去提交版本。不可承诺完整重建原本未留存的历史快照。
- 当前状态:代码已实现、本地验证完成;发布未执行;真实业务删除未执行;等待独立复核及提交/部署授权。数据库 nullable 增量不等于旧应用可无损回滚:旧应用可能重新显示删除记录,需前向修复或经过审查的兼容回退。
- 主会话已只读检查任务 diff 的状态/参与人守卫、释放锁顺序、材料授权、查询旁路、迁移和回滚边界;独立 reviewer 尚未执行。复核包 artifacts/qa-flow/20260910-rejected-delete/changes.diff 包含本副本全部任务改动及未跟踪的迁移/测试文件,不含其他副本或生产数据。
下文是原 R1/R2 阶段清单,不能继承其无 migration 前提或勾选为本追加阶段的验收结果。
## 0. 启动门
- 用户批准本版 `prd.md`、`design.md`、`implement.md` 后,才运行 `task.py start`。
@@ -2,6 +2,19 @@
## Goal
### 2026-09-09 后续阶段:退回报销的删除与重建
本阶段归属复用,不重开原 R1/R2 的全部范围;综合顺序与验证见 09-08-reimbursement-payment-closeout 的最新 design/implement。用户已明确业务取向:退回后既能修改原单重提,也能删除旧单再创建报销提交;界面使用“删除”,不要求员工理解底层作废/留痕术语。2026-09-10 行业调研后用户确认本地实施;本追加阶段允许 design 中的 nullable 删除标记迁移,旧 R1/R2 的“无 schema 变更”约束不适用于本阶段。提交、推送及部署仍另批。
- 删除退回单必须保留必要的提交、审批、退回及删除审计记录,不能只把 rejected 加入现有物理删除状态白名单。核对“退回为 draft 且已有 workflow 历史”的路径,不能只按显示状态判断是否可硬删除。
- 删除后旧单不能重提、审批、进入付款或继续修改,不再占据正常待处理列表;有权的历史/审计访问仍遵循原范围,普通列表/搜索/统计/深链行为必须一致。
- 按原申请人/合法代填参与关系设计权限,不能把管理员或 view_all 的全量读取当成删除授权;明确本人和非参与人的正反例。
- 删除与提交/审批/发票绑定并发时,只允许一个合法终态;检查活动审批、付款占用及最终状态,审批中/已通过/已支付不因本次改动变为可删。
- 解除旧单对发票的有效占用,原发票/附件证据不随历史单物理抹掉;只有仍符合当前归属、状态及去重条件的发票才可用于新单,不能覆盖其他单据的绑定或自动转移别人发票。
- 重复删除、旧深链、撤回再删除、发票重新使用均有回归。不批量删除既有真实退回单,是否删除由申请人决定,不替员工执行。
本阶段验收:桌面/手机退回单可取消或确认删除;修改重提保留;历史草稿不物理删除;本人及合法提交人可删,view_all/通配不替代参与关系;活动审批/付款/已付拒绝;列表、搜索、统计、导出和门户计数不展示删除记录;已删除深链只读且无“待处理”指引;发票重用不改变旧单凭证;删除与提交/绑定/重复删除并发及审计失败回滚可复现。
消除报销从审批通过到付款批次创建之间、以及报销回到草稿状态之后的两个运营空隙:财务应能在付款页直接发现并处理尚未组批的已通过报销,同时“全范围”不得暴露他人从未提交的私人草稿,但必须保留对已经进入过流程的退回或撤回草稿的追踪能力。
本任务来自真实业务单据 `0014` 的首次使用反馈。两个 Requirement 共享“状态空隙必须有明确责任人和可见入口”的产品原则,但必须保持代码边界、测试证据和回滚点独立,不能因一项回滚而被迫撤销另一项。
@@ -2,7 +2,7 @@
"id": "reimbursement-state-gap-finance-view",
"name": "reimbursement-state-gap-finance-view",
"title": "报销状态空隙与财务处理视角",
"description": "修复已通过报销在组批前缺少财务处理入口,以及全范围读取暴露从未提交草稿的问题。",
"description": "修复已通过报销在组批前缺少财务处理入口、草稿读取边界;追加退回报销留痕删除和发票重用。",
"status": "in_progress",
"dev_type": null,
"scope": "server,client,tests,spec",
@@ -21,6 +21,6 @@
"children": [],
"parent": null,
"relatedFiles": [],
"notes": "业务提交:R2=8e0af82,R1=5fe9f78。",
"notes": "历史业务提交:R2=8e0af82,R1=5fe9f78。2026-09-10 退回留痕删除复用本任务,独立副本 20260910-rejected-delete;本地实现与验证完成,待独立复核及另批提交/部署,不继承旧阶段已发布结论。",
"meta": {}
}