feat(api): explain holiday-aware vacation deductions

This commit is contained in:
2026-03-31 22:42:00 +02:00
parent 8acfbf8c3e
commit cb363ca5b3
7 changed files with 857 additions and 99 deletions
@@ -617,8 +617,119 @@ describe("entitlement.getBalanceDetail", () => {
pending: 0.5,
remaining: 28.5,
sickDays: 1,
deductionSummary: {
formula: "remaining = entitlement - taken - pending",
approvedVacationCount: 0,
pendingVacationCount: 0,
approvedRequestedDays: 0,
pendingRequestedDays: 0,
approvedDeductedDays: 0,
pendingDeductedDays: 0,
excludedHolidayDates: [],
holidayBasisVariants: [],
sources: {
hasCalendarHolidays: false,
hasLegacyPublicHolidayEntries: false,
},
},
vacations: [],
});
});
it("includes holiday-adjusted vacation breakdown for explainability", async () => {
const entitlement = sampleEntitlement({ carryoverDays: 1, usedDays: 1, pendingDays: 0 });
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue({ id: "singleton", vacationDefaultDays: 28 }),
},
resource: {
findUnique: vi.fn().mockImplementation(async ({ select }: { select?: Record<string, unknown> } = {}) => ({
...(select?.userId ? { userId: "user_1" } : {}),
...(select?.displayName ? { displayName: "Alice Example" } : {}),
...(select?.eid ? { eid: "EMP-001" } : {}),
})),
},
vacationEntitlement: {
findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }),
update: vi.fn().mockResolvedValue(entitlement),
},
vacation: {
findMany: vi.fn().mockImplementation(async ({ where }: { where?: { type?: string | { in?: string[] } } } = {}) => {
if (where?.type === "SICK") {
return [];
}
if (typeof where?.type === "object" && Array.isArray(where.type.in)) {
return [
{
type: "ANNUAL",
startDate: new Date("2026-05-01T00:00:00.000Z"),
endDate: new Date("2026-05-02T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
deductedDays: 1,
holidayCountryCode: "DE",
holidayCountryName: "Germany",
holidayFederalState: "BY",
holidayMetroCityName: null,
holidayCalendarDates: ["2026-05-01"],
holidayLegacyPublicHolidayDates: [],
},
];
}
return [];
}),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getBalanceDetail({ resourceId: "res_1", year: 2026 });
expect(result.deductionSummary).toEqual({
formula: "remaining = entitlement - taken - pending",
approvedVacationCount: 1,
pendingVacationCount: 0,
approvedRequestedDays: 2,
pendingRequestedDays: 0,
approvedDeductedDays: 1,
pendingDeductedDays: 0,
excludedHolidayDates: ["2026-05-01"],
holidayBasisVariants: ["Germany / BY"],
sources: {
hasCalendarHolidays: true,
hasLegacyPublicHolidayEntries: false,
},
});
expect(result.vacations).toEqual([
{
type: "ANNUAL",
status: "APPROVED",
startDate: "2026-05-01",
endDate: "2026-05-02",
isHalfDay: false,
requestedDays: 2,
deductedDays: 1,
holidayCountryCode: "DE",
holidayCountryName: "Germany",
holidayFederalState: "BY",
holidayMetroCityName: null,
holidayCalendarDates: ["2026-05-01"],
holidayLegacyPublicHolidayDates: [],
holidayDetails: [
{ date: "2026-05-01", source: "CALENDAR" },
],
holidayContext: {
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: null,
sources: {
hasCalendarHolidays: true,
hasLegacyPublicHolidayEntries: false,
},
},
},
]);
});
});
// ─── get ─────────────────────────────────────────────────────────────────────
@@ -813,8 +924,24 @@ describe("entitlement.bulkSet", () => {
describe("entitlement.getYearSummary", () => {
it("returns summary for all active resources (manager role)", async () => {
const resources = [
{ id: "res_1", displayName: "Alice", eid: "alice", chapter: "VFX" },
{ id: "res_2", displayName: "Bob", eid: "bob", chapter: "Animation" },
{
id: "res_1",
displayName: "Alice",
eid: "alice",
chapter: "VFX",
federalState: "BY",
country: { code: "DE", name: "Germany" },
metroCity: { name: "Muenchen" },
},
{
id: "res_2",
displayName: "Bob",
eid: "bob",
chapter: "Animation",
federalState: "HH",
country: { code: "DE", name: "Germany" },
metroCity: { name: "Hamburg" },
},
];
const entitlement = sampleEntitlement({ usedDays: 5, pendingDays: 2 });
const db = {
@@ -845,6 +972,12 @@ describe("entitlement.getYearSummary", () => {
expect(result[0]).toHaveProperty("resourceId");
expect(result[0]).toHaveProperty("displayName");
expect(result[0]).toHaveProperty("remainingDays");
expect(result[0]).toMatchObject({
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Muenchen",
});
});
it("filters by chapter when provided", async () => {
@@ -881,8 +1014,24 @@ describe("entitlement.getYearSummary", () => {
describe("entitlement.getYearSummaryDetail", () => {
it("returns assistant-friendly year summary detail from the canonical summary workflow", async () => {
const resources = [
{ id: "res_1", displayName: "Alice Example", eid: "EMP-001", chapter: "Delivery" },
{ id: "res_2", displayName: "Bob Example", eid: "EMP-002", chapter: "CGI" },
{
id: "res_1",
displayName: "Alice Example",
eid: "EMP-001",
chapter: "Delivery",
federalState: "BY",
country: { code: "DE", name: "Germany" },
metroCity: { name: "Muenchen" },
},
{
id: "res_2",
displayName: "Bob Example",
eid: "EMP-002",
chapter: "CGI",
federalState: "HH",
country: { code: "DE", name: "Germany" },
metroCity: { name: "Hamburg" },
},
];
const db = {
systemSettings: {
@@ -931,6 +1080,10 @@ describe("entitlement.getYearSummaryDetail", () => {
resource: "Alice Example",
eid: "EMP-001",
chapter: "Delivery",
countryCode: "DE",
countryName: "Germany",
federalState: "BY",
metroCityName: "Muenchen",
year: 2026,
entitled: 28,
carryover: 0,