feat(api): explain chargeability derivation inputs
This commit is contained in:
@@ -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),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -168,12 +168,62 @@ export const chargeabilityReportDetailInputSchema = chargeabilityReportInputSche
|
||||
|
||||
type ChargeabilityReportInput = z.infer<typeof chargeabilityReportInputSchema>;
|
||||
type ChargeabilityReportDetailInput = z.infer<typeof chargeabilityReportDetailInputSchema>;
|
||||
type ChargeabilityExplainabilityInput = ChargeabilityReportInput & {
|
||||
resourceQuery?: string | undefined;
|
||||
resourceLimit?: number | undefined;
|
||||
};
|
||||
|
||||
type ChargeabilityReportDbClient = Pick<
|
||||
PrismaClient,
|
||||
"assignment" | "resource" | "project" | "vacation" | "holidayCalendar" | "systemSettings"
|
||||
>;
|
||||
|
||||
const CHARGEABILITY_LOCATION_FIELDS = [
|
||||
"country",
|
||||
"federalState",
|
||||
"city",
|
||||
"orgUnit",
|
||||
"managementLevelGroup",
|
||||
"managementLevel",
|
||||
] as const;
|
||||
|
||||
const CHARGEABILITY_DERIVATION_FIELDS = [
|
||||
"baseAvailableHours",
|
||||
"publicHolidayCount",
|
||||
"publicHolidayWorkdayCount",
|
||||
"publicHolidayHoursDeduction",
|
||||
"absenceDayEquivalent",
|
||||
"absenceHoursDeduction",
|
||||
"effectiveAvailableHours",
|
||||
] as const;
|
||||
|
||||
function buildChargeabilityExplainability(input: ChargeabilityExplainabilityInput) {
|
||||
const activeFilters = [
|
||||
...(input.orgUnitId ? ["orgUnitId"] : []),
|
||||
...(input.managementLevelGroupId ? ["managementLevelGroupId"] : []),
|
||||
...(input.countryId ? ["countryId"] : []),
|
||||
...(input.resourceQuery ? ["resourceQuery"] : []),
|
||||
...(input.includeProposed ? ["includeProposed"] : []),
|
||||
];
|
||||
|
||||
return {
|
||||
locationFields: [...CHARGEABILITY_LOCATION_FIELDS],
|
||||
monthDerivationFields: [...CHARGEABILITY_DERIVATION_FIELDS],
|
||||
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.",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function queryChargeabilityReport(
|
||||
db: ChargeabilityReportDbClient,
|
||||
input: ChargeabilityReportInput,
|
||||
@@ -227,6 +277,7 @@ async function queryChargeabilityReport(
|
||||
chargeabilityRatio: 0,
|
||||
targetRatio: 0,
|
||||
})),
|
||||
explainability: buildChargeabilityExplainability(input),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -388,6 +439,7 @@ async function queryChargeabilityReport(
|
||||
monthKeys,
|
||||
resources: anonymizeResources(resourceRows, directory),
|
||||
groupTotals,
|
||||
explainability: buildChargeabilityExplainability(input),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -462,6 +514,7 @@ export function buildChargeabilityReportDetail(
|
||||
resourceCount: matchingResources.length,
|
||||
returnedResourceCount: resources.length,
|
||||
truncated: resources.length < matchingResources.length,
|
||||
explainability: buildChargeabilityExplainability(input),
|
||||
resources,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user