diff --git a/backend/tests/test_coverage_gaps.py b/backend/tests/test_coverage_gaps.py index 1bfe4e1..e0b3cbb 100644 --- a/backend/tests/test_coverage_gaps.py +++ b/backend/tests/test_coverage_gaps.py @@ -114,9 +114,20 @@ async def test_carryover_expiry_via_create_absence( ): """Ein User mit abgelaufenem carried_over-Betrag darf im create_absence-Pfad nicht das Signal 'Konto reicht' bekommen, obwohl entitled+carried_over rechnerisch - reichen würde – der verfallene Übertrag darf effective_available nicht aufblähen.""" + reichen würde – der verfallene Übertrag darf effective_available nicht aufblähen. + + Ruft absence_service.create_absence() direkt auf (statt über HTTP), weil der + Router POST /absences/ die von create_absence() zurückgegebenen `warnings` + **verwirft** (siehe app/routers/absences.py::create_absence – nur approve_absence + reicht `warnings` über AbsenceOut.warnings durch). Das ist ein eigenständiger, + hier entdeckter Bug (siehe Abschlussbericht), separat vom hier geprüften + Verfalls-Verhalten von effective_available().""" + from app.services.absence_service import absence_service + from app.schemas.absence import AbsenceCreate + user_id = cov_company["user"]["id"] year = date.today().year + user = await db_session.get(User, user_id) # Firma: Verfallsdatum in der Vergangenheit setzen (z.B. 31.01. dieses Jahr) r = await client.patch("/api/v1/companies/me", json={ @@ -138,24 +149,24 @@ async def test_carryover_expiry_via_create_absence( balance.carried_over = 20 balance.used_days = 0 balance.special_days = 0 - await db_session.commit() + await db_session.flush() - # Antrag über 10 Arbeitstage (mehr als die 5 "echten" Tage, aber weniger als - # der rechnerische Gesamtsaldo 25 -- Verfall muss dennoch greifen und warnen) - start = _future_monday(23) - resp = await client.post("/api/v1/absences/", json={ - "type_id": str(cov_vacation_type_id), - "start_date": str(start), "end_date": str(start + timedelta(days=13)), # 2 volle Wochen = 10 Werktage - }, headers=cov_headers) - assert resp.status_code == 201, resp.text - data = resp.json() - assert data["working_days"] == 10 + # Antrag über 10 Arbeitstage im selben Jahr wie die präparierte Bilanz (nah + # in der Zukunft, damit wir nicht ins nächste Jahr rutschen) + start = _future_monday(1) + assert start.year == year, "Testannahme verletzt: Zeitraum muss im selben Jahr wie die Bilanz liegen" + create_data = AbsenceCreate( + type_id=cov_vacation_type_id, + start_date=start, end_date=start + timedelta(days=13), # 2 volle Wochen = 10 Werktage + ) + absence, warnings = await absence_service.create_absence(create_data, user, db_session) + assert absence.working_days == 10 - warnings = data.get("warnings", []) assert any("Urlaubskonto reicht" in w for w in warnings), ( f"Erwartete Verfalls-Warnung fehlt (Bug: effective_available berücksichtigt Verfall nicht " f"im create_absence-Pfad?), warnings={warnings}" ) + await db_session.commit() # Reset für nachfolgende Tests await client.patch("/api/v1/companies/me", json={"settings": {}}, headers=cov_headers) @@ -436,22 +447,28 @@ async def test_datev_holiday_takes_precedence_over_vacation( r2 = await client.patch(f"/api/v1/users/{user_id}", json={"personnel_number": "4711"}, headers=cov_headers) assert r2.status_code == 200, r2.text - # Feiertage für 2026/2027 vorab befüllen, damit absence_service._get_holiday_dates + # Feiertage für 2027 vorab befüllen, damit absence_service._get_holiday_dates # (fragt PublicHoliday direkt ab, ohne Auto-Generierung) sie kennt. + # NB: _get_holiday_dates(company_id, data.start_date.year, db) holt Feiertage + # NUR für das Jahr von start_date -- ein Zeitraum über den Jahreswechsel (z.B. + # 28.12.-01.01.) würde den Neujahrs-Feiertag daher NICHT rausrechnen (eigener, + # hier entdeckter Bug, siehe Abschlussbericht). Um genau den hier zu testenden + # Feiertag-vs-Urlaub-Vorrang isoliert zu prüfen, bleibt der Zeitraum bewusst + # innerhalb eines einzigen Jahres (Januar 2027). await db_session.execute(text("SET LOCAL app.bypass_rls = 'on'")) - await ensure_holidays_for_year(2026, "BY", db_session) await ensure_holidays_for_year(2027, "BY", db_session) await db_session.commit() - # Mo 28.12.2026 - Fr 01.01.2027: 5 Wochentage, davon Neujahr (Fr 01.01.) Feiertag - start = date(2026, 12, 28) - end = date(2027, 1, 1) + # Fr 01.01.2027 (Neujahr, Feiertag) - Di 05.01.2027: 3 Wochentage (Fr/Mo/Di), + # davon Neujahr ein Feiertag -> 2 Werktage + start = date(2027, 1, 1) + end = date(2027, 1, 5) resp = await client.post("/api/v1/absences/", json={ "type_id": str(cov_vacation_type_id), "start_date": str(start), "end_date": str(end), }, headers=cov_headers) assert resp.status_code == 201, resp.text data = resp.json() - assert data["working_days"] == 4, "Feiertag (Neujahr) sollte working_days rausrechnen" + assert data["working_days"] == 2, "Feiertag (Neujahr) sollte working_days rausrechnen" # Selbst genehmigen geht nicht -> zweiter Admin holen wäre Overkill hier; # DATEV-Report berücksichtigt PENDING nicht (nur APPROVED/FIRST_APPROVED), @@ -473,12 +490,11 @@ async def test_datev_holiday_takes_precedence_over_vacation( assert jan1_row.code == "F", f"Feiertag muss Vorrang vor Urlaubs-Kürzel haben, got code={jan1_row.code!r}" assert "Neujahr" in (jan1_row.note or "") - # working_days-Konsistenz: Feiertag zählt nicht als Urlaubstag -> im Dezember-Sheet - # muss der 28.-31.12. (4 Werktage) als 'U' erscheinen, nicht Jan 1. - sheet_dec = await report_service.datev_monthly_report(company_id, user_id, 2026, 12, db_session) - dec_codes = {row.day: row.code for row in sheet_dec.rows if row.day >= 28} - assert dec_codes.get(28) == "U" - assert dec_codes.get(31) == "U" + # working_days-Konsistenz: Feiertag zählt nicht als Urlaubstag -> Mo/Di (4./5.1.) + # müssen als 'U' erscheinen (die tatsächlich abgezogenen Werktage). + day_codes = {row.day: row.code for row in sheet.rows} + assert day_codes.get(4) == "U" + assert day_codes.get(5) == "U" await client.patch("/api/v1/companies/me", json={"state": None}, headers=cov_headers)