import { expect, type APIRequestContext, type Page } from "@playwright/test"; import { apiGetData, apiPostData, createAuthenticatedApiClient, EMPLOYEE, type AuthenticatedApiClient, type E2EUser } from "./auth"; import { selectFirstBusinessOption } from "./select"; import { approveWorkflowTaskViaApi, waitForWorkflowTask } from "./workflow"; type MasterDataListResponse = { items: T[]; }; type LegalEntity = { id: string; name: string; }; type CostCenter = { id: string; name: string; code: string; }; type TravelRequestDetail = { id: string; requestNo: string | null; }; type TravelSubmitResponse = { requestNo: string; }; type TravelOrderImportResponse = { items: Array<{ id: string; externalOrderNo: string }>; }; type TravelAuthorizationImportResponse = { items: Array<{ id: string; externalAuthNo: string }>; }; type TravelTransactionImportResponse = { items: Array<{ id: string; externalTxnId: string }>; confirmedCount: number; }; export type PreparedTravelRequest = { requestId: string; requestNo: string; employeeId: string; legalEntityId: string; costCenterId: string; purpose: string; startDate: string; endDate: string; }; export type ImportedTravelPaymentSeed = { orderId: string; orderNo: string; authorizationId: string; authorizationNo: string; transactionId: string; transactionNo: string; }; function todayDateString(offsetDays = 0) { const date = new Date(); date.setDate(date.getDate() + offsetDays); return date.toISOString().slice(0, 10); } function toDateTime(date: string, hour: number) { return `${date}T${String(hour).padStart(2, "0")}:00:00.000Z`; } export async function loadTravelMasterData(api: APIRequestContext) { const legalEntities = await apiGetData>(api, "/api/master-data/legal-entities?enabled=true"); const legalEntity = legalEntities.items[0]; if (!legalEntity) { throw new Error("缺少可用法体,无法创建差旅申请"); } const costCenters = await apiGetData>( api, `/api/master-data/cost-centers?enabled=true&legalEntityId=${encodeURIComponent(legalEntity.id)}`, ); const costCenter = costCenters.items[0]; if (!costCenter) { throw new Error(`法体 ${legalEntity.name} 下缺少可用成本中心`); } return { legalEntity, costCenter }; } export async function createSubmittedTravelRequestViaApi(input: { user?: E2EUser; purposePrefix?: string; amount?: string; startOffsetDays?: number; endOffsetDays?: number; }) { const client = await createAuthenticatedApiClient(input.user ?? EMPLOYEE); const { legalEntity, costCenter } = await loadTravelMasterData(client.api); const suffix = Date.now(); const purpose = `${input.purposePrefix ?? "Playwright移动审批差旅"}-${suffix}`; const startOffsetDays = input.startOffsetDays ?? 1; const endOffsetDays = input.endOffsetDays ?? Math.max(startOffsetDays + 1, 2); const startDate = todayDateString(startOffsetDays); const endDate = todayDateString(endOffsetDays); const created = await apiPostData(client.api, "/api/travel-requests", { purpose, tripType: "domestic", startDate, endDate, legalEntityId: legalEntity.id, costCenterId: costCenter.id, estimatedAmount: input.amount ?? "1288.00", segments: [ { segmentRole: "outbound", transportType: "flight", fromCity: "深圳", toCity: "上海", departAt: toDateTime(startDate, 9), arriveAt: toDateTime(startDate, 12), }, ], }); const submitted = await apiPostData(client.api, `/api/travel-requests/${created.id}/submit`, {}); return { client, travelRequest: { requestId: created.id, requestNo: submitted.requestNo, employeeId: client.me.id, legalEntityId: legalEntity.id, costCenterId: costCenter.id, purpose, startDate, endDate, } satisfies PreparedTravelRequest, }; } export async function approveTravelRequestViaApi(managerClient: AuthenticatedApiClient, requestId: string) { const task = await waitForWorkflowTask(managerClient.api, { entityId: requestId, entityType: "travel_request", }); await approveWorkflowTaskViaApi(managerClient.api, task.id); return task.id; } export async function importTravelPaymentSeed(api: APIRequestContext, input: { requestId: string; employeeId: string; amount: string; orderNo?: string; authorizationNo?: string; transactionNo?: string; occurredAtDate?: string; }) { const suffix = Date.now(); const occurredAtDate = input.occurredAtDate ?? todayDateString(); const orderNo = input.orderNo ?? `PW-ORDER-${suffix}`; const authorizationNo = input.authorizationNo ?? `PW-AUTH-${suffix}`; const transactionNo = input.transactionNo ?? `PW-TXN-${suffix}`; const orderImport = await apiPostData(api, "/api/travel/orders/import", { orders: [ { source: "ctrip", externalOrderNo: orderNo, employeeId: input.employeeId, travelRequestId: input.requestId, amount: input.amount, currency: "CNY", occurredAt: occurredAtDate, }, ], }); const order = orderImport.items[0]; if (!order) { throw new Error("差旅订单导入失败,未返回订单记录"); } const authorizationImport = await apiPostData(api, "/api/travel/authorizations/import", { authorizations: [ { source: "corp-card", externalAuthNo: authorizationNo, employeeId: input.employeeId, travelRequestId: input.requestId, authorizedAmount: input.amount, currency: "CNY", occurredAt: toDateTime(occurredAtDate, 8), }, ], }); const authorization = authorizationImport.items[0]; if (!authorization) { throw new Error("企业卡授权导入失败,未返回授权记录"); } const transactionImport = await apiPostData(api, "/api/travel/transactions/import", { transactions: [ { source: "corp-card", externalTxnId: transactionNo, employeeId: input.employeeId, travelRequestId: input.requestId, cardAuthorizationId: authorization.id, travelOrderId: order.id, amount: input.amount, currency: "CNY", txnType: "charge", status: "settled", occurredAt: toDateTime(occurredAtDate, 9), }, ], }); if (transactionImport.confirmedCount < 1) { throw new Error("结算交易导入后未触发订单 confirmed 状态"); } const transaction = transactionImport.items[0]; if (!transaction) { throw new Error("差旅交易导入失败,未返回交易记录"); } return { orderId: order.id, orderNo, authorizationId: authorization.id, authorizationNo, transactionId: transaction.id, transactionNo, } satisfies ImportedTravelPaymentSeed; } export async function markTravelOrderPaidViaApi(api: APIRequestContext, input: { orderId: string; voucherNo: string; paymentDate: string; paymentChannel?: string; }) { return apiPostData>(api, `/api/travel-orders/${input.orderId}/mark-paid`, { paymentVoucherNo: input.voucherNo, paymentDate: input.paymentDate, paymentChannel: input.paymentChannel ?? "bank_transfer", }); } export async function importOrphanTravelTransaction(api: APIRequestContext, input: { requestId: string; employeeId: string; externalTxnId: string; amount: string; occurredAtDate?: string; }) { const occurredAtDate = input.occurredAtDate ?? todayDateString(); return apiPostData(api, "/api/travel/transactions/import", { transactions: [ { source: "corp-card", externalTxnId: input.externalTxnId, employeeId: input.employeeId, travelRequestId: input.requestId, amount: input.amount, currency: "CNY", txnType: "charge", status: "settled", occurredAt: toDateTime(occurredAtDate, 18), }, ], }); } export async function createAndSubmitTravelRequestViaUi(page: Page, input: { purposePrefix: string; amount: string; }) { const suffix = Date.now(); const purpose = `${input.purposePrefix}-${suffix}`; const startDate = todayDateString(1); const endDate = todayDateString(2); await page.goto("/travel-requests?tab=requests"); await expect(page.getByRole("heading", { name: "差旅管理" })).toBeVisible(); await page.locator("#travel-purpose").fill(purpose); await page.locator("#travel-amount").fill(input.amount); await page.locator("#travel-start-date").fill(startDate); await page.locator("#travel-end-date").fill(endDate); await selectFirstBusinessOption(page.locator("#travel-legal-entity")); await selectFirstBusinessOption(page.locator("#travel-cost-center")); const firstSegmentCard = page.locator(".rounded-xl.border.border-zinc-200").filter({ hasText: "第 1 段" }).first(); await firstSegmentCard.locator("input").nth(0).fill("深圳"); await firstSegmentCard.locator("input").nth(1).fill("上海"); await firstSegmentCard.locator("input").nth(2).fill(`${startDate}T09:00`); await firstSegmentCard.locator("input").nth(3).fill(`${startDate}T12:00`); await page.getByTestId("travel-request-save").click(); await expect(page).toHaveURL(/\/travel-requests\?tab=requests&id=/); const url = new URL(page.url()); const requestId = url.searchParams.get("id"); if (!requestId) { throw new Error(`保存差旅草稿后未拿到 requestId: ${page.url()}`); } await page.getByTestId("travel-request-submit").click(); await expect(page.getByTestId(`travel-request-item-${requestId}`)).toContainText("审批中"); const requestNo = (await page.locator(".card-std h3").first().textContent())?.trim() ?? requestId; return { requestId, requestNo, purpose, startDate, endDate, }; }