"""Fiscal year/period API — Phase 5.1 + 5.3 + PATCH / set-current / generate.""" from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, status 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.foundation import FiscalPeriod, FiscalYear from app.repositories.foundation import FiscalPeriodRepository, FiscalYearRepository from app.schemas.foundation import ( FiscalPeriodCreate, FiscalPeriodRead, FiscalPeriodUpdate, FiscalYearCreate, FiscalYearRead, FiscalYearUpdate, ) from app.services.fiscal_service import FiscalManagementService from shared.exceptions import NotFoundError from shared.security import CurrentUser router = APIRouter() def _apply_update(entity, data: dict) -> None: for key, value in data.items(): setattr(entity, key, value) @router.post("/years", response_model=FiscalYearRead, status_code=status.HTTP_201_CREATED) async def create_fiscal_year( body: FiscalYearCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = FiscalYearRepository(db) if body.is_current: for y in await repo.list_by_tenant(tenant_id, limit=500): y.is_current = False entity = FiscalYear(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/years", response_model=list[FiscalYearRead]) async def list_fiscal_years( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = FiscalYearRepository(db) return await repo.list_by_tenant(tenant_id, limit=100) @router.patch("/years/{year_id}", response_model=FiscalYearRead) async def update_fiscal_year( year_id: UUID, body: FiscalYearUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = FiscalYearRepository(db) entity = await repo.get(tenant_id, year_id) if entity is None: raise NotFoundError("سال مالی یافت نشد", error_code="year_not_found") data = body.model_dump(exclude_unset=True) if data.get("is_current") is True: for y in await repo.list_by_tenant(tenant_id, limit=500): y.is_current = y.id == year_id data.pop("is_current", None) entity.is_current = True _apply_update(entity, data) await db.commit() await db.refresh(entity) return entity @router.post("/years/{year_id}/set-current", response_model=FiscalYearRead) async def set_current_year( year_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) year = await svc.set_current_year(tenant_id, year_id) await db.commit() return year @router.post("/years/{year_id}/close", response_model=FiscalYearRead) async def close_fiscal_year( year_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) year = await svc.close_fiscal_year(tenant_id, year_id) await db.commit() return year @router.post("/years/{year_id}/generate-periods", response_model=list[FiscalPeriodRead]) async def generate_periods( year_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) periods = await svc.generate_monthly_periods(tenant_id, year_id) await db.commit() return periods @router.post("/periods", response_model=FiscalPeriodRead, status_code=status.HTTP_201_CREATED) async def create_fiscal_period( body: FiscalPeriodCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = FiscalPeriodRepository(db) if body.is_current: for p in await repo.list_by_tenant(tenant_id, limit=500): p.is_current = False entity = FiscalPeriod(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/periods", response_model=list[FiscalPeriodRead]) async def list_fiscal_periods( tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = FiscalPeriodRepository(db) return await repo.list_by_tenant(tenant_id, limit=100) @router.patch("/periods/{period_id}", response_model=FiscalPeriodRead) async def update_fiscal_period( period_id: UUID, body: FiscalPeriodUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = FiscalPeriodRepository(db) entity = await repo.get(tenant_id, period_id) if entity is None: raise NotFoundError("دوره مالی یافت نشد", error_code="period_not_found") data = body.model_dump(exclude_unset=True) if data.get("is_current") is True: for p in await repo.list_by_tenant(tenant_id, limit=500): p.is_current = p.id == period_id data.pop("is_current", None) entity.is_current = True _apply_update(entity, data) await db.commit() await db.refresh(entity) return entity @router.post("/periods/{period_id}/set-current", response_model=FiscalPeriodRead) async def set_current_period( period_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) period = await svc.set_current_period(tenant_id, period_id) await db.commit() return period @router.post("/periods/{period_id}/lock", response_model=FiscalPeriodRead) async def lock_period( period_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) period = await svc.lock_period(tenant_id, period_id) await db.commit() return period @router.post("/periods/{period_id}/close", response_model=FiscalPeriodRead) async def close_period( period_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) period = await svc.close_period(tenant_id, period_id) await db.commit() return period @router.post("/periods/{period_id}/reopen", response_model=FiscalPeriodRead) async def reopen_period( period_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): svc = FiscalManagementService(db) period = await svc.reopen_period(tenant_id, period_id) await db.commit() return period