import { DummyDriver, Kysely, PostgresAdapter, PostgresIntrospector, PostgresQueryCompiler } from "kysely"; import { describe, expect, it } from "vitest"; import type { Schema } from "@/lib/db/schema"; import { derivedStatusFilter } from "@/lib/employee-status-filter"; import { deriveStatusAsOf } from "@/lib/reports"; import type { EmploymentStatus } from "@/lib/types"; // Die Liste filtert in der Datenbank, weil sie dort auch blättert. Der Filter // ist damit die einzige Stelle, an der der Status *nicht* von // deriveStatusAsOf() kommt, sondern von einer zweiten Fassung derselben Regel // in SQL. Zwei Fassungen driften, und diese hier taten es: „Geplant" fragte // nur nach dem Eintrittsdatum und zählte die No-Shows mit — Leute mit einem // Eintritt in der Zukunft, deren Austritt längst verbucht war. // // tests/integration/employee-status-filter.test.ts hält die beiden gegen eine // echte Datenbank, läuft aber mangels Seed nicht in der CI. Deshalb hier // zweierlei ohne Datenbank: die erzeugte Abfrage lesen, und die // TypeScript-Ableitung an denselben Fällen prüfen. const db = new Kysely({ dialect: { createAdapter: () => new PostgresAdapter(), createDriver: () => new DummyDriver(), createIntrospector: (d) => new PostgresIntrospector(d), createQueryCompiler: () => new PostgresQueryCompiler(), }, }); const STICHTAG = "2026-09-15"; /** Die where-Klausel, die für eine Statusauswahl herauskommt. */ function bedingung(statuses: EmploymentStatus[]): string { return db .selectFrom("employees") .select("id") .where((eb) => derivedStatusFilter(eb, statuses, STICHTAG) ?? eb.val(true)) .compile() .sql.replace(/^.*? where /s, "") .replace(/\s+/g, " "); } describe("derivedStatusFilter — Geplant", () => { it("fragt nach dem Eintritt in der Zukunft", () => { expect(bedingung(["Geplant"])).toContain('"entry_date" >'); }); // Die eigentliche Zusicherung: ohne diese Hälfte war der Filter falsch. it("schliesst aus, wessen Austritt schon vollzogen ist", () => { const sql = bedingung(["Geplant"]); expect(sql).toContain('"exit_date" is null'); expect(sql).toContain('"exit_date" >'); }); }); describe("derivedStatusFilter — Ausgetreten", () => { it("verlangt ein Austrittsdatum, das erreicht ist", () => { const sql = bedingung(["Ausgetreten"]); expect(sql).toContain('"exit_date" is not null'); expect(sql).toContain('"exit_date" <='); }); }); describe("derivedStatusFilter — nicht abgedeckte Auswahl", () => { it("filtert lieber gar nicht als falsch", () => { // Eine Mischung aus beschäftigt und nicht beschäftigt hat keinen Weg in // der Oberfläche; geraten wird dafür nichts. expect(derivedStatusFilter({} as never, ["Aktiv", "Ausgetreten"], STICHTAG)).toBeNull(); expect(derivedStatusFilter({} as never, [], STICHTAG)).toBeNull(); }); }); // Dieselben Fälle noch einmal gegen die TypeScript-Ableitung: was der Filter // findet und was die Liste daneben anzeigt, muss dieselbe Person meinen. describe("Filter und Anzeige meinen dasselbe", () => { const basis = { karenz_start_date: null, karenz_return_date: null }; const faelle: { was: string; e: Parameters[0]; erwartet: EmploymentStatus }[] = [ { was: "No-Show: Eintritt in der Zukunft, Austritt bereits vollzogen", e: { ...basis, entry_date: "2026-10-01", exit_date: "2026-09-10" }, erwartet: "Ausgetreten", }, { was: "geplanter Eintritt ohne Austritt", e: { ...basis, entry_date: "2026-10-01", exit_date: null }, erwartet: "Geplant", }, { was: "geplanter Eintritt mit Befristung, beides in der Zukunft", e: { ...basis, entry_date: "2026-10-01", exit_date: "2026-12-31" }, erwartet: "Geplant", }, ]; for (const { was, e, erwartet } of faelle) { it(was, () => { expect(deriveStatusAsOf(e, STICHTAG)).toBe(erwartet); }); } });