169 lines
4.8 KiB
TypeScript
169 lines
4.8 KiB
TypeScript
import { PermissionKey } from "@capakraken/shared";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildChargeabilityReportDetail,
|
|
getChargeabilityReportDetail,
|
|
} from "../router/chargeability-report-procedure-support.js";
|
|
|
|
describe("chargeability report procedure support", () => {
|
|
it("builds a filtered detailed report with rounded percentages", () => {
|
|
const result = buildChargeabilityReportDetail(
|
|
{
|
|
monthKeys: ["2026-03"],
|
|
groupTotals: [
|
|
{
|
|
monthKey: "2026-03",
|
|
totalFte: 1.234,
|
|
chg: 0.456,
|
|
target: 0.8,
|
|
gap: -0.344,
|
|
chargeabilityRatio: 0.456,
|
|
targetRatio: 0.8,
|
|
gapRatio: -0.344,
|
|
},
|
|
],
|
|
resources: [
|
|
{
|
|
id: "resource_1",
|
|
eid: "E-001",
|
|
displayName: "Alice",
|
|
fte: 1.234,
|
|
country: "ES",
|
|
federalState: null,
|
|
city: "Barcelona",
|
|
orgUnit: "CGI",
|
|
mgmtGroup: "Senior",
|
|
mgmtLevel: "L7",
|
|
targetPct: 0.8,
|
|
months: [
|
|
{
|
|
monthKey: "2026-03",
|
|
sah: 120.44,
|
|
sahHours: 120.44,
|
|
chg: 0.456,
|
|
bd: 0.1,
|
|
mdi: 0.05,
|
|
mo: 0.07,
|
|
pdr: 0.08,
|
|
absence: 0.09,
|
|
unassigned: 0.154,
|
|
chargeabilityRatio: 0.456,
|
|
businessDevelopmentRatio: 0.1,
|
|
marketDevelopmentInnovationRatio: 0.05,
|
|
managementOverheadRatio: 0.07,
|
|
peopleDevelopmentRecruitingRatio: 0.08,
|
|
plannedAbsenceRatio: 0.09,
|
|
unassignedRatio: 0.154,
|
|
chargeabilityHours: 54.9,
|
|
businessDevelopmentHours: 12,
|
|
marketDevelopmentInnovationHours: 6,
|
|
managementOverheadHours: 8.4,
|
|
peopleDevelopmentRecruitingHours: 9.6,
|
|
plannedAbsenceHours: 10.8,
|
|
unassignedHours: 18.5,
|
|
targetRatio: 0.8,
|
|
targetHours: 96.4,
|
|
gapRatio: -0.344,
|
|
gapHours: -41.4,
|
|
derivation: {
|
|
baseAvailableHours: 128,
|
|
publicHolidayCount: 1,
|
|
publicHolidayWorkdayCount: 1,
|
|
publicHolidayHoursDeduction: 8,
|
|
absenceDayEquivalent: 0.5,
|
|
absenceHoursDeduction: 4,
|
|
effectiveAvailableHours: 120,
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "resource_2",
|
|
eid: "E-002",
|
|
displayName: "Bob",
|
|
fte: 1,
|
|
country: "DE",
|
|
federalState: "BY",
|
|
city: "Munich",
|
|
orgUnit: "CGI",
|
|
mgmtGroup: "Senior",
|
|
mgmtLevel: "L7",
|
|
targetPct: 0.75,
|
|
months: [],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
startMonth: "2026-03",
|
|
endMonth: "2026-03",
|
|
resourceQuery: "ali",
|
|
resourceLimit: 1,
|
|
includeProposed: false,
|
|
},
|
|
);
|
|
|
|
expect(result.filters).toEqual({
|
|
startMonth: "2026-03",
|
|
endMonth: "2026-03",
|
|
orgUnitId: null,
|
|
managementLevelGroupId: null,
|
|
countryId: null,
|
|
includeProposed: false,
|
|
resourceQuery: "ali",
|
|
});
|
|
expect(result.groupTotals).toEqual([
|
|
{
|
|
monthKey: "2026-03",
|
|
totalFte: 1.2,
|
|
chargeabilityPct: 45.6,
|
|
targetPct: 80,
|
|
gapPct: -34.4,
|
|
},
|
|
]);
|
|
expect(result.resourceCount).toBe(1);
|
|
expect(result.returnedResourceCount).toBe(1);
|
|
expect(result.truncated).toBe(false);
|
|
expect(result.resources[0]).toEqual(
|
|
expect.objectContaining({
|
|
displayName: "Alice",
|
|
targetPct: 80,
|
|
managementLevelGroup: "Senior",
|
|
managementLevel: "L7",
|
|
months: [
|
|
expect.objectContaining({
|
|
monthKey: "2026-03",
|
|
sah: 120.4,
|
|
chargeabilityPct: 45.6,
|
|
targetPct: 80,
|
|
gapPct: -34.4,
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("rejects detailed reports without the cost-view permission", async () => {
|
|
await expect(
|
|
getChargeabilityReportDetail(
|
|
{
|
|
db: {
|
|
resource: { findMany: async () => [] },
|
|
} as never,
|
|
permissions: new Set(),
|
|
},
|
|
{
|
|
startMonth: "2026-03",
|
|
endMonth: "2026-03",
|
|
includeProposed: false,
|
|
},
|
|
),
|
|
).rejects.toEqual(
|
|
expect.objectContaining<Partial<TRPCError>>({
|
|
code: "FORBIDDEN",
|
|
message: `Permission required: ${PermissionKey.VIEW_COSTS}`,
|
|
}),
|
|
);
|
|
});
|
|
});
|