完善付款回单凭证核验与本地重检(09-17-company-loan-principal-repayment)

区分正式付款回单、非最终回执与未知凭证,在核验构建、人工证据重建和最终核销时使用同一份有效依据。非最终说明、入账方向、状态矛盾及重复交易不能经人工覆盖。

正式回单无字面成功状态时,允许财务按原件实际记账支出、日期及说明提出核验,由本批次适用的确认人确认;不补造银行成功文字。旧核验可显式本地重检,新版本与审计原子保存,不重新外发 OCR,不沿用旧人工批准或改写已核销历史。

本地验证:82 个迁移、借款 HTTP 116 组、关联电脑/手机 18 组、共享回单数据库与真实认证 38 组、共享报销页面限定重验 24 项、最新回单/报销定向 292 项通过;9 个资金守卫变异均被捕获。服务端全量 4554 项通过、1 项既有可选跳过,后续合成数据及零页详情改动由最新定向验证覆盖。

本次无新增迁移,新增本地 PDF 文本解析依赖。后续对本提交运行完整 verify-candidate,实际候选与结果以本轮回执为准。

限制:独立代码复核及真实还款验收待完成;历史材料预验不证明真实还本或银行提交对应。未推送、未部署、未访问生产或写入真实业务数据。
这个提交包含在:
2026-10-09 06:18:09 +08:00
父节点 ce11dea976
当前提交 5a7a13524e
共修改 31 个文件,包含 1325 行新增和 49 行删除
@@ -535,6 +535,7 @@ function buildPrisma(options = {}) {
小写金额: String(item.amount), 币别: item.currency_code, 付款人账号: payerAccount,
收款人账号: payeeAccount, 单据状态: "交易成功", 凭证号: `SYN-${item.id}`,
}).map(([Name, Value]) => ({ Name, Value }))),
document: require("../../../../scripts/qa/synthetic-bank-receipt.cjs").syntheticDocumentEvidence(),
}));
const data = buildVerificationData(config, state.batch, attachment, {
status: "recognized", pagesExpected: pages.length, pagesProcessed: pages.length, pages,
@@ -573,7 +574,8 @@ describe("manual receipt verification routes", () => {
source_version: 0, source_sha256: attachment.file_sha256,
normalized_encrypted: encryptStringOrThrow(config, JSON.stringify({
status: "needs_review", pagesExpected: 1, pagesProcessed: 1,
pages: [{ pageNumber: 1, ...normalizeBankSlip(fields), amount: null, issues: ["amount_unresolved"] }],
pages: [{ pageNumber: 1, ...normalizeBankSlip(fields), amount: null, issues: ["amount_unresolved"],
document: require("../../../../scripts/qa/synthetic-bank-receipt.cjs").syntheticDocumentEvidence() }],
})) };
let stored;
tx.paymentReceiptVerification = { findFirst: vi.fn(async () => original) };
@@ -644,6 +646,26 @@ describe("manual receipt verification routes", () => {
} finally { await app.close(); await fs.rm(tempDir, { recursive: true, force: true }); }
});
it.each([0, -1, 21, 1000000000])("bounds historical page metadata while allowing local reinspection: %s", async (pagesExpected) => {
const { app, tx, state, tempDir } = await buildApp();
setupEvidence(tx, state);
const original = await tx.paymentReceiptVerification.findFirst();
original.normalized_encrypted = encryptStringOrThrow({ fieldEncryptionKey: FIELD_ENCRYPTION_KEY }, JSON.stringify({
status: "ocr_failed", pagesExpected, pagesProcessed: 0, pages: [],
}));
state.receiptAttachments[0].receipt_ocr_state = "ocr_failed";
try {
const response = await app.inject({ method: "GET", url, headers });
expect(response.statusCode).toBe(pagesExpected === 0 ? 200 : 409);
if (pagesExpected === 0) expect(response.json().data.item).toMatchObject({
documentInspectionRequired: true, pagesExpected: 0, pages: [], proposal: null,
});
expect(tx.paymentReceiptManualProposal.create).not.toHaveBeenCalled();
expect(tx.paymentBatchReceiptAttachment.update).not.toHaveBeenCalled();
expect(tx.auditLog.create).not.toHaveBeenCalled();
} finally { await app.close(); await fs.rm(tempDir, { recursive: true, force: true }); }
});
it.each(["foreign", "voided", "stale"])("does not expose details for %s evidence", async (stateKind) => {
const { app, tx, state, tempDir } = await buildApp();
setupEvidence(tx, state);