公司只有财务和出借收款经理两名办理人,新增限定公司、出借人和借款范围的关联方确认模式。 仅豁免指定公司代表的收款人关联;经办、自审、证据、交易归属、资金占用及 Q6 限制保留。 贯通付款、人工回单、银行对账、补证恢复、晚到补记与追回;电脑和手机显示实际确认身份。 授权版本、付款与对账停用事件及历史决定只增不改,新增迁移 20261009010000_loan_related_approval。 本地合成验证:82 迁移、HTTP 109 组、浏览器 33 组、数据库约束 9 组通过;13 个有效资金守卫变异均捕获。 已有服务端全量 4519 项通过、1 项原有跳过,后续定向 77 项通过;提示收尾另有 908 项合同测试及前端构建通过。 按用户授权提交后对干净候选运行正式 verify-candidate,不沿用旧候选回执。 限制:独立代码复核待完成(inline 模式);真实回单待验。本次仅本地提交,不推送、不部署。 生成关联决定后不能直接降回不理解该模式的旧代码;真实授权、录入、生产核实和付款仍须明确批准。
248 行
8.9 KiB
JavaScript
248 行
8.9 KiB
JavaScript
const {
|
|
DEFAULT_MODULE_VISIBILITY,
|
|
buildDefaultWorkspaceSetting,
|
|
getSystemNavigation,
|
|
normalizeModuleVisibilityJson,
|
|
} = require("../../src/services/workspaceSettings");
|
|
const { hasPermission, resolvePermissionsForRoles } = require("../../src/constants/permissions");
|
|
|
|
function buildPrisma(enabledFlagKeys = []) {
|
|
const enabled = new Set(enabledFlagKeys);
|
|
return {
|
|
workspaceSetting: {
|
|
findFirst: vi.fn().mockResolvedValue(null),
|
|
},
|
|
featureFlag: {
|
|
findUnique: vi.fn().mockImplementation(async ({ where }) => {
|
|
if (!enabled.has(where.flag_key)) {
|
|
return null;
|
|
}
|
|
return {
|
|
id: `flag-${where.flag_key}`,
|
|
flag_key: where.flag_key,
|
|
override_mode: "force_on",
|
|
rollout_type: "global",
|
|
department_ids: [],
|
|
note: "",
|
|
version: 1,
|
|
created_at: null,
|
|
created_by: null,
|
|
updated_at: null,
|
|
updated_by: null,
|
|
};
|
|
}),
|
|
},
|
|
};
|
|
}
|
|
|
|
function buildConfig() {
|
|
return {
|
|
announcementsEnabled: false,
|
|
attendanceEnabled: false,
|
|
budgetPrecheckEnabled: false,
|
|
cutoverConsoleEnabled: false,
|
|
employeeLifecycleEnabled: false,
|
|
globalSearchEnabled: false,
|
|
paymentBatchEnabled: false,
|
|
portalHomeEnabled: false,
|
|
presenceBoardEnabled: false,
|
|
projectDomainEnabled: false,
|
|
projectLinkingEnabled: false,
|
|
todoBoardEnabled: false,
|
|
weeklyReportEnabled: false,
|
|
workflowDefinitionAdminEnabled: false,
|
|
};
|
|
}
|
|
|
|
describe("workspace small-team navigation defaults", () => {
|
|
it.each(["manager", "ceo", "finance", "finance_mgr", "system_admin"])("projects AP separately from payment navigation for %s", async (role) => {
|
|
const employee = { id: "synthetic-actor", roleCodes: [role], permissionCodes: resolvePermissionsForRoles([role]) };
|
|
const navigation = await getSystemNavigation({
|
|
prisma: buildPrisma(["payment_batch_v1"]),
|
|
config: { ...buildConfig(), paymentBatchEnabled: true },
|
|
employee,
|
|
hasPermission: (candidate, permission) => hasPermission(candidate.permissionCodes, permission),
|
|
});
|
|
const keys = Object.values(navigation.sections).flat().map((item) => item.key);
|
|
expect(keys.includes("apOperations")).toBe(["finance", "finance_mgr"].includes(role));
|
|
if (["manager", "ceo"].includes(role)) expect(keys).toContain("paymentBatches");
|
|
});
|
|
|
|
it("keeps only the small-team task path in default main navigation", () => {
|
|
const mainModules = Object.entries(DEFAULT_MODULE_VISIBILITY)
|
|
.filter(([, placement]) => placement === "main")
|
|
.map(([key]) => key);
|
|
|
|
expect(mainModules).toEqual([
|
|
"portal",
|
|
"team",
|
|
"leaveRequests",
|
|
"reimbursements",
|
|
"assets",
|
|
"projects",
|
|
"contractsRegistry",
|
|
"purchaseRequests",
|
|
]);
|
|
expect(DEFAULT_MODULE_VISIBILITY.todos).toBe("hidden");
|
|
expect(DEFAULT_MODULE_VISIBILITY.assets).toBe("main");
|
|
expect(DEFAULT_MODULE_VISIBILITY.contractsRegistry).toBe("main");
|
|
expect(DEFAULT_MODULE_VISIBILITY.purchaseRequests).toBe("main");
|
|
expect(DEFAULT_MODULE_VISIBILITY.attendance).toBe("hidden");
|
|
});
|
|
|
|
it("normalizes unknown or invalid module visibility back to the R66 defaults", () => {
|
|
const normalized = normalizeModuleVisibilityJson({
|
|
attendance: "main",
|
|
purchaseRequests: "hidden",
|
|
unknownModule: "main",
|
|
budgets: "invalid",
|
|
});
|
|
|
|
expect(normalized.attendance).toBe("main");
|
|
expect(normalized.purchaseRequests).toBe("hidden");
|
|
expect(normalized.budgets).toBe(DEFAULT_MODULE_VISIBILITY.budgets);
|
|
expect(normalized.unknownModule).toBeUndefined();
|
|
});
|
|
|
|
it("builds a small-team internal workspace by default", () => {
|
|
const workspace = buildDefaultWorkspaceSetting();
|
|
|
|
expect(workspace.profile).toBe("small_team_internal");
|
|
expect(workspace.moduleVisibility.attendance).toBe("hidden");
|
|
});
|
|
|
|
it("keeps low-frequency and finance-only modules out of employee primary navigation", async () => {
|
|
const employee = {
|
|
id: "employee-1",
|
|
department_id: "dept-1",
|
|
roleCodes: ["employee"],
|
|
permissionCodes: resolvePermissionsForRoles(["employee"]),
|
|
};
|
|
|
|
const navigation = await getSystemNavigation({
|
|
prisma: buildPrisma(["presence_board_v1", "project_domain_v1"]),
|
|
config: { ...buildConfig(), projectDomainEnabled: true },
|
|
employee,
|
|
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
|
});
|
|
|
|
const mainLabels = navigation.sections.main.map((item) => item.label);
|
|
expect(mainLabels).toEqual(["工作台", "报销", "采购", "项目", "资产", "请假记录"]);
|
|
expect(mainLabels).not.toContain("待我处理");
|
|
expect(mainLabels).not.toContain("票据");
|
|
expect(mainLabels).not.toContain("个人设置");
|
|
|
|
const backupKeys = navigation.sections.backup.map((item) => item.key);
|
|
expect(backupKeys).not.toContain("purchaseRequests");
|
|
expect(backupKeys).not.toContain("travelRequests");
|
|
expect(backupKeys).not.toContain("invoicePool");
|
|
expect(backupKeys).not.toContain("contractsRegistry");
|
|
expect(backupKeys).not.toContain("budgets");
|
|
expect(backupKeys).not.toContain("paymentBatches");
|
|
expect(backupKeys).not.toContain("apOperations");
|
|
expect(backupKeys).not.toContain("erpIntegrations");
|
|
expect(backupKeys).not.toContain("dashboard");
|
|
});
|
|
|
|
it("shows待我处理 for business approvers and keeps admin-only users out", async () => {
|
|
const manager = {
|
|
id: "manager-1",
|
|
department_id: "dept-1",
|
|
roleCodes: ["manager"],
|
|
permissionCodes: resolvePermissionsForRoles(["manager"]),
|
|
};
|
|
|
|
const managerNavigation = await getSystemNavigation({
|
|
prisma: buildPrisma(["presence_board_v1", "project_domain_v1", "project_linking_v1"]),
|
|
config: { ...buildConfig(), projectDomainEnabled: true, projectLinkingEnabled: true, presenceBoardEnabled: true },
|
|
employee: manager,
|
|
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
|
});
|
|
|
|
expect(managerNavigation.sections.main.map((item) => item.label)).toEqual([
|
|
"工作台",
|
|
"待我处理",
|
|
"报销",
|
|
"采购",
|
|
"合同",
|
|
"项目",
|
|
"资产",
|
|
"公司借款",
|
|
"团队",
|
|
"请假记录",
|
|
]);
|
|
|
|
expect(managerNavigation.sections.main.find((item) => item.key === "companyLoans")?.href).toBe("/company-loans");
|
|
expect(manager.permissionCodes).not.toContain("loan.manage");
|
|
|
|
const adminOnly = {
|
|
id: "admin-1",
|
|
department_id: "dept-1",
|
|
roleCodes: ["admin"],
|
|
permissionCodes: resolvePermissionsForRoles(["admin"]),
|
|
};
|
|
|
|
const navigation = await getSystemNavigation({
|
|
prisma: buildPrisma(["payment_batch_v1"]),
|
|
config: { ...buildConfig(), workflowDefinitionAdminEnabled: true },
|
|
employee: adminOnly,
|
|
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
|
});
|
|
|
|
const allKeys = [
|
|
...navigation.sections.main,
|
|
...navigation.sections.admin,
|
|
...navigation.sections.backup,
|
|
].map((item) => item.key);
|
|
|
|
expect(allKeys).toContain("systemSettings");
|
|
expect(allKeys).not.toContain("auditLogs");
|
|
expect(allKeys).not.toContain("workflowTasks");
|
|
});
|
|
|
|
it("uses work names for finance navigation", async () => {
|
|
const finance = {
|
|
id: "finance-1",
|
|
department_id: "dept-1",
|
|
roleCodes: ["finance"],
|
|
permissionCodes: resolvePermissionsForRoles(["finance"]),
|
|
};
|
|
|
|
const navigation = await getSystemNavigation({
|
|
prisma: buildPrisma(["payment_batch_v1"]),
|
|
config: { ...buildConfig(), paymentBatchEnabled: true },
|
|
employee: finance,
|
|
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
|
});
|
|
|
|
const mainLabels = navigation.sections.main.map((item) => item.label);
|
|
expect(mainLabels).toContain("待我处理");
|
|
expect(mainLabels).toContain("付款");
|
|
expect(mainLabels).toContain("应付账款");
|
|
expect(mainLabels).toContain("票据");
|
|
expect(mainLabels).not.toContain("AP 运营");
|
|
expect(mainLabels).not.toContain("付款批次");
|
|
});
|
|
|
|
it("shows payment review navigation to manager reviewers without granting finance-only invoice tools", async () => {
|
|
const manager = {
|
|
id: "manager-reviewer-1",
|
|
department_id: "dept-1",
|
|
roleCodes: ["manager"],
|
|
permissionCodes: resolvePermissionsForRoles(["manager"]),
|
|
};
|
|
|
|
const navigation = await getSystemNavigation({
|
|
prisma: buildPrisma(["payment_batch_v1"]),
|
|
config: { ...buildConfig(), paymentBatchEnabled: true },
|
|
employee: manager,
|
|
hasPermission: (candidate, permissionCode) => hasPermission(candidate.permissionCodes, permissionCode),
|
|
});
|
|
|
|
const mainLabels = navigation.sections.main.map((item) => item.label);
|
|
expect(mainLabels).toContain("付款");
|
|
expect(mainLabels).not.toContain("应付账款");
|
|
expect(mainLabels).not.toContain("票据");
|
|
});
|
|
});
|