Files
timemaster/backend/tests/test_hours_payouts.py
patrickandClaude Opus 4.8 eb8dace0fd feat(overtime): Auszahlungs-Anträge durch Mitarbeiter (opt-in pro Firma)
Mitarbeiter beantragt Überstunden-Auszahlung, HR genehmigt/lehnt ab.
Firmen-Opt-in payout_request_enabled (Default aus - nur HR-Direktbuchung).
Saldo-Abzug erst bei Genehmigung; Reject/Cancel bucht nichts.

- Migration 0042: hours_payouts.status/decided_by/decided_at/rejection_reason
  + companies.payout_request_enabled (nur Spalten, RLS unveraendert)
- Router: POST /hr/payouts/request|{id}/approve|reject|cancel; list status-Filter;
  HR-Direktbuchung bleibt (status approved)
- Frontend: PayoutRequestCard (Selbstbedienung in AbsencesPage), HR-Page
  Status-Spalte + Genehmigen/Ablehnen, CompanySettings-Toggle
- 4 pytest-Cases

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 22:43:26 +02:00

88 lines
3.8 KiB
Python
Raw Permalink 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.
"""Tests für Auszahlungs-Anträge mit Genehmigung (router/hours_payouts.py)."""
import pytest
import pytest_asyncio
from httpx import AsyncClient
@pytest_asyncio.fixture(scope="session", loop_scope="session")
async def payout_headers(client: AsyncClient):
resp = await client.post("/api/v1/auth/register", json={
"company_name": "Payout GmbH",
"first_name": "Paul",
"last_name": "Out",
"email": "admin@payoutgmbh.de",
"password": "Secret123",
})
assert resp.status_code == 201, resp.text
return {"Authorization": f"Bearer {resp.json()['access_token']}"}
@pytest.mark.asyncio(loop_scope="session")
async def test_request_disabled_by_default(client: AsyncClient, payout_headers):
r = await client.post("/api/v1/hr/payouts/request",
json={"hours": 4}, headers=payout_headers)
assert r.status_code == 403
@pytest.mark.asyncio(loop_scope="session")
async def test_enable_and_request_approve(client: AsyncClient, payout_headers):
# Firma schaltet Selbstbedienung frei
up = await client.patch("/api/v1/companies/me",
json={"payout_request_enabled": True}, headers=payout_headers)
assert up.status_code == 200, up.text
assert up.json()["payout_request_enabled"] is True
# Antrag stellen Status requested, kein Saldo-Abzug
r = await client.post("/api/v1/hr/payouts/request",
json={"hours": 5, "note": "Bitte auszahlen"}, headers=payout_headers)
assert r.status_code == 201, r.text
pid = r.json()["id"]
assert r.json()["status"] == "requested"
bal = await client.get("/api/v1/absences/overtime-balance", headers=payout_headers)
assert float(bal.json()["taken_hours"]) == 0.0
# HR genehmigt jetzt Saldo-Abzug
ap = await client.post(f"/api/v1/hr/payouts/{pid}/approve", json={}, headers=payout_headers)
assert ap.status_code == 200, ap.text
assert ap.json()["status"] == "approved"
bal2 = await client.get("/api/v1/absences/overtime-balance", headers=payout_headers)
assert float(bal2.json()["taken_hours"]) == 5.0
# Erneutes Genehmigen scheitert
ap2 = await client.post(f"/api/v1/hr/payouts/{pid}/approve", json={}, headers=payout_headers)
assert ap2.status_code == 409
@pytest.mark.asyncio(loop_scope="session")
async def test_reject_no_deduction(client: AsyncClient, payout_headers):
r = await client.post("/api/v1/hr/payouts/request",
json={"hours": 3}, headers=payout_headers)
pid = r.json()["id"]
before = float((await client.get("/api/v1/absences/overtime-balance",
headers=payout_headers)).json()["taken_hours"])
rej = await client.post(f"/api/v1/hr/payouts/{pid}/reject",
json={"rejection_reason": "kein Budget"}, headers=payout_headers)
assert rej.status_code == 200
assert rej.json()["status"] == "rejected"
assert rej.json()["rejection_reason"] == "kein Budget"
after = float((await client.get("/api/v1/absences/overtime-balance",
headers=payout_headers)).json()["taken_hours"])
assert after == before # kein Abzug bei Ablehnung
@pytest.mark.asyncio(loop_scope="session")
async def test_cancel_own_request(client: AsyncClient, payout_headers):
r = await client.post("/api/v1/hr/payouts/request",
json={"hours": 2}, headers=payout_headers)
pid = r.json()["id"]
c = await client.post(f"/api/v1/hr/payouts/{pid}/cancel", json={}, headers=payout_headers)
assert c.status_code == 200
assert c.json()["status"] == "cancelled"
# Genehmigen eines zurückgezogenen Antrags scheitert
ap = await client.post(f"/api/v1/hr/payouts/{pid}/approve", json={}, headers=payout_headers)
assert ap.status_code == 409