feat(api): explain chargeability derivation inputs

This commit is contained in:
2026-03-31 22:43:33 +02:00
parent cb363ca5b3
commit 1b5f19c72c
2 changed files with 128 additions and 0 deletions
@@ -132,6 +132,38 @@ describe("chargeability report router", () => {
expect(strictMonth).toBeDefined();
expect(proposedMonth).toBeDefined();
expect(strict.explainability).toEqual({
locationFields: [
"country",
"federalState",
"city",
"orgUnit",
"managementLevelGroup",
"managementLevel",
],
monthDerivationFields: [
"baseAvailableHours",
"publicHolidayCount",
"publicHolidayWorkdayCount",
"publicHolidayHoursDeduction",
"absenceDayEquivalent",
"absenceHoursDeduction",
"effectiveAvailableHours",
],
activeFilters: [],
formulas: {
sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours",
chargeabilityPct: "chargeabilityHours / sahHours",
targetHours: "sahHours * targetPct",
gapHours: "chargeabilityHours - targetHours",
},
notes: [
"Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.",
"Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.",
"Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.",
],
});
expect(withProposed.explainability.activeFilters).toEqual(["includeProposed"]);
expect(strictMonth?.chg).toBeGreaterThan(0);
expect(proposedMonth?.chg).toBeGreaterThan(strictMonth?.chg ?? 0);
expect(proposedMonth?.chg).toBeCloseTo((strictMonth?.chg ?? 0) * 2, 5);
@@ -352,6 +384,13 @@ describe("chargeability report router", () => {
const augsburg = report.resources.find((resource) => resource.city === "Augsburg");
const munich = report.resources.find((resource) => resource.city === "Munich");
expect(augsburg?.federalState).toBe("BY");
expect(augsburg?.months[0]?.derivation?.publicHolidayCount).toBe(
(munich?.months[0]?.derivation?.publicHolidayCount ?? 0) + 1,
);
expect(augsburg?.months[0]?.derivation?.publicHolidayHoursDeduction).toBe(
(munich?.months[0]?.derivation?.publicHolidayHoursDeduction ?? 0) + 8,
);
expect(augsburg?.months[0]?.sah).toBe((munich?.months[0]?.sah ?? 0) - 8);
});
@@ -513,11 +552,43 @@ describe("chargeability report router", () => {
expect(result.resourceCount).toBe(1);
expect(result.returnedResourceCount).toBe(1);
expect(result.truncated).toBe(false);
expect(result.explainability).toEqual({
locationFields: [
"country",
"federalState",
"city",
"orgUnit",
"managementLevelGroup",
"managementLevel",
],
monthDerivationFields: [
"baseAvailableHours",
"publicHolidayCount",
"publicHolidayWorkdayCount",
"publicHolidayHoursDeduction",
"absenceDayEquivalent",
"absenceHoursDeduction",
"effectiveAvailableHours",
],
activeFilters: ["resourceQuery"],
formulas: {
sah: "baseAvailableHours - publicHolidayHoursDeduction - absenceHoursDeduction = effectiveAvailableHours",
chargeabilityPct: "chargeabilityHours / sahHours",
targetHours: "sahHours * targetPct",
gapHours: "chargeabilityHours - targetHours",
},
notes: [
"Location fields explain why two resources can have different SAH in the same month because country, federal state, and city holidays may differ.",
"Holiday deductions and absence deductions are tracked separately; absence does not deduct days that are already public holidays.",
"Include proposed work changes chargeability ratios and hours, but it does not change holiday or absence-based SAH derivation.",
],
});
expect(result.resources).toEqual([
expect.objectContaining({
displayName: "Alice",
targetPct: 80,
country: "ES",
federalState: null,
city: "Barcelona",
managementLevelGroup: "Senior",
managementLevel: "L7",
@@ -527,6 +598,10 @@ describe("chargeability report router", () => {
sah: expect.any(Number),
chargeabilityPct: expect.any(Number),
gapPct: expect.any(Number),
derivation: expect.objectContaining({
baseAvailableHours: expect.any(Number),
effectiveAvailableHours: expect.any(Number),
}),
}),
],
}),