import { expect, test } from "@playwright/test"; import { createAuthenticatedApiClient, EMPLOYEE, FINANCE_MGR, login, logout, MANAGER } from "./helpers/auth"; import { createAndSubmitTravelRequestViaUi, importTravelPaymentSeed } from "./helpers/travel"; function extractInvoiceCode(text: string) { const match = text.match(/发票(?:号|号码):([A-Z0-9]+)-/); if (!match?.[1]) { throw new Error(`Failed to parse invoice code from text: ${text}`); } return match[1]; } test("E2E 差旅支付主链路:申请→审批→支付→绑票", async ({ page }) => { const employeeApi = await createAuthenticatedApiClient(EMPLOYEE); const financeMgrApi = await createAuthenticatedApiClient(FINANCE_MGR); try { await login(page, EMPLOYEE); const draft = await createAndSubmitTravelRequestViaUi(page, { purposePrefix: "Playwright差旅支付", amount: "1888.00", }); await logout(page, EMPLOYEE.name); await login(page, MANAGER); await page.goto("/workflow/tasks?status=pending&page=1"); const taskRow = page.getByTestId("workflow-task-row").filter({ hasText: draft.requestId }).first(); await expect(taskRow).toBeVisible({ timeout: 30_000 }); await taskRow.getByTestId("workflow-task-detail-link").click(); await page.getByTestId("workflow-task-approve").click(); await expect(page.getByTestId("workflow-action-confirm-dialog")).toContainText("确认后将进入下一审批节点"); await page.getByTestId("workflow-action-confirm-submit").click(); await expect(page.getByTestId("workflow-task-summary")).toContainText("状态:"); await expect(page.getByTestId("workflow-task-summary")).toContainText("已完成"); await logout(page, MANAGER.name); const paymentSeed = await importTravelPaymentSeed(financeMgrApi.api, { requestId: draft.requestId, employeeId: employeeApi.me.id, amount: "1888.00", occurredAtDate: draft.startDate, }); await login(page, FINANCE_MGR); await page.goto(`/travel-requests?tab=requests&id=${draft.requestId}`); const orderCard = page.locator(".rounded-xl.border.border-zinc-200").filter({ hasText: paymentSeed.orderNo }).last(); await expect(orderCard).toBeVisible({ timeout: 30_000 }); await expect(orderCard).toContainText("已匹配交易"); await orderCard.getByRole("button", { name: "标记已支付" }).click(); await page.locator("#travel-order-payment-voucher").fill(`BANK-${Date.now()}`); await page.locator("#travel-order-payment-date").fill(draft.startDate); await page.getByRole("button", { name: "确认已支付" }).click(); await expect(orderCard).toContainText("已支付", { timeout: 30_000 }); await expect(orderCard).not.toContainText("标记已支付"); await page.goto("/invoice-pool"); const invoiceFileName = `travel-payment-${Date.now()}.pdf`; const uploadResponsePromise = page.waitForResponse((response) => { const url = new URL(response.url()); return response.request().method() === "POST" && url.pathname === "/api/invoice-pool/upload" && response.ok(); }); await page.getByRole("button", { name: "上传票据" }).click(); await page.getByTestId("invoice-upload-file").setInputFiles({ name: invoiceFileName, mimeType: "application/pdf", buffer: Buffer.from(`%PDF-1.4\nTRAVEL_PAYMENT_${Date.now()}\n%%EOF\n`, "utf8"), }); await page.getByTestId("invoice-upload-submit").click(); const uploadPayload = (await (await uploadResponsePromise).json()) as { data?: { invoicePoolId?: string | null } }; const invoiceId = uploadPayload.data?.invoicePoolId; if (!invoiceId) { throw new Error(`上传发票后未返回票据 ID: ${JSON.stringify(uploadPayload)}`); } await expect(page.getByTestId("invoice-detail-panel")).toBeVisible(); await expect(page.getByTestId("invoice-detail-panel")).toContainText("状态:可用", { timeout: 90_000 }); const invoiceDetailText = await page.getByTestId("invoice-detail-panel").innerText(); const invoiceCode = extractInvoiceCode(invoiceDetailText); await page.goto("/invoice-pool"); await expect(page.getByTestId("invoice-pool-table")).toBeVisible(); await page.getByRole("button", { name: "绑定" }).click(); const bindCard = page.locator(".card-std").filter({ hasText: "绑定业务单据" }).first(); await bindCard.getByRole("combobox").first().selectOption("travelOrder"); await page.getByTestId("invoice-bind-invoice").selectOption(invoiceId); await page.getByTestId("invoice-bind-travel-request").selectOption(draft.requestId); await page.getByTestId("invoice-bind-travel-order").selectOption(paymentSeed.orderId); const bindResponsePromise = page.waitForResponse((response) => response.url().includes(`/api/invoice-pool/${invoiceId}/bind`) && response.request().method() === "POST"); await page.getByTestId("invoice-bind-submit").click(); const bindResponse = await bindResponsePromise; expect(bindResponse.ok()).toBeTruthy(); await expect(page.getByText("绑定成功")).toBeVisible({ timeout: 30_000 }); await page.goto(`/travel-requests?tab=requests&id=${draft.requestId}`); const paidOrderCard = page.locator(".rounded-xl.border.border-zinc-200").filter({ hasText: paymentSeed.orderNo }).last(); await expect(paidOrderCard).toContainText("已支付"); await expect(paidOrderCard).not.toContainText("当前没有已绑定发票。", { timeout: 30_000 }); await expect(paidOrderCard).toContainText(invoiceCode); await expect(paidOrderCard).toContainText("ERP 状态:"); await logout(page, FINANCE_MGR.name); } finally { await Promise.all([employeeApi.dispose(), financeMgrApi.dispose()]); } });