完善借款、编号、付款与预算并发及验收(09-06-release-gate-integrity)

汇总已落盘的借款本金与还款、兼容历史编号的周期化编号、付款责任与回单核验、报销进度和排序、PWA/采购事务、错误原因与日期校验及ERP退役。
统一共享预算池锁序和锁后判断,恢复差旅原预算选择,补原因码固定夹具及反向完整性检查。
纳入配套迁移、测试、受管QA、分层验收合同、规约及任务记录;原始证据留在ignored artifacts。

关联任务:09-17-company-loan-principal-repayment、09-17-erp-exclusive-retirement、09-18-reimbursement-periodic-numbering、09-08-reimbursement-payment-closeout、09-18-local-concurrency-product-fixes、09-19-budget-pool-concurrency、09-05-error-explainability-payment-readiness。
预算补审由Claude定向验证2文件928项通过,原4446/28及r6结果仍仅对应各自历史输入;本轮未重跑测试。
完整新候选门禁、R1真实入口接入与12项公网义务仍开放。生产编号未启用、差旅仍冻结;不推送、不部署,不宣称发布资格。
这个提交包含在:
2026-09-19 13:11:04 +08:00
父节点 729d90576f
当前提交 1f8d5b658a
共修改 367 个文件,包含 21079 行新增和 12287 行删除
@@ -739,3 +739,85 @@ test.describe("R7E 审批动作二次确认", () => {
});
}
});
for (const mobile of [false, true]) {
for (const action of ["approve", "return", "transfer", "already_processed", "failed"] as const) {
test(`财务审批原地进度 ${mobile ? "手机" : "桌面"} ${action}`, async ({ page }) => {
const reimbursementId = "synthetic-finance-progress";
let changed = false;
let readsAfterAction = 0;
let writes = 0;
const expectedResult = action === "return" ? "已退回修改" : action === "transfer" ? "已转审" : "财务复核通过";
const source = {
id: reimbursementId, formNumber: "BX-20260917-0003", status: "reviewing",
reimbursementDate: "2026-09-17", totalAmount: "100.00", description: "合成审批进度验证",
owner: { id: "synthetic-owner", employeeNo: "SYN-1", name: "合成申请人" },
submitter: { id: "synthetic-owner", employeeNo: "SYN-1", name: "合成申请人" },
items: [{ id: "synthetic-item", expenseTypeId: "synthetic-type", expenseDate: "2026-09-17",
amount: "100.00", description: "合成明细", assetPriorApprovalBasis: null,
expenseType: { id: "synthetic-type", code: "synthetic", name: "合成费用", category: "office", enabled: true, requiresInvoice: false } }],
invoices: [], attachments: [],
};
await mockWorkflowConfirmApis(page);
await page.route("**/api/me", (route) => fulfillJson(route, { ...mockMe, roleCodes: action === "transfer" ? ["finance", "manager"] : ["finance"],
permissions: ["expense.approve", "expense.transfer", "expense.view_all"] }));
await page.route(`**/api/reimbursements/${reimbursementId}`, async (route) => {
if (changed) readsAfterAction++;
await fulfillJson(route, { ...source, status: changed && action !== "transfer" ? (action === "return" ? "draft" : "approved") : "reviewing",
processingProgress: { stages: [{ key: "synthetic-finance-task", label: "财务复核",
state: changed ? (action === "return" ? "exception" : action === "transfer" ? "current" : "completed") : "current", actorName: "合成财务", occurredAt: null,
result: changed ? expectedResult : "等待财务复核", comment: null, detail: null, targetHref: null }] } });
});
await page.route(`**/api/workflow/tasks/${pendingTask.id}`, (route) => fulfillJson(route, {
...pendingTaskDetail, task: { ...pendingTaskDetail.task, stepName: "财务复核", assigneeRole: "finance",
status: changed ? "completed" : "pending" },
workflowInstance: { ...pendingTaskDetail.workflowInstance, entityType: "reimbursement",
contextData: { standardViolations: [], budgetCheck: null, budgetCheckStatus: "available", parseStatus: "ready" } },
reimbursement: source, leaveRequest: null, history: [], allowedActions: changed ? [] : ["approve", "return", "transfer"],
}));
await page.route(`**/api/workflow/tasks/${pendingTask.id}/*`, async (route) => {
if (route.request().method() !== "POST") return route.fallback();
writes++;
changed = action !== "failed";
if (action === "already_processed" || action === "failed") {
await route.fulfill({ status: action === "failed" ? 500 : 409, contentType: "application/json",
body: JSON.stringify({ requestId: "synthetic-progress", error: { code: action === "failed" ? "SYS_001" : "VALID_001",
message: "合成失败", details: action === "already_processed" ? { task: "already_processed" } : {} } }) });
} else await fulfillJson(route, { ok: true });
});
await page.setViewportSize(mobile ? { width: 390, height: 844 } : { width: 1280, height: 900 });
await login(page, MANAGER);
await page.goto(`${mobile ? "/mobile/approvals" : "/workflow/tasks"}/${pendingTask.id}`);
const progress = page.getByTestId("reimbursement-approval-processing-progress");
await expect(progress).toContainText("等待财务复核");
const documentMarker = await page.evaluate(() => {
const marker = crypto.randomUUID(); document.documentElement.dataset.syntheticDocument = marker; return marker;
});
if (action === "transfer") {
await page.getByTestId(mobile ? "mobile-approvals-transfer" : "workflow-task-transfer-menu-item").click();
await page.getByTestId(mobile ? "mobile-approvals-transfer-to" : "workflow-task-transfer-approver").selectOption("employee-transfer-1");
await page.getByTestId(mobile ? "mobile-approvals-transfer-reason" : "workflow-task-transfer-reason").fill("合成转审理由用于验证处理进度刷新");
await page.getByTestId(mobile ? "mobile-approvals-transfer-submit" : "workflow-task-transfer").click();
await page.getByTestId("workflow-transfer-confirm-dialog").getByRole("button", { name: "确认转审", exact: true }).click();
} else {
if (action === "return") {
await page.getByTestId(mobile ? "mobile-approvals-return" : "workflow-task-return-toggle").click();
await page.getByTestId(mobile ? "mobile-approvals-comment" : "workflow-task-return-comment").fill("请补充合成资料");
await page.getByTestId(mobile ? "mobile-approvals-return-submit" : "workflow-task-return").click();
} else await page.getByTestId(mobile ? "mobile-approvals-approve" : "workflow-task-approve").click();
await page.getByTestId("workflow-action-confirm-submit").click();
}
await expect.poll(() => writes).toBe(1);
if (action === "failed") {
await expect(page.getByTestId(mobile ? "mobile-approvals-approve" : "workflow-task-approve")).toBeEnabled();
await expect(progress).toContainText("等待财务复核");
expect(readsAfterAction).toBe(0);
} else {
await expect(progress).toContainText(expectedResult);
await expect(progress.locator("li")).toHaveAttribute("data-state", action === "return" ? "exception" : action === "transfer" ? "current" : "completed");
expect(readsAfterAction).toBeGreaterThan(0);
}
expect(await page.evaluate(() => document.documentElement.dataset.syntheticDocument)).toBe(documentMarker);
});
}
}