import { expect, test } from "@playwright/test"; import { apiGetData, createAuthenticatedApiClient, FINANCE_MGR, MANAGER, login } from "./helpers/auth"; import { approveTravelRequestViaApi, createSubmittedTravelRequestViaApi, importOrphanTravelTransaction, importTravelPaymentSeed, markTravelOrderPaidViaApi, } from "./helpers/travel"; type ReconciliationBatchDetailResponse = { summary: { pendingItems: number; }; }; test("E2E 差旅对账批次:创建→确认异常→收口", async ({ page }) => { const managerApi = await createAuthenticatedApiClient(MANAGER); const financeMgrApi = await createAuthenticatedApiClient(FINANCE_MGR); const isolatedStartOffsetDays = 45 + Math.floor(Math.random() * 120); const created = await createSubmittedTravelRequestViaApi({ purposePrefix: "Playwright差旅对账", amount: "1666.00", startOffsetDays: isolatedStartOffsetDays, endOffsetDays: isolatedStartOffsetDays + 1, }); try { await approveTravelRequestViaApi(managerApi, created.travelRequest.requestId); const paymentSeed = await importTravelPaymentSeed(financeMgrApi.api, { requestId: created.travelRequest.requestId, employeeId: created.travelRequest.employeeId, amount: "1666.00", occurredAtDate: created.travelRequest.startDate, }); await markTravelOrderPaidViaApi(financeMgrApi.api, { orderId: paymentSeed.orderId, voucherNo: `RECON-${Date.now()}`, paymentDate: created.travelRequest.startDate, }); const orphanTxnNo = `PW-ORPHAN-${Date.now()}`; await importOrphanTravelTransaction(financeMgrApi.api, { requestId: created.travelRequest.requestId, employeeId: created.travelRequest.employeeId, externalTxnId: orphanTxnNo, amount: "88.00", occurredAtDate: created.travelRequest.startDate, }); await login(page, FINANCE_MGR); await page.goto("/travel-requests?tab=reconciliation"); await expect(page.getByRole("heading", { name: "差旅管理" })).toBeVisible(); await page.locator("#travel-recon-create-legal-entity").selectOption(created.travelRequest.legalEntityId); await page.locator("#travel-recon-create-cost-center").selectOption(created.travelRequest.costCenterId); await page.locator("#travel-recon-create-period-start").fill(created.travelRequest.startDate); await page.locator("#travel-recon-create-period-end").fill(created.travelRequest.endDate); const createBatchResponsePromise = page.waitForResponse( (response) => response.request().method() === "POST" && new URL(response.url()).pathname === "/api/travel/reconciliation/batches" && response.ok(), ); await page.getByRole("button", { name: "创建批次" }).click(); const createBatchResponse = await createBatchResponsePromise; const createBatchPayload = (await createBatchResponse.json()) as { data?: { id?: string } }; const batchId = createBatchPayload.data?.id; if (!batchId) { throw new Error(`创建批次后未返回 batchId: ${JSON.stringify(createBatchPayload)}`); } await expect(page).toHaveURL(new RegExp(`batchId=${batchId}`), { timeout: 30_000 }); const detailSection = () => page.locator(".card-std").filter({ hasText: "异常与明细" }).first(); const orphanItemCard = () => page.locator(".rounded-xl.border.border-zinc-200").filter({ hasText: orphanTxnNo }).first(); const fetchBatchDetail = () => apiGetData( financeMgrApi.api, `/api/travel/reconciliation/batches/${batchId}`, ); await expect(page.getByText("ERP 摘要桥")).toBeVisible(); await expect(page.getByText("异常与明细")).toBeVisible(); await expect(orphanItemCard()).toBeVisible({ timeout: 30_000 }); await expect(orphanItemCard()).toContainText("待确认"); let pendingConfirmCount = (await fetchBatchDetail()).summary.pendingItems; while (pendingConfirmCount > 0) { page.once("dialog", async (dialog) => { await dialog.accept("Playwright 已确认孤儿交易"); }); await Promise.all([ page.waitForResponse( (response) => response.request().method() === "POST" && /\/api\/travel\/reconciliation\/items\/[^/]+\/confirm$/.test(new URL(response.url()).pathname) && response.ok(), ), detailSection().getByRole("button", { name: "确认差异" }).first().click(), ]); await expect .poll(async () => (await fetchBatchDetail()).summary.pendingItems, { timeout: 30_000, }) .toBe(pendingConfirmCount - 1); pendingConfirmCount -= 1; await page.reload(); await expect(page).toHaveURL(new RegExp(`batchId=${batchId}`)); await expect(page.getByText("异常与明细")).toBeVisible(); } await expect(orphanItemCard()).toContainText("已确认", { timeout: 30_000 }); await page.getByRole("button", { name: "Finalize" }).click(); await expect(page.getByText("阶段状态").locator("..")).toContainText(/已入队|已发送|已跳过|未触发/, { timeout: 30_000, }); await expect(page.getByText("阶段状态")).toBeVisible(); await expect(page.getByText("应付摘要")).toBeVisible(); } finally { await Promise.all([created.client.dispose(), managerApi.dispose(), financeMgrApi.dispose()]); } });