"""Foundation CRUD API — Phase 5.1 + PATCH/archive + COA templates.""" from __future__ import annotations from uuid import UUID from fastapi import APIRouter, Depends, Query, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.deps import get_db, get_pagination, require_tenant from app.core.security import get_current_user from app.models.foundation import Account, ChartOfAccounts, CostCenter, Currency, Project from app.models.types import AccountStatus from app.repositories.foundation import ( AccountRepository, ChartOfAccountsRepository, CostCenterRepository, CurrencyRepository, ProjectRepository, ) from app.schemas.foundation import ( AccountCreate, AccountRead, AccountUpdate, ChartOfAccountsCreate, ChartOfAccountsRead, ChartOfAccountsUpdate, CoaTemplateImportRequest, CoaTemplateRead, CostCenterCreate, CostCenterRead, CostCenterUpdate, CurrencyCreate, CurrencyRead, CurrencyUpdate, ProjectCreate, ProjectRead, ProjectUpdate, ) from app.services.coa_templates import COA_TEMPLATES, import_coa_template from shared.exceptions import AppError, NotFoundError from shared.pagination import PaginationParams from shared.security import CurrentUser router = APIRouter() def _apply_update(entity, data: dict) -> None: for key, value in data.items(): setattr(entity, key, value) # ── Charts ────────────────────────────────────────────────────────────────── @router.post("/charts", response_model=ChartOfAccountsRead, status_code=status.HTTP_201_CREATED) async def create_chart( body: ChartOfAccountsCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ChartOfAccountsRepository(db) if body.is_default: for existing in await repo.list_by_tenant(tenant_id, limit=500): existing.is_default = False entity = ChartOfAccounts(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/charts", response_model=list[ChartOfAccountsRead]) async def list_charts( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ChartOfAccountsRepository(db) return await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size) @router.get("/charts/{chart_id}", response_model=ChartOfAccountsRead) async def get_chart( chart_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): entity = await ChartOfAccountsRepository(db).get(tenant_id, chart_id) if entity is None: raise NotFoundError("دفتر حساب یافت نشد", error_code="chart_not_found") return entity @router.patch("/charts/{chart_id}", response_model=ChartOfAccountsRead) async def update_chart( chart_id: UUID, body: ChartOfAccountsUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ChartOfAccountsRepository(db) entity = await repo.get(tenant_id, chart_id) if entity is None: raise NotFoundError("دفتر حساب یافت نشد", error_code="chart_not_found") data = body.model_dump(exclude_unset=True) if data.get("is_default") is True: for existing in await repo.list_by_tenant(tenant_id, limit=500): if existing.id != chart_id: existing.is_default = False _apply_update(entity, data) await db.commit() await db.refresh(entity) return entity @router.post("/charts/{chart_id}/archive", response_model=ChartOfAccountsRead) async def archive_chart( chart_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ChartOfAccountsRepository(db) entity = await repo.get(tenant_id, chart_id) if entity is None: raise NotFoundError("دفتر حساب یافت نشد", error_code="chart_not_found") entity.is_active = False entity.is_default = False await db.commit() await db.refresh(entity) return entity # ── Templates (static paths before /{account_id}) ──────────────────────────── @router.get("/templates", response_model=list[CoaTemplateRead]) async def list_coa_templates(_user: CurrentUser = Depends(get_current_user)): return list(COA_TEMPLATES.values()) @router.get("/templates/{template_id}", response_model=CoaTemplateRead) async def get_coa_template(template_id: str, _user: CurrentUser = Depends(get_current_user)): tpl = COA_TEMPLATES.get(template_id) if tpl is None: raise NotFoundError("قالب یافت نشد", error_code="template_not_found") return tpl @router.post( "/templates/{template_id}/import", response_model=list[AccountRead], status_code=status.HTTP_201_CREATED, ) async def import_template( template_id: str, body: CoaTemplateImportRequest, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): accounts = await import_coa_template( db, tenant_id=tenant_id, template_id=template_id, chart_id=body.chart_id, chart_name=body.chart_name, chart_code=body.chart_code, ) await db.commit() return accounts # ── Currencies ────────────────────────────────────────────────────────────── @router.post("/currencies", response_model=CurrencyRead, status_code=status.HTTP_201_CREATED) async def create_currency( body: CurrencyCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CurrencyRepository(db) if body.is_base: for existing in await repo.list_by_tenant(tenant_id, limit=500): existing.is_base = False entity = Currency(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/currencies", response_model=list[CurrencyRead]) async def list_currencies( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CurrencyRepository(db) return await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size) @router.patch("/currencies/{currency_id}", response_model=CurrencyRead) async def update_currency( currency_id: UUID, body: CurrencyUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CurrencyRepository(db) entity = await repo.get(tenant_id, currency_id) if entity is None: raise NotFoundError("ارز یافت نشد", error_code="currency_not_found") data = body.model_dump(exclude_unset=True) if data.get("is_base") is True: for existing in await repo.list_by_tenant(tenant_id, limit=500): if existing.id != currency_id: existing.is_base = False _apply_update(entity, data) await db.commit() await db.refresh(entity) return entity @router.post("/currencies/{currency_id}/set-base", response_model=CurrencyRead) async def set_base_currency( currency_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CurrencyRepository(db) entity = await repo.get(tenant_id, currency_id) if entity is None: raise NotFoundError("ارز یافت نشد", error_code="currency_not_found") if not entity.is_active: raise AppError( "ارز غیرفعال نمی‌تواند پایه باشد", status_code=422, error_code="currency_inactive", ) for existing in await repo.list_by_tenant(tenant_id, limit=500): existing.is_base = existing.id == currency_id await db.commit() await db.refresh(entity) return entity @router.post("/currencies/{currency_id}/archive", response_model=CurrencyRead) async def archive_currency( currency_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CurrencyRepository(db) entity = await repo.get(tenant_id, currency_id) if entity is None: raise NotFoundError("ارز یافت نشد", error_code="currency_not_found") if entity.is_base: raise AppError( "ارز پایه قابل آرشیو نیست", status_code=422, error_code="base_currency_archive", ) entity.is_active = False await db.commit() await db.refresh(entity) return entity # ── Cost centers ──────────────────────────────────────────────────────────── @router.post("/cost-centers", response_model=CostCenterRead, status_code=status.HTTP_201_CREATED) async def create_cost_center( body: CostCenterCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CostCenterRepository(db) entity = CostCenter(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/cost-centers", response_model=list[CostCenterRead]) async def list_cost_centers( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CostCenterRepository(db) return await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size) @router.patch("/cost-centers/{cost_center_id}", response_model=CostCenterRead) async def update_cost_center( cost_center_id: UUID, body: CostCenterUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CostCenterRepository(db) entity = await repo.get(tenant_id, cost_center_id) if entity is None: raise NotFoundError("مرکز هزینه یافت نشد", error_code="cost_center_not_found") _apply_update(entity, body.model_dump(exclude_unset=True)) await db.commit() await db.refresh(entity) return entity @router.post("/cost-centers/{cost_center_id}/archive", response_model=CostCenterRead) async def archive_cost_center( cost_center_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = CostCenterRepository(db) entity = await repo.get(tenant_id, cost_center_id) if entity is None: raise NotFoundError("مرکز هزینه یافت نشد", error_code="cost_center_not_found") entity.is_active = False await db.commit() await db.refresh(entity) return entity # ── Projects ──────────────────────────────────────────────────────────────── @router.post("/projects", response_model=ProjectRead, status_code=status.HTTP_201_CREATED) async def create_project( body: ProjectCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ProjectRepository(db) entity = Project(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("/projects", response_model=list[ProjectRead]) async def list_projects( tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ProjectRepository(db) return await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size) @router.patch("/projects/{project_id}", response_model=ProjectRead) async def update_project( project_id: UUID, body: ProjectUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ProjectRepository(db) entity = await repo.get(tenant_id, project_id) if entity is None: raise NotFoundError("پروژه یافت نشد", error_code="project_not_found") _apply_update(entity, body.model_dump(exclude_unset=True)) await db.commit() await db.refresh(entity) return entity @router.post("/projects/{project_id}/archive", response_model=ProjectRead) async def archive_project( project_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = ProjectRepository(db) entity = await repo.get(tenant_id, project_id) if entity is None: raise NotFoundError("پروژه یافت نشد", error_code="project_not_found") entity.is_active = False await db.commit() await db.refresh(entity) return entity # ── Accounts (param routes last) ──────────────────────────────────────────── @router.post("", response_model=AccountRead, status_code=status.HTTP_201_CREATED) async def create_account( body: AccountCreate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = AccountRepository(db) entity = Account(tenant_id=tenant_id, **body.model_dump()) await repo.add(entity) await db.commit() await db.refresh(entity) return entity @router.get("", response_model=list[AccountRead]) async def list_accounts( chart_id: UUID | None = Query(default=None), tenant_id: UUID = Depends(require_tenant), pagination: PaginationParams = Depends(get_pagination), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = AccountRepository(db) if chart_id is not None: return await repo.list_by_chart( tenant_id, chart_id, offset=pagination.offset, limit=pagination.page_size ) return await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size) @router.get("/{account_id}", response_model=AccountRead) async def get_account( account_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = AccountRepository(db) entity = await repo.get(tenant_id, account_id) if entity is None: raise NotFoundError("حساب یافت نشد", error_code="account_not_found") return entity @router.patch("/{account_id}", response_model=AccountRead) async def update_account( account_id: UUID, body: AccountUpdate, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = AccountRepository(db) entity = await repo.get(tenant_id, account_id) if entity is None: raise NotFoundError("حساب یافت نشد", error_code="account_not_found") _apply_update(entity, body.model_dump(exclude_unset=True)) await db.commit() await db.refresh(entity) return entity @router.post("/{account_id}/archive", response_model=AccountRead) async def archive_account( account_id: UUID, tenant_id: UUID = Depends(require_tenant), db: AsyncSession = Depends(get_db), _user: CurrentUser = Depends(get_current_user), ): repo = AccountRepository(db) entity = await repo.get(tenant_id, account_id) if entity is None: raise NotFoundError("حساب یافت نشد", error_code="account_not_found") entity.status = AccountStatus.INACTIVE await db.commit() await db.refresh(entity) return entity