区分正式付款回单、非最终回执与未知凭证,在核验构建、人工证据重建和最终核销时使用同一份有效依据。非最终说明、入账方向、状态矛盾及重复交易不能经人工覆盖。 正式回单无字面成功状态时,允许财务按原件实际记账支出、日期及说明提出核验,由本批次适用的确认人确认;不补造银行成功文字。旧核验可显式本地重检,新版本与审计原子保存,不重新外发 OCR,不沿用旧人工批准或改写已核销历史。 本地验证:82 个迁移、借款 HTTP 116 组、关联电脑/手机 18 组、共享回单数据库与真实认证 38 组、共享报销页面限定重验 24 项、最新回单/报销定向 292 项通过;9 个资金守卫变异均被捕获。服务端全量 4554 项通过、1 项既有可选跳过,后续合成数据及零页详情改动由最新定向验证覆盖。 本次无新增迁移,新增本地 PDF 文本解析依赖。后续对本提交运行完整 verify-candidate,实际候选与结果以本轮回执为准。 限制:独立代码复核及真实还款验收待完成;历史材料预验不证明真实还本或银行提交对应。未推送、未部署、未访问生产或写入真实业务数据。
265 行
24 KiB
JavaScript
265 行
24 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
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;
|
|
const own = !h.browser; const browser = h.browser ?? await chromium.launch({ headless: true, args: ['--disable-background-networking'] });
|
|
const origin = h.origin ?? await h.app.listen({ host: '127.0.0.1', port: 0 });
|
|
const root = path.resolve(__dirname, '../..'); mkdirSync(path.join(root, 'artifacts/qa-flow/loan-related-20261008'), { recursive: true });
|
|
const output = h.output ?? mkdtempSync(path.join(root, 'artifacts/qa-flow/loan-related-20261008/browser-'));
|
|
const f = relatedParty.helpers; const checks = [];
|
|
const oldWait = config.loanReleaseWaitDays; config.loanReleaseWaitDays = 0;
|
|
const oldOrigin = config.appOrigin; config.appOrigin = origin;
|
|
try {
|
|
for (const s of relatedParty.ui) {
|
|
const mobile = s.mode === 'mobile'; const prefix = mobile ? '/mobile' : ''; const contexts = [];
|
|
console.log(JSON.stringify({ stage: 'loan_related_browser_start', mode: s.mode }));
|
|
async function login(who, route = `${prefix}/company-loans`) {
|
|
const context = await browser.newContext({ viewport: mobile ? { width: 390, height: 844 } : { width: 1440, height: 1000 }, serviceWorkers: 'block' });
|
|
contexts.push(context); await context.route('**/*', r => r.request().url().startsWith(`${origin}/`) ? r.continue() : r.abort());
|
|
const p = await context.newPage();
|
|
await p.goto(`${origin}/login?redirect=${encodeURIComponent(route)}`);
|
|
await p.getByTestId('login-identifier').fill(who.email); await p.getByTestId('login-password').fill(password); await p.getByTestId('login-submit').click();
|
|
await p.waitForURL(url => !url.pathname.includes('login'));
|
|
return p;
|
|
}
|
|
const go = async (p, b) => { await p.goto(`${origin}${prefix}/payment-batches?id=${b.id}`); await expect(p.getByLabel('关联方还款模式')).toBeVisible({ timeout: 20000 }); };
|
|
const paymentButton = (p, kind) => p.getByTestId(mobile ? 'payment-batch-mobile-actions' : 'payment-batch-action-panel').getByTestId(`payment-batch-action-${kind}`);
|
|
async function submit(p, fill, confirm = '确认') {
|
|
const d = p.getByRole('dialog'); if (fill) await fill(d);
|
|
const response = p.waitForResponse(r => r.url().startsWith(`${origin}/api/`) &&
|
|
!r.url().includes('/api/auth/') && ['POST', 'PATCH'].includes(r.request().method()));
|
|
await d.getByRole('button', { name: confirm, exact: true }).click();
|
|
const result = await response;
|
|
if (!result.ok()) {
|
|
const failure = (await result.json().catch(() => ({}))).details?.reasonCode;
|
|
throw new Error(`related ${s.mode} ${confirm}: HTTP ${result.status()} ${failure ?? ''}`);
|
|
}
|
|
try { await expect(d).not.toBeVisible({ timeout: 15000 }); }
|
|
catch { throw new Error(`related ${s.mode} dialog: ${await d.getByRole('alert').allTextContents()}`); }
|
|
return result;
|
|
}
|
|
async function recon(p, label, fill) {
|
|
await p.getByRole('region', { name: '银行结果对账', exact: true }).getByRole('button', { name: label, exact: true }).click();
|
|
return submit(p, async d => { if (fill) await fill(d); if (await d.getByLabel('依据与说明').count()) await d.getByLabel('依据与说明').fill('合成材料唯一定位具体银行指令及全部候选批次'); });
|
|
}
|
|
const review = (p, label) => recon(p, label, d => d.getByLabel('复核结论').selectOption('approve'));
|
|
async function upload(p, kind) {
|
|
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, overrides = {}, document = {}) {
|
|
const response = p.waitForResponse(r => r.url().endsWith(`/payment-batches/${b.id}/receipt-attachments`) && r.request().method() === 'POST');
|
|
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, ...overrides });
|
|
await p.reload(); return result;
|
|
}
|
|
try {
|
|
const fp = await login(finance); const mp = await login(manager);
|
|
await fp.getByRole('tab', { name: '公司代表授权', exact: true }).click();
|
|
await fp.getByRole('button', { name: '登记公司代表授权范围', exact: true }).click();
|
|
const dialog = fp.getByRole('dialog');
|
|
await expect(dialog.getByLabel('授权出借人').locator('option').nth(1)).toBeAttached();
|
|
for (let i = 0; !(await dialog.getByLabel('授权出借人').locator(`option[value="${s.lender.id}"]`).count()); i++) {
|
|
assert.ok(i < 12); await expect(dialog.getByRole('button', { name: '下一页出借人' })).toBeEnabled();
|
|
const response = fp.waitForResponse(r => r.url().includes('/loan-lenders?page=')); await dialog.getByRole('button', { name: '下一页出借人' }).click(); await response;
|
|
}
|
|
await dialog.getByLabel('授权出借人').selectOption(s.lender.id);
|
|
await dialog.getByRole('checkbox', { name: new RegExp(s.loan.loanNo) }).check();
|
|
await submit(fp, d => d.getByLabel('代表权及授权范围依据').fill('合成已确认公司代表权,限定本公司与这笔借款'));
|
|
const a = await prisma.loanRelatedAuthorization.findFirstOrThrow({ where: { lender_id: s.lender.id }, orderBy: { version: 'desc' } });
|
|
await expect(fp.getByRole('button', { name: '代表公司确认授权范围', exact: true })).toHaveCount(0);
|
|
await mp.reload();
|
|
await expect(mp.getByRole('button', { name: '登记借入', exact: true })).toHaveCount(0);
|
|
await expect(mp.getByRole('button', { name: '登记公司代表授权范围', exact: true })).toHaveCount(0);
|
|
const card = mp.locator('section.card-std').filter({ hasText: s.lender.name });
|
|
await expect(card).toBeVisible();
|
|
await card.getByRole('button', { name: '代表公司确认授权范围' }).click();
|
|
await expect(mp.getByRole('dialog')).toContainText(s.loan.loanNo);
|
|
await submit(mp, d => d.getByLabel('确认或停止的理由').fill('合成代表公司确认已登记范围与本人关联身份'));
|
|
await fp.getByRole('tab', { name: '出借人', exact: true }).click();
|
|
const lenderCard = fp.locator('section.card-std').filter({ hasText: s.lender.name });
|
|
for (let page = 1; !(await lenderCard.count()); page++) {
|
|
assert.ok(page < 20);
|
|
const next = fp.getByRole('button', { name: '下一页', exact: true }); await expect(next).toBeEnabled();
|
|
const loaded = fp.waitForResponse(r => r.url().includes(`/loan-lenders?page=${page + 1}&`));
|
|
await next.click(); await loaded;
|
|
await expect(fp.getByRole('status').filter({ hasText: '加载中…' })).toHaveCount(0);
|
|
}
|
|
await fp.getByRole('button', { name: '申请还本', exact: true }).click();
|
|
const creation = fp.getByRole('dialog'); await creation.getByLabel('出借人', { exact: true }).selectOption(s.lender.id);
|
|
await creation.getByLabel(`分配 ${s.loan.originReference}`, { exact: true }).fill('10.00');
|
|
await submit(fp, d => d.getByLabel('说明', { exact: true }).fill('合成关联方正常还本'), '提交还款申请');
|
|
await fp.waitForURL(/payment-batches\?id=/);
|
|
const b = await prisma.paymentBatch.findFirstOrThrow({ where: { loan_repayment: { lender_id: s.lender.id } }, orderBy: { created_at: 'desc' }, include: { items: true } });
|
|
await go(mp, b);
|
|
await expect(mp.getByTestId('payment-batch-action-downloadOrder')).toHaveCount(0);
|
|
await mp.getByRole('button', { name: '查看完整账号', exact: true }).click();
|
|
await mp.getByRole('dialog').getByLabel('查看收款账号原因').fill('合成公司代表核对本笔收款账号');
|
|
await mp.getByRole('dialog').getByRole('button', { name: '确认查看', exact: true }).click();
|
|
await expect(mp.getByRole('dialog')).toContainText(s.lender.bankAccount);
|
|
await mp.getByRole('dialog').getByRole('button', { name: '返回', exact: true }).click();
|
|
await paymentButton(mp, 'review').click(); await submit(mp, undefined, '代表公司确认');
|
|
await go(fp, b); await paymentButton(fp, 'downloadOrder').click();
|
|
const downloaded = fp.waitForEvent('download'); await submit(fp, undefined, '确认下载'); await downloaded;
|
|
assert.equal((await receiptUpload(fp, b, '1000', `RP-UI-${s.mode}`)).status, 'matched');
|
|
await expect(paymentButton(fp, 'execute')).toBeEnabled({ timeout: 15000 });
|
|
await paymentButton(fp, 'execute').click(); await submit(fp, undefined, '确认执行');
|
|
assert.equal((await prisma.loanRepaymentAllocation.findFirstOrThrow({ where: { repayment_id: b.loan_repayment_id } })).reservation_state, 'settled');
|
|
await expect(fp.getByLabel('关联方还款模式')).toBeVisible();
|
|
checks.push(`${s.mode}_related_scope_company_confirmation_payee_read_export_ocr_settlement`);
|
|
|
|
const returned = await f.draft(s, '9.00'); await go(mp, returned);
|
|
await mp.getByRole('button', { name: '更多', exact: true }).click();
|
|
await mp.getByRole('menuitem', { name: '退回付款申请', exact: true }).click();
|
|
await submit(mp, d => d.getByLabel('付款退回理由').fill('合成公司代表退回,请更正付款金额'), '确认退回');
|
|
await expect(mp.getByTestId('payment-batch-action-review')).toHaveCount(0);
|
|
assert.equal((await prisma.loanRepaymentAllocation.findFirstOrThrow({ where: { repayment_id: returned.loanRepaymentId } })).reservation_state, 'reserved');
|
|
await go(fp, returned); await expect(fp.getByText('退回未释放占用,财务须取消原申请后重新提交。', { exact: false })).toBeVisible();
|
|
await fp.getByRole('button', { name: '更多', exact: true }).click();
|
|
await fp.getByRole('menuitem', { name: '取消批次', exact: true }).click();
|
|
await submit(fp, d => d.getByLabel('批次取消原因').fill('合成按公司代表退回要求取消后重新提交'), '确认取消');
|
|
assert.equal((await prisma.loanRepaymentAllocation.findFirstOrThrow({ where: { repayment_id: returned.loanRepaymentId } })).reservation_state, 'released');
|
|
checks.push(`${s.mode}_related_return_reason_finance_cancel_and_retained_decision`);
|
|
|
|
const manual = await f.batch(s, '20.00'); await go(fp, manual);
|
|
assert.equal((await receiptUpload(fp, manual, null, `RP-UI-MANUAL-${s.mode}`)).status, 'needs_review');
|
|
await fp.getByRole('button', { name: '核验详情', exact: true }).click();
|
|
let d = fp.getByRole('dialog'); await d.getByLabel('第1页对应付款明细').selectOption(manual.items[0].id);
|
|
await d.getByRole('button', { name: '更正识别', exact: true }).click();
|
|
await d.getByLabel('第1页更正小写金额').fill('20.00');
|
|
await submit(fp, d => d.getByLabel('更正说明(仅关联可留空)').fill('合成回单原件金额清晰,人工补录'), '保存核验');
|
|
await go(mp, manual); await mp.getByRole('button', { name: '核验详情', exact: true }).click();
|
|
await expect(mp.getByRole('dialog').getByRole('button', { name: '保存核验' })).toHaveCount(0);
|
|
await submit(mp, d => d.getByLabel('复核说明').fill('合成公司代表逐项核对回单原件'), '公司代表确认');
|
|
await go(fp, manual); await expect(paymentButton(fp, 'execute')).toBeEnabled();
|
|
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);
|
|
await expect(fp.getByRole('button', { name: '登记人工未提交', exact: true })).toHaveCount(0);
|
|
await recon(fp, '登记银行提交', async d => { await d.getByLabel('使用的导出文件(手工录入可不选)').selectOption({ index: 1 }); await d.getByLabel('银行指令号').fill(ref); await d.getByLabel('实际提交时间').fill(new Date().toISOString().slice(0, 16)); });
|
|
await recon(fp, '确认提交清单完整'); await go(mp, failed); await review(mp, '公司代表确认提交清单');
|
|
await go(fp, failed); await recon(fp, '登记失败回单归属', async d => { await d.getByLabel('对应银行提交').selectOption({ index: 1 }); await d.getByLabel('失败回单(终态书面证据可不选)').selectOption({ index: 1 }); await d.getByLabel('排除其他候选批次的依据').fill('合成其他同金额批次对应不同的银行指令'); });
|
|
await go(mp, failed); await review(mp, '公司代表确认失败归属');
|
|
await go(fp, failed); await recon(fp, '登记银行终态事实', async d => { await d.getByLabel('对应银行提交').selectOption({ index: 1 }); await d.getByLabel('失败回单(终态书面证据可不选)').selectOption({ index: 1 }); });
|
|
await go(mp, failed); await review(mp, '公司代表确认终态事实'); await recon(mp, '确认银行未付款');
|
|
await go(fp, failed); await recon(fp, '发起补证恢复'); await recon(fp, '送审补证恢复');
|
|
await go(mp, failed); await review(mp, '公司代表确认补证恢复');
|
|
await go(fp, failed); await recon(fp, '重验依据并取消释放');
|
|
assert.equal(await prisma.loanRepaymentAllocation.count({ where: { repayment_id: failed.loanRepaymentId, reservation_state: 'reserved' } }), 0);
|
|
checks.push(`${s.mode}_related_submission_failure_attribution_terminal_recovery_release`);
|
|
|
|
const late = await f.setup('100.00'); const old = await f.batch(late, '80.00'); await f.terminate(late, old, true);
|
|
const paid = await f.batch(late, '60.00'); await f.receipt(paid.id, '6000', `RP-UI-ALREADY-${s.mode}`, { payeeAccount: late.lender.bankAccount });
|
|
// Seed the distinct already-paid transaction; every new reconciliation
|
|
// action under test below is made through the rendered dialogs.
|
|
const execute = await h.relatedParty.helpers.execute(paid); assert.equal(execute.item.status, 'executed');
|
|
const count = await prisma.paymentBatch.count();
|
|
async function bankFields(d, amount, reference, evidenceId, incoming = false) {
|
|
await d.getByLabel('对应的成功银行回单').selectOption(evidenceId);
|
|
await d.getByLabel('回单付款账号').fill(incoming ? late.lender.bankAccount : '8'.repeat(16)); await d.getByLabel('回单收款账号').fill(incoming ? '8'.repeat(16) : late.lender.bankAccount);
|
|
await d.getByLabel('回单人民币金额').fill(amount); await d.getByLabel('回单流水号').fill(reference);
|
|
await d.getByLabel('为什么不属于其他候选批次或交易').fill('合成原件指明原批次,逐笔排除其他同金额付款');
|
|
}
|
|
await go(fp, old); const lateEvidence = await upload(fp, 'late_success'); await recon(fp, '登记晚到成功', d => bankFields(d, '80.00', `RP-UI-LATE-${s.mode}`, lateEvidence));
|
|
await go(mp, old); await review(mp, '公司代表确认银行事实'); await go(fp, old); await recon(fp, '登记补记或追回');
|
|
await expect(fp.getByRole('region', { name: '银行结果对账', exact: true })).toContainText('待追回 ¥40.00');
|
|
for (const [amount, left] of [['15.00', '25.00'], ['25.00', '0.00']]) {
|
|
const returnEvidence = await upload(fp, 'incoming_return'); await recon(fp, '登记追回到账', d => bankFields(d, amount, `RP-UI-RETURN-${s.mode}-${amount}`, returnEvidence, true));
|
|
await go(mp, old); await review(mp, '公司代表确认银行事实'); await go(fp, old); await recon(fp, '登记补记或追回');
|
|
await expect(fp.getByRole('region', { name: '银行结果对账', exact: true })).toContainText(`待追回 ¥${left}`);
|
|
assert.equal(await prisma.loanFreezeReason.count({ where: { lender_id: late.lender.id, status: 'active' } }), left === '0.00' ? 0 : 1);
|
|
}
|
|
assert.equal(await prisma.paymentBatch.count(), count);
|
|
checks.push(`${s.mode}_related_late_success_partial_and_full_return_no_payment_batch`);
|
|
const duplicateEvidence = await upload(fp, 'late_success'); await recon(fp, '登记晚到成功', d => bankFields(d, '80.00', `RP-UI-LATE-${s.mode}`, duplicateEvidence)); const correctionEvidence = await upload(fp, 'bank_terminal');
|
|
await go(mp, old); await recon(mp, '公司代表确认银行事实', async d => {
|
|
await d.getByLabel('复核结论').selectOption('reject');
|
|
const field = d.getByLabel('误关联或重复登记的更正证据(晚到成功必填)'); await field.selectOption(correctionEvidence);
|
|
await d.getByLabel('关闭原因').selectOption('duplicate');
|
|
});
|
|
assert.equal(await prisma.loanFreezeReason.count({ where: { lender_id: late.lender.id, status: 'active' } }), 0);
|
|
await expect(mp.getByRole('region', { name: '银行结果对账', exact: true })).toContainText('已确认误报');
|
|
assert.equal(await mp.evaluate(() => document.documentElement.scrollWidth > innerWidth + 1), false);
|
|
await mp.screenshot({ path: path.join(output, `related-${s.mode}-closed.png`), fullPage: true });
|
|
checks.push(`${s.mode}_related_evidenced_duplicate_dismissal_and_no_overflow`);
|
|
const tech = await login(ordinary); await tech.waitForURL(/\/403$/); await expect(tech.getByRole('heading', { name: '公司借款', exact: true })).toHaveCount(0);
|
|
assert.equal(await prisma.notification.count({ where: { recipient_employee_id: ordinary.id, target_id: { in: [b.id, failed.id, old.id] } } }), 0);
|
|
// Stopping from the actual scope card uses the current grant only.
|
|
await mp.goto(`${origin}${prefix}/company-loans`);
|
|
const originalCard = mp.locator('section.card-std').filter({ hasText: s.lender.name });
|
|
await originalCard.getByRole('button', { name: '停止新付款授权', exact: true }).click();
|
|
await submit(mp, d => d.getByLabel('确认或停止的理由').fill('合成暂停后续新付款,保留对账'));
|
|
assert.equal((await prisma.loanRelatedAuthorizationEvent.findFirstOrThrow({ where: { authorization_id: a.id }, orderBy: { sequence: 'desc' } })).payment_enabled, false);
|
|
checks.push(`${s.mode}_technical_employee_excluded_and_scope_stop_via_ui`);
|
|
} catch (error) {
|
|
for (const [index, context] of contexts.entries()) {
|
|
const p = context.pages()[0]; if (!p) continue;
|
|
await p.screenshot({ path: path.join(output, `failed-${s.mode}-${index}.png`), fullPage: true }).catch(() => {});
|
|
writeFileSync(path.join(output, `failed-${s.mode}-${index}.json`), JSON.stringify({
|
|
path: new URL(p.url()).pathname, alerts: await p.getByRole('alert').allTextContents(),
|
|
headings: await p.getByRole('heading').allTextContents(), buttons: await p.getByRole('button').allTextContents(),
|
|
}, null, 2));
|
|
}
|
|
throw error;
|
|
} finally { for (const context of contexts) await context.close(); }
|
|
}
|
|
} finally { config.appOrigin = oldOrigin; if (oldWait === undefined) delete config.loanReleaseWaitDays; else config.loanReleaseWaitDays = oldWait; if (own) await browser.close(); }
|
|
console.log(JSON.stringify({ stage: 'loan_related_browser', status: 'PASS', checks, output: path.relative(root, output) }));
|
|
return checks;
|
|
};
|