diff --git a/app/api/export/cornerstone/route.ts b/app/api/export/cornerstone/route.ts index ea73e09..9fac086 100644 --- a/app/api/export/cornerstone/route.ts +++ b/app/api/export/cornerstone/route.ts @@ -21,14 +21,14 @@ export async function GET(request: NextRequest) { const stichtag = asOf ?? todayIso(); const { zeilen } = await withUser(gate.userId, async (tx) => { - const { rows, managerNummer } = await ladeExportMitarbeiter(tx, { asOf, filters }); + const { rows, managerKennung } = await ladeExportMitarbeiter(tx, { asOf, filters }); // Die eine Angabe, die die gemeinsame Auswahl nicht mitbringt: die // Kostenstelle hängt an der Planstelle, nicht an der Person. Erst jetzt, // weil erst jetzt feststeht, um welche Planstellen es geht. const positionIds = [...new Set(rows.map((r) => r.position_id).filter((id): id is string => Boolean(id)))]; const kontext: CornerstoneKontext = { - managerNummer, + managerKennung, kostenstelle: await loadKontierungen(tx, { asOf: stichtag, positionIds }), }; diff --git a/lib/cornerstone.ts b/lib/cornerstone.ts index c38ddc7..c499a58 100644 --- a/lib/cornerstone.ts +++ b/lib/cornerstone.ts @@ -178,8 +178,8 @@ export type CornerstoneQuelle = { }; export type CornerstoneKontext = { - /** Personalnummer je Mitarbeiterkennung — Cornerstone verweist über die User ID. */ - managerNummer: Map; + /** Cornerstone-ID je Mitarbeiterkennung — der Verweis auf die vorgesetzte Person. */ + managerKennung: Map; /** Kostenstelle je Planstelle zum Stichtag. */ kostenstelle: Map; }; @@ -209,7 +209,16 @@ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext 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; + // Der Verweis auf die vorgesetzte Person geht über **deren** User ID, also + // über ihre Cornerstone-ID. Hier stand die Personalnummer: die ist in dieser + // Datei die Local System ID, und Cornerstone hätte den Verweis entweder ins + // Leere laufen lassen oder — schlimmer — auf jemand anderen gelegt, dessen + // User ID zufällig so aussieht wie eine Personalnummer. + // + // Leer, wenn die vorgesetzte Person selbst keine Cornerstone-ID trägt. Aus + // demselben Grund wie oben: eine Kennung der falschen Art ist schlimmer als + // keine, weil niemand ihr ansieht, dass sie falsch ist. + const chef = p.manager_id ? k.managerKennung.get(p.manager_id) : undefined; const ausgetreten = p.status === "Ausgetreten"; return { @@ -222,7 +231,7 @@ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext Suffix: p.title_suffix.join(" "), Username: kennung, Approver: "", - Manager: chef ? String(chef) : "", + Manager: chef ?? "", Absent: "", "Allow Reconciliation": "", Email: p.company_email ?? "", @@ -274,6 +283,10 @@ export function baueCornerstoneZeile(p: CornerstoneQuelle, k: CornerstoneKontext // Cornerstone-Position-ID. Sie hier einzutragen hiesse, eine Kennung des // Zielsystems zu erfinden. "Position ID": "", + // Der Hay-Grade, so wie er gespeichert ist. Wer keinen trägt, bekommt den + // „Generic Grade" — und der ist laut der Tabelle des Kunden der + // Bindestrich, nicht die leere Zelle. Hier wird deshalb nichts übersetzt; + // siehe lib/hay-grade.ts. "Grade ID": p.paygrade, "Cost Center ID": kst?.code ?? "", // Leer: Cornerstone erwartet seine eigene Standortkennung („01"), und diff --git a/lib/export-auswahl.ts b/lib/export-auswahl.ts index bee9076..25003fb 100644 --- a/lib/export-auswahl.ts +++ b/lib/export-auswahl.ts @@ -58,8 +58,8 @@ export async function ladeExportMitarbeiter( lookups: OrgLookups; orgMaps: OrgMaps; managerName: Map; - /** Personalnummer je Kennung — für Exporte, die über Nummern verweisen. */ - managerNummer: Map; + /** Cornerstone-ID je Kennung — für Exporte, die über die Kennung des Zielsystems verweisen. */ + managerKennung: Map; dependentsCounts: Map; }> { const statuses = parseStatuses(filters.status); @@ -86,7 +86,9 @@ export async function ladeExportMitarbeiter( const [employees, lookupResult, allEmployees, dependentsCounts, placements, lines] = await Promise.all([ employeeQuery().execute(), loadOrgLookups(tx), - tx.selectFrom("employees").select(["id", "first_name", "last_name", "personnel_number"]).orderBy("id").execute(), + // Absichtlich schmal: die Liste umfasst **alle** Personen, nicht nur die + // gefilterten, und dient nur dazu, einen Verweis aufzulösen. + tx.selectFrom("employees").select(["id", "first_name", "last_name", "cornerstone_id"]).orderBy("id").execute(), loadDependentsCounts(tx), loadPlacements(tx, { asOf: stichtag }), loadReportingLineMap(tx, stichtag), @@ -119,7 +121,7 @@ export async function ladeExportMitarbeiter( lookups: lookupResult.lookups, orgMaps, managerName: new Map(allEmployees.map((e) => [e.id, fmtName(e.first_name, e.last_name)])), - managerNummer: new Map(allEmployees.map((e) => [e.id, e.personnel_number])), + managerKennung: new Map(allEmployees.map((e) => [e.id, e.cornerstone_id])), dependentsCounts, }; } diff --git a/tests/unit/cornerstone.test.ts b/tests/unit/cornerstone.test.ts index c03c134..666bd41 100644 --- a/tests/unit/cornerstone.test.ts +++ b/tests/unit/cornerstone.test.ts @@ -36,7 +36,12 @@ const UUID = "8f2b1c40-9d3e-4a77-b2c1-5e6f70a81234"; const CS_ID = "10000382"; const kontext: CornerstoneKontext = { - managerNummer: new Map([["chef", 1000]]), + managerKennung: new Map([ + ["chef", "10000117"], + // Eine vorgesetzte Person, die selbst keine Cornerstone-ID trägt — bei 435 + // von 785 Personen der Normalfall, nicht der Sonderfall. + ["chef-ohne-kennung", null], + ]), kostenstelle: new Map([["pos-1", { id: "k", code: "41130", name: "Waffelproduktion" }]]), }; @@ -284,12 +289,20 @@ describe("Benutzername und Benutzer-ID", () => { }); describe("Kennungen statt Namen", () => { - it("verweist auf die vorgesetzte Person über deren Personalnummer", () => { - // Cornerstone verknüpft über die User ID, nicht über den Namen. - expect(zeile({ manager_id: "chef" }).Manager).toBe("1000"); + it("verweist auf die vorgesetzte Person über deren Cornerstone-ID", () => { + // Cornerstone verknüpft über die User ID, nicht über den Namen und nicht + // über die Personalnummer: die ist in dieser Datei die Local System ID. + expect(zeile({ manager_id: "chef" }).Manager).toBe("10000117"); expect(zeile({ manager_id: null }).Manager).toBe(""); }); + it("lässt den Verweis leer, wenn die vorgesetzte Person keine Kennung trägt", () => { + // Lieber keine Angabe als eine Kennung der falschen Art: eine + // Personalnummer im Feld Manager sähe aus wie eine User ID und träfe im + // Zielsystem womöglich eine andere Person. + expect(zeile({ manager_id: "chef-ohne-kennung" }).Manager).toBe(""); + }); + it("nimmt als Kostenstelle den Code der Planstelle", () => { expect(zeile()["Cost Center ID"]).toBe("41130"); expect(zeile({ position_id: null })["Cost Center ID"]).toBe("");