借款对账 S4–S8:人工未提交、晚到补记、追回与界面收口(09-17-company-loan-principal-repayment)

新增三类证据与双人复核的人工未提交、逐原因冻结、晚到付款逐借款补记和实际退款追回;补齐对账与补证恢复界面,保护已消费依据、最终归属重验及全局交易唯一性。

验证:81 迁移、HTTP 95 组、浏览器 13 组、数据库约束 9 组;13 项有效变异被捕获,部分由下游或数据库保护检出。构建、类型、合同和治理检查通过;修复后同模型只读静态复核通过。

真实回单待验、整体业务验收未通过;S0 存量、人员、银行材料与业务独立验收未完成。3 个新增迁移须纳入获批发布清单。本次仅本地提交,不推送、不部署。
这个提交包含在:
2026-10-07 21:58:03 +08:00
父节点 2e00d30fd0
当前提交 431c36eae3
共修改 32 个文件,包含 1902 行新增和 25 行删除
+119
查看文件
@@ -750,6 +750,12 @@ model Employee {
loan_recoveries_reviewed LoanReleaseRecovery[] @relation("LoanReleaseReviewer")
loan_recoveries_consumed LoanReleaseRecovery[] @relation("LoanReleaseConsumer")
loan_recovery_events LoanReleaseRecoveryEvent[]
loan_not_submitted_proposals LoanNotSubmittedProposal[]
loan_not_submitted_reviews LoanNotSubmittedReview[]
loan_freezes_created LoanFreezeReason[] @relation("LoanFreezeCreator")
loan_freezes_closed LoanFreezeReason[] @relation("LoanFreezeCloser")
loan_bank_events_proposed LoanBankResultEvent[] @relation("LoanBankEventProposer")
loan_bank_events_reviewed LoanBankResultEvent[] @relation("LoanBankEventReviewer")
}
@@ -1999,6 +2005,9 @@ model PaymentBatch {
loan_failure_evidence_uses LoanFailureEvidenceUse[]
@@map("payment_batches")
loan_release_recoveries LoanReleaseRecovery[]
loan_freeze_reasons LoanFreezeReason[]
loan_bank_result_events LoanBankResultEvent[]
loan_not_submitted_proposals LoanNotSubmittedProposal[]
loan_material_revision LoanReleaseMaterialRevision?
}
@@ -3897,6 +3906,7 @@ model LoanLender {
bank_institution BankInstitution @relation(fields: [bank_institution_id], references: [id], onDelete: Restrict)
loans CompanyLoan[]
repayments LoanRepayment[]
freeze_reasons LoanFreezeReason[]
@@unique([legal_entity_id, id])
@@unique([created_by, request_key])
@@ -3931,6 +3941,8 @@ model CompanyLoan {
allocations LoanRepaymentAllocation[]
evidences LoanEvidence[]
supplemental_settlements LoanSupplementalSettlement[]
@@unique([created_by, request_key])
@@unique([legal_entity_id, lender_id, origin_reference])
@@index([lender_id, confirmation_status, received_date])
@@ -4125,6 +4137,7 @@ model LoanBankSubmissionAttestation {
// Loan bank-result reconciliation S3 (migration 20261008010000_loan_bank_terminal_facts).
// Evidence is stored apart from payment receipts; history triggers live in the migration.
model LoanBankTerminalEvidence {
evidence_kind String @default("bank_terminal") @db.VarChar(32)
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
storage_path String
@@ -4141,6 +4154,8 @@ model LoanBankTerminalEvidence {
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict)
uploader Employee @relation(fields: [uploaded_by], references: [id], onDelete: Restrict)
facts LoanBankTerminalFact[]
bank_result_events LoanBankResultEvent[] @relation("LoanBankEventEvidence")
bank_result_corrections LoanBankResultEvent[] @relation("LoanBankEventCorrection")
@@index([payment_batch_id], map: "loan_bank_terminal_evidences_batch_idx")
@@map("loan_bank_terminal_evidences")
@@ -4244,3 +4259,107 @@ model LoanReleaseMaterialRevision {
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
@@map("loan_release_material_revisions")
}
model LoanNotSubmittedProposal {
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
sequence Int
status String @default("proposed") @db.VarChar(16)
proposed_by String @db.Uuid
proposed_at DateTime @default(now())
payload_json Json
snapshot_json Json
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
proposer Employee @relation(fields: [proposed_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
reviews LoanNotSubmittedReview[]
@@unique([payment_batch_id, sequence])
@@map("loan_not_submitted_proposals")
}
model LoanNotSubmittedReview {
id String @id @default(uuid()) @db.Uuid
proposal_id String @db.Uuid
reviewed_by String @db.Uuid
decision String @db.VarChar(16)
reason String @db.VarChar(500)
reviewed_at DateTime @default(now())
proposal LoanNotSubmittedProposal @relation(fields: [proposal_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
reviewer Employee @relation(fields: [reviewed_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
@@unique([proposal_id, reviewed_by])
@@map("loan_not_submitted_reviews")
}
model LoanFreezeReason {
id String @id @default(uuid()) @db.Uuid
lender_id String @db.Uuid
payment_batch_id String @db.Uuid
kind String @db.VarChar(32)
source_key String @db.VarChar(160)
status String @default("active") @db.VarChar(16)
created_by String @db.Uuid
created_at DateTime @default(now())
closed_by String? @db.Uuid
closed_at DateTime?
closure_basis Json?
lender LoanLender @relation(fields: [lender_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
creator Employee @relation("LoanFreezeCreator", fields: [created_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
closer Employee? @relation("LoanFreezeCloser", fields: [closed_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
@@unique([kind, source_key])
@@index([lender_id, status])
@@map("loan_freeze_reasons")
}
model LoanBankResultEvent {
id String @id @default(uuid()) @db.Uuid
payment_batch_id String @db.Uuid
kind String @db.VarChar(16)
parent_id String? @db.Uuid
evidence_id String @db.Uuid
request_key String @db.Uuid
proposed_by String @db.Uuid
proposed_at DateTime @default(now())
payload_encrypted String
fingerprint String @db.VarChar(64)
key_id String @db.VarChar(64)
amount Decimal @db.Decimal(14,2)
snapshot_json Json
status String @default("proposed") @db.VarChar(16)
reviewed_by String? @db.Uuid
reviewed_at DateTime?
review_note String? @db.VarChar(1000)
correction_evidence_id String? @db.Uuid
payment_batch PaymentBatch @relation(fields: [payment_batch_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
evidence LoanBankTerminalEvidence @relation("LoanBankEventEvidence", fields: [evidence_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
correction_evidence LoanBankTerminalEvidence? @relation("LoanBankEventCorrection", fields: [correction_evidence_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
proposer Employee @relation("LoanBankEventProposer", fields: [proposed_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
reviewer Employee? @relation("LoanBankEventReviewer", fields: [reviewed_by], references: [id], onDelete: Restrict, onUpdate: NoAction)
parent LoanBankResultEvent? @relation("LoanBankEventParent", fields: [parent_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
children LoanBankResultEvent[] @relation("LoanBankEventParent")
settlements LoanSupplementalSettlement[]
claim LoanBankEventClaim?
@@unique([payment_batch_id, request_key])
@@index([payment_batch_id, kind])
@@map("loan_bank_result_events")
}
model LoanSupplementalSettlement {
id String @id @default(uuid()) @db.Uuid
event_id String @db.Uuid
loan_id String @db.Uuid
bank_amount Decimal @db.Decimal(14,2)
settled_amount Decimal @db.Decimal(14,2)
receivable_amount Decimal @db.Decimal(14,2)
created_at DateTime @default(now())
event LoanBankResultEvent @relation(fields: [event_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
loan CompanyLoan @relation(fields: [loan_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
@@unique([event_id, loan_id])
@@map("loan_supplemental_settlements")
}
model LoanBankEventClaim {
fingerprint String @id @db.VarChar(64)
key_id String @db.VarChar(64)
event_id String @unique @db.Uuid
created_at DateTime @default(now())
event LoanBankResultEvent @relation(fields: [event_id], references: [id], onDelete: Restrict, onUpdate: NoAction)
@@map("loan_bank_event_claims")
}