完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
@@ -53,6 +53,8 @@ const me = {
const reimbursementId = "reimbursement-action-confirm";
const paymentBatchId = "payment-batch-action-confirm";
const reimbursementAttachmentId = "reimbursement-attachment-action-confirm";
const paymentReceiptId = "payment-receipt-action-confirm";
function reimbursementListItem() {
return {
@@ -78,6 +80,50 @@ function reimbursementListItem() {
};
}
function reimbursementDetail() {
return {
...reimbursementListItem(),
submitterId: me.id,
ownerId: me.id,
departmentId: "department-action-confirm",
department: { id: "department-action-confirm", name: "合成部门" },
submittedAt: null,
projectId: null,
linkedTravelRequestId: null,
linkedTravelRequest: null,
owner: { id: me.id, employee_no: me.employeeNo, name: me.name },
submitter: { id: me.id, employee_no: me.employeeNo, name: me.name },
items: [],
invoices: [],
attachments: [
{
id: reimbursementAttachmentId,
originalName: "合成报销敏感附件.pdf",
mimeType: "application/pdf",
fileSize: 1024,
fileSha256: "synthetic-attachment-hash",
governanceStatus: "approved",
sensitivityLevel: "confidential",
attachmentType: "supporting_document",
downloadPolicy: "reason_required",
reviewedBy: null,
reviewedAt: null,
reviewNote: null,
archivedBy: null,
archivedAt: null,
archiveNote: null,
uploadedBy: { id: me.id, employeeNo: me.employeeNo, name: me.name },
createdAt: "2026-05-15T08:05:00.000Z",
},
],
budgetHint: null,
pendingWorkflow: null,
workflowProgress: null,
approvalHistory: [],
riskFlags: [],
};
}
function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
return {
id: paymentBatchId,
@@ -96,7 +142,7 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
completedReviewCount: status === "draft" ? 0 : 1,
nextStep: status === "draft" ? "first_review" : "ready_for_execute",
makerCheckerRequired: false,
reviewerPermission: "risk.review.manage",
reviewerPermission: "payment.batch.review",
},
reviewComment: null,
secondaryReviewComment: null,
@@ -111,6 +157,7 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
executedBy: null,
cancelledAt: null,
cancelledBy: null,
cancelledReason: null,
items: [
{
id: "payment-batch-item-action-confirm",
@@ -133,11 +180,20 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed") {
};
}
async function mockSystemActionApis(page: Page) {
async function mockSystemActionApis(
page: Page,
options: { batchStatus?: "draft" | "reviewed"; meOverride?: Partial<typeof me> } = {},
) {
const currentMe = { ...me, ...options.meOverride };
const batchStatus = options.batchStatus ?? "reviewed";
const calls = {
deleteReimbursement: 0,
deleteReimbursementAttachment: 0,
exportReimbursements: 0,
executePaymentBatch: 0,
voidPaymentReceipt: 0,
attachmentDeleteReasons: [] as string[],
receiptVoidReasons: [] as string[],
};
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
@@ -191,7 +247,7 @@ async function mockSystemActionApis(page: Page) {
}
if (path === "/api/me") {
await fulfillJson(route, me);
await fulfillJson(route, currentMe);
return;
}
@@ -220,6 +276,22 @@ async function mockSystemActionApis(page: Page) {
return;
}
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "GET") {
await fulfillJson(route, reimbursementDetail());
return;
}
if (
path === `/api/reimbursements/${reimbursementId}/attachments/${reimbursementAttachmentId}` &&
request.method() === "DELETE"
) {
calls.deleteReimbursementAttachment += 1;
const body = request.postDataJSON() as { reason?: string };
calls.attachmentDeleteReasons.push(body.reason ?? "");
await fulfillJson(route, { attachmentId: reimbursementAttachmentId, outcome: "deleted" });
return;
}
if (path === `/api/reimbursements/${reimbursementId}` && request.method() === "DELETE") {
calls.deleteReimbursement += 1;
await fulfillJson(route, { id: reimbursementId, deleted: true });
@@ -250,7 +322,45 @@ async function mockSystemActionApis(page: Page) {
}
if (path === "/api/payment-batches" && request.method() === "GET") {
await fulfillJson(route, { items: [paymentBatch("reviewed")] });
await fulfillJson(route, { items: [paymentBatch(batchStatus)] });
return;
}
if (path === `/api/payment-batches/${paymentBatchId}/receipt-attachments` && request.method() === "GET") {
await fulfillJson(route, {
items: [
{
id: paymentReceiptId,
originalName: "合成银行回单.pdf",
mimeType: "application/pdf",
fileSize: 2048,
fileSha256: "synthetic-receipt-hash",
governanceStatus: "approved",
sensitivityLevel: "confidential",
attachmentType: "payment_receipt",
downloadPolicy: "reason_required",
reviewedBy: null,
reviewedAt: null,
reviewNote: null,
archivedBy: null,
archivedAt: null,
archiveNote: null,
uploadedBy: { id: me.id, employeeNo: me.employeeNo, name: me.name },
createdAt: "2026-05-15T08:20:00.000Z",
},
],
});
return;
}
if (
path === `/api/payment-batches/${paymentBatchId}/receipt-attachments/${paymentReceiptId}/void` &&
request.method() === "POST"
) {
calls.voidPaymentReceipt += 1;
const body = request.postDataJSON() as { reason?: string };
calls.receiptVoidReasons.push(body.reason ?? "");
await fulfillJson(route, { attachmentId: paymentReceiptId, outcome: "voided", voidedAt: "2026-05-15T08:30:00.000Z" });
return;
}
@@ -320,4 +430,69 @@ test.describe("R7E 全站高风险操作二次确认", () => {
await page.getByTestId("action-confirm-submit").click();
await expect.poll(() => calls.executePaymentBatch).toBe(1);
});
test("高敏附件删除要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
const calls = await mockSystemActionApis(page);
await login(page, user);
await page.goto(`/reimbursements/${reimbursementId}`);
const attachment = page.locator(".detail-evidence-card").filter({ hasText: "合成报销敏感附件.pdf" });
await expect(attachment).toBeVisible();
await attachment.getByRole("button", { name: "删除" }).click();
const dialog = page.getByTestId("attachment-delete-confirm-dialog");
await expect(dialog).toContainText("合成报销敏感附件.pdf");
await expect(dialog).toContainText("无法在 OA 内恢复");
expect(calls.deleteReimbursementAttachment).toBe(0);
await page.getByTestId("attachment-delete-confirm-submit").click();
await expect(dialog).toContainText("请填写至少 2 个字的删除理由");
expect(calls.deleteReimbursementAttachment).toBe(0);
await dialog.getByRole("button", { name: "取消" }).click();
expect(calls.deleteReimbursementAttachment).toBe(0);
await attachment.getByRole("button", { name: "删除" }).click();
await page.getByRole("textbox", { name: "删除理由" }).fill("附件重复上传");
await page.getByTestId("attachment-delete-confirm-submit").click();
await expect.poll(() => calls.deleteReimbursementAttachment).toBe(1);
expect(calls.attachmentDeleteReasons).toEqual(["附件重复上传"]);
});
test("付款回单作废要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
const calls = await mockSystemActionApis(page);
await login(page, user);
await page.goto(`/payment-batches?id=${paymentBatchId}`);
const receipt = page.locator(".detail-evidence-card").filter({ hasText: "合成银行回单.pdf" });
await expect(receipt).toBeVisible();
await receipt.getByRole("button", { name: "作废" }).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toContainText("合成银行回单.pdf");
await expect(dialog).toContainText("文件记录会保留并标记为已作废");
await expect(page.getByRole("button", { name: "确认作废" })).toBeDisabled();
expect(calls.voidPaymentReceipt).toBe(0);
await page.getByRole("textbox", { name: "回单作废原因" }).fill("一");
await expect(page.getByRole("button", { name: "确认作废" })).toBeDisabled();
expect(calls.voidPaymentReceipt).toBe(0);
await dialog.getByRole("button", { name: "返回" }).click();
expect(calls.voidPaymentReceipt).toBe(0);
await receipt.getByRole("button", { name: "作废" }).click();
await page.getByRole("textbox", { name: "回单作废原因" }).fill("回单文件上传有误");
await page.getByRole("button", { name: "确认作废" }).click();
await expect.poll(() => calls.voidPaymentReceipt).toBe(1);
expect(calls.receiptVoidReasons).toEqual(["回单文件上传有误"]);
});
test("只有超级管理员职责且未兼任经理或负责人时不显示付款复核", async ({ page }) => {
await mockSystemActionApis(page, {
batchStatus: "draft",
meOverride: { roleCodes: ["admin"] },
});
await login(page, user);
await page.goto(`/payment-batches?id=${paymentBatchId}`);
await expect(page.getByText("PAY-ACTION-001").first()).toBeVisible();
await expect(page.getByRole("button", { name: "复核", exact: true })).toHaveCount(0);
});
});