import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { adminClient, createHrUser, deleteTestEmployee, deleteTestUser, hireTestEmployee, isoDateOffset, pickSeededTeam, signInAs, teamLeadId, 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 to `employees` immediately; // apply_due_pending_changes() applies it once due. describe("effective-dated mutations", () => { let hrUser: TestUser; let hrClient: SupabaseClient; let teamA: { id: string }; let teamB: { id: string }; const employeeIds: string[] = []; beforeAll(async () => { hrUser = await createHrUser({ active: true }); hrClient = await signInAs(hrUser); teamA = await pickSeededTeam(); teamB = await pickSeededTeam(teamA.id); }); afterAll(async () => { for (const id of employeeIds) await deleteTestEmployee(id); await deleteTestUser(hrUser); }); async function freshEmployee(teamId: string): Promise { const id = await hireTestEmployee(hrClient, teamId); employeeIds.push(id); return id; } it("transfer_employee with today's date writes immediately", async () => { const employeeId = await freshEmployee(teamA.id); const newLead = await teamLeadId(teamB.id); const { error } = await hrClient.rpc("transfer_employee", { payload: { employee_id: employeeId, effective_date: isoDateOffset(0), new_team_id: teamB.id }, }); expect(error).toBeNull(); const { data: employee } = await adminClient.from("employees").select("team_id, manager_id").eq("id", employeeId).single(); expect(employee?.team_id).toBe(teamB.id); expect(employee?.manager_id).toBe(newLead); }); it("transfer_employee with a future date defers the write and applies it once due", async () => { const employeeId = await freshEmployee(teamA.id); const newLead = await teamLeadId(teamB.id); const { error } = await hrClient.rpc("transfer_employee", { payload: { employee_id: employeeId, effective_date: isoDateOffset(30), new_team_id: teamB.id }, }); 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. const { data: unchanged } = await adminClient.from("employees").select("team_id").eq("id", employeeId).single(); expect(unchanged?.team_id).toBe(teamA.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.new_team_id).toBe(teamB.id); // 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: employee } = await adminClient.from("employees").select("team_id, manager_id").eq("id", employeeId).single(); expect(employee?.team_id).toBe(teamB.id); expect(employee?.manager_id).toBe(newLead); 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(teamA.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(teamA.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); }); });