借款还款同金额回单须经独立复核确认归属后方可核销(09-17-company-loan-principal-repayment)

- 同一出借人同金额的其他批次曾导出或上传回单(含确认失败后已取消的)时,
  最终核销入口要求已复核的归属确认;OCR 匹配与人工回单复核均不能绕过,
  证据不足保持待对账,本金与占用不变;存在候选时失败回单不能释放占用
- 新表 loan_receipt_attributions(迁移 20261006010000):绑定交易指纹、
  回单核验与人工录入证据、当时候选集合;依据类型不含时间吻合;复核人须
  当前有资格且独立于登记人、批次创建人、提交人、导出人、回单上传人、
  人工录入人与出借员工;失效记录在重新登记时标为 superseded,历史只增不改
- 借款回单上传取出借人锁,与核销候选检查串行;串行只保证按当时已知候选
  判断,核销之后才出现的新证据留待晚到结果对账流程处理
- 付款就绪检查反映归属要求;页面登记/复核/失效重登,写明同额分期每期需确认
- 测试:两个错误核销复现改为回归断言;证据绑定、确定性并发两种顺序、
  PC 浏览器完整登记复核重登流程;变异测试均变红

注意:借款功能尚不具备真实还款验收条件(失败回单与对账流程、离职后确认
失败、真实回单样本、手机端弹窗、独立复核待完成);未推送、未部署。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-06 20:20:51 +08:00
共同撰写人 Claude Opus 5.5
父节点 b639422211
当前提交 b88790d396
共修改 13 个文件,包含 718 行新增和 31 行删除
@@ -1,3 +1,27 @@
## 2026-10-06 C 回单批次归属保守拦截(用户批准实施;本地合成,未提交、未部署)
规则:同一出借人、同金额的其他借款批次只要曾导出制单或上传回单(含确认失败后已取消的),本批次的匹配回单就不能证明归属。
- 最终拦截在 `settleLoanRepayment`(`assertReceiptAttributed`):执行事务内 `claimVerifiedReceipts` 已为本项生成交易指纹认领,OCR 匹配与已通过的人工回单复核都经过此处;只有绑定同一交易指纹、且覆盖当前全部候选批次的已复核归属确认才能放行,否则 409 `loan.receipt_attribution_required`,本金与占用不变。
- 确认银行失败:存在候选批次即 409,不释放占用(失败回单暂无归属确认路径,留待对账流程设计)。
- 归属确认与人工回单复核分离(新表 `loan_receipt_attributions`,迁移 `20261006010000_loan_receipt_attribution`):财务登记依据类型(客户方流水号、附言含批次号、银行书面查询结果、其他候选均已对应其他交易或确认未付;不含“时间吻合”)+ 依据说明 + 排除其他候选的理由;绑定 `claimVerifiedReceipts(readOnly)` 计划认领的交易指纹和当时候选集合。独立复核人须当前有资格,且不是登记人、制单人、提交人或出借人关联员工;批准时回单证据或候选集合变化即拒绝。出现新候选后原批准失效。DB:每批次至多一条待复核/已通过(部分唯一索引)、复核人≠登记人、已复核记录与提案字段不可改、禁止删除。
- 页面:还款详情显示候选批次与归属状态,财务登记、经理/总经理独立复核;说明“时间吻合只能作辅助说明”。
- 代价:此后同一出借人同金额的正常还款(如固定金额分期)都需走归属确认。精确关联(附言写入批次号、OCR 提取客户方流水号/附言)待获准的真实建行回单样本确认字段后再做。
验证:
- `RUN_ISOLATED_LOAN_DB_TEST=1 RUN_ISOLATED_LOAN_BROWSER=1 node scripts/qa/test-company-loan-db.cjs`:74 迁移,HTTP 27 组、浏览器 5 组、DB 约束全部 PASS。原两个复现用例改为回归断言:错误回单执行 409 且本金/占用不变;C2 经 OCR 与经人工回单复核均被拦。新增:正确回单在有候选时同样需归属确认、确认后核销成功;重复登记 409;“时间吻合”类型 400;登记人兼经理不能自审、驳回后仍拦;新候选出现使批准失效;歧义失败回单不能释放;无候选时不需也不接受归属确认;已复核记录 DB 层不可改/删。
- 5 个变异(去掉核销拦截、候选排除已取消批次、去掉失败拦截、覆盖校验恒真、允许登记人复核)均变红,源码已恢复。
- server 4456 通过;prisma validate、client typecheck/build、UI strict、错误码合同通过。
- 首轮同模型只读复核:BLOCK——已批准的归属一旦因新候选或换回单失效,既不能重新登记(exists)、也不能改(触发器)、又因已导出不能取消,占用永久卡死并阻塞出借人维护(复核人副本实测成立)。已修:新增终态 `superseded`(及 `superseded_at`),重新登记时同一事务内把失效的待复核/已通过记录标为已取代;触发器只放行 proposed→approved/rejected/superseded 与 approved→superseded,复核字段不可改。同轮按非阻断意见补:复核人另排除导出制单人与所认领回单的上传人;`receipt-readiness` 在需归属确认时返回 `ready:false, receiptAttributionRequired:true`;状态返回 `stale`(回单或候选已变化);页面在失效时允许重新登记、财务按钮要求 `loan.manage`,并写明同额分期每期都需确认、存在同额候选时失败回单不能释放额度须线下对账。新增回归:失效批准被重新登记取代后核销成功;换回单后旧批准不能放行、导出人兼经理不能复核、重新登记后核销成功;已通过不能改为驳回、已取代不能复活。3 个变异(不取代、复核不排除导出人、就绪不检查)均变红。
- 第二轮同模型只读复核:PASS_WITH_NOTES,死局已解除,无阻断。按其意见加固:复核人另排除同一交易所有回单的上传人及人工回单提案人;驳回不再依赖回单(无回单时待复核记录可关闭);无已核验回单时登记返回 `loan.receipt_attribution_unavailable`,状态返回 `receiptAvailable`,页面不显示无效的登记按钮;批次详情中的归属展示在回单重建异常(如密钥不一致)时降级而非 500;触发器禁止未复核记录写入复核字段;取代审计记录原因(交易变化/候选变化)。新增回归:人工回单提案人不能复核归属、独立人员批准后核销;回单作废后待复核记录可驳回、重新登记返回 unavailable。
- 后续项(第二轮非阻断 5):存在同额候选且银行确实失败时系统内无释放路径,需设计线下对账结论回写(如复用归属复核确认失败),与 unknown 解除、作废附件计数、晚到结果一并处理。
- 复核其余非阻断(未处理,记录):已执行的历史同额批次永远是候选,等额分期从第二期起每期需确认,且一旦银行真实失败将无法确认失败、释放占用(与“失败归属/对账流程”一并后续设计);部分唯一索引、CHECK、触发器不在 Prisma schema 中,今后 `migrate dev`/diff 可能误报漂移(推测,仓库已有 r46 先例);`ON DELETE RESTRICT` 使带归属记录的批次/员工无法物理删除(当前无此路径)。
- 第三轮补齐(用户转述的复核意见指出前述回执遗漏的三处):
- 证据绑定:归属记录新增 `verification_ids`、`manual_proposal_ids`(迁移内同表,提案后不可改),批准与核销均要求与当前计划认领的证据集合一致;同一交易重新扫描或重新人工录入即失效,取代审计原因记为 `receipt_evidence_changed`。核销处在执行事务内以只读方式重建计划认领比对。
- 并发窗口:借款批次回单上传在批次锁后取出借人锁(批次→出借人,与既有链一致),与核销的候选检查串行。确定性测试:独立连接持出借人锁,两种排队顺序分别验证“核销在先→200、随后上传成功”“上传在先→核销 409”。去掉上传锁的变异使排队断言确定性变红。
- 浏览器:PC 端两个独立登录会话完成财务登记→经理独立复核→新增同额候选后显示“须重新登记并复核”→重新登记→再次复核→核销 200;登记人看不到复核按钮、复核人看不到登记按钮。测试环境整页刷新会丢会话,改用每步新会话。
- 验证:HTTP 32 组、浏览器 7 组、74 迁移全部 PASS;去掉证据绑定、去掉上传出借人锁两个变异均变红;server 4456、prisma validate、client typecheck/build、UI strict、错误码合同、敏感扫描、diff 检查通过。
- 未覆盖:手机端未操作归属弹窗(与 PC 共用组件,仅 PC 浏览器覆盖);真实回单字段;独立复核仍为同模型。
## 2026-10-05 复核阻断 B1 修复与回单归属复现(用户批准 A/B/D/恢复手册 + C 复现调研;本地合成,未提交、未部署)
背景:同模型只读复核(页面与剩余资金流程)结论 BLOCK:在途还款期间做出借人身份/账户维护会使 identity_version 变化,之后登记付款、取消、确认失败全部被拒,已付款项永久无法登记;导出后原审批人或出借员工离职同样卡死。另报回单批次归属未校验。