完善付款回单凭证核验与本地重检(09-17-company-loan-principal-repayment)
区分正式付款回单、非最终回执与未知凭证,在核验构建、人工证据重建和最终核销时使用同一份有效依据。非最终说明、入账方向、状态矛盾及重复交易不能经人工覆盖。 正式回单无字面成功状态时,允许财务按原件实际记账支出、日期及说明提出核验,由本批次适用的确认人确认;不补造银行成功文字。旧核验可显式本地重检,新版本与审计原子保存,不重新外发 OCR,不沿用旧人工批准或改写已核销历史。 本地验证:82 个迁移、借款 HTTP 116 组、关联电脑/手机 18 组、共享回单数据库与真实认证 38 组、共享报销页面限定重验 24 项、最新回单/报销定向 292 项通过;9 个资金守卫变异均被捕获。服务端全量 4554 项通过、1 项既有可选跳过,后续合成数据及零页详情改动由最新定向验证覆盖。 本次无新增迁移,新增本地 PDF 文本解析依赖。后续对本提交运行完整 verify-candidate,实际候选与结果以本轮回执为准。 限制:独立代码复核及真实还款验收待完成;历史材料预验不证明真实还本或银行提交对应。未推送、未部署、未访问生产或写入真实业务数据。
这个提交包含在:
@@ -4,6 +4,7 @@ const { mkdtempSync, mkdirSync, writeFileSync } = require('node:fs');
|
||||
const { createRequire } = require('node:module');
|
||||
const req = createRequire(path.resolve(__dirname, '../../client/package.json'));
|
||||
const { chromium, expect } = req('@playwright/test');
|
||||
const { syntheticBankReceiptPdf } = require('./synthetic-bank-receipt.cjs');
|
||||
|
||||
module.exports = async function relatedBrowser(h) {
|
||||
const { finance, manager, ordinary, password, prisma, relatedParty, config } = h;
|
||||
@@ -52,11 +53,15 @@ module.exports = async function relatedBrowser(h) {
|
||||
const response = await recon(p, '上传银行证据', async d => { await d.getByLabel('证据类别').selectOption(kind); await d.getByLabel('证据文件').setInputFiles({ name: `${kind}.pdf`, mimeType: 'application/pdf', buffer: await f.pdf() }); });
|
||||
return (await response.json()).data.id;
|
||||
}
|
||||
async function receiptUpload(p, b, cents, ref) {
|
||||
async function receiptUpload(p, b, cents, ref, overrides = {}, document = {}) {
|
||||
const response = p.waitForResponse(r => r.url().endsWith(`/payment-batches/${b.id}/receipt-attachments`) && r.request().method() === 'POST');
|
||||
await p.getByTestId('payment-batch-receipt-file-input').setInputFiles({ name: 'synthetic-receipt.pdf', mimeType: 'application/pdf', buffer: await f.pdf() });
|
||||
const status = overrides.transactionStatus === null ? 'booked' : overrides.transactionStatus ?? 'success';
|
||||
await p.getByTestId('payment-batch-receipt-file-input').setInputFiles({ name: 'synthetic-receipt.pdf', mimeType: 'application/pdf',
|
||||
buffer: await syntheticBankReceiptPdf({ status, tag: ref, reference: ref,
|
||||
amount: cents === null ? String(b.items[0].amount) : `${BigInt(cents) / 100n}.${String(BigInt(cents) % 100n).padStart(2, '0')}`,
|
||||
payerAccount: '8'.repeat(16), payeeAccount: s.lender.bankAccount, ...document }) });
|
||||
assert.equal((await response).status(), 200);
|
||||
const result = await f.processUploadedReceipt(cents, ref, { payeeAccount: s.lender.bankAccount });
|
||||
const result = await f.processUploadedReceipt(cents, ref, { payeeAccount: s.lender.bankAccount, ...overrides });
|
||||
await p.reload(); return result;
|
||||
}
|
||||
try {
|
||||
@@ -141,6 +146,47 @@ module.exports = async function relatedBrowser(h) {
|
||||
await paymentButton(fp, 'execute').click(); await submit(fp, undefined, '确认执行');
|
||||
checks.push(`${s.mode}_related_manual_receipt_confirmation_and_rebuilt_settlement`);
|
||||
|
||||
const booked = await f.batch(s, '18.00'); await go(fp, booked);
|
||||
assert.equal((await receiptUpload(fp, booked, '1800', `RP-UI-BOOKED-${s.mode}`, { transactionStatus: null })).status, 'needs_review');
|
||||
await fp.getByRole('button', { name: '核验详情', exact: true }).click();
|
||||
d = fp.getByRole('dialog');
|
||||
await expect(d).toContainText('状态:未确认');
|
||||
await d.getByLabel('第1页对应付款明细').selectOption(booked.items[0].id);
|
||||
await expect(d.getByRole('button', { name: '保存核验', exact: true })).toBeDisabled();
|
||||
await d.getByLabel('第1页付款凭证依据').selectOption('booked_debit');
|
||||
await d.getByLabel('第1页原件记账日期').fill('2026-10-01');
|
||||
await d.getByRole('button', { name: '更正识别', exact: true }).click();
|
||||
await expect(d.getByLabel('第1页更正单据状态')).toHaveCount(0);
|
||||
await d.getByLabel('更正说明(仅关联可留空)').fill('合成正式回单为本公司借方记账,未记载成功状态字样');
|
||||
await expect(d.getByRole('button', { name: '保存核验', exact: true })).toBeDisabled();
|
||||
await d.getByRole('checkbox', { name: /已查看第1页完整原件/ }).check();
|
||||
await submit(fp, undefined, '保存核验');
|
||||
await fp.getByRole('button', { name: '核验详情', exact: true }).click();
|
||||
await expect(fp.getByRole('dialog').getByRole('button', { name: '公司代表确认', exact: true })).toHaveCount(0);
|
||||
await fp.getByRole('dialog').getByRole('button', { name: '关闭', exact: true }).click();
|
||||
await expect(fp.getByTestId('payment-batch-action-execute')).toHaveCount(0);
|
||||
await go(mp, booked); await mp.getByRole('button', { name: '核验详情', exact: true }).click();
|
||||
d = mp.getByRole('dialog');
|
||||
await expect(d).toContainText('已记账支出(人工核对)'); await expect(d).toContainText('记账日期 2026-10-01');
|
||||
await expect(d.getByRole('button', { name: '保存核验', exact: true })).toHaveCount(0);
|
||||
assert.equal(await mp.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false);
|
||||
await mp.screenshot({ path: path.join(output, `related-${s.mode}-booked-proof.png`), fullPage: true });
|
||||
await submit(mp, current => current.getByLabel('复核说明').fill('合成代表公司核对原件借方记账日期与付款明细'), '公司代表确认');
|
||||
await go(fp, booked); await expect(paymentButton(fp, 'execute')).toBeEnabled();
|
||||
await paymentButton(fp, 'execute').click(); await submit(fp, undefined, '确认执行');
|
||||
assert.equal((await prisma.loanRepaymentAllocation.findFirstOrThrow({ where: { repayment_id: booked.loanRepaymentId } })).reservation_state, 'settled');
|
||||
checks.push(`${s.mode}_booked_receipt_proof_dialog_company_confirmation_and_settlement`);
|
||||
|
||||
const nonfinal = await f.batch(s, '12.00'); await go(fp, nonfinal);
|
||||
assert.equal((await receiptUpload(fp, nonfinal, '1200', `RP-UI-NONFINAL-${s.mode}`, {}, { final: false })).status, 'needs_review');
|
||||
await fp.getByRole('button', { name: '核验详情', exact: true }).click();
|
||||
d = fp.getByRole('dialog'); await expect(d).toContainText('非最终回执');
|
||||
await expect(d.getByRole('button', { name: '更正识别', exact: true })).toHaveCount(0);
|
||||
await expect(d.getByRole('button', { name: '保存核验', exact: true })).toBeDisabled();
|
||||
await d.getByRole('button', { name: '关闭', exact: true }).click();
|
||||
await expect(fp.getByTestId('payment-batch-action-execute')).toHaveCount(0);
|
||||
checks.push(`${s.mode}_nonfinal_receipt_has_no_manual_override_or_settlement`);
|
||||
|
||||
const failed = await f.batch(s, '7.00'); await f.batch(s, '7.00'); const ref = `RP-UI-FAIL-${s.mode}`.toUpperCase();
|
||||
await f.receipt(failed.id, '700', ref, { payeeAccount: s.lender.bankAccount, transactionStatus: 'failed' });
|
||||
await go(fp, failed);
|
||||
|
||||
在新工单中引用
屏蔽一个用户