支持财务经办与公司代表关联方还款确认(09-17-company-loan-principal-repayment)
公司只有财务和出借收款经理两名办理人,新增限定公司、出借人和借款范围的关联方确认模式。 仅豁免指定公司代表的收款人关联;经办、自审、证据、交易归属、资金占用及 Q6 限制保留。 贯通付款、人工回单、银行对账、补证恢复、晚到补记与追回;电脑和手机显示实际确认身份。 授权版本、付款与对账停用事件及历史决定只增不改,新增迁移 20261009010000_loan_related_approval。 本地合成验证:82 迁移、HTTP 109 组、浏览器 33 组、数据库约束 9 组通过;13 个有效资金守卫变异均捕获。 已有服务端全量 4519 项通过、1 项原有跳过,后续定向 77 项通过;提示收尾另有 908 项合同测试及前端构建通过。 按用户授权提交后对干净候选运行正式 verify-candidate,不沿用旧候选回执。 限制:独立代码复核待完成(inline 模式);真实回单待验。本次仅本地提交,不推送、不部署。 生成关联决定后不能直接降回不理解该模式的旧代码;真实授权、录入、生产核实和付款仍须明确批准。
这个提交包含在:
@@ -192,6 +192,10 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
headers: { ...finance.headers, 'content-type': `multipart/form-data; boundary=${boundary}` },
|
||||
payload: Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="files"; filename="receipt.pdf"\r\nContent-Type: application/pdf\r\n\r\n`), file, Buffer.from(`\r\n--${boundary}--\r\n`)]) });
|
||||
assert.equal(response.statusCode, 200, 'receipt upload');
|
||||
const result = await processUploadedReceipt(amount, reference, overrides, { normalizedPage });
|
||||
return { attachment: response.json().data.items[0], result };
|
||||
}
|
||||
async function processUploadedReceipt(amount, reference, overrides = {}, { normalizedPage = false } = {}) {
|
||||
const slip = { amount, currency: 'CNY', payerAccount, payeeAccount: bankAccount, transactionStatus: 'success', reference, ...overrides };
|
||||
// normalizedPage: one explicitly synthetic defence-in-depth case that the
|
||||
// real normalizer cannot produce; every other receipt uses the real chain.
|
||||
@@ -199,7 +203,7 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
? async () => ({ status: 'recognized', pagesExpected: 1, pagesProcessed: 1, pages: [{ pageNumber: 1, issues: [], ...slip }] })
|
||||
: (args) => recognizePaymentReceipt({ ...args, createClient: () => ({ BankSlipOCR: async () => ({ BankSlipInfos: bankSlipFields(slip) }) }) });
|
||||
const result = await runReceiptOcrOnce({ prisma, config, recognize });
|
||||
return { attachment: response.json().data.items[0], result };
|
||||
return result;
|
||||
}
|
||||
const exportResponse = await app.inject({ method: 'POST', url: `/api/payment-batches/${batch.id}/payment-order`, headers: finance.headers, payload: {} });
|
||||
assert.equal(exportResponse.statusCode, 200, `loan bank-order export ${exportResponse.statusCode === 200 ? '' : JSON.stringify(exportResponse.json().details)}`);
|
||||
@@ -368,11 +372,19 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
requestKey: randomUUID(), lenderId, reason: '合成还本', allocations: [{ loanId, amount }] })).item;
|
||||
async function exportOrder(batchId, status = 200, reason) {
|
||||
const response = await app.inject({ method: 'POST', url: `/api/payment-batches/${batchId}/payment-order`, headers: finance.headers, payload: {} });
|
||||
assert.equal(response.statusCode, status, `export ${response.statusCode} ${status === 200 ? '' : response.body.slice(0, 200)}`);
|
||||
assert.equal(response.statusCode, status, `export ${response.statusCode} ${response.headers['content-type']?.includes('json') ? response.json().details?.reasonCode ?? '' : '<binary omitted>'}`);
|
||||
if (reason) assert.equal(response.json().details?.reasonCode, reason);
|
||||
}
|
||||
const lenderState = async (id) => (await call(finance, 'GET', '/loan-lenders')).items.find((item) => item.id === id);
|
||||
const independent = await actor(['ceo']);
|
||||
const runRelated = () => require('./company-loan-related-fixture.cjs')({ prisma, pool, call, app, req, finance, manager, ordinary, independent, config, password,
|
||||
verifiedLender, confirmedLoan, repay, exportOrder, receipt, processUploadedReceipt });
|
||||
if (process.env.RUN_ISOLATED_LOAN_RELATED_ONLY === '1') {
|
||||
const relatedParty = await runRelated();
|
||||
if (process.env.RUN_ISOLATED_LOAN_BROWSER === '1') await require('./company-loan-related-browser.cjs')({ app, finance, manager, ordinary, password, prisma, config, relatedParty });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Lender linked to a manager: that manager cannot approve, an independent CEO can.
|
||||
const linked = await verifiedLender('合成出借人乙', manager.id, '4');
|
||||
@@ -1173,6 +1185,8 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
config, req, verifiedLender, confirmedLoan, repay, exportOrder, register, attest, uploadEvidence, proposeFact, reviewFact, confirmNotPaid, receipt }));
|
||||
checks.push(...await require('./company-loan-bank-results-fixture.cjs')({ prisma, pool, call, app, finance, manager, independent, dual, actor,
|
||||
config, req, verifiedLender, confirmedLoan, repay, exportOrder, register, attest, uploadEvidence, proposeFact, reviewFact, confirmNotPaid, receipt }));
|
||||
const relatedParty = await runRelated();
|
||||
checks.push(...relatedParty.checks);
|
||||
delete config.loanReleaseWaitDays;
|
||||
|
||||
// History guards in the database.
|
||||
@@ -1328,7 +1342,7 @@ module.exports = async function runLoanHttpFixture(pool, scratch) {
|
||||
reconciliationUi.late.push({ id: original.id, lenderId: lateLender.id, reference: `UI-LATE-${mode}`,
|
||||
payer: decryptStringOrThrow(config, saved.payment_account_snapshot_json.accountNumberEncrypted), payee: lateLender.bankAccount });
|
||||
}
|
||||
await require('./company-loan-browser.cjs')({ app, finance, manager, ordinary, password, company, bank, lender, loan, config, prisma, attributionUi, reconciliationUi });
|
||||
await require('./company-loan-browser.cjs')({ app, finance, manager, ordinary, password, company, bank, lender, loan, config, prisma, attributionUi, reconciliationUi, relatedParty });
|
||||
}
|
||||
console.log(JSON.stringify({ stage: 'loan_ledger_http', status: 'PASS', checks }));
|
||||
} finally { await app.close(); await prisma.$disconnect(); }
|
||||
|
||||
在新工单中引用
屏蔽一个用户