Files
app/src/__tests__/attendance-workfund.test.ts
BOHA a679f6e8ac fix(attendance): KPI cards + Bilance now use the print-recap formulas
Verification found three divergences from the binding print recap:

- getWorkfund (Bilance page): overtime included nemoc (print Přesčas is
  worked + dovolená only) and missing ignored neplacené volno (print
  Chybějící hodiny counts every approved absence). Both now match; the
  response carries covered (manko base) + covered_prescas (Přesčas base)
  and the page's year aggregate sums them separately.
- AttendanceAdmin fond bar: used = worked + dovolená + worked_holiday +
  svatek — after v2.4.45 the last two fields hold the SAME value, so
  holiday work counted twice (regression). Coverage is now worked + all
  approved absences, and the ± badges show the exact print values
  (ut.prescas / ut.manko) instead of a derived delta; bar color follows
  Přesčas/Chybějící hodiny.
- Fond itself was already aligned everywhere (non-holiday weekdays × 8,
  prorated for the running month).

+1 route-parity test (real DB): worked 80 + dov 24 + nem 16 + nepl 8 vs
fond 176 → covered 128, covered_prescas 104, missing 48, overtime 0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 00:47:26 +02:00

125 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import prisma from "../config/database";
import { getWorkfund } from "../services/attendance.service";
/**
* Print-recap parity for the Bilance page's monthly fund overview
* (getWorkfund), per the binding formulas (computeMzdaSummary):
* overtime (Přesčas) = max(0, worked + dovolená fond)
* missing (Chybějící hodiny) = max(0, fond (worked + dovolená + nemoc
* + neplacené volno))
* The two coverage bases differ deliberately: only dovolená earns toward
* overtime, every approved absence covers the fond.
*/
const USERNAME = "wf_parity_test_user";
let userId: number;
const june = (day: string) => new Date(`2026-06-${day}T00:00:00Z`);
const dt = (day: string, hm: string) => new Date(`2026-06-${day}T${hm}:00`);
beforeAll(async () => {
await prisma.attendance.deleteMany({
where: { users: { username: USERNAME } },
});
await prisma.users.deleteMany({ where: { username: USERNAME } });
// getAttendanceUsers includes only users whose role carries
// attendance.record — the seeded admin role has every permission.
const adminRole = await prisma.roles.findFirst({ where: { name: "admin" } });
if (!adminRole) throw new Error("Admin role not found — seed the DB first");
const user = await prisma.users.create({
data: {
username: USERNAME,
email: `${USERNAME}@test.local`,
password_hash:
"$2a$12$LJ3m4ys3Lg4oLBFnYP2amuPBzJnJBbGzCl5Y6X9Y8r0q5.s3L6OyO",
first_name: "Workfund",
last_name: "Parity",
is_active: true,
role_id: adminRole.id,
},
});
userId = user.id;
// June 2026 (fond 176 = 22 non-holiday weekdays × 8):
// 10 work days of 8h net (08:0016:30, 30min break) → worked 80
const workDays = ["01", "02", "03", "04", "05", "08", "09", "10", "11", "12"];
for (const d of workDays) {
await prisma.attendance.create({
data: {
user_id: userId,
shift_date: june(d),
arrival_time: dt(d, "08:00"),
departure_time: dt(d, "16:30"),
break_start: dt(d, "12:00"),
break_end: dt(d, "12:30"),
leave_type: "work",
},
});
}
// dovolená 24, nemoc 16, neplacené volno 8
const leaves: Array<[string, string, number]> = [
["15", "vacation", 8],
["16", "vacation", 8],
["17", "vacation", 8],
["18", "sick", 8],
["19", "sick", 8],
["22", "unpaid", 8],
];
for (const [d, type, hours] of leaves) {
await prisma.attendance.create({
data: {
user_id: userId,
shift_date: june(d),
leave_type: type as never,
leave_hours: hours,
},
});
}
});
afterAll(async () => {
await prisma.attendance.deleteMany({ where: { user_id: userId } });
await prisma.users.deleteMany({ where: { id: userId } });
});
describe("getWorkfund — print-recap parity", () => {
it("computes overtime from worked+dovolená and missing from all approved absences", async () => {
const data = await getWorkfund(2026);
// getWorkfund's future-year early return types months as a bare {} —
// narrow to the populated shape (2026 is never a future year here).
const months = data.months as Record<
string,
{
fund: number;
users: Record<
string,
{
worked: number;
covered: number;
covered_prescas: number;
overtime: number;
missing: number;
}
>;
}
>;
const juneData = months["6"];
expect(juneData).toBeTruthy();
expect(juneData.fund).toBe(176);
const us = juneData.users[String(userId)];
expect(us).toBeTruthy();
expect(us.worked).toBe(80);
// covered (manko base): 80 + 24 + 16 + 8
expect(us.covered).toBe(128);
// covered_prescas (Přesčas base): 80 + 24 — sick/unpaid excluded
expect(us.covered_prescas).toBe(104);
// Chybějící hodiny: 176 128
expect(us.missing).toBe(48);
// Přesčas: max(0, 104 176)
expect(us.overtime).toBe(0);
});
});