收紧付款复核资料与阶段编辑(08-01-reimbursement-notification-progress-clarity)
这个提交包含在:
@@ -16,10 +16,12 @@ export type PaymentBatchItem = {
|
||||
paymentVoucherNoOverride: string | null;
|
||||
paymentDateOverride: string | null;
|
||||
paymentChannelOverride: PaymentChannel | null;
|
||||
payeeName: string | null;
|
||||
bankName: string | null;
|
||||
bankAccountMasked: string | null;
|
||||
status: string;
|
||||
failureCode: string | null;
|
||||
failureMessage: string | null;
|
||||
snapshotJson: Record<string, unknown>;
|
||||
createdAt: string | null;
|
||||
openRiskFlags: RiskFlag[];
|
||||
};
|
||||
@@ -138,9 +140,8 @@ export async function createPaymentBatch(payload: {
|
||||
export async function updatePaymentBatchItem(batchId: string, itemId: string, payload: {
|
||||
paymentVoucherNoOverride?: string | null;
|
||||
paymentDateOverride?: string | null;
|
||||
paymentChannelOverride?: PaymentChannel | null;
|
||||
}) {
|
||||
return apiRequestData<{ item: Pick<PaymentBatchItem, "id" | "paymentVoucherNoOverride" | "paymentDateOverride" | "paymentChannelOverride"> }>(
|
||||
return apiRequestData<{ item: Pick<PaymentBatchItem, "id" | "paymentVoucherNoOverride" | "paymentDateOverride"> }>(
|
||||
`/api/payment-batches/${batchId}/items/${itemId}`,
|
||||
{
|
||||
method: "PATCH",
|
||||
|
||||
@@ -1402,10 +1402,21 @@ export function ApOperationsPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mb-2 text-sm font-medium text-zinc-700">快照</div>
|
||||
<pre className="overflow-x-auto rounded-lg border border-zinc-200 bg-zinc-950/95 p-4 text-xs text-zinc-100">
|
||||
{prettyJson(failedDetailItem.snapshotJson)}
|
||||
</pre>
|
||||
<div className="mb-2 text-sm font-medium text-zinc-700">付款信息</div>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3">
|
||||
<div className="text-xs text-zinc-500">收款人</div>
|
||||
<div className="mt-1 font-medium text-zinc-900">{failedDetailItem.payeeName ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3">
|
||||
<div className="text-xs text-zinc-500">开户银行</div>
|
||||
<div className="mt-1 font-medium text-zinc-900">{failedDetailItem.bankName ?? "-"}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-3">
|
||||
<div className="text-xs text-zinc-500">银行账户</div>
|
||||
<div className="mt-1 font-medium text-zinc-900">{failedDetailItem.bankAccountMasked ?? "-"}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
downloadPaymentBatchReceiptAttachment,
|
||||
executePaymentBatch,
|
||||
fetchPaymentBatchCandidates,
|
||||
fetchPaymentBatchDetail,
|
||||
fetchPaymentBatchReceiptAttachments,
|
||||
fetchPaymentBatches,
|
||||
reviewPaymentBatch,
|
||||
updatePaymentBatchItem,
|
||||
uploadPaymentBatchReceiptAttachments,
|
||||
voidPaymentBatchReceiptAttachment,
|
||||
type PaymentBatch,
|
||||
type PaymentBatchReceiptAttachment,
|
||||
type PaymentChannel,
|
||||
} from "../api/paymentBatches";
|
||||
@@ -102,6 +102,7 @@ export function PaymentBatchesPage() {
|
||||
(me?.roleCodes.includes("manager") || me?.roleCodes.includes("ceo"));
|
||||
const [candidateSearch, setCandidateSearch] = useState("");
|
||||
const [selectedCandidateIds, setSelectedCandidateIds] = useState<string[]>([]);
|
||||
const [selectedPaymentChannel, setSelectedPaymentChannel] = useState<PaymentChannel>("bank");
|
||||
const receiptInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const [error, setError] = useState("");
|
||||
const [showCreatePanel, setShowCreatePanel] = useState(false);
|
||||
@@ -131,9 +132,12 @@ export function PaymentBatchesPage() {
|
||||
|
||||
const selectedBatchId = searchParams.get("id") ?? "";
|
||||
const batches = useMemo(() => listQuery.data?.items ?? [], [listQuery.data?.items]);
|
||||
const selectedBatch = useMemo<PaymentBatch | null>(() => {
|
||||
return batches.find((item) => item.id === selectedBatchId) ?? null;
|
||||
}, [batches, selectedBatchId]);
|
||||
const selectedBatchQuery = useQuery({
|
||||
queryKey: ["payment-batches", "detail", selectedBatchId],
|
||||
enabled: Boolean(selectedBatchId),
|
||||
queryFn: () => fetchPaymentBatchDetail(selectedBatchId),
|
||||
});
|
||||
const selectedBatch = selectedBatchQuery.data?.item ?? null;
|
||||
const receiptAttachmentsQuery = useQuery({
|
||||
queryKey: ["payment-batch-receipts", selectedBatch?.id],
|
||||
enabled: Boolean(selectedBatch?.id),
|
||||
@@ -143,11 +147,11 @@ export function PaymentBatchesPage() {
|
||||
const canEditSelectedBatch =
|
||||
canManageBatch &&
|
||||
!!selectedBatch &&
|
||||
["draft", "reviewed", "failed"].includes(selectedBatch.status);
|
||||
["reviewed", "failed"].includes(selectedBatch.status);
|
||||
const canEditReceiptAttachments =
|
||||
canManageBatch &&
|
||||
!!selectedBatch &&
|
||||
selectedBatch.status !== "cancelled";
|
||||
["reviewed", "failed"].includes(selectedBatch.status);
|
||||
const canUploadReceipt = canEditReceiptAttachments && receiptAttachmentsQuery.isSuccess;
|
||||
const canReviewSelectedBatch =
|
||||
canReviewBatch &&
|
||||
@@ -194,11 +198,13 @@ export function PaymentBatchesPage() {
|
||||
createPaymentBatch({
|
||||
entityType: "reimbursement",
|
||||
entityIds: selectedCandidateIds,
|
||||
defaultPaymentChannel: selectedPaymentChannel,
|
||||
}),
|
||||
onSuccess: async ({ item }) => {
|
||||
setError("");
|
||||
setSelectedCandidateIds([]);
|
||||
setCandidateSearch("");
|
||||
setSelectedPaymentChannel("bank");
|
||||
const next = new URLSearchParams(searchParams);
|
||||
next.set("id", item.id);
|
||||
setSearchParams(next, { replace: true });
|
||||
@@ -214,7 +220,9 @@ export function PaymentBatchesPage() {
|
||||
|
||||
const reviewMutation = useMutation({
|
||||
mutationFn: async (batchId: string) => {
|
||||
const batch = listQuery.data?.items.find((item) => item.id === batchId);
|
||||
const batch = selectedBatch?.id === batchId
|
||||
? selectedBatch
|
||||
: listQuery.data?.items.find((item) => item.id === batchId);
|
||||
const resolveRiskFlagIds = (batch?.items ?? []).flatMap((item) => item.openRiskFlags.map((flag) => flag.id));
|
||||
return reviewPaymentBatch(batchId, { resolveRiskFlagIds });
|
||||
},
|
||||
@@ -261,11 +269,10 @@ export function PaymentBatchesPage() {
|
||||
});
|
||||
|
||||
const updateItemMutation = useMutation({
|
||||
mutationFn: async (payload: { batchId: string; itemId: string; paymentVoucherNoOverride?: string | null; paymentDateOverride?: string | null; paymentChannelOverride?: PaymentChannel | null }) =>
|
||||
mutationFn: async (payload: { batchId: string; itemId: string; paymentVoucherNoOverride?: string | null; paymentDateOverride?: string | null }) =>
|
||||
updatePaymentBatchItem(payload.batchId, payload.itemId, {
|
||||
paymentVoucherNoOverride: payload.paymentVoucherNoOverride,
|
||||
paymentDateOverride: payload.paymentDateOverride,
|
||||
paymentChannelOverride: payload.paymentChannelOverride,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
setError("");
|
||||
@@ -430,8 +437,23 @@ export function PaymentBatchesPage() {
|
||||
<div className="px-2 py-3 text-sm text-zinc-500">没有可加入批次的已审批报销。</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="max-w-sm space-y-2">
|
||||
<Label htmlFor="payment-batch-default-channel">付款渠道</Label>
|
||||
<select
|
||||
id="payment-batch-default-channel"
|
||||
data-testid="payment-batch-default-channel"
|
||||
className="select-std w-full"
|
||||
value={selectedPaymentChannel}
|
||||
disabled={!canManageBatch}
|
||||
onChange={(event) => setSelectedPaymentChannel(event.target.value as PaymentChannel)}
|
||||
>
|
||||
{PAYMENT_CHANNEL_OPTIONS.map((option) => (
|
||||
<option key={option} value={option}>{paymentChannelLabel(option)}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div className="text-xs text-zinc-500">
|
||||
已选择 {selectedCandidateIds.length} 项;系统自动生成编号和标题,默认按银行转账提交负责人复核。
|
||||
已选择 {selectedCandidateIds.length} 项;系统自动生成编号和标题,按{paymentChannelLabel(selectedPaymentChannel)}提交负责人复核。
|
||||
</div>
|
||||
</div>
|
||||
<Button type="button" disabled={createMutation.isPending || !canManageBatch || selectedCandidateIds.length === 0} onClick={() => createMutation.mutate()}>
|
||||
@@ -441,7 +463,22 @@ export function PaymentBatchesPage() {
|
||||
) : null}
|
||||
|
||||
{selectedBatchId ? (
|
||||
selectedBatch ? (
|
||||
selectedBatchQuery.isLoading ? (
|
||||
<div className="finance-command-panel text-sm text-zinc-500">正在加载付款批次...</div>
|
||||
) : selectedBatchQuery.isError ? (
|
||||
<div className="finance-command-panel flex flex-wrap items-center justify-between gap-3 text-sm text-red-600">
|
||||
<span>{resolveError(selectedBatchQuery.error)}</span>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={selectedBatchQuery.isFetching}
|
||||
onClick={() => void selectedBatchQuery.refetch()}
|
||||
>
|
||||
{selectedBatchQuery.isFetching ? "重新加载中..." : "重新加载"}
|
||||
</Button>
|
||||
</div>
|
||||
) : selectedBatch ? (
|
||||
<div className="space-y-5">
|
||||
<div className="finance-command-panel space-y-3" data-testid="payment-batch-detail-panel">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
@@ -586,31 +623,42 @@ export function PaymentBatchesPage() {
|
||||
</div>
|
||||
<div className="text-sm text-zinc-600">{formatMoney(item.amount)} · {paymentItemStatusLabel(item.status)}</div>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-4">
|
||||
<Input
|
||||
defaultValue={item.paymentVoucherNoOverride ?? ""}
|
||||
placeholder="凭证号"
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentVoucherNoOverride: event.target.value || null })}
|
||||
/>
|
||||
<DateTextInput
|
||||
defaultValue={item.paymentDateOverride ?? ""}
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentDateOverride: event.target.value || null })}
|
||||
/>
|
||||
<select
|
||||
className="select-std"
|
||||
defaultValue={item.paymentChannelOverride ?? ""}
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onChange={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentChannelOverride: (event.target.value as PaymentChannel | "") || null })}
|
||||
>
|
||||
<option value="">默认渠道</option>
|
||||
{PAYMENT_CHANNEL_OPTIONS.map((option) => <option key={option} value={option}>{paymentChannelLabel(option)}</option>)}
|
||||
</select>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
|
||||
{item.failureMessage ? `失败原因:${item.failureMessage}` : "付款异常:无"}
|
||||
</div>
|
||||
<div className="mt-3 detail-fact-grid text-sm md:grid-cols-4">
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">收款人</div><div className="detail-fact-card__value">{item.payeeName || "-"}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">开户银行</div><div className="detail-fact-card__value">{item.bankName || "-"}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">银行账户</div><div className="detail-fact-card__value">{item.bankAccountMasked || "-"}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">付款渠道</div><div className="detail-fact-card__value">{paymentChannelLabel(item.paymentChannelOverride ?? selectedBatch.defaultPaymentChannel)}</div></div>
|
||||
</div>
|
||||
{["reviewed", "failed"].includes(selectedBatch.status) ? (
|
||||
<div className="mt-3 grid gap-3 md:grid-cols-3" data-testid="payment-batch-item-payment-fields">
|
||||
<Input
|
||||
aria-label={`付款凭证号 ${item.entityCode}`}
|
||||
defaultValue={item.paymentVoucherNoOverride ?? ""}
|
||||
placeholder="付款凭证号"
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentVoucherNoOverride: event.target.value || null })}
|
||||
/>
|
||||
<DateTextInput
|
||||
aria-label={`实际付款日期 ${item.entityCode}`}
|
||||
defaultValue={item.paymentDateOverride ?? ""}
|
||||
disabled={!canEditSelectedBatch || !["pending", "failed"].includes(item.status)}
|
||||
onBlur={(event) => updateItemMutation.mutate({ batchId: selectedBatch.id, itemId: item.id, paymentDateOverride: event.target.value || null })}
|
||||
/>
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
|
||||
{item.failureMessage ? `失败原因:${item.failureMessage}` : "付款异常:无"}
|
||||
</div>
|
||||
</div>
|
||||
) : ["executed", "cancelled"].includes(selectedBatch.status) ? (
|
||||
<div className="mt-3 detail-fact-grid text-sm md:grid-cols-3" data-testid="payment-batch-item-payment-readonly">
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">付款凭证号</div><div className="detail-fact-card__value">{item.paymentVoucherNoOverride || "-"}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">实际付款日期</div><div className="detail-fact-card__value">{formatDisplayDate(item.paymentDateOverride ?? selectedBatch.defaultPaymentDate)}</div></div>
|
||||
<div className="detail-fact-card"><div className="detail-fact-card__label">付款结果</div><div className="detail-fact-card__value">{item.failureMessage || paymentItemStatusLabel(item.status)}</div></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border border-zinc-200 bg-zinc-50 px-3 py-2 text-sm text-zinc-600">
|
||||
付款复核通过后,由财务登记付款凭证号和实际付款日期。
|
||||
</div>
|
||||
)}
|
||||
<RiskFlagsCard items={item.openRiskFlags} title="关联开放风险" />
|
||||
</div>
|
||||
))}
|
||||
@@ -621,7 +669,11 @@ export function PaymentBatchesPage() {
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-base font-semibold text-zinc-900">银行回单</h3>
|
||||
<p className="mt-1 text-sm text-zinc-500">先在线下银行完成付款,再上传回单;系统不会连接银行自动转账。</p>
|
||||
<p className="mt-1 text-sm text-zinc-500">
|
||||
{selectedBatch.status === "draft"
|
||||
? "付款复核通过后,由财务在线下完成付款并上传银行回单。"
|
||||
: "先在线下银行完成付款,再上传回单;系统不会连接银行自动转账。"}
|
||||
</p>
|
||||
</div>
|
||||
{canUploadReceipt ? (
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -686,12 +738,12 @@ export function PaymentBatchesPage() {
|
||||
>
|
||||
下载
|
||||
</Button>
|
||||
{canManageBatch ? (
|
||||
{canEditReceiptAttachments ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
disabled={selectedBatch.status === "cancelled" || Boolean(attachment.archivedAt) || voidReceiptMutation.isPending}
|
||||
disabled={Boolean(attachment.archivedAt) || voidReceiptMutation.isPending}
|
||||
onClick={() => {
|
||||
setActionReason("");
|
||||
setReasonAction({
|
||||
|
||||
@@ -415,6 +415,8 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
|
||||
await expect(page.locator('input[placeholder*="UUID"], input[placeholder*="uuid"], input[placeholder*="entityId"], input[placeholder*="内部 ID"], input[placeholder*="内部ID"]')).toHaveCount(0);
|
||||
|
||||
await candidateRow.getByRole("checkbox").check();
|
||||
await page.getByTestId("payment-batch-default-channel").selectOption("cash");
|
||||
await expect(page.getByText("按现金提交负责人复核", { exact: false })).toBeVisible();
|
||||
await page.getByRole("button", { name: "创建并提交复核", exact: true }).click();
|
||||
|
||||
await expect(page).toHaveURL(new RegExp(`/payment-batches\\?id=${CREATED_PAYMENT_BATCH_ID}$`));
|
||||
@@ -423,6 +425,7 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
|
||||
expect(createPayload).toEqual({
|
||||
entityType: "reimbursement",
|
||||
entityIds: [PAYMENT_CANDIDATE_ENTITY_ID],
|
||||
defaultPaymentChannel: "cash",
|
||||
});
|
||||
expectNoPageErrors(pageErrors);
|
||||
});
|
||||
|
||||
@@ -156,7 +156,6 @@ function paymentBatch(status: "draft" | "reviewed" | "failed", suffix: string) {
|
||||
status: failed ? "failed" : "pending",
|
||||
failureCode: failed ? "BANK_RECEIPT_REQUIRED" : null,
|
||||
failureMessage: failed ? "缺少银行回单附件" : null,
|
||||
snapshotJson: {},
|
||||
createdAt: "2026-05-27T09:05:00.000Z",
|
||||
openRiskFlags: failed ? [riskFlag] : [],
|
||||
},
|
||||
|
||||
@@ -114,9 +114,16 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
|
||||
const candidateRow = createCard.getByTestId("payment-batch-candidate-row").filter({ hasText: reimbursementDisplayCode }).first();
|
||||
await expect(candidateRow).toBeVisible();
|
||||
await candidateRow.getByRole("checkbox").check();
|
||||
await createCard.getByTestId("payment-batch-default-channel").selectOption("bank");
|
||||
await createCard.getByRole("button", { name: "创建并提交复核" }).click();
|
||||
|
||||
const createBatchPayload = (await (await createBatchResponsePromise).json()) as {
|
||||
const createBatchResponse = await createBatchResponsePromise;
|
||||
expect(createBatchResponse.request().postDataJSON()).toMatchObject({
|
||||
entityType: "reimbursement",
|
||||
entityIds: [reimbursementId],
|
||||
defaultPaymentChannel: "bank",
|
||||
});
|
||||
const createBatchPayload = (await createBatchResponse.json()) as {
|
||||
data?: { item?: { id?: string; name?: string } };
|
||||
};
|
||||
const batchId = createBatchPayload.data?.item?.id;
|
||||
@@ -130,19 +137,28 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
|
||||
await expect(batchDetailCard).toContainText("待付款复核");
|
||||
await expect(batchDetailCard).toContainText("已提交付款复核,等待负责人处理");
|
||||
await expect(batchDetailCard).toContainText("0/1");
|
||||
const draftItemsPanel = page.getByTestId("payment-batch-items-panel");
|
||||
await expect(draftItemsPanel).toContainText(EMPLOYEE.name);
|
||||
await expect(draftItemsPanel).toContainText("开户银行");
|
||||
await expect(draftItemsPanel).toContainText("银行账户");
|
||||
await expect(draftItemsPanel).toContainText("银行转账");
|
||||
await expect(draftItemsPanel.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
|
||||
const draftReceiptPanel = page.getByTestId("payment-batch-receipt-panel");
|
||||
await expect(draftReceiptPanel).toContainText("付款复核通过后,由财务在线下完成付款并上传银行回单");
|
||||
await expect(draftReceiptPanel.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
|
||||
await expect(draftReceiptPanel.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
|
||||
await logout(page, FINANCE.name);
|
||||
|
||||
await login(page, MANAGER);
|
||||
await page.goto("/ap-operations");
|
||||
await page.getByRole("tab", { name: /待付款/ }).click();
|
||||
const pendingRow = page.locator("tr").filter({ hasText: batchName }).first();
|
||||
await expect(pendingRow).toBeVisible();
|
||||
await pendingRow.getByRole("button", { name: "快捷复核" }).click();
|
||||
await expect(page.getByRole("dialog")).toContainText(batchName);
|
||||
await page.getByPlaceholder("请输入复核意见").fill("R78 focused regression review");
|
||||
await page.getByRole("button", { name: "提交复核" }).click();
|
||||
await expect(pendingRow).toContainText("复核 1/1");
|
||||
await page.goto(`/payment-batches?id=${batchId}`);
|
||||
const managerBatchDetail = page.getByTestId("payment-batch-detail-panel").filter({ hasText: batchName });
|
||||
await expect(managerBatchDetail).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-items-panel")).toContainText(EMPLOYEE.name);
|
||||
await managerBatchDetail.getByRole("button", { name: "复核通过" }).click();
|
||||
await expect(page.getByTestId("action-confirm-dialog")).toContainText("独立复核");
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await expect(managerBatchDetail).toContainText("1/1");
|
||||
|
||||
await logout(page, MANAGER.name);
|
||||
|
||||
@@ -153,6 +169,7 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
|
||||
await expect(executedBatchDetailCard).toContainText("1/1");
|
||||
const receiptCard = page.getByTestId("payment-batch-receipt-panel");
|
||||
await expect(receiptCard).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-item-payment-fields")).toBeVisible();
|
||||
await expect(executedBatchDetailCard.getByRole("button", { name: "上传银行回单" }).first()).toBeVisible();
|
||||
await expect(executedBatchDetailCard.getByRole("button", { name: "登记付款完成" })).toHaveCount(0);
|
||||
const receiptUploadResponsePromise = page.waitForResponse((response) => {
|
||||
@@ -181,6 +198,12 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
|
||||
await page.getByTestId("action-confirm-submit").click();
|
||||
await executeBatchResponsePromise;
|
||||
await expect(executedBatchDetailCard).toContainText("已付款", { timeout: 30_000 });
|
||||
await expect(page.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
|
||||
await expect(page.getByTestId("payment-batch-item-payment-readonly")).toBeVisible();
|
||||
await expect(receiptCard.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
|
||||
await expect(receiptCard.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
await expect(receiptCard.getByRole("button", { name: "作废" })).toHaveCount(0);
|
||||
await expect(receiptCard.getByRole("button", { name: "下载" })).toBeVisible();
|
||||
|
||||
await page.goto(`/reimbursements/${reimbursementId}`);
|
||||
await reimbursementDetail.assertLoaded();
|
||||
|
||||
@@ -124,7 +124,7 @@ function reimbursementDetail() {
|
||||
};
|
||||
}
|
||||
|
||||
function paymentBatch(status: "draft" | "reviewed" = "reviewed", highRisk = false) {
|
||||
function paymentBatch(status: "draft" | "reviewed" | "executed" | "cancelled" = "reviewed", highRisk = false) {
|
||||
return {
|
||||
id: paymentBatchId,
|
||||
batchNo: "PAY-ACTION-001",
|
||||
@@ -169,10 +169,12 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed", highRisk = fals
|
||||
paymentVoucherNoOverride: null,
|
||||
paymentDateOverride: null,
|
||||
paymentChannelOverride: null,
|
||||
payeeName: "合成收款员工",
|
||||
bankName: "合成银行",
|
||||
bankAccountMasked: "6222********5678",
|
||||
status: "pending",
|
||||
failureCode: null,
|
||||
failureMessage: null,
|
||||
snapshotJson: {},
|
||||
createdAt: "2026-05-15T08:00:00.000Z",
|
||||
openRiskFlags: [],
|
||||
},
|
||||
@@ -183,7 +185,7 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed", highRisk = fals
|
||||
async function mockSystemActionApis(
|
||||
page: Page,
|
||||
options: {
|
||||
batchStatus?: "draft" | "reviewed";
|
||||
batchStatus?: "draft" | "reviewed" | "executed" | "cancelled";
|
||||
hasReceipt?: boolean;
|
||||
receiptQueryError?: boolean;
|
||||
highRisk?: boolean;
|
||||
@@ -198,6 +200,7 @@ async function mockSystemActionApis(
|
||||
deleteReimbursementAttachment: 0,
|
||||
exportReimbursements: 0,
|
||||
executePaymentBatch: 0,
|
||||
paymentBatchDetail: 0,
|
||||
uploadPaymentReceipt: 0,
|
||||
voidPaymentReceipt: 0,
|
||||
attachmentDeleteReasons: [] as string[],
|
||||
@@ -330,7 +333,24 @@ async function mockSystemActionApis(
|
||||
}
|
||||
|
||||
if (path === "/api/payment-batches" && request.method() === "GET") {
|
||||
await fulfillJson(route, { items: [paymentBatch(batchStatus, options.highRisk)] });
|
||||
const listItem = paymentBatch(batchStatus, options.highRisk);
|
||||
await fulfillJson(route, {
|
||||
items: [{
|
||||
...listItem,
|
||||
items: listItem.items.map((item) => ({
|
||||
...item,
|
||||
payeeName: null,
|
||||
bankName: null,
|
||||
bankAccountMasked: null,
|
||||
})),
|
||||
}],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/payment-batches/${paymentBatchId}` && request.method() === "GET") {
|
||||
calls.paymentBatchDetail += 1;
|
||||
await fulfillJson(route, { item: paymentBatch(batchStatus, options.highRisk) });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -502,6 +522,34 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await expect(page.getByTestId("payment-batch-current-processing")).toContainText("线下付款并上传银行回单");
|
||||
});
|
||||
|
||||
test("付款详情按批次状态渐进展示收款依据和财务动作", async ({ page }) => {
|
||||
const calls = await mockSystemActionApis(page, { batchStatus: "draft", hasReceipt: true });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
|
||||
const draftItems = page.getByTestId("payment-batch-items-panel");
|
||||
await expect.poll(() => calls.paymentBatchDetail).toBe(1);
|
||||
await expect(draftItems).toContainText("合成收款员工");
|
||||
await expect(draftItems).toContainText("合成银行");
|
||||
await expect(draftItems).toContainText("6222********5678");
|
||||
await expect(draftItems).toContainText("银行转账");
|
||||
await expect(draftItems.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
|
||||
await expect(page.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "作废" })).toHaveCount(0);
|
||||
|
||||
await page.unrouteAll({ behavior: "wait" });
|
||||
await mockSystemActionApis(page, { batchStatus: "executed", hasReceipt: true });
|
||||
await page.reload();
|
||||
|
||||
await expect(page.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
|
||||
await expect(page.getByTestId("payment-batch-item-payment-readonly")).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "作废" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "下载" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("高敏附件删除要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
|
||||
const calls = await mockSystemActionApis(page);
|
||||
await login(page, user);
|
||||
|
||||
在新工单中引用
屏蔽一个用户