test(P22PROD1-5): 差旅支付与对账E2E补齐及稳态修复
这个提交包含在:
@@ -0,0 +1,122 @@
|
||||
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<ReconciliationBatchDetailResponse>(
|
||||
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()]);
|
||||
}
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户