Files
alpenwerk-hr/tests/unit/offboarding.test.ts
Maximilian Stubhan 521e4ecf00
Some checks failed
CI / Lint, Typen, Tests, Build (push) Failing after 6m6s
CI / Integrationstests (echtes Postgres) (push) Failing after 5m26s
Show the Offboarding tab from the day the exit is recorded, not the day it takes effect
The tab was keyed off employee.status === "Ausgetreten", and that stays
wrong for weeks: terminate_employee writes exit_date immediately no
matter how far out the date is, but only flips status once the date
itself arrives. A termination entered today for four weeks out left the
tab invisible for the entire notice period — exactly the stretch in
which IT access, hardware and deregistration actually get worked
through, and exactly where the checklist was supposed to live "next to
Onboarding," per the report that caught this.

The rule now reads exit_date instead: not null, and not a No Show
(which sets exit_date too, to the entry day, but never worked a day and
gets no checklist). rehire_employee resets both exit_date and
exit_reason to null, so a rehired person's tab still disappears the
same way it did before — nothing about that case changed, only the
signal the check reads.

Verified against the real database with the exact shape from the
report: a termination dated 30 days out. Status stays "Aktiv", the
checklist exists immediately, and the tab's own predicate says yes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 17:41:07 +02:00

122 lines
4.9 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
OFFBOARDING_GRUPPEN,
OFFBOARDING_PUNKTE,
fortschritt,
gehoertOffboarding,
istErledigt,
punktZu,
type AufgabenStand,
} from "@/lib/offboarding";
// Vom selben Blatt Papier wie die Onboarding-Liste, für den anderen Weg. Die
// Mechanik selbst (was als erledigt zählt, wie der Fortschritt entsteht) ist
// mit lib/onboarding.ts geteilt (lib/checklist.ts) und dort schon geprüft —
// hier geht es um das, was an dieser Liste besonders ist.
function stand(teil: Partial<AufgabenStand> & { item_key: string }): AufgabenStand {
return { erledigt: false, wert: null, kommentar: null, updated_at: null, updated_by_name: null, ...teil };
}
describe("Das Verzeichnis", () => {
it("hat eindeutige Schlüssel — sie stehen in den Daten", () => {
const keys = OFFBOARDING_PUNKTE.map((p) => p.key);
expect(new Set(keys).size).toBe(keys.length);
});
it("bildet jeden Punkt aus dem Blatt ab", () => {
expect(OFFBOARDING_PUNKTE).toHaveLength(11);
for (const key of [
"br_verstaendigung",
"kuendigungsschreiben",
"bestaetigung_kdn_bei",
"lgv_austritt",
"gkk_abmeldung",
"if6020_austritt",
"endabrechnung",
"offboarding_gestartet",
]) {
expect(punktZu(key), key).toBeDefined();
}
});
it("führt die beiden Vermerke als Text, nicht als Haken", () => {
// Ein Resturlaubssaldo abgehakt wäre wertlos — die Zahl selbst ist der Zweck.
expect(punktZu("resturlaub")?.art).toBe("text");
expect(punktZu("ueberleitung_salden_betrag")?.art).toBe("text");
});
it("unterscheidet die Aufgabe von ihrem Betrag", () => {
// Das Blatt nennt „Überleitung Salden für Auszahlung" zweimal: einmal als
// abzuhakende Aufgabe, einmal als Feld für den tatsächlichen Betrag. Zwei
// verschiedene Fragen, zwei verschiedene Punkte.
expect(punktZu("ueberleitung_salden")?.art).toBe("haken");
expect(punktZu("ueberleitung_salden_betrag")?.art).toBe("text");
expect(punktZu("ueberleitung_salden")?.key).not.toBe(punktZu("ueberleitung_salden_betrag")?.key);
});
it("führt keinen Ja/Nein-Punkt — anders als beim Onboarding", () => {
// Auf diesem Blatt gibt es keine Stelle mit zwei Kästchen nebeneinander.
expect(OFFBOARDING_PUNKTE.some((p) => p.art === "janein")).toBe(false);
});
it("gruppiert vollständig — kein Punkt fällt zwischen die Gruppen", () => {
const ausGruppen = OFFBOARDING_GRUPPEN.flatMap((g) => g.punkte.map((p) => p.key));
expect(new Set(ausGruppen)).toEqual(new Set(OFFBOARDING_PUNKTE.map((p) => p.key)));
});
});
describe("fortschritt", () => {
it("ist auf einer leeren Liste vollständig offen", () => {
const f = fortschritt([]);
expect(f.erledigt).toBe(0);
expect(f.gesamt).toBe(OFFBOARDING_PUNKTE.length);
});
it("zählt Haken und Text zusammen", () => {
const f = fortschritt([
stand({ item_key: "endabrechnung", erledigt: true }),
stand({ item_key: "resturlaub", wert: "12 Tage" }),
]);
expect(f.erledigt).toBe(2);
expect(f.offen).not.toContain("Endabrechnung");
});
});
describe("istErledigt", () => {
it("verlangt bei einem Haken die ausdrückliche Markierung", () => {
const punkt = punktZu("gkk_abmeldung")!;
expect(istErledigt(punkt, stand({ item_key: punkt.key, erledigt: true }))).toBe(true);
expect(istErledigt(punkt, stand({ item_key: punkt.key }))).toBe(false);
expect(istErledigt(punkt, undefined)).toBe(false);
});
});
describe("gehoertOffboarding", () => {
// Geprüft wird exit_date, nicht status: eine künftige Beendigung setzt
// exit_date sofort, aber status erst am Stichtag selbst. Bis dahin steht
// die Person weiterhin als „Aktiv" — mit status als Bedingung bliebe der
// Reiter bis zum letzten Arbeitstag unsichtbar, genau dann, wenn er
// gebraucht wird.
it("bejaht bei einem bereits wirksamen Austritt", () => {
expect(gehoertOffboarding({ exit_date: "2026-08-01", exit_reason: "Kündigung AN" })).toBe(true);
});
it("bejaht schon bei einem erst künftig wirksamen Austritt", () => {
expect(gehoertOffboarding({ exit_date: "2099-01-01", exit_reason: "Kündigung AN" })).toBe(true);
});
it("verneint bei No Show, obwohl exit_date ebenso gesetzt ist", () => {
// Nie einen Tag gearbeitet: kein IT-Zugang, keine GKK-Anmeldung, kein
// Dienstzettel. Der Reiter wäre ein Etikett ohne Inhalt.
expect(gehoertOffboarding({ exit_date: "2026-08-01", exit_reason: "No Show" })).toBe(false);
});
it("verneint ohne Austritt — auch nach einer Wiedereinstellung, die exit_date zurücksetzt", () => {
// Beides führt auf dieselbe Datenlage: wer nie ausgeschieden ist, und wer
// wiedereingestellt wurde (rehire_employee setzt exit_date und
// exit_reason ausdrücklich auf null), sehen für diese Regel gleich aus.
expect(gehoertOffboarding({ exit_date: null, exit_reason: null })).toBe(false);
});
});