"""Stunden-Auszahlung: HR/Admin bucht Überstunden-Stunden zur Lohn-Auszahlung aus.""" from decimal import Decimal from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.dependencies import get_client_ip, require_role from app.models.audit_log import AuditLog from app.models.hours_payout import HoursPayout from app.models.overtime_balance import OvertimeBalance from app.models.user import User, UserRole from app.schemas.hours_payout import HoursPayoutCreate, HoursPayoutListResponse, HoursPayoutOut router = APIRouter(tags=["Stunden-Auszahlung"]) _hr_roles = (UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) _all_roles = (UserRole.EMPLOYEE, UserRole.MANAGER, UserRole.HR, UserRole.COMPANY_ADMIN, UserRole.SUPER_ADMIN) def _build_out(payout: HoursPayout, user: User | None, creator: User | None) -> HoursPayoutOut: out = HoursPayoutOut.model_validate(payout) out.user_name = ( f"{user.first_name} {user.last_name}" if user else str(payout.user_id) ) out.created_by_name = ( f"{creator.first_name} {creator.last_name}" if creator else str(payout.created_by) ) return out # ── GET /hr/payouts ─────────────────────────────────────────────────────────── @router.get("/hr/payouts", response_model=HoursPayoutListResponse) async def list_payouts( user_id: UUID | None = Query(None), year: int | None = Query(None, ge=2000, le=2100), month: int | None = Query(None, ge=1, le=12), current_user: User = require_role(*_all_roles), db: AsyncSession = Depends(get_db), ): """Alle Auszahlungen der eigenen Firma, optional gefiltert nach Mitarbeiter / Monat. EMPLOYEE und MANAGER sehen ausschließlich ihre eigenen Auszahlungen. """ # Employees und Manager sehen nur ihre eigenen Daten – Query-Param wird ignoriert if current_user.role not in _hr_roles: user_id = current_user.id filters = [HoursPayout.company_id == current_user.company_id] if user_id is not None: filters.append(HoursPayout.user_id == user_id) if year is not None: filters.append(HoursPayout.period_year == year) if month is not None: filters.append(HoursPayout.period_month == month) total_count = await db.scalar( select(func.count()).select_from(HoursPayout).where(*filters) ) rows = list(await db.scalars( select(HoursPayout).where(*filters).order_by(HoursPayout.created_at.desc()) )) result: list[HoursPayoutOut] = [] for payout in rows: user = await db.get(User, payout.user_id) creator = await db.get(User, payout.created_by) result.append(_build_out(payout, user, creator)) return HoursPayoutListResponse(payouts=result, total_count=total_count or 0) # ── POST /hr/payouts ────────────────────────────────────────────────────────── @router.post("/hr/payouts", response_model=HoursPayoutOut, status_code=201) async def create_payout( request: Request, data: HoursPayoutCreate, current_user: User = require_role(*_hr_roles), db: AsyncSession = Depends(get_db), ): """Neue Auszahlung anlegen – reduziert sofort den Überstunden-Saldo.""" # Ziel-User prüfen target = await db.get(User, data.user_id) if not target or target.company_id != current_user.company_id: raise HTTPException(404, "Mitarbeiter nicht gefunden") # OvertimeBalance laden oder anlegen ob = await db.scalar( select(OvertimeBalance).where(OvertimeBalance.user_id == data.user_id) ) if ob is None: ob = OvertimeBalance( user_id=data.user_id, company_id=current_user.company_id, total_hours=Decimal("0"), taken_hours=Decimal("0"), ) db.add(ob) await db.flush() # id erzeugen # Warnung bei Überziehung (kein Hard-Block) hours = Decimal(str(data.hours)) if ob.available_hours < hours: # Wir blockieren nicht – Auszahlung trotzdem buchen (wie FZA mit overdraft) pass # Saldo anpassen ob.taken_hours += hours # Auszahlungs-Datensatz anlegen payout = HoursPayout( company_id=current_user.company_id, user_id=data.user_id, hours=hours, period_year=data.period_year, period_month=data.period_month, note=data.note, created_by=current_user.id, ) db.add(payout) await db.flush() # payout.id erzeugen # AuditLog db.add(AuditLog( company_id=current_user.company_id, user_id=current_user.id, action="hours_payout_created", entity_type="hours_payout", entity_id=payout.id, new_value={ "user_id": str(data.user_id), "hours": str(hours), "period_year": data.period_year, "period_month": data.period_month, "note": data.note, }, ip=get_client_ip(request), )) await db.commit() await db.refresh(payout) creator = await db.get(User, payout.created_by) return _build_out(payout, target, creator) # ── DELETE /hr/payouts/{payout_id} ─────────────────────────────────────────── @router.delete("/hr/payouts/{payout_id}", status_code=204) async def delete_payout( payout_id: UUID, request: Request, current_user: User = require_role(*_hr_roles), db: AsyncSession = Depends(get_db), ): """Auszahlung stornieren – stellt die Stunden in den Überstunden-Saldo zurück.""" payout = await db.get(HoursPayout, payout_id) if payout is None or payout.company_id != current_user.company_id: raise HTTPException(404, "Auszahlung nicht gefunden") # OvertimeBalance laden und Stunden zurückbuchen ob = await db.scalar( select(OvertimeBalance).where(OvertimeBalance.user_id == payout.user_id) ) if ob is not None: ob.taken_hours = max(Decimal("0"), ob.taken_hours - payout.hours) # AuditLog db.add(AuditLog( company_id=current_user.company_id, user_id=current_user.id, action="hours_payout_deleted", entity_type="hours_payout", entity_id=payout.id, old_value={ "user_id": str(payout.user_id), "hours": str(payout.hours), "period_year": payout.period_year, "period_month": payout.period_month, "note": payout.note, }, ip=get_client_ip(request), )) await db.delete(payout) await db.commit()