import { describe, expect, it } from "vitest"; import { todayIso } from "@/lib/format"; import { resolveReportingLines, type OmHolder, type OmUnit } from "@/lib/om-reporting"; import { adminClient } from "./helpers"; // Die Berichtslinien-Regel existiert zweimal: als om_reporting_lines() in // der Datenbank und als resolveReportingLines() im Anwendungscode. Zwei // Fassungen derselben Regel driften auseinander, und die Abweichung fällt // niemandem auf — im Organigramm stünde einfach eine andere Führungskraft // als im Export. Also über den gesamten Bestand gegeneinanderhalten. describe("om_reporting_lines stimmt mit resolveReportingLines überein", () => { const asOf = todayIso(); async function fromDatabase() { const { data, error } = await adminClient.rpc("om_reporting_lines", { p_as_of: asOf }); if (error) throw new Error(error.message); return data ?? []; } async function fromTypeScript() { const [{ data: units }, { data: assignments }] = await Promise.all([ adminClient.from("org_units").select("id, parent_id"), adminClient .from("position_assignments") .select("employee_id, position_id, valid_from, valid_to, om_positions(org_unit_id, is_chief, valid_from, valid_to)") .lte("valid_from", asOf) .or(`valid_to.is.null,valid_to.gt.${asOf}`), ]); const { data: employees } = await adminClient .from("employees") .select("id, karenz_start_date, karenz_return_date"); const absentById = new Map( (employees ?? []).map((e) => [ e.id, Boolean( e.karenz_start_date && e.karenz_start_date <= asOf && (!e.karenz_return_date || asOf < e.karenz_return_date) ), ]) ); const omUnits: OmUnit[] = (units ?? []).map((u) => ({ id: u.id, parentId: u.parent_id })); const holders: OmHolder[] = (assignments ?? []) .filter((a) => { const p = a.om_positions as unknown as { valid_from: string; valid_to: string | null } | null; return p && p.valid_from <= asOf && (p.valid_to === null || p.valid_to > asOf); }) .map((a) => { const p = a.om_positions as unknown as { org_unit_id: string; is_chief: boolean }; return { employeeId: a.employee_id, positionId: a.position_id, orgUnitId: p.org_unit_id, isChief: p.is_chief, absent: absentById.get(a.employee_id) ?? false, }; }); return resolveReportingLines(omUnits, holders); } it("liefert für jede Person dieselbe formale und tatsächliche Führungskraft", async () => { const [db, ts] = await Promise.all([fromDatabase(), fromTypeScript()]); expect(db.length).toBe(ts.length); expect(db.length).toBeGreaterThan(0); const tsById = new Map(ts.map((l) => [l.employeeId, l])); const abweichungen = db .map((row) => { const mine = tsById.get(row.employee_id); if (!mine) return `${row.employee_id}: fehlt in der TypeScript-Fassung`; if (mine.actingManagerId !== row.acting_manager_id) return `${row.employee_id}: acting DB=${row.acting_manager_id} TS=${mine.actingManagerId}`; if (mine.formalManagerId !== row.formal_manager_id) return `${row.employee_id}: formal DB=${row.formal_manager_id} TS=${mine.formalManagerId}`; return null; }) .filter(Boolean); expect(abweichungen.slice(0, 10)).toEqual([]); }); it("gibt genau einer Person keine Führungskraft — der obersten Leitung", async () => { const db = await fromDatabase(); const wurzel = db.filter((r) => r.acting_manager_id === null); expect(wurzel).toHaveLength(1); }); it("erzeugt keine Berichtslinie auf sich selbst", async () => { const db = await fromDatabase(); expect(db.filter((r) => r.acting_manager_id === r.employee_id)).toEqual([]); }); it("lässt jede Berichtslinie an der obersten Leitung enden", async () => { // Ein Ring in den abgeleiteten Linien wäre im Organigramm ein Teilbaum, // der nie gerendert wird — und niemand würde es merken. const db = await fromDatabase(); const managerOf = new Map(db.map((r) => [r.employee_id, r.acting_manager_id])); for (const start of db) { const gesehen = new Set(); let cur: string | null = start.employee_id; while (cur && !gesehen.has(cur)) { gesehen.add(cur); cur = managerOf.get(cur) ?? null; } expect(cur, `Ring in der Berichtslinie ab ${start.employee_id}`).toBeNull(); } }); });