feat(api): explain holiday-aware vacation deductions
This commit is contained in:
@@ -110,6 +110,13 @@ const sampleVacation = {
|
||||
note: "Summer vacation",
|
||||
isHalfDay: false,
|
||||
halfDayPart: null,
|
||||
deductedDays: null,
|
||||
holidayCountryCode: null,
|
||||
holidayCountryName: null,
|
||||
holidayFederalState: null,
|
||||
holidayMetroCityName: null,
|
||||
holidayCalendarDates: null,
|
||||
holidayLegacyPublicHolidayDates: null,
|
||||
requestedById: "user_1",
|
||||
approvedById: null,
|
||||
approvedAt: null,
|
||||
@@ -636,6 +643,70 @@ describe("vacation router", () => {
|
||||
|
||||
expect(db.vacation.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps mixed vacation ranges chargeable when only some days are holidays", async () => {
|
||||
const createdVacation = {
|
||||
...sampleVacation,
|
||||
startDate: new Date("2026-11-14T00:00:00.000Z"),
|
||||
endDate: new Date("2026-11-16T00:00:00.000Z"),
|
||||
};
|
||||
const db = createVacationDb({
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
userId: "user_1",
|
||||
countryId: "country_de",
|
||||
metroCityId: "city_muc",
|
||||
federalState: "BY",
|
||||
country: { code: "DE", name: "Germany" },
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
vacation: {
|
||||
findFirst: vi.fn().mockResolvedValue(null),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
create: vi.fn().mockResolvedValue(createdVacation),
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createProtectedCaller(db);
|
||||
const result = await caller.create({
|
||||
resourceId: "res_1",
|
||||
type: VacationType.ANNUAL,
|
||||
startDate: new Date("2026-11-14T00:00:00.000Z"),
|
||||
endDate: new Date("2026-11-16T00:00:00.000Z"),
|
||||
});
|
||||
|
||||
expect(result.effectiveDays).toBe(2);
|
||||
expect(db.vacation.create).toHaveBeenCalledWith(expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
deductedDays: 2,
|
||||
holidayCountryCode: "DE",
|
||||
holidayCountryName: "Germany",
|
||||
holidayFederalState: "BY",
|
||||
holidayMetroCityName: "Muenchen",
|
||||
holidayCalendarDates: ["2026-11-15"],
|
||||
holidayLegacyPublicHolidayDates: [],
|
||||
}),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("previewRequest", () => {
|
||||
@@ -833,6 +904,9 @@ describe("vacation router", () => {
|
||||
data: expect.objectContaining({
|
||||
status: VacationStatus.APPROVED,
|
||||
rejectionReason: null,
|
||||
deductedDays: 5,
|
||||
holidayCalendarDates: [],
|
||||
holidayLegacyPublicHolidayDates: [],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -865,6 +939,53 @@ describe("vacation router", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects approval when the current holiday context reduces the request to zero days", async () => {
|
||||
const db = createVacationDb({
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
countryId: "country_de",
|
||||
metroCityId: "city_muc",
|
||||
federalState: "BY",
|
||||
country: { code: "DE", name: "Germany" },
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]),
|
||||
},
|
||||
vacation: {
|
||||
findUnique: vi.fn().mockResolvedValue({
|
||||
...sampleVacation,
|
||||
startDate: new Date("2026-11-15T00:00:00.000Z"),
|
||||
endDate: new Date("2026-11-15T00:00:00.000Z"),
|
||||
}),
|
||||
update: vi.fn(),
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
});
|
||||
|
||||
const caller = createManagerCaller(db);
|
||||
await expect(caller.approve({ id: "vac_1" })).rejects.toThrow(
|
||||
"Vacation no longer deducts any vacation days for the current holiday calendar and cannot be approved",
|
||||
);
|
||||
expect(db.vacation.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("forbids regular users from approving", async () => {
|
||||
const db = {};
|
||||
const caller = createProtectedCaller(db);
|
||||
@@ -974,11 +1095,33 @@ describe("vacation router", () => {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }),
|
||||
},
|
||||
vacation: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{ id: "vac_1", resourceId: "res_1" },
|
||||
{ id: "vac_2", resourceId: "res_2" },
|
||||
]),
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 2 }),
|
||||
findMany: vi.fn().mockImplementation(async ({ where }: { where?: { status?: string; type?: string } } = {}) => {
|
||||
if (where?.type === VacationType.PUBLIC_HOLIDAY) {
|
||||
return [];
|
||||
}
|
||||
if (where?.status === VacationStatus.PENDING) {
|
||||
return [
|
||||
{
|
||||
id: "vac_1",
|
||||
resourceId: "res_1",
|
||||
type: VacationType.ANNUAL,
|
||||
startDate: new Date("2026-06-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-06-05T00:00:00.000Z"),
|
||||
isHalfDay: false,
|
||||
},
|
||||
{
|
||||
id: "vac_2",
|
||||
resourceId: "res_2",
|
||||
type: VacationType.OTHER,
|
||||
startDate: new Date("2026-06-08T00:00:00.000Z"),
|
||||
endDate: new Date("2026-06-08T00:00:00.000Z"),
|
||||
isHalfDay: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
update: vi.fn().mockResolvedValue(sampleVacation),
|
||||
},
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
@@ -989,11 +1132,12 @@ describe("vacation router", () => {
|
||||
const result = await caller.batchApprove({ ids: ["vac_1", "vac_2"] });
|
||||
|
||||
expect(result.approved).toBe(2);
|
||||
expect(db.vacation.updateMany).toHaveBeenCalledWith(
|
||||
expect(db.vacation.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: { in: ["vac_1", "vac_2"] } },
|
||||
where: { id: "vac_1" },
|
||||
data: expect.objectContaining({
|
||||
status: VacationStatus.APPROVED,
|
||||
deductedDays: 5,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -1005,10 +1149,25 @@ describe("vacation router", () => {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "mgr_1" }),
|
||||
},
|
||||
vacation: {
|
||||
findMany: vi.fn().mockResolvedValue([
|
||||
{ id: "vac_1", resourceId: "res_1" },
|
||||
]),
|
||||
updateMany: vi.fn().mockResolvedValue({ count: 1 }),
|
||||
findMany: vi.fn().mockImplementation(async ({ where }: { where?: { status?: string; type?: string } } = {}) => {
|
||||
if (where?.type === VacationType.PUBLIC_HOLIDAY) {
|
||||
return [];
|
||||
}
|
||||
if (where?.status === VacationStatus.PENDING) {
|
||||
return [
|
||||
{
|
||||
id: "vac_1",
|
||||
resourceId: "res_1",
|
||||
type: VacationType.ANNUAL,
|
||||
startDate: new Date("2026-06-01T00:00:00.000Z"),
|
||||
endDate: new Date("2026-06-01T00:00:00.000Z"),
|
||||
isHalfDay: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}),
|
||||
update: vi.fn().mockResolvedValue(sampleVacation),
|
||||
},
|
||||
resource: {
|
||||
findUnique: vi.fn().mockResolvedValue(null),
|
||||
|
||||
Reference in New Issue
Block a user