"""Phase 5.9 — Payroll API.""" from __future__ import annotations from datetime import date from decimal import Decimal from uuid import UUID from fastapi import APIRouter, Depends, status from pydantic import BaseModel from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, require_tenant from app.core.security import get_current_user from app.models.payroll import Department, Employee, PayrollPeriod from app.models.types import EmploymentStatus, PayrollPeriodStatus from app.repositories.base import TenantBaseRepository from app.services.payroll_service import PayrollAccountingService from shared.security import CurrentUser router = APIRouter() class DepartmentCreate(BaseModel): code: str name: str class EmployeeCreate(BaseModel): employee_code: str first_name: str last_name: str department_id: UUID | None = None base_salary: Decimal = Decimal("0") hire_date: date | None = None class PayrollPeriodCreate(BaseModel): name: str start_date: date end_date: date class CalculatePayrollRequest(BaseModel): payroll_period_id: UUID employee_id: UUID class PostPayrollRequest(BaseModel): profile_id: UUID | None = None class DepartmentRepo(TenantBaseRepository[Department]): model = Department class EmployeeRepo(TenantBaseRepository[Employee]): model = Employee class PayrollPeriodRepo(TenantBaseRepository[PayrollPeriod]): model = PayrollPeriod @router.post("/departments", status_code=status.HTTP_201_CREATED) async def create_department( body: DepartmentCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = DepartmentRepo(db) entity = Department(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() return {"id": str(entity.id)} @router.get("/departments") async def list_departments( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): deps = await DepartmentRepo(db).list_by_tenant(tenant_id, limit=100) return [{"id": str(d.id), "code": d.code, "name": d.name} for d in deps] @router.post("/employees", status_code=status.HTTP_201_CREATED) async def create_employee( body: EmployeeCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = EmployeeRepo(db) entity = Employee(tenant_id=tenant_id, employment_status=EmploymentStatus.ACTIVE, **body.model_dump()) await repo.add(entity) await db.commit() return {"id": str(entity.id), "employee_code": entity.employee_code} @router.get("/employees") async def list_employees( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = EmployeeRepo(db) employees = await repo.list_by_tenant(tenant_id, limit=100) return [{"id": str(e.id), "code": e.employee_code, "name": f"{e.first_name} {e.last_name}"} for e in employees] @router.post("/periods", status_code=status.HTTP_201_CREATED) async def create_payroll_period( body: PayrollPeriodCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = PayrollPeriodRepo(db) entity = PayrollPeriod(tenant_id=tenant_id, status=PayrollPeriodStatus.OPEN, **body.model_dump()) await repo.add(entity) await db.commit() return {"id": str(entity.id)} @router.get("/periods") async def list_payroll_periods( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): periods = await PayrollPeriodRepo(db).list_by_tenant(tenant_id, limit=100) return [ { "id": str(p.id), "name": p.name, "start_date": p.start_date.isoformat(), "end_date": p.end_date.isoformat(), "status": p.status.value, } for p in periods ] @router.post("/calculate") async def calculate_payroll( body: CalculatePayrollRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = PayrollAccountingService(db) payroll = await svc.calculate_payroll(tenant_id, body.payroll_period_id, body.employee_id) await db.commit() return { "id": str(payroll.id), "gross_salary": str(payroll.gross_salary), "net_salary": str(payroll.net_salary), "status": payroll.status.value, } @router.post("/payrolls/{payroll_id}/post") async def post_payroll( payroll_id: UUID, body: PostPayrollRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), user: CurrentUser = Depends(get_current_user), ): from app.models.types import PayrollStatus from app.services.payroll_service import PayrollRepo payroll_repo = PayrollRepo(db) payroll = await payroll_repo.get(tenant_id, payroll_id) if payroll is None: from shared.exceptions import NotFoundError raise NotFoundError("حقوق یافت نشد", error_code="payroll_not_found") payroll.status = PayrollStatus.APPROVED svc = PayrollAccountingService(db) result = await svc.post_payroll(tenant_id, payroll_id, actor_user_id=user.user_id, profile_id=body.profile_id) await db.commit() return {"id": str(result.id), "status": result.status.value, "voucher_id": str(result.voucher_id)}