fix(api): honor vacation deduction snapshots

This commit is contained in:
2026-03-31 21:00:11 +02:00
parent a490d68a3b
commit e8c0d3c3eb
2 changed files with 300 additions and 45 deletions
@@ -197,6 +197,18 @@ describe("entitlement.getBalance", () => {
it("counts sick days separately", async () => {
const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0 });
const vacationFindMany = vi.fn().mockImplementation(async ({ where }: { where?: { type?: unknown } } = {}) => {
if (where?.type === "SICK") {
return [
{
startDate: new Date("2026-03-10"),
endDate: new Date("2026-03-12"),
isHalfDay: false,
},
];
}
return [];
});
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
@@ -206,20 +218,7 @@ describe("entitlement.getBalance", () => {
update: vi.fn().mockResolvedValue(entitlement),
},
vacation: {
findMany: vi
.fn()
// Public holiday vacations for holiday context
.mockResolvedValueOnce([])
// First call: balance-type vacations (for syncEntitlement)
.mockResolvedValueOnce([])
// Second call: sick days
.mockResolvedValueOnce([
{
startDate: new Date("2026-03-10"),
endDate: new Date("2026-03-12"),
isHalfDay: false,
},
]),
findMany: vacationFindMany,
},
};
@@ -329,25 +328,26 @@ describe("entitlement.getBalance", () => {
}),
},
vacation: {
findMany: vi
.fn()
// 2025 holiday context
.mockResolvedValueOnce([])
// 2025 balance vacations
.mockResolvedValueOnce([
{
startDate: new Date("2025-06-10T00:00:00.000Z"),
endDate: new Date("2025-06-17T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
},
])
// 2026 holiday context
.mockResolvedValueOnce([])
// 2026 balance vacations
.mockResolvedValueOnce([])
// 2026 sick days
.mockResolvedValueOnce([]),
findMany: vi.fn().mockImplementation(async ({ where }: {
where?: { type?: unknown; startDate?: { lte?: Date }; endDate?: { gte?: Date } };
} = {}) => {
if (where?.type === "SICK" || where?.type === "PUBLIC_HOLIDAY") {
return [];
}
if (where?.startDate?.lte?.toISOString().startsWith("2025-12-31")) {
return [
{
startDate: new Date("2025-06-10T00:00:00.000Z"),
endDate: new Date("2025-06-17T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
},
];
}
return [];
}),
},
};
@@ -366,6 +366,211 @@ describe("entitlement.getBalance", () => {
}),
);
});
it("deducts only the non-holiday days from mixed vacation ranges", async () => {
const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 });
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
userId: "user_1",
countryId: "country_de",
metroCityId: "city_muc",
federalState: "BY",
country: { code: "DE" },
metroCity: { name: "Muenchen" },
}),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([
{
id: "cal_muc",
name: "Muenchen lokal",
scopeType: "CITY",
priority: 10,
createdAt: new Date("2026-01-01T00:00:00.000Z"),
entries: [
{
date: new Date("2020-11-15T00:00:00.000Z"),
name: "Lokaler Stadtfeiertag",
isRecurringAnnual: true,
},
],
},
]),
},
vacationEntitlement: {
findUnique: mockEntitlementFindUniqueByYear({ 2026: entitlement }),
update: vi.fn().mockImplementation(async ({ data }) => ({
...entitlement,
...data,
})),
},
vacation: {
findMany: vi.fn().mockImplementation(async ({ where }: { where?: { type?: unknown } } = {}) => {
if (where?.type === "PUBLIC_HOLIDAY" || where?.type === "SICK") {
return [];
}
return [
{
startDate: new Date("2026-11-14T00:00:00.000Z"),
endDate: new Date("2026-11-16T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
},
];
}),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getBalance({ resourceId: "res_1", year: 2026 });
expect(result.usedDays).toBe(2);
expect(result.remainingDays).toBe(28);
});
it("prefers persisted holiday snapshots over the current holiday context", async () => {
const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 });
const resourceFindUnique = vi.fn().mockImplementation(async ({ select }: { select?: Record<string, unknown> } = {}) => ({
...(select?.userId ? { userId: "user_1" } : {}),
...(select?.federalState ? { federalState: "BY" } : {}),
...(select?.country ? { country: { code: "DE" } } : {}),
...(select?.metroCity ? { metroCity: { name: "Augsburg" } } : {}),
}));
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
resource: {
findUnique: resourceFindUnique,
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([
{
id: "cal_augsburg",
name: "Augsburg lokal",
scopeType: "CITY",
priority: 10,
createdAt: new Date("2026-01-01T00:00:00.000Z"),
entries: [
{
date: new Date("2020-08-08T00:00:00.000Z"),
name: "Hohes Friedensfest",
isRecurringAnnual: true,
},
],
},
]),
},
vacationEntitlement: {
findUnique: mockEntitlementFindUniqueByYear({ 2028: entitlement }),
update: vi.fn().mockImplementation(async ({ data }) => ({
...entitlement,
...data,
})),
},
vacation: {
findMany: vi
.fn()
.mockResolvedValueOnce([
{
startDate: new Date("2028-08-08T00:00:00.000Z"),
endDate: new Date("2028-08-08T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
deductedDays: 1,
holidayCountryCode: "DE",
holidayFederalState: "BY",
holidayMetroCityName: "Augsburg",
holidayCalendarDates: [],
holidayLegacyPublicHolidayDates: [],
},
])
.mockResolvedValueOnce([]),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getBalance({ resourceId: "res_1", year: 2028 });
expect(result.usedDays).toBe(1);
expect(result.remainingDays).toBe(29);
expect(db.holidayCalendar.findMany).not.toHaveBeenCalled();
});
it("falls back to the current holiday context for legacy vacations without snapshots", async () => {
const entitlement = sampleEntitlement({ usedDays: 0, pendingDays: 0, entitledDays: 30, carryoverDays: 0 });
const db = {
systemSettings: {
findUnique: vi.fn().mockResolvedValue(null),
},
resource: {
findUnique: vi.fn().mockResolvedValue({
userId: "user_1",
countryId: "country_de",
metroCityId: "city_aug",
federalState: "BY",
country: { code: "DE" },
metroCity: { name: "Augsburg" },
}),
},
holidayCalendar: {
findMany: vi.fn().mockResolvedValue([
{
id: "cal_augsburg",
name: "Augsburg lokal",
scopeType: "CITY",
priority: 10,
createdAt: new Date("2026-01-01T00:00:00.000Z"),
entries: [
{
date: new Date("2020-08-08T00:00:00.000Z"),
name: "Hohes Friedensfest",
isRecurringAnnual: true,
},
],
},
]),
},
vacationEntitlement: {
findUnique: mockEntitlementFindUniqueByYear({ 2028: entitlement }),
update: vi.fn().mockImplementation(async ({ data }) => ({
...entitlement,
...data,
})),
},
vacation: {
findMany: vi
.fn()
.mockResolvedValueOnce([
{
startDate: new Date("2028-08-08T00:00:00.000Z"),
endDate: new Date("2028-08-08T00:00:00.000Z"),
status: "APPROVED",
isHalfDay: false,
deductedDays: null,
holidayCountryCode: null,
holidayFederalState: null,
holidayMetroCityName: null,
holidayCalendarDates: null,
holidayLegacyPublicHolidayDates: null,
},
])
.mockResolvedValueOnce([])
.mockResolvedValueOnce([]),
},
};
const caller = createProtectedCaller(db);
const result = await caller.getBalance({ resourceId: "res_1", year: 2028 });
expect(result.usedDays).toBe(0);
expect(result.remainingDays).toBe(30);
expect(db.holidayCalendar.findMany).toHaveBeenCalled();
});
});
describe("entitlement.getBalanceDetail", () => {