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>
88 lines
3.8 KiB
Python
88 lines
3.8 KiB
Python
"""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
|