完善付款回单凭证核验与本地重检(09-17-company-loan-principal-repayment)
区分正式付款回单、非最终回执与未知凭证,在核验构建、人工证据重建和最终核销时使用同一份有效依据。非最终说明、入账方向、状态矛盾及重复交易不能经人工覆盖。 正式回单无字面成功状态时,允许财务按原件实际记账支出、日期及说明提出核验,由本批次适用的确认人确认;不补造银行成功文字。旧核验可显式本地重检,新版本与审计原子保存,不重新外发 OCR,不沿用旧人工批准或改写已核销历史。 本地验证:82 个迁移、借款 HTTP 116 组、关联电脑/手机 18 组、共享回单数据库与真实认证 38 组、共享报销页面限定重验 24 项、最新回单/报销定向 292 项通过;9 个资金守卫变异均被捕获。服务端全量 4554 项通过、1 项既有可选跳过,后续合成数据及零页详情改动由最新定向验证覆盖。 本次无新增迁移,新增本地 PDF 文本解析依赖。后续对本提交运行完整 verify-candidate,实际候选与结果以本轮回执为准。 限制:独立代码复核及真实还款验收待完成;历史材料预验不证明真实还本或银行提交对应。未推送、未部署、未访问生产或写入真实业务数据。
这个提交包含在:
@@ -1,6 +1,7 @@
|
||||
const { encryptStringOrThrow, decryptStringOrThrow } = require("../../../src/services/crypto");
|
||||
const { normalizeBankSlip } = require("../../../src/services/paymentReceiptMatching");
|
||||
const { buildVerificationData, claimVerifiedReceipts, transactionFingerprint, canRetryReceiptRecognition } = require("../../../src/services/paymentReceiptVerification");
|
||||
const { syntheticDocumentEvidence } = require("../../../../scripts/qa/synthetic-bank-receipt.cjs");
|
||||
|
||||
const config = { fieldEncryptionKey: Buffer.alloc(32, 7).toString("base64") };
|
||||
|
||||
@@ -8,7 +9,7 @@ function fixture() {
|
||||
const page = { pageNumber: 1, ...normalizeBankSlip(Object.entries({
|
||||
小写金额: "10.00", 币别: "人民币", 付款人账号: "000011112222",
|
||||
收款人账号: "000033334444", 单据状态: "交易成功", 凭证号: "SYN-REF",
|
||||
}).map(([Name, Value]) => ({ Name, Value }))) };
|
||||
}).map(([Name, Value]) => ({ Name, Value }))), document: syntheticDocumentEvidence() };
|
||||
const batch = {
|
||||
id: "batch", entity_type: "reimbursement",
|
||||
payment_account_snapshot_json: { bankInstitutionCode: "CCB", accountNumberEncrypted: encryptStringOrThrow(config, page.payerAccount) },
|
||||
@@ -31,6 +32,23 @@ function fixture() {
|
||||
}
|
||||
|
||||
describe("receipt evidence and transaction claims", () => {
|
||||
test.each([null, { version: 0 }, { ...syntheticDocumentEvidence(), kind: "non_final" },
|
||||
{ ...syntheticDocumentEvidence(), direction: "credit" }, { ...syntheticDocumentEvidence(), explicitSuccess: false }])(
|
||||
"field matches alone cannot prove final payment (%j)", (document) => {
|
||||
const { batch, attachment, recognition } = fixture();
|
||||
recognition.pages[0].document = document;
|
||||
const data = buildVerificationData(config, batch, attachment, recognition);
|
||||
expect(data.status).toBe("needs_review");
|
||||
expect(data.matches).toBeUndefined();
|
||||
});
|
||||
|
||||
test("final claim rebuild rejects an old matched record with no document basis", async () => {
|
||||
const { tx, batch, verification, recognition } = fixture();
|
||||
delete recognition.pages[0].document;
|
||||
verification.normalized_encrypted = encryptStringOrThrow(config, JSON.stringify(recognition));
|
||||
await expect(claimVerifiedReceipts(tx, config, batch)).rejects.toMatchObject({ code: "receipt_match_changed" });
|
||||
expect(tx.paymentReceiptClaim.create).not.toHaveBeenCalled();
|
||||
});
|
||||
test("bank transactions already used for supplements or returns cannot be claimed again", async () => {
|
||||
const { tx, batch } = fixture();
|
||||
tx.loanBankEventClaim.count.mockResolvedValue(1);
|
||||
|
||||
在新工单中引用
屏蔽一个用户