Ask for someone's organisation on a day they actually have one

An employee starting 01.09. showed "Keine Führungskraft (Geschäftsführung)"
although their team has one — Josef Bauer, on chief position 60000752. The
reporting line was requested as of today, and today that person holds no
assignment, so om_reporting_lines() returned no row at all.

The database function was right; the caller asked the wrong question.

What made it look like a data problem rather than a date problem: the header
did show the unit and the position, because pickPlacements() falls back to
the next best assignment when none is current. Two notions of where someone
sits — one forgiving, one strict — sitting next to each other on the same
page.

orgAsOf() pulls the date into the employment: the first day for someone not
yet started, the last for someone who has left, today otherwise. Exit dates
are exclusive throughout the model, so the last working day is the day
before.

Anyone already gone had the same defect for the same reason, which is why
the rule covers both ends rather than special-casing the case that was
reported.

Verified against the live database: as of today no row, as of 2026-09-01 the
manager is Josef Bauer. Six unit tests over the boundaries, checked by
mutation — remove the future-entry branch and one fails.

Open positions still resolve as of today: they belong to the organisation,
not to the person whose file is open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 16:23:21 +02:00
parent 92685f0ac0
commit 27e0e8a8ce
3 changed files with 112 additions and 20 deletions

View File

@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from "vitest";
import { orgAsOf } from "@/lib/placement";
// orgAsOf() ist reine Rechnung, liegt aber neben den Ladefunktionen — und die
// ziehen über lib/db das Paket `server-only` herein, das ausserhalb von
// Next.js beim Import wirft.
vi.mock("server-only", () => ({}));
// Der Stichtag, zu dem die Organisation einer Person betrachtet wird.
//
// „Heute" ist die naheliegende und für zwei Gruppen falsche Antwort. Wer
// später anfängt oder schon gegangen ist, hat heute keine laufende
// Besetzung — om_reporting_lines() liefert dann keine Zeile, und die Akte
// behauptet „Keine Führungskraft", obwohl das Team eine hat. Genau so ist es
// aufgefallen.
const HEUTE = "2026-08-10";
describe("Stichtag der Organisation", () => {
it("bleibt heute für eine laufende Beschäftigung", () => {
expect(orgAsOf({ entry_date: "2015-03-01", exit_date: null }, HEUTE)).toBe(HEUTE);
});
it("nimmt bei künftigem Eintritt den ersten Tag", () => {
// Sonst steht in der Akte einer Person, die am 01.09. anfängt, heute
// keine Führungskraft — obwohl die Planstelle einer Einheit mit Leitung
// zugeordnet ist.
expect(orgAsOf({ entry_date: "2026-09-01", exit_date: null }, HEUTE)).toBe("2026-09-01");
});
it("nimmt bei Austritt den letzten Arbeitstag", () => {
// exit_date ist ausschliessend wie überall im Modell; am Austrittstag
// selbst besteht die Besetzung nicht mehr. Zu diesem Tag gefragt käme
// wieder nichts heraus.
expect(orgAsOf({ entry_date: "2015-03-01", exit_date: "2026-07-31" }, HEUTE)).toBe("2026-07-30");
});
it("bleibt heute, solange ein künftiger Austritt noch nicht wirksam ist", () => {
expect(orgAsOf({ entry_date: "2015-03-01", exit_date: "2026-12-31" }, HEUTE)).toBe(HEUTE);
});
it("nimmt am Eintrittstag selbst diesen Tag, nicht den Vortag", () => {
// Grenzfall: heute ist der erste Arbeitstag. Die Zuordnung gilt ab
// heute, also ist heute richtig.
expect(orgAsOf({ entry_date: HEUTE, exit_date: null }, HEUTE)).toBe(HEUTE);
});
it("behandelt Ein- und Austritt am selben Tag ohne Sprung ins Nichts", () => {
// Wer den Dienst nicht antritt: die Datenbank erlaubt exit = entry. Der
// Stichtag liegt dann vor dem Eintritt und liefert nichts — das ist
// richtig so, denn eine Organisation hatte diese Person nie.
expect(orgAsOf({ entry_date: "2026-07-31", exit_date: "2026-07-31" }, HEUTE)).toBe("2026-07-30");
});
});