import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { adminClient, chiefOfUnit, createHrUser, createTestPosition, deleteTestEmployee, deleteTestPosition, deleteTestUser, hireTestEmployee, isoDateOffset, pickSeededUnit, signInAs, type TestUser, } from "./helpers"; import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database } from "@/lib/supabase/types"; // Deferred/effective-dated changes (supabase/migrations/20260714120200_ // effective_dating_rpcs.sql): a future "wirksam ab" date must queue a // pending_org_changes row instead of writing immediately; // apply_due_pending_changes() applies it once due. // // Im OM-Modell ist eine Versetzung der Wechsel auf eine Zielplanstelle, und // die Berichtslinie wird nicht mehr mitgeschrieben. Geprüft wird deshalb die // laufende Besetzung und was om_reporting_lines daraus ableitet — nicht mehr // employees.team_id/manager_id, die es nicht mehr gibt. describe("effective-dated mutations", () => { let hrUser: TestUser; let hrClient: SupabaseClient; let unitA: { id: string }; let unitB: { id: string }; const employeeIds: string[] = []; const positionIds: string[] = []; beforeAll(async () => { hrUser = await createHrUser({ active: true }); hrClient = await signInAs(hrUser); unitA = await pickSeededUnit(); unitB = await pickSeededUnit(unitA.id); }); afterAll(async () => { for (const id of employeeIds) await deleteTestEmployee(id); for (const id of positionIds) await deleteTestPosition(id); await deleteTestUser(hrUser); }); /** Eine Wegwerf-Planstelle in `unitId`, alt genug für einen Eintritt vor 30 Tagen. */ async function freshPosition(unitId: string): Promise { const positionId = await createTestPosition(hrClient, unitId, { valid_from: isoDateOffset(-40) }); positionIds.push(positionId); return positionId; } async function freshEmployee(unitId: string): Promise { const id = await hireTestEmployee(hrClient, await freshPosition(unitId)); employeeIds.push(id); return id; } async function lineOf(employeeId: string) { const { data } = await adminClient .rpc("om_reporting_lines", { p_as_of: isoDateOffset(0) }) .eq("employee_id", employeeId) .single(); return data as unknown as { org_unit_id: string; formal_manager_id: string | null }; } it("transfer_employee with today's date writes immediately", async () => { const employeeId = await freshEmployee(unitA.id); const target = await freshPosition(unitB.id); const { error } = await hrClient.rpc("transfer_employee", { payload: { employee_id: employeeId, effective_date: isoDateOffset(0), target_position_id: target }, }); expect(error).toBeNull(); const { data: assignment } = await adminClient .from("position_assignments") .select("position_id") .eq("employee_id", employeeId) .is("valid_to", null) .single(); expect(assignment?.position_id).toBe(target); const line = await lineOf(employeeId); expect(line.org_unit_id).toBe(unitB.id); expect(line.formal_manager_id).toBe(await chiefOfUnit(unitB.id)); }); it("transfer_employee with a future date defers the write and applies it once due", async () => { const employeeId = await freshEmployee(unitA.id); const target = await freshPosition(unitB.id); const { error } = await hrClient.rpc("transfer_employee", { payload: { employee_id: employeeId, effective_date: isoDateOffset(30), target_position_id: target }, }); expect(error).toBeNull(); // Not written yet — this is the exact bug the migration fixes: a // future-dated transfer must not overwrite the live record today. expect((await lineOf(employeeId)).org_unit_id).toBe(unitA.id); const { data: pending } = await adminClient .from("pending_org_changes") .select("id, status, payload") .eq("employee_id", employeeId) .eq("change_type", "transfer") .single(); expect(pending?.status).toBe("pending"); expect(pending?.payload.target_position_id).toBe(target); // Fast-forward: simulate the effective date having arrived, then run // the same function the daily cron route calls. await adminClient.from("pending_org_changes").update({ effective_date: isoDateOffset(0) }).eq("id", pending!.id); const { data: appliedCount, error: applyError } = await adminClient.rpc("apply_due_pending_changes"); expect(applyError).toBeNull(); expect(appliedCount).toBeGreaterThanOrEqual(1); const { data: assignment } = await adminClient .from("position_assignments") .select("position_id") .eq("employee_id", employeeId) .is("valid_to", null) .single(); expect(assignment?.position_id).toBe(target); expect((await lineOf(employeeId)).org_unit_id).toBe(unitB.id); const { data: appliedRow } = await adminClient .from("pending_org_changes") .select("status, applied_at") .eq("id", pending!.id) .single(); expect(appliedRow?.status).toBe("applied"); expect(appliedRow?.applied_at).not.toBeNull(); }); it("promote_employee with a future date does not change job_title/paygrade until applied", async () => { const employeeId = await freshEmployee(unitA.id); const { error } = await hrClient.rpc("promote_employee", { payload: { employee_id: employeeId, effective_date: isoDateOffset(14), new_title: "Senior Testperson", new_paygrade: "D" }, }); expect(error).toBeNull(); const { data: unchanged } = await adminClient.from("employees").select("job_title, paygrade").eq("id", employeeId).single(); expect(unchanged?.job_title).not.toBe("Senior Testperson"); const { data: pending } = await adminClient .from("pending_org_changes") .select("id") .eq("employee_id", employeeId) .eq("change_type", "promotion") .single(); await adminClient.from("pending_org_changes").update({ effective_date: isoDateOffset(0) }).eq("id", pending!.id); await adminClient.rpc("apply_due_pending_changes"); const { data: employee } = await adminClient.from("employees").select("job_title, paygrade").eq("id", employeeId).single(); expect(employee?.job_title).toBe("Senior Testperson"); expect(employee?.paygrade).toBe("D"); }); it("start_karenz with a future date sets karenz_start_date immediately but keeps status Aktiv", async () => { const employeeId = await freshEmployee(unitA.id); const startDate = isoDateOffset(20); const returnDate = isoDateOffset(200); const { error } = await hrClient.rpc("start_karenz", { payload: { employee_id: employeeId, karenz_start_date: startDate, planned_return_date: returnDate }, }); expect(error).toBeNull(); const { data: employee } = await adminClient .from("employees") .select("status, karenz_start_date") .eq("id", employeeId) .single(); expect(employee?.status).toBe("Aktiv"); expect(employee?.karenz_start_date).toBe(startDate); const { data: pending } = await adminClient .from("pending_org_changes") .select("id") .eq("employee_id", employeeId) .eq("change_type", "karenz_start") .single(); await adminClient.from("pending_org_changes").update({ effective_date: isoDateOffset(0) }).eq("id", pending!.id); await adminClient.rpc("apply_due_pending_changes"); const { data: afterApply } = await adminClient .from("employees") .select("status, karenz_return_date") .eq("id", employeeId) .single(); expect(afterApply?.status).toBe("Karenz"); expect(afterApply?.karenz_return_date).toBe(returnDate); }); });