import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const projectId = '01M00000000000000000000101'; const receivableId = '01M00000000000000000000102'; const invoiceId = '01M00000000000000000000103'; const sourceDocumentId = '01M00000000000000000000108'; function envelope(data: unknown, totalElements?: number) { return { data, ...(typeof totalElements === 'number' ? { meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 } } : {}), requestId: '01M00000000000000000000199', }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000104', username: 'finance-e2e', displayName: '财务验收用户', departmentName: '财务部', mustChangePassword: false, }, roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }], activeRole: 'FINANCE_MANAGER', permissions: [ 'receivable:receipt:view', 'receivable:receipt:create', 'receivable:receipt:export', 'receivable:invoice:view', 'receivable:invoice:create', 'receivable:invoice:export', 'receivable:invoice:review', 'source:form:view', ], }; } function receivable() { return { publicId: receivableId, businessNo: 'REC-E2E-001', company: { publicId: '01M00000000000000000000105', code: 'C-E2E', name: '凯迪测试公司' }, project: { publicId: projectId, code: 'P-E2E', name: '应收穿透项目' }, contract: { publicId: '01M00000000000000000000106', code: 'HT-E2E', name: '测试合同' }, counterparty: { publicId: '01M00000000000000000000107', code: 'KH-E2E', name: '测试客户' }, businessDate: '2026-08-15', amount: '125000.00', settledAmount: '25000.00', outstandingAmount: '100000.00', currency: 'CNY', status: 'PARTIALLY_SETTLED', sourceInvoiceId: invoiceId, version: 1, createdAt: '2026-08-15T08:00:00Z', updatedAt: '2026-08-16T08:00:00Z', }; } function invoice(overrides: Record = {}) { return { publicId: invoiceId, businessNo: 'INV-E2E-001', company: receivable().company, project: receivable().project, contract: receivable().contract, counterparty: receivable().counterparty, invoiceType: 'SPECIAL', amount: '125000.00', currency: 'CNY', taxRate: '0.13', requestedDate: '2026-08-15', invoiceNo: 'FP-E2E-001', issuedDate: '2026-08-15', status: 'ISSUED', contractLimit: '500000.00', contractCommittedAmount: '125000.00', contractAvailableAmount: '375000.00', version: 2, createdAt: '2026-08-15T08:00:00Z', updatedAt: '2026-08-16T08:00:00Z', allowedActions: [], ...overrides, }; } async function installFixture(page: Page, receivableQueries: URLSearchParams[]) { const userSession = session(); 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/receivables/references') { return route.fulfill({ json: envelope({ companies: [receivable().company], projects: [receivable().project], counterparties: [receivable().counterparty], contracts: [receivable().contract], receivables: [], }), }); } if (pathname === '/api/v1/receivables') { receivableQueries.push(url.searchParams); return route.fulfill({ json: envelope([receivable()], 1) }); } if (pathname === `/api/v1/invoices/${invoiceId}`) return route.fulfill({ json: envelope(invoice()) }); if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) }); return route.fulfill({ status: 204 }); }); } async function installInvoiceReviewFixture( page: Page, invoiceQueries: URLSearchParams[], reviewRequests: Array>, reviewHeaders: Array>, ) { const userSession = session(); let currentInvoice = invoice({ businessNo: 'INV-E2E-RETURN-001', sourceDocumentId, sourceVersionId: '01M00000000000000000000109', invoiceNo: null, issuedDate: null, status: 'REVIEWING', version: 7, allowedActions: ['REVIEW'], }); 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=page14-e2e; Path=/' }, json: envelope({ token: 'page14-e2e' }), }); } if (request.method() === 'GET' && pathname === '/api/v1/receivables/references') { return route.fulfill({ json: envelope({ companies: [receivable().company], projects: [receivable().project], counterparties: [receivable().counterparty], contracts: [receivable().contract], receivables: [], }), }); } if (request.method() === 'GET' && pathname === '/api/v1/invoices') { invoiceQueries.push(new URLSearchParams(url.searchParams)); return route.fulfill({ json: envelope([currentInvoice], 1) }); } if (request.method() === 'GET' && pathname === `/api/v1/invoices/${invoiceId}`) { return route.fulfill({ json: envelope(currentInvoice) }); } if (request.method() === 'POST' && pathname === `/api/v1/invoices/${invoiceId}/review`) { const payload = request.postDataJSON() as Record; reviewRequests.push(payload); reviewHeaders.push(request.headers()); currentInvoice = invoice({ ...currentInvoice, status: 'RETURNED', version: 8, reviewOpinion: payload.opinion, allowedActions: [], }); return route.fulfill({ json: envelope(currentInvoice) }); } if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) }); return route.fulfill({ status: 204 }); }); } test('PAGE-14 shows the project receivable and drills into its exact source invoice', async ({ page }) => { const receivableQueries: URLSearchParams[] = []; await installFixture(page, receivableQueries); await page.goto(`/finance/receipts-invoices?tab=receivables&projectId=${projectId}&keyword=REC-E2E-001`); await expect(page.getByRole('heading', { name: '收款与发票', exact: true })).toBeVisible(); await expect.poll(() => receivableQueries.length).toBeGreaterThan(0); const query = receivableQueries.at(-1)!; expect(query.get('projectId')).toBe(projectId); expect(query.get('keyword')).toBe('REC-E2E-001'); expect(query.get('sort')).toBe('updatedAt,desc'); await expect(page.getByRole('button', { name: '导出', exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: '登记收款', exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: '新建开票申请', exact: true })).toHaveCount(0); await expect(page.getByText('100,000.00', { exact: true })).toBeVisible(); await expect(page.getByText('部分核销', { exact: true })).toBeVisible(); await page.getByRole('button', { name: '查看', exact: true }).click(); const drawer = page.locator('.t-drawer'); await expect(drawer.getByText('应收金额', { exact: true })).toBeVisible(); await expect(drawer.getByText('未收金额', { exact: true })).toBeVisible(); await drawer.getByRole('button', { name: '查看来源发票', exact: true }).click(); await expect(drawer.locator('.t-drawer__header')).toHaveText('INV-E2E-001'); await expect(drawer.getByText('FP-E2E-001', { exact: true })).toBeVisible(); await expect(drawer.getByText(/13%/)).toBeVisible(); }); test('PAGE-14 returns an OA-08 invoice with exact correction fields', async ({ page }) => { const invoiceQueries: URLSearchParams[] = []; const reviewRequests: Array> = []; const reviewHeaders: Array> = []; await installInvoiceReviewFixture(page, invoiceQueries, reviewRequests, reviewHeaders); await page.goto('/finance/receipts-invoices?tab=invoices'); await expect(page.getByText('INV-E2E-RETURN-001', { exact: true }).first()).toBeVisible(); await expect.poll(() => invoiceQueries.length).toBeGreaterThan(0); const initialListRequests = invoiceQueries.length; await page.getByRole('button', { name: '更多操作', exact: true }).click(); await page.getByText('审核申请', { exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '审核申请' }); await dialog.getByText('退回补件', { exact: true }).click(); await expect(dialog.getByText('需要更正的来源字段', { exact: true })).toBeVisible(); await expect(dialog.getByText('驳回', { exact: true })).toHaveCount(0); await dialog.getByRole('button', { name: '确认', exact: true }).click(); await expect(page.locator('.t-message').filter({ hasText: '请填写审核意见' })).toBeVisible(); await dialog.locator('textarea').fill('请更正本次开票金额和开票要求'); await dialog.getByRole('button', { name: '确认', exact: true }).click(); await expect(page.locator('.t-message').filter({ hasText: '请选择至少一个需要更正的来源字段' })).toBeVisible(); await dialog.locator('label.t-checkbox').filter({ hasText: '本次开票金额' }).click(); await dialog.locator('label.t-checkbox').filter({ hasText: '开票要求' }).click(); await dialog.getByRole('button', { name: '确认', exact: true }).click(); await expect.poll(() => reviewRequests.length).toBe(1); expect(reviewRequests[0]).toEqual({ version: 7, decision: 'RETURN', opinion: '请更正本次开票金额和开票要求', returnPoints: ['invoiceAmount', 'invoiceRequirements'], }); expect(reviewHeaders[0]['x-xsrf-token']).toBe('page14-e2e'); expect(reviewHeaders[0]['idempotency-key']).toBeTruthy(); await expect(dialog).toBeHidden(); await expect.poll(() => invoiceQueries.length).toBeGreaterThan(initialListRequests); await expect(page.getByText('已退回', { exact: true })).toBeVisible(); await page.getByRole('button', { name: '查看', exact: true }).click(); const drawer = page.locator('.t-drawer'); await expect(drawer.getByText('该候选已退回来源表单,需由原申请人更正并重新完成审批。')).toBeVisible(); await drawer.getByRole('button', { name: '查看来源表单', exact: true }).click(); await expect(page).toHaveURL(`/forms/OA-08/${sourceDocumentId}`); });