import type { Page, Route } from '@playwright/test'; import { expect, test } from '@playwright/test'; const contractId = '01M00000000000000000000301'; const payableId = '01M00000000000000000000302'; const projectId = '01M00000000000000000000303'; const sourceDocumentId = '01M00000000000000000000304'; const costCategoryId = '01M00000000000000000000305'; const companyId = '01M00000000000000000000306'; function envelope(data: unknown, totalElements?: number) { return { data, ...(typeof totalElements === 'number' ? { meta: { page: 1, size: 20, totalElements, totalPages: totalElements ? 1 : 0 } } : {}), requestId: '01M00000000000000000000399', }; } function session() { return { authenticated: true, user: { publicId: '01M00000000000000000000307', username: 'payable-e2e', displayName: '应付登记员', departmentName: '财务部', mustChangePassword: false, }, roles: [{ code: 'FINANCE_MANAGER', name: '财务管理人员', workbenchRoute: '/workbench/finance' }], activeRole: 'FINANCE_MANAGER', permissions: ['contractcost:ledger:view', 'contractcost:payable:create', 'contractcost:payable:confirm'], }; } function contract(overrides: Record = {}) { return { publicId: contractId, businessNo: 'HT-E2E-013', name: '合同成本验收合同', company: { publicId: companyId, code: 'GS-E2E', name: '凯迪测试公司' }, project: { publicId: projectId, code: 'XM-E2E', name: '合同成本验收项目' }, counterparty: { publicId: '01M00000000000000000000308', code: 'GYS-E2E', name: '测试供应商' }, originalAmount: '1000.00', approvedChangeAmount: '0.00', settlementAmount: '0.00', invoicedAmount: '0.00', pendingPayableAmount: '0.00', payableAmount: '0.00', paidAmount: '0.00', availableAmount: '1000.00', currency: 'CNY', status: 'ACTIVE', version: 7, updatedAt: '2026-08-16T08:00:00Z', allowedActions: ['CREATE_PAYABLE'], ...overrides, }; } function payable(status: 'PENDING' | 'CONFIRMED' = 'PENDING') { return { publicId: payableId, businessNo: 'PAY-E2E-013', businessDate: '2026-08-16', costCategory: { publicId: costCategoryId, code: 'CC-E2E', name: '测试成本类别' }, amount: '128.50', invoicedAmount: '0.00', paidAmount: '0.00', unpaidAmount: '128.50', status, version: status === 'PENDING' ? 0 : 1, updatedAt: '2026-08-16T08:05:00Z', allowedActions: status === 'PENDING' ? ['CONFIRM'] : [], }; } function summary(currentContract: ReturnType) { return { contractCount: 1, originalAmount: currentContract.originalAmount, approvedChangeAmount: currentContract.approvedChangeAmount, settlementAmount: currentContract.settlementAmount, invoicedAmount: currentContract.invoicedAmount, pendingPayableAmount: currentContract.pendingPayableAmount, payableAmount: currentContract.payableAmount, paidAmount: currentContract.paidAmount, availableAmount: currentContract.availableAmount, }; } interface FixtureOptions { createRequests: Array>; confirmRequests: Array>; commandHeaders: Array>; sourceQueries: URLSearchParams[]; } async function installFixture(page: Page, options: FixtureOptions) { const userSession = session(); let currentContract = contract(); let currentPayables: Array> = []; const detail = () => ({ contract: currentContract, changes: [], settlements: [], payables: currentPayables, invoices: [], payments: [], }); 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=payable-e2e-csrf; Path=/' }, json: envelope({ token: 'payable-e2e-csrf' }), }); } if (request.method() === 'GET' && pathname === '/api/v1/contract-costs/references') { return route.fulfill({ json: envelope({ companies: [currentContract.company], projects: [currentContract.project], counterparties: [currentContract.counterparty], costCategories: [{ publicId: costCategoryId, code: 'CC-E2E', name: '测试成本类别' }], }), }); } if (request.method() === 'GET' && pathname === '/api/v1/contract-costs/contracts') { return route.fulfill({ json: { ...envelope({ items: [currentContract], summary: summary(currentContract) }, 1), }, }); } if (request.method() === 'GET' && pathname === `/api/v1/contract-costs/contracts/${contractId}`) { return route.fulfill({ json: envelope(detail()) }); } if (request.method() === 'GET' && pathname === '/api/v1/source/forms') { options.sourceQueries.push(new URLSearchParams(url.searchParams)); return route.fulfill({ json: envelope( [ { publicId: sourceDocumentId, businessNo: 'OA04-E2E-013', sourceNo: 'OA-04-SRC-013', formType: 'OA-04', formName: '应付资金确认单', templateVersion: 1, sourceSystem: 'OA', sourceVersion: '1', company: currentContract.company, project: currentContract.project, applicantName: '应付申请员', businessDate: '2026-08-16', status: 'APPROVED', version: 1, createdByName: '应付申请员', createdAt: '2026-08-16T07:00:00Z', updatedAt: '2026-08-16T07:30:00Z', allowedActions: [], }, ], 1, ), }); } if (request.method() === 'POST' && pathname === `/api/v1/contract-costs/contracts/${contractId}/payables`) { options.createRequests.push(request.postDataJSON() as Record); options.commandHeaders.push(request.headers()); currentContract = contract({ pendingPayableAmount: '128.50', availableAmount: '871.50', version: 8, updatedAt: '2026-08-16T08:05:00Z', }); currentPayables = [payable()]; return route.fulfill({ json: envelope(detail()) }); } if (request.method() === 'POST' && pathname === `/api/v1/contract-costs/payables/${payableId}/confirm`) { options.confirmRequests.push(request.postDataJSON() as Record); options.commandHeaders.push(request.headers()); currentContract = contract({ payableAmount: '128.50', availableAmount: '871.50', version: 9, updatedAt: '2026-08-16T08:10:00Z', }); currentPayables = [payable('CONFIRMED')]; return route.fulfill({ json: envelope(detail()) }); } if (request.method() === 'GET') return route.fulfill({ json: envelope([], 0) }); return route.fulfill({ status: 204 }); }); } test('PAGE-13 registers an approved OA-04 payable and confirms it with separate versions', async ({ page }) => { const createRequests: Array> = []; const confirmRequests: Array> = []; const commandHeaders: Array> = []; const sourceQueries: URLSearchParams[] = []; await installFixture(page, { createRequests, confirmRequests, commandHeaders, sourceQueries }); await page.goto('/finance/contracts-costs'); await expect(page.getByRole('heading', { name: '合同与成本台账', exact: true })).toBeVisible(); await expect(page.getByText('HT-E2E-013', { exact: true }).first()).toBeVisible(); await page.getByRole('button', { name: '查看', exact: true }).click(); const drawer = page.locator('.t-drawer').filter({ hasText: 'HT-E2E-013' }); await drawer.getByRole('button', { name: '登记应付', exact: true }).click(); const dialog = page.locator('.t-dialog').filter({ hasText: '登记应付' }); const businessDate = 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(businessDate); await dialog.getByPlaceholder('请输入应付业务编号', { exact: true }).fill('PAY-E2E-013'); await dialog.getByPlaceholder('请选择成本类别', { exact: true }).click(); await page.getByText('CC-E2E 测试成本类别', { exact: true }).click(); await dialog.getByPlaceholder('请输入金额', { exact: true }).fill('128.50'); await dialog.getByRole('button', { name: '提交', exact: true }).click(); await expect(dialog.getByText('请选择已批准的 OA-04 来源审批单', { exact: true })).toBeVisible(); expect(createRequests).toHaveLength(0); await dialog.getByPlaceholder('请选择已批准的应付资金确认单', { exact: true }).click(); await page.getByText('OA04-E2E-013 · OA-04-SRC-013 · 应付资金确认单', { exact: true }).click(); await dialog.getByRole('button', { name: '提交', exact: true }).click(); await expect.poll(() => createRequests.length).toBe(1); expect(createRequests[0]).toEqual({ businessNo: 'PAY-E2E-013', costCategoryId, sourceDocumentId, businessDate, amount: '128.50', contractVersion: 7, }); expect(commandHeaders[0]['x-xsrf-token']).toBe('payable-e2e-csrf'); expect(commandHeaders[0]['idempotency-key']).toBeTruthy(); await expect(dialog).toBeHidden(); await expect(drawer.getByText('PAY-E2E-013', { exact: true })).toBeVisible(); await expect(drawer.getByText('待处理', { exact: true })).toBeVisible(); expect(sourceQueries).toHaveLength(1); expect(sourceQueries[0].get('formType')).toBe('OA-04'); expect(sourceQueries[0].get('projectId')).toBe(projectId); expect(sourceQueries[0].get('status')).toBe('APPROVED'); expect(sourceQueries[0].get('page')).toBe('1'); expect(sourceQueries[0].get('size')).toBe('100'); await drawer.getByRole('button', { name: '确认', exact: true }).click(); const popconfirm = page.locator('.t-popconfirm').filter({ hasText: '确认这笔应付已完成财务核对?' }); await popconfirm.locator('.t-popconfirm__confirm').click(); await expect.poll(() => confirmRequests.length).toBe(1); expect(confirmRequests[0]).toEqual({ version: 0 }); expect(commandHeaders[1]['x-xsrf-token']).toBe('payable-e2e-csrf'); expect(commandHeaders[1]['idempotency-key']).toBeTruthy(); await expect(drawer.getByText('已确认', { exact: true })).toBeVisible(); await expect(drawer.getByRole('button', { name: '确认', exact: true })).toHaveCount(0); await expect(page.getByText('应付已确认', { exact: true })).toBeVisible(); });