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>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user