diff --git a/lib/cornerstone.ts b/lib/cornerstone.ts index 18876bd..adb4344 100644 --- a/lib/cornerstone.ts +++ b/lib/cornerstone.ts @@ -99,10 +99,13 @@ export function csDatum(iso: string | null | undefined): string { } /** - * Der Anmeldename: vorname.nachname, klein, ohne Umlaute und Leerzeichen. + * Der Konto­name im Verzeichnisdienst: vorname.nachname, klein, ohne Umlaute. + * + * Nur für das Customfield AD. Der **Username** in Cornerstone ist etwas + * anderes: er ist die UUID aus Alpenwerk, dieselbe wie die User ID. * * Abgeleitet und nicht aus der dienstlichen Adresse geschnitten: die Adresse - * ist freiwillig, der Name ist es nicht. Cornerstone braucht das Feld. + * ist freiwillig, der Name ist es nicht. */ export function benutzername(vorname: string, nachname: string): string { const sauber = (s: string) => @@ -182,21 +185,36 @@ export type CornerstoneKontext = { /** Eine Zeile — die Zuordnung selbst, ohne Datenbank und ohne Dateiformat. */ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext): CornerstoneZeile { - const nummer = p.personnel_number; - const name = benutzername(p.first_name, p.last_name); + // Zwei Kennungen, zwei Herkünfte: + // + // User ID + Username die UUID aus Alpenwerk (employees.id) + // Local System ID die Personalnummer, wie LOGA sie vergibt + // + // Benutzername und Benutzer-ID sind in Alpenwerk stets derselbe Wert — + // liefen sie auseinander, zeigte Cornerstone zwei Kennungen für eine + // Person, und jeder Verweis darauf träfe womöglich die falsche. + // + // Die UUID und nicht die Personalnummer, weil sie die Kennung ist, die + // sich nie ändert: eine Personalnummer kann berichtigt werden, und dann + // wäre die Person in Cornerstone eine andere. + // + // Der Anmeldename des Verzeichnisdienstes steht nur im Customfield AD. + const kennung = p.id; + const nummer = String(p.personnel_number); + const adName = benutzername(p.first_name, p.last_name); const kst = p.position_id ? k.kostenstelle.get(p.position_id) : undefined; const chef = p.manager_id ? k.managerNummer.get(p.manager_id) : undefined; const ausgetreten = p.status === "Ausgetreten"; return { - "User ID": String(nummer), - "Local System ID": String(nummer), + "User ID": kennung, + "Local System ID": nummer, Prefix: p.title_prefix.join(" "), "First Name": p.first_name, "Middle Name": "", "Last Name": p.last_name, Suffix: p.title_suffix.join(" "), - Username: name, + Username: kennung, Approver: "", Manager: chef ? String(chef) : "", Absent: "", @@ -261,7 +279,7 @@ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext // Kopfzeile, weil die Vorlage sie führt. "Retention Rules": "", Organisationsstufe: "", - "Customfield ID AD": name, + "Customfield ID AD": adName, "Customfield ID Doxis": `${DOXIS_PRAEFIX}${nummer}`, "Customfield ID Interflex": String(nummer), "Customfield ID LGVplus": `${LGV_PRAEFIX}${nummer}`, diff --git a/tests/unit/cornerstone.test.ts b/tests/unit/cornerstone.test.ts index f0c6126..ff0db5b 100644 --- a/tests/unit/cornerstone.test.ts +++ b/tests/unit/cornerstone.test.ts @@ -30,6 +30,8 @@ import { toCsv } from "@/lib/export"; const VORGABE = "User ID,Local System ID,Prefix,First Name,Middle Name,Last Name,Suffix,Username,Approver,Manager,Absent,Allow Reconciliation,Email,Required Training Approvals,Gender,Exempt,Ethnicity,Months of Service,Compensation Currency,Address Line 1,Address Line 2,City,State,Zip,Mailstop,Country,Original Hire Date,Last Hire Date,Phone,Mobile,Home Phone,Fax,Status,User activation date,User de-activation date,Personal Email,Language,Time Zone,User Type,Employment Status,Leave Reason,Termination Type,Termination Reason,Termination Date,Eligible For Rehire,Rehired Employee,Division ID,Position ID,Grade ID,Cost Center ID,Location ID,Retention Rules,Organisationsstufe,Customfield ID AD,Customfield ID Doxis,Customfield ID Interflex,Customfield ID LGVplus"; +const UUID = "8f2b1c40-9d3e-4a77-b2c1-5e6f70a81234"; + const kontext: CornerstoneKontext = { managerNummer: new Map([["chef", 1000]]), kostenstelle: new Map([["pos-1", { id: "k", code: "41130", name: "Waffelproduktion" }]]), @@ -37,7 +39,7 @@ const kontext: CornerstoneKontext = { function person(teil: Partial = {}): CornerstoneQuelle { return { - id: "e1", + id: UUID, personnel_number: 9002, first_name: "Max", last_name: "Alpentest", @@ -140,7 +142,7 @@ describe("Eine Zeile gegen das Muster vom 27.09.2026", () => { "Local System ID": "9002", "First Name": "Max", "Last Name": "Alpentest", - Username: "max.alpentest", + Username: UUID, Email: "m.test@alpenwerk.test.com", Gender: "male", "Compensation Currency": "EUR", @@ -235,6 +237,36 @@ describe("Abhängige Felder", () => { }); }); +describe("Benutzername und Benutzer-ID", () => { + it("tragen denselben Wert", () => { + // In Alpenwerk sind die beiden stets im Gleichklang. Liefen sie + // auseinander, zeigte Cornerstone zwei Kennungen für eine Person, und + // jeder Verweis darauf (Manager, Berichte) träfe womöglich die falsche. + const z = zeile(); + expect(z.Username).toBe(z["User ID"]); + }); + + it("sind die UUID aus Alpenwerk, nicht die Personalnummer", () => { + // Die UUID aendert sich nie; eine Personalnummer kann berichtigt werden, + // und dann waere die Person in Cornerstone eine andere. + const z = zeile(); + expect(z["User ID"]).toBe(UUID); + expect(z.Username).toBe(UUID); + }); + + it("fuehren die LOGA-Personalnummer in Local System ID", () => { + expect(zeile()["Local System ID"]).toBe("9002"); + }); + + it("lassen den Anmeldenamen im Customfield AD stehen", () => { + // Der Verzeichnisdienst kennt die Person unter vorname.nachname; das ist + // nicht der Username in Cornerstone, sondern eine eigene Angabe. + const z = zeile(); + expect(z["Customfield ID AD"]).toBe("max.alpentest"); + expect(z["Customfield ID AD"]).not.toBe(z.Username); + }); +}); + describe("Kennungen statt Namen", () => { it("verweist auf die vorgesetzte Person über deren Personalnummer", () => { // Cornerstone verknüpft über die User ID, nicht über den Namen.