import { Buffer } from 'node:buffer'; import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const paymentId = '01M00000000000000000000201'; const companyId = '01M00000000000000000000202'; const projectId = '01M00000000000000000000203'; const contractId = '01M00000000000000000000204'; const supplierId = '01M00000000000000000000205'; const bankAccountId = '01M00000000000000000000206'; const fileId = '01M00000000000000000000207'; function envelope(data: unknown, totalElements?: number) { return { data, ...(typeof totalElements === 'number' ? { meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 } } : {}), requestId: '01M00000000000000000000299', }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000208', username: 'payment-e2e', displayName: '付款结果登记员', departmentName: '财务部', mustChangePassword: false, }, roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }], activeRole: 'FINANCE_MANAGER', permissions: ['payment:request:view', 'payment:request:record-result'], }; } function payment(overrides: Record = {}) { return { publicId: paymentId, businessNo: 'PAY-E2E-001', company: { publicId: companyId, code: 'GS-E2E', name: '凯迪测试公司' }, project: { publicId: projectId, code: 'XM-E2E', name: '付款结果项目' }, contract: { publicId: contractId, code: 'HT-E2E', name: '测试合同' }, payable: null, supplier: { publicId: supplierId, code: 'GYS-E2E', name: '测试供应商' }, bankAccount: { publicId: bankAccountId, supplierId, accountName: '测试账户', bankName: '测试银行', maskedAccountNo: '****0001', versionNo: 1, }, sourceDocumentId: null, originalPaymentId: null, repaymentSequence: 0, requestedAmount: '1000.00', approvedAmount: '1000.00', frozenAmount: '1000.00', paidAmount: '0.00', currency: 'CNY', requestedDate: '2026-08-15', purpose: '付款结果登记验收', status: 'OFFLINE_PENDING', submittedByName: null, approvedByName: '财务审批员', resultRecordedByName: null, resultVerifiedByName: null, version: 2, createdAt: '2026-08-15T08:00:00Z', updatedAt: '2026-08-15T08:00:00Z', allowedActions: ['RECORD_SUCCESS'], ...overrides, }; } interface FixtureOptions { paymentQueries: URLSearchParams[]; recordRequests: Array>; recordHeaders: Array>; } async function installFixture(page: Page, options: FixtureOptions) { const userSession = session(); let currentPayment = payment(); await page.route('**/api/v1/**', async (route: Route) => { const request = route.request(); const url = new URL(request.url()); const pathname = url.pathname; if (pathname === '/api/v1/auth/session') return route.fulfill({ json: envelope(userSession) }); if (pathname === '/api/v1/auth/profile') return route.fulfill({ json: envelope(userSession.user) }); if (pathname === '/api/v1/auth/roles') return route.fulfill({ json: envelope(userSession.roles) }); if (pathname === '/api/v1/auth/csrf') { return route.fulfill({ headers: { 'set-cookie': 'XSRF-TOKEN=payment-e2e-csrf; Path=/' }, json: envelope({ token: 'payment-e2e-csrf' }), }); } if (request.method() === 'GET' && pathname === '/api/v1/payments/references') { return route.fulfill({ json: envelope({ companies: [], projects: [], suppliers: [], contracts: [], payables: [], bankAccounts: [], attachmentRules: [], }), }); } if (request.method() === 'GET' && pathname === '/api/v1/payments') { options.paymentQueries.push(new URLSearchParams(url.searchParams)); return route.fulfill({ json: envelope([currentPayment], 1) }); } if (request.method() === 'POST' && pathname === '/api/v1/files') { return route.fulfill({ json: envelope({ publicId: fileId, originalName: 'payment-receipt.pdf', mediaType: 'application/pdf', sizeBytes: 24, sha256: 'd'.repeat(64), scanStatus: 'AVAILABLE', }), }); } if (request.method() === 'POST' && pathname === `/api/v1/payments/${paymentId}/confirm-offline-result`) { options.recordRequests.push(request.postDataJSON() as Record); options.recordHeaders.push(request.headers()); currentPayment = payment({ status: 'RESULT_RECORDED', version: 3, resultRecordedByName: '付款结果登记员', allowedActions: [], updatedAt: '2026-08-16T08:05:00Z', }); return route.fulfill({ json: envelope(currentPayment) }); } if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) }); return route.fulfill({ status: 204 }); }); } test('PAGE-15 blocks a result without evidence and records a successful offline payment', async ({ page }) => { const paymentQueries: URLSearchParams[] = []; const recordRequests: Array> = []; const recordHeaders: Array> = []; await installFixture(page, { paymentQueries, recordRequests, recordHeaders }); await page.goto('/finance/payments?tab=offline'); await expect(page.getByRole('heading', { name: '进度款与付款', exact: true })).toBeVisible(); await expect(page.getByText('PAY-E2E-001', { exact: true }).first()).toBeVisible(); await expect.poll(() => paymentQueries.length).toBeGreaterThan(0); const query = paymentQueries.at(-1)!; expect(query.get('tab')).toBe('offline'); expect(query.get('sort')).toBe('updatedAt,desc'); expect(query.get('page')).toBe('1'); expect(query.get('size')).toBe('20'); const initialListRequests = paymentQueries.length; await page.getByRole('button', { name: '更多操作', exact: true }).click(); await page.getByText('登记付款成功', { exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '登记付款成功' }); const resultDate = await page.evaluate(() => { const now = new Date(); return new Date(now.getTime() - now.getTimezoneOffset() * 60_000).toISOString().slice(0, 10); }); await expect(dialog.locator('.t-date-picker input')).toHaveValue(resultDate); await dialog.getByRole('button', { name: '确认', exact: true }).click(); await expect(dialog.getByText('请上传回单或结果附件。', { exact: true })).toBeVisible(); expect(recordRequests).toHaveLength(0); await dialog.getByPlaceholder('请输入金额', { exact: true }).fill('1000'); await dialog.locator('textarea').fill('线下转账已完成'); await dialog.locator('input[type="file"]').setInputFiles({ name: 'payment-receipt.pdf', mimeType: 'application/pdf', buffer: Buffer.from('payment-result-evidence'), }); await dialog.getByRole('button', { name: '上传已选文件', exact: true }).click(); await expect(dialog.getByText('可用', { exact: true })).toBeVisible(); await dialog.getByRole('button', { name: '确认', exact: true }).click(); await expect.poll(() => recordRequests.length).toBe(1); expect(recordRequests[0]).toEqual({ version: 2, resultAt: resultDate, amount: '1000.00', receiptFileIds: [fileId], remark: '线下转账已完成', }); expect(recordHeaders[0]['x-xsrf-token']).toBe('payment-e2e-csrf'); expect(recordHeaders[0]['idempotency-key']).toBeTruthy(); await expect(dialog).toBeHidden(); await expect.poll(() => paymentQueries.length).toBeGreaterThan(initialListRequests); await expect(page.getByText('结果待复核', { exact: true })).toBeVisible(); await expect(page.getByText('登记付款成功已完成', { exact: true })).toBeVisible(); });