借款出借人线上确认借款并以站内通知直达(09-17-company-loan-principal-repayment)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

- 新增出借人确认声明:财务发起,出借人本人查看原件后确认或退回;快照与决定不可改删,金额、日期、原件或身份变化后整组失效
- 余额确认接受纸质协议或有效线上声明二选一,代账余额核对材料仍必需;声明与公司代表授权分离
- 普通员工无常驻入口,财务发起后仅通知对应出借人并直达确认页
- 新增迁移 20261010010000_loan_lender_acknowledgements(仅新增三表)
- 测试:单测、隔离 HTTP/浏览器、完整借款回归;独立只读复核无阻断;同步发布记录与操作手册

Co-Authored-By: Claude Opus 5.5 <[email protected]>
这个提交包含在:
2026-10-10 15:22:12 +08:00
共同撰写人 Claude Opus 5.5
父节点 2063ff1e93
当前提交 4a5cd8f2b3
共修改 34 个文件,包含 1610 行新增和 41 行删除
@@ -10,6 +10,7 @@ module.exports = async function reconciliationBrowser({ browser, origin, output,
for (const mobile of [false, true]) {
const mode = mobile ? 'mobile' : 'desktop';
let view;
const uploadedByKind = new Map();
async function login(who, batch) {
if (view) await view.context.close();
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block' });
@@ -28,22 +29,34 @@ module.exports = async function reconciliationBrowser({ browser, origin, output,
const dialog = view.page.getByRole('dialog');
if (fill) await fill(dialog);
if (await dialog.getByLabel('依据与说明').count()) await dialog.getByLabel('依据与说明').fill('合成银行材料唯一定位本批次并独立核对全部指令');
const invalid = await dialog.locator('input:invalid, select:invalid, textarea:invalid').evaluateAll(elements =>
elements.map(el => el.getAttribute('aria-label') ?? el.labels?.[0]?.textContent?.trim().slice(0, 80) ?? el.tagName));
assert.deepEqual(invalid, [], `synthetic UI ${name}: invalid form controls`);
await dialog.getByRole('button', { name: '确认', exact: true }).click();
try { await expect(dialog).not.toBeVisible(); }
catch { throw new Error(`synthetic UI ${name}: ${await dialog.getByRole('alert').allTextContents()}`); }
catch {
await view.page.screenshot({ path: path.join(output, `${mode}-reconciliation-failure.png`), fullPage: true });
throw new Error(`synthetic UI ${name}: ${await dialog.getByRole('alert').allTextContents()}; submit=${await dialog.locator('button[type="submit"]').textContent()}`);
}
}
async function review(name = '独立复核银行事实') {
await action(name, d => d.getByLabel('复核结论').selectOption('approve'));
}
async function upload(kind) {
const uploaded = view.page.waitForResponse(response => new URL(response.url()).pathname.endsWith('/loan-terminal-evidences') &&
response.request().method() === 'POST');
await action('上传银行证据', async d => {
await d.getByLabel('证据类别').selectOption(kind);
await d.getByLabel('证据文件').setInputFiles({ name: `${kind}.pdf`, mimeType: 'application/pdf', buffer: await fixtures.bankLetterPdf() });
});
const response = await uploaded; assert.equal(response.status(), 200);
uploadedByKind.set(kind, (await response.json()).data.id);
}
async function bankFields(d, s, amount, reference, incoming = false) {
const options = d.getByLabel('对应的成功银行回单').locator('option');
await d.getByLabel('对应的成功银行回单').selectOption({ index: (await options.count()) - 1 });
const evidenceId = uploadedByKind.get(incoming ? 'incoming_return' : 'late_success');
assert.ok(evidenceId, 'use the specific uploaded document, never the placeholder or an older option');
await d.getByLabel('对应的成功银行回单').selectOption(evidenceId);
await expect(d.getByLabel('对应的成功银行回单')).toHaveValue(evidenceId);
await d.getByLabel('回单付款账号').fill(incoming ? s.payee : s.payer);
await d.getByLabel('回单收款账号').fill(incoming ? s.payer : s.payee);
await d.getByLabel('回单人民币金额').fill(amount);