借款登记支持上传原借入回单识别预填(09-17-company-loan-principal-repayment)
- 新增识别与保存接口:可读 PDF 本地解析,必要时有界调用现有回单 OCR;预填上下文加密绑定经办人、文件和 30 分钟 - 适配建行单位客户专用回单表格版式:按位置归行排序、竖排栏头、顶行无标签日期、大小写金额 - 拦截非最终、失败/处理中、多笔交易(含跨页无关联、编号冲突)、非人民币、公司支出和其他收款主体 - 金额、日期、编号整段校验;借款与原件同事务保存,失败回滚并清理对象;同一出借人原件唯一 - 保存绑定所选出借人版本;前端冻结核对时版本,迟到结果与切换账号不覆盖、不重发 - 测试:单测、隔离 HTTP/浏览器、变异;对账浏览器场景固定 UTC;五轮独立复核通过 - 无新增迁移 Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
@@ -111,6 +111,8 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
await call(finance, 'POST', `/company-loans/${loan.id}/confirm`, { expectedVersion: 1, accountingConfirmationRef: '合成核对' }, 409, 'loan.evidence_required');
|
||||
const material = Buffer.from('%PDF-1.4\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n');
|
||||
async function upload(kind, data = material, filename = 'synthetic.pdf', type = 'application/pdf', status = 200, loanId = loan.id) {
|
||||
// An original borrow-in is unique per lender; each synthetic loan has its own.
|
||||
if (kind === 'incoming' && data === material) data = Buffer.from(`%PDF-1.4\n% synthetic-incoming-${loanId}\n1 0 obj << /Type /Catalog >> endobj\n%%EOF\n`);
|
||||
const boundary = `synthetic-${randomUUID()}`;
|
||||
const payload = Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: ${type}\r\n\r\n`), data, Buffer.from(`\r\n--${boundary}--\r\n`)]);
|
||||
const response = await app.inject({ method: 'POST', url: `/api/company-loans/${loanId}/evidences/${kind}`,
|
||||
@@ -416,6 +418,14 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
}
|
||||
const employeeHarness = { prisma, pool, call, app, req, finance, manager, ordinary,
|
||||
config, password, actor, company, bank, confirmedLoan, repay, exportOrder, receipt };
|
||||
// Incoming-receipt scenarios add lenders; in a full run they go last so they
|
||||
// cannot shift the pagination other browser scenarios rely on.
|
||||
const runIncoming = () => require('./company-loan-incoming-fixture.cjs')({ ...employeeHarness, verifiedLender });
|
||||
if (process.env.RUN_ISOLATED_LOAN_INCOMING_ONLY === '1') {
|
||||
const incomingReceipt = await runIncoming();
|
||||
if (process.env.RUN_ISOLATED_LOAN_BROWSER === '1') await require('./company-loan-incoming-browser.cjs')({ app, finance, otherFinance: await actor(['finance']), password, prisma, config, company, incomingReceipt });
|
||||
return;
|
||||
}
|
||||
const employeeSources = await require('./company-loan-employee-fixture.cjs')(employeeHarness);
|
||||
if (process.env.RUN_ISOLATED_LOAN_EMPLOYEE_MUTATIONS === '1') await require('./company-loan-employee-mutations.cjs')(
|
||||
employeeHarness, require('./company-loan-employee-fixture.cjs'));
|
||||
@@ -1391,6 +1401,7 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
}
|
||||
await require('./company-loan-browser.cjs')({ app, finance, manager, ordinary, password, company, bank, lender, loan, config, prisma, attributionUi, reconciliationUi, relatedParty, employeeSources });
|
||||
}
|
||||
await runIncoming();
|
||||
console.log(JSON.stringify({ stage: 'loan_ledger_http', status: 'PASS', checks }));
|
||||
} finally { await app.close(); await prisma.$disconnect(); }
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户