借款对账 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 行删除
@@ -1,4 +1,7 @@
{
"loan.bank_result_reconcile_required": "银行对账事实、归属或证据尚不满足条件,请补齐材料并独立复核;占用或冻结继续保留。",
"loan.lender_frozen": "该出借人存在尚未处理的银行对账事项,暂停新还款、复核和导出;仍可登记银行事实并对账。",
"loan.not_submitted_invalid": "人工未提交的证据、清单或双人复核不满足要求,请补齐后重新登记复核。",
"loan.recovery_required": "释放依据已进入补证流程,须完成最新一轮独立复核后才能取消释放。",
"loan.recovery_unavailable": "当前批次或恢复轮次不支持此操作;已释放、已核销的还款不能重新开启。",
"loan.recovery_exists": "已有补证中或有效的恢复复核,请继续处理当前轮次。",
@@ -284,6 +284,7 @@ function buildPrisma(options = {}) {
const tx = {
businessNumberPolicy: { findUnique: vi.fn(async () => null) },
loanBankEventClaim: { count: vi.fn().mockResolvedValue(0) },
paymentReceiptClaim: { count: vi.fn(async () => 0), findMany: vi.fn(async () => []), create: vi.fn() },
paymentReceiptTransactionAlias: { count: vi.fn(async () => 0), findMany: vi.fn(async () => []), create: vi.fn() },
paymentReceiptManualProposal: { findMany: vi.fn(async () => []) },
@@ -22,6 +22,7 @@ function fixture() {
const tx = {
$queryRaw: vi.fn().mockResolvedValue([{ id: attachment.id }]),
paymentReceiptVerification: { findFirst: vi.fn().mockResolvedValue(verification) },
loanBankEventClaim: { count: vi.fn().mockResolvedValue(0) },
paymentReceiptClaim: { count: vi.fn().mockResolvedValue(0), findMany: vi.fn().mockResolvedValue([]), create: vi.fn() },
paymentReceiptTransactionAlias: { count: vi.fn().mockResolvedValue(0), findMany: vi.fn().mockResolvedValue([]), create: vi.fn() },
paymentReceiptManualProposal: { findMany: vi.fn().mockResolvedValue([]) },
@@ -30,6 +31,12 @@ function fixture() {
}
describe("receipt evidence and transaction claims", () => {
test("bank transactions already used for supplements or returns cannot be claimed again", async () => {
const { tx, batch } = fixture();
tx.loanBankEventClaim.count.mockResolvedValue(1);
await expect(claimVerifiedReceipts(tx, config, batch)).rejects.toMatchObject({ code: "receipt_transaction_reused" });
expect(tx.paymentReceiptClaim.create).not.toHaveBeenCalled();
});
test("mixed historical cash items do not require bank snapshots or bank receipt claims", async () => {
const { batch, attachment, recognition, tx } = fixture();
batch.items.push({ id: "cash-item", status: "pending", amount: "10.00", currency_code: "CNY",