借款还款同金额回单须经独立复核确认归属后方可核销(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 行删除
+34
查看文件
@@ -735,6 +735,8 @@ model Employee {
loan_lender_identities LoanLender[]
loan_evidence_uploads LoanEvidence[]
loan_receipt_attributions_proposed LoanReceiptAttribution[] @relation("LoanReceiptAttributionProposer")
loan_receipt_attributions_reviewed LoanReceiptAttribution[] @relation("LoanReceiptAttributionReviewer")
@@map("employees")
}
@@ -1974,6 +1976,7 @@ model PaymentBatch {
@@index([legal_entity_id], map: "idx_payment_batches_legal_entity")
@@index([payment_account_id], map: "idx_payment_batches_payment_account")
@@index([payment_order_last_exported_by], map: "idx_payment_batches_order_exported_by")
loan_receipt_attributions LoanReceiptAttribution[]
@@map("payment_batches")
}
@@ -3979,3 +3982,34 @@ model LoanEvidence {
@@unique([loan_id, file_sha256, kind])
@@map("loan_evidences")
}
// Independently reviewed receipt-to-batch attribution for ambiguous loan
// repayments (same lender and amount as another exported/receipted batch).
// Partial unique index (one proposed/approved per batch) and history trigger
// live in migration 20261006010000_loan_receipt_attribution.
model LoanReceiptAttribution {
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
fingerprint String @db.VarChar(64)
key_id String @db.VarChar(64)
verification_ids Json
manual_proposal_ids Json
competitor_batch_ids Json
basis_kind String @db.VarChar(32)
basis_note String @db.VarChar(1000)
exclusion_note String @db.VarChar(1000)
status String @default("proposed") @db.VarChar(16)
proposed_by String @db.Uuid
proposed_at DateTime @default(now())
reviewed_by String? @db.Uuid
reviewed_at DateTime?
review_note String? @db.VarChar(500)
superseded_at DateTime?
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict)
proposer Employee @relation("LoanReceiptAttributionProposer", fields: [proposed_by], references: [id], onDelete: Restrict)
reviewer Employee? @relation("LoanReceiptAttributionReviewer", fields: [reviewed_by], references: [id], onDelete: Restrict)
@@index([payment_batch_id, fingerprint], map: "loan_receipt_attributions_batch_fingerprint_idx")
@@map("loan_receipt_attributions")
}