"""Tests für agent-03-abwesenheit""" import pytest import pytest_asyncio from datetime import date, timedelta from httpx import AsyncClient # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest_asyncio.fixture(scope="session", loop_scope="session") async def abs_company(client: AsyncClient): """Eigene Company für Absence-Tests.""" resp = await client.post("/api/v1/auth/register", json={ "company_name": "Absence AG", "first_name": "Admin", "last_name": "Test", "email": "admin@absenceag.de", "password": "Secret123", }) assert resp.status_code == 201 tokens = resp.json() me = await client.get( "/api/v1/auth/me", headers={"Authorization": f"Bearer {tokens['access_token']}"}, ) return {"tokens": tokens, "user": me.json()} @pytest_asyncio.fixture(scope="session", loop_scope="session") async def abs_headers(abs_company): return {"Authorization": f"Bearer {abs_company['tokens']['access_token']}"} @pytest_asyncio.fixture(scope="session", loop_scope="session") async def abs_approver_headers(client: AsyncClient, abs_headers): """Zweiter Admin in Absence AG – kann Abwesenheiten anderer genehmigen.""" resp = await client.post("/api/v1/users/invite", json={ "first_name": "Approver", "last_name": "Admin", "email": "approver@absenceag.de", "role": "COMPANY_ADMIN", "initial_password": "Secret123", }, headers=abs_headers) assert resp.status_code == 201, resp.text login = await client.post("/api/v1/auth/login", json={ "email": "approver@absenceag.de", "password": "Secret123", }) assert login.status_code == 200, login.text return {"Authorization": f"Bearer {login.json()['access_token']}"} @pytest_asyncio.fixture(scope="session", loop_scope="session") async def vacation_type_id(client: AsyncClient, abs_headers): """Urlaubs-Typ der Company holen.""" resp = await client.get("/api/v1/absence-types/", headers=abs_headers) assert resp.status_code == 200 types = resp.json() assert len(types) > 0, "Default-Abwesenheitstypen fehlen" vacation = next((t for t in types if t["name"] == "Urlaub"), types[0]) return vacation["id"] @pytest_asyncio.fixture(scope="session", loop_scope="session") async def sick_type_id(client: AsyncClient, abs_headers): resp = await client.get("/api/v1/absence-types/", headers=abs_headers) types = resp.json() sick = next((t for t in types if t["name"] == "Krankheit"), types[0]) return sick["id"] # ── Default Types ────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_default_absence_types_created_on_register(client: AsyncClient, abs_headers): """Nach der Registrierung müssen Default-Typen vorhanden sein.""" resp = await client.get("/api/v1/absence-types/", headers=abs_headers) assert resp.status_code == 200 types = resp.json() names = {t["name"] for t in types} assert "Urlaub" in names assert "Krankheit" in names assert "Homeoffice" in names assert "Dienstreise" in names assert "Sonderurlaub" in names # ── Absence Types CRUD ───────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_absence_type(client: AsyncClient, abs_headers): resp = await client.post( "/api/v1/absence-types/", json={ "name": "Elternzeit", "color": "#F59E0B", "requires_approval": True, "deducts_vacation": False, "is_paid": False, }, headers=abs_headers, ) assert resp.status_code == 201 assert resp.json()["name"] == "Elternzeit" @pytest.mark.asyncio async def test_update_absence_type(client: AsyncClient, abs_headers, vacation_type_id): resp = await client.patch( f"/api/v1/absence-types/{vacation_type_id}", json={"max_days_per_year": 30}, headers=abs_headers, ) assert resp.status_code == 200 assert resp.json()["max_days_per_year"] == 30 # ── Absences ─────────────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_absence_vacation(client: AsyncClient, abs_headers, vacation_type_id): next_monday = date.today() + timedelta(days=(7 - date.today().weekday())) resp = await client.post( "/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(next_monday), "end_date": str(next_monday + timedelta(days=4)), }, headers=abs_headers, ) assert resp.status_code == 201 data = resp.json() assert data["status"] == "pending" assert data["working_days"] > 0 @pytest.mark.asyncio async def test_create_absence_sick_auto_approved(client: AsyncClient, abs_headers, sick_type_id): """Krankheit hat requires_approval=False → sofort approved.""" resp = await client.post( "/api/v1/absences/", json={ "type_id": str(sick_type_id), "start_date": str(date.today()), "end_date": str(date.today()), }, headers=abs_headers, ) assert resp.status_code == 201 # Krankheit erfordert keine Genehmigung → approved data = resp.json() assert data["status"] == "approved" @pytest.mark.asyncio async def test_list_absences(client: AsyncClient, abs_headers): resp = await client.get("/api/v1/absences/", headers=abs_headers) assert resp.status_code == 200 data = resp.json() assert "total" in data assert "items" in data @pytest.mark.asyncio async def test_get_absence_by_id(client: AsyncClient, abs_headers, vacation_type_id): next_monday = date.today() + timedelta(days=(7 - date.today().weekday()) + 7) create_resp = await client.post( "/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(next_monday), "end_date": str(next_monday + timedelta(days=2)), }, headers=abs_headers, ) absence_id = create_resp.json()["id"] resp = await client.get(f"/api/v1/absences/{absence_id}", headers=abs_headers) assert resp.status_code == 200 assert resp.json()["id"] == absence_id @pytest.mark.asyncio async def test_approve_absence(client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id): next_monday = date.today() + timedelta(days=(7 - date.today().weekday()) + 14) create_resp = await client.post( "/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(next_monday), "end_date": str(next_monday + timedelta(days=4)), }, headers=abs_headers, ) absence_id = create_resp.json()["id"] resp = await client.post(f"/api/v1/absences/{absence_id}/approve", headers=abs_approver_headers) assert resp.status_code == 200 assert resp.json()["status"] == "approved" @pytest.mark.asyncio async def test_reject_absence(client: AsyncClient, abs_headers, vacation_type_id): next_monday = date.today() + timedelta(days=(7 - date.today().weekday()) + 21) create_resp = await client.post( "/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(next_monday), "end_date": str(next_monday + timedelta(days=4)), }, headers=abs_headers, ) absence_id = create_resp.json()["id"] resp = await client.post( f"/api/v1/absences/{absence_id}/reject", json={"rejection_reason": "Urlaubssperre in diesem Zeitraum"}, headers=abs_headers, ) assert resp.status_code == 200 assert resp.json()["status"] == "rejected" assert resp.json()["rejection_reason"] == "Urlaubssperre in diesem Zeitraum" @pytest.mark.asyncio async def test_cancel_absence(client: AsyncClient, abs_headers, vacation_type_id): """Eigenen ausstehenden Antrag stornieren.""" next_monday = date.today() + timedelta(days=(7 - date.today().weekday()) + 28) create_resp = await client.post( "/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(next_monday), "end_date": str(next_monday + timedelta(days=4)), }, headers=abs_headers, ) absence_id = create_resp.json()["id"] resp = await client.delete(f"/api/v1/absences/{absence_id}", headers=abs_headers) assert resp.status_code == 200 assert resp.json()["status"] == "cancelled" @pytest.mark.asyncio async def test_calendar(client: AsyncClient, abs_headers): resp = await client.get( "/api/v1/absences/calendar", params={"year": 2026, "month": 4}, headers=abs_headers, ) assert resp.status_code == 200 assert isinstance(resp.json(), list) # ── Vacation Balance ─────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_own_balance(client: AsyncClient, abs_headers): resp = await client.get("/api/v1/absences/balance", params={"year": 2026}, headers=abs_headers) assert resp.status_code == 200 data = resp.json() assert "entitled_days" in data assert "remaining_days" in data assert data["entitled_days"] == 30 @pytest.mark.asyncio async def test_balance_deducted_after_approve(client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id, abs_company): """Urlaubskonto wird nach Genehmigung abgezogen.""" user_id = abs_company["user"]["id"] # Initiales Konto before = await client.get("/api/v1/absences/balance", params={"year": 2026}, headers=abs_headers) used_before = before.json()["used_days"] # Urlaub beantragen (Mo-Fr = 5 Tage) next_monday = date.today() + timedelta(days=(7 - date.today().weekday()) + 35) create_resp = await client.post( "/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(next_monday), "end_date": str(next_monday + timedelta(days=4)), }, headers=abs_headers, ) absence_id = create_resp.json()["id"] working_days = create_resp.json()["working_days"] # Genehmigen (zweiter Admin, nicht self-approval) approve_resp = await client.post(f"/api/v1/absences/{absence_id}/approve", headers=abs_approver_headers) assert approve_resp.status_code == 200, approve_resp.text # Konto prüfen after = await client.get("/api/v1/absences/balance", params={"year": 2026}, headers=abs_headers) assert after.json()["used_days"] == used_before + working_days # ── Public Holidays ──────────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_create_and_list_public_holiday(client: AsyncClient, abs_headers): resp = await client.post( "/api/v1/public-holidays/", json={ "country": "DE", "state": None, "date": "2026-01-01", "name": "Neujahr", }, headers=abs_headers, ) assert resp.status_code == 201 assert resp.json()["name"] == "Neujahr" list_resp = await client.get( "/api/v1/public-holidays/", params={"year": 2026, "country": "DE"}, headers=abs_headers, ) assert list_resp.status_code == 200 holidays = list_resp.json() assert any(h["name"] == "Neujahr" for h in holidays) # ── Working Days Calculation ─────────────────────────────────────────────────── def test_calc_working_days_full_week(): from app.services.absence_service import AbsenceService svc = AbsenceService() # Mo-Fr days = svc._calc_working_days( date(2026, 3, 30), date(2026, 4, 3), set(), False, False ) assert days == 5.0 def test_calc_working_days_with_holiday(): from app.services.absence_service import AbsenceService svc = AbsenceService() # Mo-Fr mit Feiertag am Mittwoch days = svc._calc_working_days( date(2026, 3, 30), date(2026, 4, 3), {date(2026, 4, 1)}, # Mittwoch = Feiertag False, False ) assert days == 4.0 def test_calc_working_days_half_day(): from app.services.absence_service import AbsenceService svc = AbsenceService() # Mo-Fr, halber Montag days = svc._calc_working_days( date(2026, 3, 30), date(2026, 4, 3), set(), True, False ) assert days == 4.5 def test_calc_working_days_weekend_only(): from app.services.absence_service import AbsenceService svc = AbsenceService() days = svc._calc_working_days( date(2026, 3, 28), date(2026, 3, 29), set(), False, False ) assert days == 0.0 # ── Krankmeldung (agent-05) ─────────────────────────────────────────────────── @pytest.mark.asyncio async def test_quick_sick_creates_approved_absence(client: AsyncClient, abs_headers): """POST /absences/quick-sick → sofort approved, ohne Typ-Auswahl.""" today = date.today() resp = await client.post( "/api/v1/absences/quick-sick", json={"start_date": str(today), "end_date": str(today)}, headers=abs_headers, ) assert resp.status_code == 201 data = resp.json() assert data["status"] == "approved" assert data["certificate_required_by"] is not None @pytest.mark.asyncio async def test_certificate_required_by_uses_type_threshold( client: AsyncClient, abs_headers, sick_type_id ): """certificate_required_by = start_date + AbsenceType.certificate_after_days (default 3).""" start = date(2027, 6, 1) resp = await client.post( "/api/v1/absences/", json={ "type_id": str(sick_type_id), "start_date": str(start), "end_date": str(start), }, headers=abs_headers, ) assert resp.status_code == 201 data = resp.json() expected = start + timedelta(days=3) assert data["certificate_required_by"] == str(expected) @pytest.mark.asyncio async def test_certificate_required_by_respects_type_override( client: AsyncClient, abs_headers, sick_type_id ): """PATCH AbsenceType.certificate_after_days = 7 → neue Absences nutzen 7.""" upd = await client.patch( f"/api/v1/absence-types/{sick_type_id}", json={"certificate_after_days": 7}, headers=abs_headers, ) assert upd.status_code == 200 assert upd.json()["certificate_after_days"] == 7 start = date(2027, 7, 5) resp = await client.post( "/api/v1/absences/", json={ "type_id": str(sick_type_id), "start_date": str(start), "end_date": str(start), }, headers=abs_headers, ) assert resp.status_code == 201 expected = start + timedelta(days=7) assert resp.json()["certificate_required_by"] == str(expected) # Reset auf default für nachfolgende Tests await client.patch( f"/api/v1/absence-types/{sick_type_id}", json={"certificate_after_days": 3}, headers=abs_headers, ) @pytest.mark.asyncio async def test_mark_certificate_received(client: AsyncClient, abs_headers, sick_type_id): """HR/Admin markiert Attest-Eingang per PATCH /absences/{id}/certificate.""" start = date(2027, 8, 10) create = await client.post( "/api/v1/absences/", json={ "type_id": str(sick_type_id), "start_date": str(start), "end_date": str(start), }, headers=abs_headers, ) absence_id = create.json()["id"] assert create.json()["certificate_received_at"] is None resp = await client.patch( f"/api/v1/absences/{absence_id}/certificate", json={}, # default = today headers=abs_headers, ) assert resp.status_code == 200 assert resp.json()["certificate_received_at"] == str(date.today()) @pytest.mark.asyncio async def test_sick_stats_bradford_factor(client: AsyncClient, abs_headers): """GET /absences/sick-stats: Bradford = episodes² × total_days, rolling 12 Monate.""" resp = await client.get("/api/v1/absences/sick-stats", headers=abs_headers) assert resp.status_code == 200 stats = resp.json() # Vorherige Tests haben mindestens eine Krankmeldung im 12-Monats-Fenster erzeugt assert len(stats) >= 1 row = stats[0] assert row["episodes"] >= 1 assert row["total_days"] >= 0.0 # Bradford-Formel verifizieren expected = float(row["episodes"]) ** 2 * row["total_days"] assert abs(row["bradford_factor"] - expected) < 0.001 # ── agent-11 PR1: Vertretung · Stornierung · Kommentare ──────────────────────── @pytest_asyncio.fixture(scope="session", loop_scope="session") async def substitute_user_id(client: AsyncClient, abs_headers): """Mitarbeiter, der als Vertretung eingetragen werden kann.""" resp = await client.post("/api/v1/users/invite", json={ "first_name": "Sub", "last_name": "Stitute", "email": "sub@absenceag.de", "role": "EMPLOYEE", "initial_password": "Secret123", }, headers=abs_headers) assert resp.status_code == 201, resp.text return resp.json()["id"] def _future_monday(weeks: int) -> date: return date.today() + timedelta(days=(7 - date.today().weekday()) + 7 * weeks) @pytest.mark.asyncio async def test_cancellation_request_flow( client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id ): """Genehmigten Antrag → Stornoantrag → Manager genehmigt → cancelled + Urlaub zurück.""" start = _future_monday(7) create = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=4)), }, headers=abs_headers) aid = create.json()["id"] working_days = create.json()["working_days"] approve = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) assert approve.status_code == 200 used_after_approve = (await client.get( "/api/v1/absences/balance", params={"year": start.year}, headers=abs_headers )).json()["used_days"] # Stornoantrag durch Mitarbeiter (Owner) req = await client.post( f"/api/v1/absences/{aid}/request-cancellation", json={"reason": "Plan geaendert"}, headers=abs_headers, ) assert req.status_code == 200, req.text assert req.json()["status"] == "cancellation_requested" # Manager genehmigt die Stornierung ok = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) assert ok.status_code == 200, ok.text assert ok.json()["status"] == "cancelled" used_after_cancel = (await client.get( "/api/v1/absences/balance", params={"year": start.year}, headers=abs_headers )).json()["used_days"] assert used_after_cancel == used_after_approve - int(working_days) @pytest.mark.asyncio async def test_cancellation_request_rejected_keeps_approved( client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id ): start = _future_monday(9) create = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=2)), }, headers=abs_headers) aid = create.json()["id"] await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) await client.post(f"/api/v1/absences/{aid}/request-cancellation", json={}, headers=abs_headers) rej = await client.post( f"/api/v1/absences/{aid}/reject", json={"rejection_reason": "Vertretung fehlt"}, headers=abs_approver_headers, ) assert rej.status_code == 200, rej.text assert rej.json()["status"] == "approved" @pytest.mark.asyncio async def test_request_cancellation_requires_approved( client: AsyncClient, abs_headers, vacation_type_id ): """PENDING-Antrag kann nicht zur Stornierung eingereicht werden (nur direkt löschen).""" start = _future_monday(11) create = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=1)), }, headers=abs_headers) aid = create.json()["id"] req = await client.post(f"/api/v1/absences/{aid}/request-cancellation", json={}, headers=abs_headers) assert req.status_code == 409 @pytest.mark.asyncio async def test_substitute_filter_and_notification( client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id, substitute_user_id ): """Antrag mit Vertretung → Vertreter sieht ihn unter ?as_substitute=true.""" start = _future_monday(13) create = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=2)), "substitute_id": substitute_user_id, }, headers=abs_headers) assert create.status_code == 201, create.text aid = create.json()["id"] assert create.json()["substitute_id"] == substitute_user_id await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) sub_login = await client.post("/api/v1/auth/login", json={ "email": "sub@absenceag.de", "password": "Secret123", }) sub_headers = {"Authorization": f"Bearer {sub_login.json()['access_token']}"} lst = await client.get("/api/v1/absences/?as_substitute=true", headers=sub_headers) assert lst.status_code == 200, lst.text assert any(a["id"] == aid for a in lst.json()["items"]) @pytest.mark.asyncio async def test_absence_comments( client: AsyncClient, abs_headers, vacation_type_id ): """Kommentar posten + System-Kommentar bei Stornoantrag erscheint im Thread.""" start = _future_monday(15) create = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=1)), "note": "Brueckentag", }, headers=abs_headers) aid = create.json()["id"] add = await client.post( f"/api/v1/absences/{aid}/comments", json={"body": "Bitte zuegig pruefen"}, headers=abs_headers, ) assert add.status_code == 201, add.text assert add.json()["is_system"] is False assert add.json()["author_name"] lst = await client.get(f"/api/v1/absences/{aid}/comments", headers=abs_headers) assert lst.status_code == 200 bodies = [c["body"] for c in lst.json()] assert "Bitte zuegig pruefen" in bodies # ── agent-11 PR2: Urlaubsanspruch (Verfall · Pro-rata · Teilzeit) ────────────── def test_effective_available_not_expired(): import uuid as _u from app.services.absence_service import AbsenceService from app.models.vacation_balance import VacationBalance b = VacationBalance(user_id=_u.uuid4(), year=2026, entitled_days=30, special_days=0, carried_over=5, used_days=3) assert AbsenceService.effective_available(b, False) == 32 # 30 + 5 - 3 assert AbsenceService.effective_available(b, True) == 30 # Resturlaub verfällt (3 < 5) def test_effective_available_expired_overdraw(): import uuid as _u from app.services.absence_service import AbsenceService from app.models.vacation_balance import VacationBalance b = VacationBalance(user_id=_u.uuid4(), year=2026, entitled_days=30, special_days=0, carried_over=5, used_days=8) assert AbsenceService.effective_available(b, True) == 27 # 30 - max(0, 8 - 5) @pytest.mark.asyncio async def test_company_vacation_settings_roundtrip(client: AsyncClient, abs_headers): r = await client.patch("/api/v1/companies/me", json={ "vacation_default_days": 28, "vacation_prorate_first_year": True, "vacation_from_schedule": False, }, headers=abs_headers) assert r.status_code == 200, r.text assert r.json()["vacation_default_days"] == 28 await client.patch("/api/v1/companies/me", json={"vacation_default_days": 30}, headers=abs_headers) @pytest.mark.asyncio async def test_prorate_entry_year(client: AsyncClient, abs_headers): inv = await client.post("/api/v1/users/invite", json={ "first_name": "Pro", "last_name": "Rata", "email": "prorata@absenceag.de", "role": "EMPLOYEE", "initial_password": "Secret123", }, headers=abs_headers) uid = inv.json()["id"] yr = date.today().year await client.patch(f"/api/v1/users/{uid}", json={"entry_date": f"{yr}-07-01"}, headers=abs_headers) bal = await client.get(f"/api/v1/absences/balance/{uid}?year={yr}", headers=abs_headers) assert bal.status_code == 200, bal.text assert bal.json()["entitled_days"] == 15 # 30 * 6/12 (Eintritt Juli) @pytest.mark.asyncio async def test_part_time_entitlement_from_schedule(client: AsyncClient, abs_headers): sched = await client.post("/api/v1/time/schedules", json={ "name": "Teilzeit 3 Tage", "mon_h": 8, "tue_h": 8, "wed_h": 8, "thu_h": 0, "fri_h": 0, "valid_from": "2026-01-01", }, headers=abs_headers) assert sched.status_code == 201, sched.text sid = sched.json()["id"] await client.patch("/api/v1/companies/me", json={ "vacation_from_schedule": True, "vacation_default_days": 30, }, headers=abs_headers) inv = await client.post("/api/v1/users/invite", json={ "first_name": "Teil", "last_name": "Zeit", "email": "teilzeit@absenceag.de", "role": "EMPLOYEE", "initial_password": "Secret123", }, headers=abs_headers) uid = inv.json()["id"] await client.patch(f"/api/v1/users/{uid}", json={"work_schedule_id": sid}, headers=abs_headers) yr = date.today().year bal = await client.get(f"/api/v1/absences/balance/{uid}?year={yr}", headers=abs_headers) assert bal.status_code == 200, bal.text assert bal.json()["entitled_days"] == 18 # 30 * 3/5 await client.patch("/api/v1/companies/me", json={"vacation_from_schedule": False}, headers=abs_headers) # ── agent-12: Zwei-Stufen-Genehmigung ───────────────────────────────────────── @pytest.mark.asyncio async def test_two_stage_approval_flow( client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id ): await client.patch("/api/v1/companies/me", json={"two_stage_approval_enabled": True}, headers=abs_headers) inv = await client.post("/api/v1/users/invite", json={ "first_name": "Two", "last_name": "Stage", "email": "twostage@absenceag.de", "role": "EMPLOYEE", "initial_password": "Secret123", }, headers=abs_headers) assert inv.status_code == 201, inv.text login = await client.post("/api/v1/auth/login", json={ "email": "twostage@absenceag.de", "password": "Secret123"}) emp_h = {"Authorization": f"Bearer {login.json()['access_token']}"} start = _future_monday(17) cr = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=2)), }, headers=emp_h) aid = cr.json()["id"] # Erste Stufe: Approver A → first_approved (noch kein Abzug) r1 = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) assert r1.status_code == 200, r1.text assert r1.json()["status"] == "first_approved" # Gleicher Genehmiger darf die finale Stufe nicht erteilen same = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) assert same.status_code == 409 # Finale Stufe: anderer Admin → approved r2 = await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_headers) assert r2.status_code == 200, r2.text assert r2.json()["status"] == "approved" await client.patch("/api/v1/companies/me", json={"two_stage_approval_enabled": False}, headers=abs_headers) @pytest.mark.asyncio async def test_two_stage_reject_at_first_stage( client: AsyncClient, abs_headers, abs_approver_headers, vacation_type_id ): await client.patch("/api/v1/companies/me", json={"two_stage_approval_enabled": True}, headers=abs_headers) login = await client.post("/api/v1/auth/login", json={ "email": "twostage@absenceag.de", "password": "Secret123"}) emp_h = {"Authorization": f"Bearer {login.json()['access_token']}"} start = _future_monday(19) cr = await client.post("/api/v1/absences/", json={ "type_id": str(vacation_type_id), "start_date": str(start), "end_date": str(start + timedelta(days=1)), }, headers=emp_h) aid = cr.json()["id"] await client.post(f"/api/v1/absences/{aid}/approve", headers=abs_approver_headers) rej = await client.post(f"/api/v1/absences/{aid}/reject", json={"rejection_reason": "Doch nicht"}, headers=abs_headers) assert rej.status_code == 200, rej.text assert rej.json()["status"] == "rejected" await client.patch("/api/v1/companies/me", json={"two_stage_approval_enabled": False}, headers=abs_headers)