import { describe, it, expect } from "vitest"; import { calculateShoringRatio, type ShoringAssignment } from "../allocation/shoring-ratio.js"; function make(resourceId: string, countryCode: string | null, hoursPerDay = 8, workingDays = 20): ShoringAssignment { return { resourceId, countryCode, hoursPerDay, workingDays }; } describe("calculateShoringRatio", () => { it("returns zeros for empty assignments", () => { const r = calculateShoringRatio([]); expect(r.totalHours).toBe(0); expect(r.offshoreRatio).toBe(0); expect(r.isAboveThreshold).toBe(false); }); it("100% German → 0% offshore", () => { const r = calculateShoringRatio([make("r1", "DE"), make("r2", "DE")]); expect(r.onshoreRatio).toBe(100); expect(r.offshoreRatio).toBe(0); expect(r.isAboveThreshold).toBe(false); }); it("100% offshore → 100% offshore, above threshold", () => { const r = calculateShoringRatio([make("r1", "ES"), make("r2", "IN")]); expect(r.offshoreRatio).toBe(100); expect(r.isAboveThreshold).toBe(true); }); it("50/50 mix → 50% offshore, below 55% threshold", () => { const r = calculateShoringRatio([make("r1", "DE"), make("r2", "ES")]); expect(r.offshoreRatio).toBe(50); expect(r.isAboveThreshold).toBe(false); }); it("40% onshore / 60% offshore → above 55% threshold", () => { const assignments = [ make("r1", "DE", 8, 20), // 160h make("r2", "DE", 8, 20), // 160h → 320h onshore make("r3", "ES", 8, 20), // 160h make("r4", "IN", 8, 20), // 160h make("r5", "IN", 8, 20), // 160h → 480h offshore ]; const r = calculateShoringRatio(assignments); expect(r.offshoreRatio).toBe(60); expect(r.isAboveThreshold).toBe(true); }); it("custom threshold: 30% → 40% offshore is above", () => { const assignments = [ make("r1", "DE", 8, 20), // 160h onshore make("r2", "DE", 8, 20), // 160h onshore → 320h make("r3", "ES", 4, 20), // 80h offshore make("r4", "IN", 4, 20), // 80h offshore → 160h ]; // Total 480h, offshore 160h = 33% const r = calculateShoringRatio(assignments, 30); expect(r.offshoreRatio).toBe(33); expect(r.isAboveThreshold).toBe(true); }); it("resource without country counts as offshore", () => { const r = calculateShoringRatio([make("r1", "DE"), make("r2", null)]); expect(r.offshoreRatio).toBe(50); expect(r.unknownCount).toBe(1); expect(r.byCountry["UNKNOWN"]).toBeDefined(); }); it("country code is case-insensitive", () => { const r = calculateShoringRatio([make("r1", "de"), make("r2", "De")]); expect(r.onshoreRatio).toBe(100); expect(r.offshoreRatio).toBe(0); }); it("custom onshore country code", () => { const r = calculateShoringRatio([make("r1", "ES"), make("r2", "DE")], 55, "ES"); expect(r.onshoreCountryCode).toBe("ES"); expect(r.onshoreHours).toBe(160); // r1 is onshore expect(r.offshoreHours).toBe(160); // r2 is offshore (DE is not onshore here) }); it("byCountry breakdown is correct", () => { const r = calculateShoringRatio([ make("r1", "DE", 8, 10), make("r2", "ES", 8, 10), make("r3", "ES", 4, 10), make("r4", "IN", 8, 10), ]); expect(r.byCountry["DE"]!.resourceCount).toBe(1); expect(r.byCountry["ES"]!.resourceCount).toBe(2); expect(r.byCountry["IN"]!.resourceCount).toBe(1); expect(r.byCountry["DE"]!.hours).toBe(80); expect(r.byCountry["ES"]!.hours).toBe(120); expect(r.byCountry["IN"]!.hours).toBe(80); }); it("exactly at threshold is above", () => { // 55% offshore = at threshold = isAboveThreshold true (>= not >) // Need 55/100 = 11/20 ratio const assignments = [ make("r1", "DE", 9, 20), // 180h onshore make("r2", "ES", 11, 20), // 220h offshore ]; // Total 400h, offshore 220h = 55% const r = calculateShoringRatio(assignments); expect(r.offshoreRatio).toBe(55); expect(r.isAboveThreshold).toBe(true); }); it("weighted by hours — FTE matters", () => { const r = calculateShoringRatio([ make("r1", "DE", 8, 20), // 160h full-time onshore make("r2", "ES", 2, 20), // 40h part-time offshore ]); // Total 200h, offshore 40h = 20% expect(r.offshoreRatio).toBe(20); expect(r.isAboveThreshold).toBe(false); }); });